87d7b854 by 华明祺

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

- 协议勾选区域固定在页面底部,优化用户体验
- 联系方式改为非必填,但填写时验证手机号格式
- 支付逻辑使用 await-to-js 重构,统一错误处理
- 支付流程添加 loading 状态,防止重复提交
- 支付成功后传递 orderId,跳转时获取并展示订单详情
- 支付成功页面优化:标签不换行、值支持自动换行
- 新增获取订单详情接口 /common/order/{orderId}
1 parent 7d54d38d
...@@ -195,11 +195,11 @@ export function regionsList(params) { ...@@ -195,11 +195,11 @@ export function regionsList(params) {
195 export function carUrl(data, type) { 195 export function carUrl(data, type) {
196 return uni.uploadFile({ 196 return uni.uploadFile({
197 url: `${config.baseUrl_api}/person/info/getPersonInfoFromCert/${type}`, 197 url: `${config.baseUrl_api}/person/info/getPersonInfoFromCert/${type}`,
198 header: { 198 // header: {
199 'Authorization': uni.getStorageSync('token'), 199 // 'Authorization': uni.getStorageSync('token'),
200 'Content-Language': 'zh_CN', 200 // 'Content-Language': 'zh_CN',
201 'Accept-Language': 'zh-CN,zh', 201 // 'Accept-Language': 'zh-CN,zh',
202 }, 202 // },
203 name: 'pic', 203 name: 'pic',
204 filePath: data 204 filePath: data
205 }).then(res => { 205 }).then(res => {
...@@ -237,10 +237,10 @@ export function addPersonCommit(data) { ...@@ -237,10 +237,10 @@ export function addPersonCommit(data) {
237 } 237 }
238 238
239 export function getZtxFeeConfig() { 239 export function getZtxFeeConfig() {
240 return request({ 240 return request({
241 url: '/system/config/getZtxFeeConfig', 241 url: '/system/config/getZtxFeeConfig',
242 method: 'get' 242 method: 'get'
243 }) 243 })
244 } 244 }
245 // 图片上传 245 // 图片上传
246 export function uploadImg(e) { 246 export function uploadImg(e) {
...@@ -747,10 +747,10 @@ export function personalCommit(id) { ...@@ -747,10 +747,10 @@ export function personalCommit(id) {
747 }) 747 })
748 } 748 }
749 export function getNewCountByRangeId(rangeId) { 749 export function getNewCountByRangeId(rangeId) {
750 return request({ 750 return request({
751 url: `/person/paymentNew/getNewCountByRangeId/${rangeId}`, 751 url: `/person/paymentNew/getNewCountByRangeId/${rangeId}`,
752 method: 'get', 752 method: 'get',
753 }) 753 })
754 } 754 }
755 755
756 export function delPayment(payIds) { 756 export function delPayment(payIds) {
...@@ -774,11 +774,11 @@ export function delcertified(ids) { ...@@ -774,11 +774,11 @@ export function delcertified(ids) {
774 } 774 }
775 775
776 export function editYear(data) { 776 export function editYear(data) {
777 return request({ 777 return request({
778 url: `/person/paymentNew/editYear/${data.payId}?payId=${data.payId}&year=${data.year}`, 778 url: `/person/paymentNew/editYear/${data.payId}?payId=${data.payId}&year=${data.year}`,
779 method: 'post', 779 method: 'post',
780 params: data 780 params: data
781 }) 781 })
782 } 782 }
783 783
784 export function editGroupYear(data) { 784 export function editGroupYear(data) {
...@@ -1283,135 +1283,143 @@ export function checkPersonByPersonId(perId) { ...@@ -1283,135 +1283,143 @@ export function checkPersonByPersonId(perId) {
1283 } 1283 }
1284 // 获取团体会员优惠政策 1284 // 获取团体会员优惠政策
1285 export function canUseDiscount(params) { 1285 export function canUseDiscount(params) {
1286 return request({ 1286 return request({
1287 url: `/system/certifiedNew/canUseDiscount`, 1287 url: `/system/certifiedNew/canUseDiscount`,
1288 method: 'get', 1288 method: 'get',
1289 params 1289 params
1290 }) 1290 })
1291 } 1291 }
1292 // 获取团体会员一年缴费价格 1292 // 获取团体会员一年缴费价格
1293 export function getMyMemberCertUnitFee(params) { 1293 export function getMyMemberCertUnitFee(params) {
1294 return request({ 1294 return request({
1295 url: `/system/certifiedNew/getMyMemberCertUnitFee`, 1295 url: `/system/certifiedNew/getMyMemberCertUnitFee`,
1296 method: 'get', 1296 method: 'get',
1297 params 1297 params
1298 }) 1298 })
1299 } 1299 }
1300 export function checkBusinessLicense(data) { 1300 export function checkBusinessLicense(data) {
1301 return request({ 1301 return request({
1302 url: `/member/info/checkBusinessLicense`, 1302 url: `/member/info/checkBusinessLicense`,
1303 method: 'post', 1303 method: 'post',
1304 params: data 1304 params: data
1305 }) 1305 })
1306 } 1306 }
1307 1307
1308 // 生成团体订单renewYear 1308 // 生成团体订单renewYear
1309 export function certifiedNew(params) { 1309 export function certifiedNew(params) {
1310 return request({ 1310 return request({
1311 url: `/system/certifiedNew/commit`, 1311 url: `/system/certifiedNew/commit`,
1312 method: 'post', 1312 method: 'post',
1313 params 1313 params
1314 }) 1314 })
1315 } 1315 }
1316 1316
1317 // 模拟回调 1317 // 模拟回调
1318 export function callBack2(orderId) { 1318 export function callBack2(orderId) {
1319 return request({ 1319 return request({
1320 url: `/system/certifiedNew/callBack2/${orderId}`, 1320 url: `/system/certifiedNew/callBack2/${orderId}`,
1321 method: 'get', 1321 method: 'get',
1322 }) 1322 })
1323 } 1323 }
1324 export function pcallBack2(orderId) { 1324 export function pcallBack2(orderId) {
1325 return request({ 1325 return request({
1326 url: `/person/paymentRangeNew/callBack2/${orderId}`, 1326 url: `/person/paymentRangeNew/callBack2/${orderId}`,
1327 method: 'get', 1327 method: 'get',
1328 }) 1328 })
1329 } 1329 }
1330 // 优惠政策回显 1330 // 优惠政策回显
1331 export function getZtxDiscountPolicy(params) { 1331 export function getZtxDiscountPolicy(params) {
1332 return request({ 1332 return request({
1333 url: '/system/config/getZtxDiscountPolicy', 1333 url: '/system/config/getZtxDiscountPolicy',
1334 method: 'get', 1334 method: 'get',
1335 params 1335 params
1336 }) 1336 })
1337 } 1337 }
1338 // 考官列表 1338 // 考官列表
1339 export function listApi(params) { 1339 export function listApi(params) {
1340 return request({ 1340 return request({
1341 url: `/member/examiner/list`, 1341 url: `/member/examiner/list`,
1342 method: 'get', 1342 method: 'get',
1343 params 1343 params
1344 }) 1344 })
1345 } 1345 }
1346 1346
1347 // 考官列表 1347 // 考官列表
1348 export function examinerDel(id) { 1348 export function examinerDel(id) {
1349 return request({ 1349 return request({
1350 url: `/member/examiner/${id}`, 1350 url: `/member/examiner/${id}`,
1351 method: 'delete' 1351 method: 'delete'
1352 }) 1352 })
1353 } 1353 }
1354 1354
1355 // 添加考官 1355 // 添加考官
1356 export function otherAdd(memId, ids) { 1356 export function otherAdd(memId, ids) {
1357 return request({ 1357 return request({
1358 url: `/member/examiner/otherAdd/${memId}/${ids}`, 1358 url: `/member/examiner/otherAdd/${memId}/${ids}`,
1359 method: 'post' 1359 method: 'post'
1360 }) 1360 })
1361 } 1361 }
1362 1362
1363 export function commitExamPointApply(params) { 1363 export function commitExamPointApply(params) {
1364 return request({ 1364 return request({
1365 url: `/member/examPointApply/commit?selfSelect=${params.selfSelect}`, 1365 url: `/member/examPointApply/commit?selfSelect=${params.selfSelect}`,
1366 method: 'post', 1366 method: 'post',
1367 params 1367 params
1368 }) 1368 })
1369 } 1369 }
1370 1370
1371 export function getMyStatus() { 1371 export function getMyStatus() {
1372 return request({ 1372 return request({
1373 url: `/member/examPointApply/getMyStatus` 1373 url: `/member/examPointApply/getMyStatus`
1374 }) 1374 })
1375 } 1375 }
1376 1376
1377 // 个人会员缴费支付 1377 // 个人会员缴费支付
1378 export function goPay(id) { 1378 export function goPay(id) {
1379 return request({ 1379 return request({
1380 url: `/person/paymentRangeNew/pay/${id}`, 1380 url: `/person/paymentRangeNew/pay/${id}`,
1381 method: 'post' 1381 method: 'post'
1382 }) 1382 })
1383 } 1383 }
1384 // 缴费单列表学员 1384 // 缴费单列表学员
1385 export function listAPI(params) { 1385 export function listAPI(params) {
1386 return request({ 1386 return request({
1387 url: `/person/paymentNew/list`, 1387 url: `/person/paymentNew/list`,
1388 method: 'get', 1388 method: 'get',
1389 params 1389 params
1390 }) 1390 })
1391 } 1391 }
1392 1392
1393 1393
1394 // 删除学员 1394 // 删除学员
1395 export function paymentNewDel(id) { 1395 export function paymentNewDel(id) {
1396 return request({ 1396 return request({
1397 url: `/person/paymentNew/${id}`, 1397 url: `/person/paymentNew/${id}`,
1398 method: 'delete' 1398 method: 'delete'
1399 }) 1399 })
1400 } 1400 }
1401 1401
1402 1402
1403 // 缴费单列表 1403 // 缴费单列表
1404 export function memberInsertPersons(data) { 1404 export function memberInsertPersons(data) {
1405 return request({ 1405 return request({
1406 url: `/person/paymentNew/memberInsertPersons/${data.rangeId}/${data.year}/${data.idcCode}`, 1406 url: `/person/paymentNew/memberInsertPersons/${data.rangeId}/${data.year}/${data.idcCode}`,
1407 method: 'post', 1407 method: 'post',
1408 data 1408 data
1409 }) 1409 })
1410 } 1410 }
1411 export function createMemberPayRange(data) { 1411 export function createMemberPayRange(data) {
1412 return request({ 1412 return request({
1413 url: `/person/paymentRangeNew/createMemberPayRange`, 1413 url: `/person/paymentRangeNew/createMemberPayRange`,
1414 method: 'post', 1414 method: 'post',
1415 data 1415 data
1416 }) 1416 })
1417 }
1418
1419 // 获取订单详情
1420 export function getOrderInfo(orderId) {
1421 return request({
1422 url: `/common/order/${orderId}`,
1423 method: 'get'
1424 })
1417 } 1425 }
...\ No newline at end of file ...\ No newline at end of file
......
1 { 1 {
2 "dependencies": { 2 "dependencies": {
3 "await-to-js": "^3.0.0",
3 "crypto-js": "^4.1.1", 4 "crypto-js": "^4.1.1",
4 "dayjs": "^1.11.6", 5 "dayjs": "^1.11.6",
5 "lodash": "^4.17.21", 6 "lodash": "^4.17.21",
......
1 <template> 1 <template>
2 <view> 2 <view>
3 <uni-segmented-control class="whitebg" :current="current" :values="items" @clickItem="onClickItem" 3 <uni-segmented-control class="whitebg" :current="current" :values="items" @clickItem="onClickItem"
4 styleType="text" activeColor="#C40F18"></uni-segmented-control> 4 styleType="text" activeColor="#C40F18"></uni-segmented-control>
5 <view class="hasfixedbottom"> 5 <view class="hasfixedbottom">
6 <view> 6 <view>
7 <uni-forms ref="baseForm" :border="true" :modelValue="baseFormData" label-width="80"> 7 <uni-forms ref="baseForm" :border="true" :modelValue="baseFormData" label-width="80">
8 <view class="nolineform"> 8 <view class="nolineform">
9 <uni-forms-item label="姓名" required name="name" v-show="current === 0"> 9 <uni-forms-item label="姓名" required name="name" v-show="current === 0">
10 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle" 10 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle"
11 v-model="baseFormData.name" placeholder="请输入姓名" /> 11 v-model="baseFormData.name" placeholder="请输入姓名" />
12 </uni-forms-item> 12 </uni-forms-item>
13 <uni-forms-item label="证件类型" required name="idcType"> 13 <uni-forms-item label="证件类型" required name="idcType">
14 <uni-data-select v-model="baseFormData.idcType" style="width: 360rpx;" 14 <uni-data-select v-model="baseFormData.idcType" style="width: 360rpx;"
15 @change="changeIdcType" :clear="false" :disabled="current === 0" 15 @change="changeIdcType" :clear="false" :disabled="current === 0"
16 :localdata="idcTypeList"></uni-data-select> 16 :localdata="idcTypeList"></uni-data-select>
17 </uni-forms-item> 17 </uni-forms-item>
18 <uni-forms-item label="证件照" required v-show="current === 1"> 18 <uni-forms-item label="证件照" required v-show="current === 1">
19 <view class="upCard"> 19 <view class="upCard">
20 <uni-file-picker v-model="cardObj" @delete="delimgFont" return-type="object" limit="1" 20 <uni-file-picker v-model="cardObj" @delete="delimgFont" return-type="object" limit="1"
21 @select="upIdCardImgFront" :image-styles="imageStylesZJ"> 21 @select="upIdCardImgFront" :image-styles="imageStylesZJ">
22 <image v-if="!baseFormData.card" class="sfz" 22 <image v-if="!baseFormData.card" class="sfz"
23 :src="config.baseUrl_api+'/fs/static/login/sfz.png'"> 23 :src="config.baseUrl_api+'/fs/static/login/sfz.png'">
24 </image> 24 </image>
25 </uni-file-picker> 25 </uni-file-picker>
26 </view> 26 </view>
27 </uni-forms-item> 27 </uni-forms-item>
28 <uni-forms-item label="姓名" required name="name" v-show="current === 1"> 28 <uni-forms-item label="姓名" required name="name" v-show="current === 1">
29 <text v-if="disabledName">{{baseFormData.name}}</text> 29 <text v-if="disabledName">{{baseFormData.name}}</text>
30 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle" 30 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle"
31 v-model="baseFormData.name" v-else placeholder="请输入姓名" /> 31 v-model="baseFormData.name" v-else placeholder="请输入姓名" />
32 </uni-forms-item> 32 </uni-forms-item>
33 <uni-forms-item label="证件号码" required name="idcCode" v-show="current === 0"> 33 <uni-forms-item label="证件号码" required name="idcCode" v-show="current === 0">
34 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle" 34 <uni-easyinput :styles="inputstyle" :clearable='false' :placeholderStyle="placeholderStyle"
35 v-model="baseFormData.idcCode" @blur="giveBirthDay" placeholder="请输入证件号码" /> 35 v-model="baseFormData.idcCode" @blur="giveBirthDay" placeholder="请输入证件号码" />
36 </uni-forms-item> 36 </uni-forms-item>
37 <uni-forms-item label="证件号码" required name="idcCode" v-show="current === 1"> 37 <uni-forms-item label="证件号码" required name="idcCode" v-show="current === 1">
38 <text>{{baseFormData.idcCode}}</text> 38 <text>{{baseFormData.idcCode}}</text>
39 </uni-forms-item> 39 </uni-forms-item>
40 <uni-forms-item label="性别" required name="sex"> 40 <uni-forms-item label="性别" required name="sex">
41 <text v-if="baseFormData.sex=='0'"></text> 41 <text v-if="baseFormData.sex=='0'"></text>
42 <text v-else-if="baseFormData.sex=='1'"></text> 42 <text v-else-if="baseFormData.sex=='1'"></text>
43 <!-- <uni-data-checkbox v-model="baseFormData.sex" @change="changeSex" :localdata="sexs" /> --> 43 <!-- <uni-data-checkbox v-model="baseFormData.sex" @change="changeSex" :localdata="sexs" /> -->
44 </uni-forms-item> 44 </uni-forms-item>
45 <uni-forms-item label="出生日期" required name="birth"> 45 <uni-forms-item label="出生日期" required name="birth">
46 {{baseFormData.birth?.slice(0,10)}} 46 {{baseFormData.birth?.slice(0,10)}}
47 <!-- <uni-datetime-picker type="date" placeholder="YYYY-MM-DD" :border='false' 47 <!-- <uni-datetime-picker type="date" placeholder="YYYY-MM-DD" :border='false'
48 :clear-icon="false" v-model="baseFormData.birth" /> --> 48 :clear-icon="false" v-model="baseFormData.birth" /> -->
49 </uni-forms-item> 49 </uni-forms-item>
50 <uni-forms-item label="联系方式" name="phone"> 50 <uni-forms-item label="联系方式" name="phone">
51 <uni-easyinput :styles="inputstyle" :placeholderStyle="placeholderStyle" 51 <uni-easyinput :styles="inputstyle" :placeholderStyle="placeholderStyle"
52 v-model="baseFormData.phone" placeholder="请输入联系方式" /> 52 v-model="baseFormData.phone" placeholder="请输入联系方式" />
53 </uni-forms-item> 53 </uni-forms-item>
54 54
55 55
56 <uni-forms-item label="所在地区"> 56 <!-- <uni-forms-item label="所在地区">
57 <uni-data-picker class="fixUniFormItemStyle" v-model="baseFormData.cityId" 57 <uni-data-picker class="fixUniFormItemStyle" v-model="baseFormData.cityId"
58 :localdata="regionsList" popup-title="请选择所在地区"></uni-data-picker> 58 :localdata="regionsList" popup-title="请选择所在地区"></uni-data-picker>
59 </uni-forms-item> 59 </uni-forms-item>
60 <uni-forms-item label="详细地址"><uni-easyinput :styles="inputstyle" 60 <uni-forms-item label="详细地址"><uni-easyinput :styles="inputstyle"
61 :placeholderStyle="placeholderStyle" v-model="baseFormData.address" 61 :placeholderStyle="placeholderStyle" v-model="baseFormData.address"
62 placeholder="请输入详细地址" /></uni-forms-item> 62 placeholder="请输入详细地址" /></uni-forms-item>
63 63
64 <uni-forms-item label="头像" required> 64 <uni-forms-item label="头像" required>
65 <uni-file-picker v-model="photoArr" @delete="delPhoto" return-type="object" limit="1" 65 <uni-file-picker v-model="photoArr" @delete="delPhoto" return-type="object" limit="1"
66 @select="upPhoto" :del-ico="false" :image-styles="imageStylesTx"></uni-file-picker> 66 @select="upPhoto" :del-ico="false" :image-styles="imageStylesTx"></uni-file-picker>
67 <image mode="aspectFill" v-if="baseFormData.photo2" style="height:200rpx;width:200rpx;" :src="config.baseUrl_api + baseFormData.photo2"/> 67 <image mode="aspectFill" v-if="baseFormData.photo2" style="height:200rpx;width:200rpx;" :src="config.baseUrl_api + baseFormData.photo2"/>
68 </uni-forms-item> 68 </uni-forms-item> -->
69 </view> 69 </view>
70 </uni-forms> 70 </uni-forms>
71 </view> 71 </view>
72 <view class="agreeline"> 72 </view>
73 <image @click="changeAgree(agree)" v-if="agree" 73 <view class="fixed-agreeline">
74 :src="config.baseUrl_api+'/fs/static/login/xz_dwn@2x.png'"></image> 74 <view class="agreeline">
75 <image @click="changeAgree(agree)" v-else :src="config.baseUrl_api+'/fs/static/login/xz@2x.png'"> 75 <image @click="changeAgree(agree)" v-if="agree"
76 </image> 76 :src="config.baseUrl_api+'/fs/static/login/xz_dwn@2x.png'"></image>
77 <view>我已阅读<text @click="openpopup">《入会须知》</text></view> 77 <image @click="changeAgree(agree)" v-else :src="config.baseUrl_api+'/fs/static/login/xz@2x.png'">
78 </view> 78 </image>
79 </view> 79 <view>我已阅读<text @click="openpopup">《入会须知》</text></view>
80 <view class="fixedBottom"><button class="btn-red" @click="goSubmit">确 定</button></view> 80 </view>
81 81 </view>
82 <!-- 会员须知 --> 82 <view class="fixedBottom"><button class="btn-red" @click="goSubmit">确 定</button></view>
83 <uni-popup ref="popup" type="bottom" background-color="#fff" animation :disable-scroll="true" :mask-click="false"> 83
84 <view class="tt">入会须知</view> 84 <!-- 会员须知 -->
85 <view class="popBody"> 85 <uni-popup ref="popup" type="bottom" background-color="#fff" animation :disable-scroll="true"
86 _{{baseFormData.name}}_欢迎您申请成为中国跆拳道协会(以下简称中国跆协)会员,请确保本次申请是经过您本人或监护人授权同意后的自愿行为,请您务必仔细阅读本入会须知。 86 :mask-click="false">
87 <br /> 87 <view class="tt">入会须知</view>
88 一、中国跆协会员分为个人会员和单位会员。 88 <view class="popBody">
89 <br /> 89 _{{baseFormData.name}}_欢迎您申请成为中国跆拳道协会(以下简称中国跆协)会员,请确保本次申请是经过您本人或监护人授权同意后的自愿行为,请您务必仔细阅读本入会须知。
90 二、成为本协会会员条件:遵守中国跆协章程和协会各项规章制度及相关决议,按期交纳会费,积极支持和参与中国跆拳道事业发展的社会各届人士或地方跆拳道协会、俱乐部、培训机构等,均可自愿申请成为中国跆协会员。<br /> 90 <br />
91 三、个人会员为在中国工作和生活的跆拳道爱好者,16 周岁以下应有监护人协助申请,会员须为中国公民。<br /> 91 一、中国跆协会员分为个人会员和单位会员。
92 四、会员入会需向所在区域内中国跆协单位会员提出入会申请,并按程序报中国跆协批准,按规定交纳会费。<br /> 92 <br />
93 五、会员享有《中国跆拳道协会会员管理办法》规定的会员权利。 93 二、成为本协会会员条件:遵守中国跆协章程和协会各项规章制度及相关决议,按期交纳会费,积极支持和参与中国跆拳道事业发展的社会各届人士或地方跆拳道协会、俱乐部、培训机构等,均可自愿申请成为中国跆协会员。<br />
94 <br /> 94 三、个人会员为在中国工作和生活的跆拳道爱好者,16 周岁以下应有监护人协助申请,会员须为中国公民。<br />
95 六、会员应履行《中国跆拳道协会会员管理办法》规定的会员义务。 95 四、会员入会需向所在区域内中国跆协单位会员提出入会申请,并按程序报中国跆协批准,按规定交纳会费。<br />
96 <br /> 96 五、会员享有《中国跆拳道协会会员管理办法》规定的会员权利。
97 七、凡中国跆协会员,须按照《中国跆拳道协会会员会费标准(2021 版)》按时交纳年度会费。<br /> 97 <br />
98 八、会员行为违反《中国跆拳道协会会员管理办法》中规定的,按照相关处罚规定进行处理。<br /> 98 六、会员应履行《中国跆拳道协会会员管理办法》规定的会员义务。
99 九、其它会员相关内容请查看《中国跆拳道协会章程》《中国跆拳道协会会员管理办法》。<br /> 99 <br />
100 100 七、凡中国跆协会员,须按照《中国跆拳道协会会员会费标准(2021 版)》按时交纳年度会费。<br />
101 <button @click="closepopup" class="btn-red">我已阅读</button> 101 八、会员行为违反《中国跆拳道协会会员管理办法》中规定的,按照相关处罚规定进行处理。<br />
102 </view> 102 九、其它会员相关内容请查看《中国跆拳道协会章程》《中国跆拳道协会会员管理办法》。<br />
103 </uni-popup> 103
104 104 <button @click="closepopup" class="btn-red">我已阅读</button>
105 <uni-popup ref="infoConfirm" type="center" :disable-scroll="true" :mask-click="false"> 105 </view>
106 <view class="tt">确认信息</view> 106 </uni-popup>
107 <view class="popBody"> 107
108 <view> 108 <uni-popup ref="infoConfirm" type="center" :disable-scroll="true" :mask-click="false">
109 109 <view class="tt">确认信息</view>
110 </view> 110 <view class="popBody">
111 111 <view>
112 <button @click="closepopup" class="btn-red">已确认</button> 112
113 </view> 113 </view>
114 </uni-popup> 114
115 115 <button @click="closepopup" class="btn-red">已确认</button>
116 </view> 116 </view>
117 </template> 117 </uni-popup>
118 118
119 <script setup> 119 </view>
120 import { 120 </template>
121 ref 121
122 } from 'vue' 122 <script setup>
123 import * as api from '@/common/api.js' 123 import {
124 import { 124 ref
125 onLoad 125 } from 'vue'
126 } from '@dcloudio/uni-app' 126 import * as api from '@/common/api.js'
127 import config from '@/config.js' 127 import {
128 import * as aes2 from '@/common/utils.js' 128 onLoad
129 const current = ref(0) 129 } from '@dcloudio/uni-app'
130 const popup = ref(null) 130 import config from '@/config.js'
131 const infoConfirm = ref(null) 131 import * as aes2 from '@/common/utils.js'
132 const agree = ref(false) 132 const current = ref(0)
133 const perId = ref() 133 const popup = ref(null)
134 const photoArr = ref({}) 134 const infoConfirm = ref(null)
135 const regionsList = ref([]) 135 const agree = ref(false)
136 const cardObj = ref({}) 136 const perId = ref()
137 const disabledName = ref(true) 137 const photoArr = ref({})
138 const baseFormData = ref({ 138 const regionsList = ref([])
139 photo: '', 139 const cardObj = ref({})
140 sex: '', 140 const disabledName = ref(true)
141 idcType: '0', 141 const baseFormData = ref({
142 perType: '1', // (1:个人会员;2:教练;3:考官;4:裁判;5:临时会员;) 142 photo: '',
143 }) 143 sex: '',
144 const items = ref(['身份证添加', '证件照录入']) 144 idcType: '0',
145 const idcTypeList = ref([{ 145 perType: '1', // (1:个人会员;2:教练;3:考官;4:裁判;5:临时会员;)
146 value: '0', 146 })
147 text: "身份证" 147 const items = ref(['身份证添加', '证件照录入'])
148 }, 148 const idcTypeList = ref([{
149 { 149 value: '0',
150 value: '1', 150 text: "身份证"
151 text: "来往大陆(内地)通行证" 151 },
152 }, 152 {
153 { 153 value: '1',
154 value: '3', 154 text: "来往大陆(内地)通行证"
155 text: "护照" 155 },
156 }, { 156 // {
157 value: '4', 157 // value: '3',
158 text: '户口本' 158 // text: "护照"
159 }, { 159 // },
160 value: '5', 160 {
161 text: '香港身份证' 161 value: '4',
162 } 162 text: '户口本'
163 ]) 163 },
164 const sexs = ref([{ 164 {
165 text: '女', 165 value: '5',
166 value: '1' 166 text: '香港身份证'
167 }, { 167 }
168 text: '男', 168 ])
169 value: '0' 169 const sexs = ref([{
170 }]) 170 text: '女',
171 const placeholderStyle = ref('text-align: right;font-size:30rpx') 171 value: '1'
172 const inputstyle = ref({ 172 }, {
173 borderColor: '#fff', 173 text: '男',
174 fontSize: '30rpx' 174 value: '0'
175 }) 175 }])
176 const imageStylesTx = ref({ 176 const placeholderStyle = ref('text-align: right;font-size:30rpx')
177 width: '210rpx', 177 const inputstyle = ref({
178 height: '280rpx', 178 borderColor: '#fff',
179 background: { 179 fontSize: '30rpx'
180 color: '#F4F6FA' 180 })
181 }, 181 const imageStylesTx = ref({
182 border: { 182 width: '210rpx',
183 radius: '2px' 183 height: '280rpx',
184 } 184 background: {
185 }); 185 color: '#F4F6FA'
186 186 },
187 const imageStylesZJ = ref({ 187 border: {
188 width: '500rpx', 188 radius: '2px'
189 height: '316rpx' 189 }
190 }); 190 });
191 onLoad((option) => { 191
192 if (option.tab == '1') { 192 const imageStylesZJ = ref({
193 current.value = 1 193 width: '500rpx',
194 baseFormData.value.sourceFlag = 1 194 height: '316rpx'
195 baseFormData.value.idcType = option.idcType || 0 195 });
196 if (baseFormData.value.idcType == '3') { 196 onLoad((option) => {
197 disabledName.value = false 197 if (option.tab == '1') {
198 } else { 198 current.value = 1
199 disabledName.value = true 199 baseFormData.value.sourceFlag = 1
200 } 200 baseFormData.value.idcType = option.idcType || 0
201 } 201 if (baseFormData.value.idcType == '3') {
202 // console.log(current.value,option.tab) 202 disabledName.value = false
203 getRegionsList() 203 } else {
204 }) 204 disabledName.value = true
205 205 }
206 function getRegionsList() { 206 }
207 api.regionsList().then(res => { 207 // console.log(current.value,option.tab)
208 regionsList.value = res.data 208 // getRegionsList()
209 }) 209 })
210 } 210
211 211 function getRegionsList() {
212 function onClickItem(e) { 212 api.regionsList().then(res => {
213 if (current.value != e.currentIndex) { 213 regionsList.value = res.data
214 current.value = e.currentIndex 214 })
215 } 215 }
216 cardObj.value = {} 216
217 photoArr.value = {} 217 function onClickItem(e) {
218 if (current.value == 0) { 218 if (current.value != e.currentIndex) {
219 baseFormData.value = { 219 current.value = e.currentIndex
220 photo: '', 220 }
221 idcType: '0', 221 cardObj.value = {}
222 perType: '1' 222 photoArr.value = {}
223 } 223 if (current.value == 0) {
224 } else { 224 baseFormData.value = {
225 baseFormData.value = { 225 photo: '',
226 photo: '', 226 idcType: '0',
227 idcType: '0', 227 perType: '1'
228 perType: '1', 228 }
229 sourceFlag: 1 229 } else {
230 } 230 baseFormData.value = {
231 } 231 photo: '',
232 } 232 idcType: '0',
233 233 perType: '1',
234 234 sourceFlag: 1
235 function changeAgree(item) { 235 }
236 agree.value = !item 236 }
237 } 237 }
238 238
239 //身份证识别 239
240 function upIdCardImgFront(e) { 240 function changeAgree(item) {
241 let file = e.tempFiles[0] 241 agree.value = !item
242 if (!file) { 242 }
243 return 243
244 } 244 //身份证识别
245 uni.showLoading({ 245 function upIdCardImgFront(e) {
246 title: '加载中' 246 let file = e.tempFiles[0]
247 }); 247 if (!file) {
248 baseFormData.value.card = e.tempFiles; 248 return
249 // console.log(e) 249 }
250 // const formData = new FormData() 250 uni.showLoading({
251 // formData.append('pic', e.tempFiles[0].file) 251 title: '加载中'
252 api.carUrl(e.tempFilePaths[0], baseFormData.value.idcType).then(res => { 252 });
253 console.log(res) 253 baseFormData.value.card = e.tempFiles;
254 if (res.data) { 254
255 baseFormData.value.sex = res.data.sex 255 // console.log(e)
256 baseFormData.value.birth = res.data.birth 256 // const formData = new FormData()
257 baseFormData.value.idcCode = res.data.code 257 // formData.append('pic', e.tempFiles[0].file)
258 baseFormData.value.name = res.data.name 258 api.carUrl(e.tempFilePaths[0], baseFormData.value.idcType).then(res => {
259 baseFormData.value.uuid = res.data.uuid 259 uni.hideLoading()
260 baseFormData.value.cityId = res.data.cityId 260
261 baseFormData.value.address = res.data.address 261 if (res.data) {
262 photoArr.value = {} 262 baseFormData.value.sex = res.data.sex
263 getExtractInfo({ 263 baseFormData.value.birth = res.data.birth
264 idcCode: baseFormData.value.idcCode, 264 baseFormData.value.idcCode = res.data.code
265 idcType: baseFormData.value.idcType, 265 baseFormData.value.name = res.data.name
266 perType: baseFormData.value.perType 266 baseFormData.value.uuid = res.data.uuid
267 }) 267 // baseFormData.value.cityId = res.data.cityId
268 268 // baseFormData.value.address = res.data.address
269 } else { 269 } else {
270 uni.hideLoading() 270 uni.showToast({
271 uni.showModal({ 271 title: res.msg,
272 content: res.msg, 272 duration: 2000,
273 success: function(modalRes) { 273 icon: 'none'
274 274 })
275 } 275 }
276 }) 276
277 } 277 })
278 278 }
279 }) 279
280 } 280 function upPhoto(e) {
281 281 const tempFilePaths = e.tempFilePaths;
282 function upPhoto(e) { 282 const imgUrl = tempFilePaths[0]
283 const tempFilePaths = e.tempFilePaths; 283 if (!imgUrl) {
284 const imgUrl = tempFilePaths[0] 284 return
285 if (!imgUrl) { 285 }
286 return 286 wx.cropImage({
287 } 287 src: imgUrl,
288 wx.cropImage({ 288 cropScale: '4:5',
289 src: imgUrl, 289 success: function(resp) {
290 cropScale: '4:5', 290 uni.showLoading({
291 success: function(resp) { 291 title: '加载中'
292 uni.showLoading({ 292 });
293 title: '加载中' 293 api.uploadImgCorpPhoto(resp.tempFilePath).then(data => {
294 }); 294 console.log(data)
295 api.uploadImgCorpPhoto(resp.tempFilePath).then(data => { 295 baseFormData.value.photo = data.data.fang;
296 console.log(data) 296 baseFormData.value.photo2 = data.data.yuan;
297 baseFormData.value.photo = data.data.fang; 297 photoArr.value = {
298 baseFormData.value.photo2 = data.data.yuan; 298 url: config.baseUrl_api + baseFormData.value.photo,
299 photoArr.value = { 299 name: '头像',
300 url: config.baseUrl_api+baseFormData.value.photo, 300 extname: 'jpg'
301 name: '头像', 301 }
302 extname: 'jpg' 302 });
303 } 303 },
304 }); 304 fail: function(err) {
305 }, 305 photoArr.value = {}
306 fail: function(err) { 306 }
307 photoArr.value = {} 307 })
308 } 308
309 }) 309
310 310 }
311 311
312 } 312
313 313 function delimgFont(n) {
314 314 photoArr.value = {}
315 function delimgFont(n) { 315 cardObj.value = {}
316 photoArr.value = {} 316 baseFormData.value = {
317 cardObj.value = {} 317 photo: '',
318 baseFormData.value = { 318 idcType: baseFormData.value.idcType,
319 photo: '', 319 perType: '1',
320 idcType: baseFormData.value.idcType, 320 sourceFlag: 1
321 perType: '1', 321 };
322 sourceFlag: 1 322 }
323 }; 323
324 } 324
325 325 function delPhoto(n) {
326 326 photoArr.value = {};
327 function delPhoto(n) { 327 baseFormData.value.photo = '';
328 photoArr.value = {}; 328 baseFormData.value.photo2 = '';
329 baseFormData.value.photo = ''; 329 }
330 baseFormData.value.photo2 = ''; 330
331 } 331
332 332 function getExtractInfo(obj) {
333 333 photoArr.value = {}
334 function getExtractInfo(obj) { 334 // baseFormData.value = {
335 photoArr.value = {} 335 // photo: '',
336 // baseFormData.value = { 336 // idcType: baseFormData.value.idcType,
337 // photo: '', 337 // idcCode: baseFormData.value.idcCode,
338 // idcType: baseFormData.value.idcType, 338 // perType: '1'
339 // idcCode: baseFormData.value.idcCode, 339 // };
340 // perType: '1' 340 uni.showLoading({
341 // }; 341 title: '加载中'
342 uni.showLoading({ 342 })
343 title: '加载中' 343 api.extractInfoFromChinaIdCard(obj).then(res => {
344 }) 344 if (res.data.perCode) {
345 api.extractInfoFromChinaIdCard(obj).then(res => { 345 // if(baseFormData.value.idcType != 3){
346 if (res.data.perCode) { 346 disabledName.value = true
347 // if(baseFormData.value.idcType != 3){ 347 // }
348 disabledName.value = true 348 perId.value = res.data.perId
349 // } 349 baseFormData.value.sex = res.data.sex
350 perId.value = res.data.perId 350 baseFormData.value.birth = res.data.birth
351 baseFormData.value.sex = res.data.sex 351 baseFormData.value.name = res.data.name
352 baseFormData.value.birth = res.data.birth 352 baseFormData.value.phone = res.data.phone
353 baseFormData.value.name = res.data.name 353 // baseFormData.value.cityId = res.data.cityId
354 baseFormData.value.phone = res.data.phone 354 // baseFormData.value.address = res.data.address
355 baseFormData.value.cityId = res.data.cityId 355 if (res.data.photo) {
356 baseFormData.value.address = res.data.address 356 console.log(res.data.photo)
357 if (res.data.photo) { 357 if (res.data.photo.indexOf('http') == -1) {
358 console.log(res.data.photo) 358 baseFormData.value.photo = res.data.photo
359 if (res.data.photo.indexOf('http') == -1) { 359 let obj = {
360 baseFormData.value.photo = res.data.photo 360 url: config.baseUrl_api + res.data.photo,
361 let obj = { 361 name: '头像',
362 url: config.baseUrl_api + res.data.photo, 362 extname: 'jpg'
363 name: '头像', 363 }
364 extname: 'jpg' 364 photoArr.value = obj
365 } 365 } else {
366 photoArr.value = obj 366 baseFormData.value.photo = res.data.photo
367 } else { 367 let obj = {
368 baseFormData.value.photo = res.data.photo 368 url: res.data.photo,
369 let obj = { 369 name: '头像',
370 url: res.data.photo, 370 extname: 'jpg'
371 name: '头像', 371 }
372 extname: 'jpg' 372 photoArr.value = obj
373 } 373 }
374 photoArr.value = obj 374
375 } 375 }
376 376 // baseFormData.value.name = res.data.name
377 } 377 baseFormData.value.perId = res.data.perId
378 // baseFormData.value.name = res.data.name 378 console.log(res.data.photo, baseFormData.value.photo)
379 baseFormData.value.perId = res.data.perId 379 uni.hideLoading()
380 console.log(res.data.photo, baseFormData.value.photo) 380 } else {
381 uni.hideLoading() 381 uni.hideLoading()
382 } else { 382 // 新会员
383 uni.hideLoading() 383 if (res.data.sex) {
384 // 新会员 384 baseFormData.value.sex = res.data.sex
385 if (res.data.sex) { 385 baseFormData.value.birth = res.data.birth
386 baseFormData.value.sex = res.data.sex 386 }
387 baseFormData.value.birth = res.data.birth 387 if (baseFormData.value.idcType != 3 && current.value == 1) {
388 } 388 disabledName.value = true
389 if (baseFormData.value.idcType != 3 && current.value == 1) { 389 } else {
390 disabledName.value = true 390 disabledName.value = false
391 } else { 391 }
392 disabledName.value = false 392 return
393 } 393 }
394 return 394 })
395 } 395 }
396 }) 396
397 } 397
398 398 function giveBirthDay() {
399 399 if (!baseFormData.value.idcCode) {
400 function giveBirthDay() { 400 return
401 // 判断身份证正确性/赋值生日 401 }
402 if (baseFormData.value.idcType == 0) { 402
403 if (!(/(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(baseFormData.value.idcCode))) { 403 // 判断身份证正确性/赋值生日
404 uni.showToast({ 404 if (baseFormData.value.idcType == 0) {
405 title: '请输入正确的身份证号码', 405 if (!(/(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(baseFormData.value.idcCode))) {
406 duration: 2000, 406 uni.showToast({
407 icon: 'none' 407 title: '请输入正确的身份证号码',
408 }) 408 duration: 2000,
409 } else { 409 icon: 'none'
410 getExtractInfo({ 410 })
411 idcCode: baseFormData.value.idcCode, 411 } else {
412 idcType: baseFormData.value.idcType, 412 getExtractInfo({
413 perType: baseFormData.value.perType 413 idcCode: baseFormData.value.idcCode,
414 }) 414 idcType: baseFormData.value.idcType,
415 } 415 perType: baseFormData.value.perType
416 } 416 })
417 // if (baseFormData.value.idcType == 1 || baseFormData.value.idcType == 3) { 417 }
418 // //转换为大写并判断位数12 418 }
419 // baseFormData.value.idcCode = baseFormData.value.idcCode.toUpperCase() 419 // if (baseFormData.value.idcType == 1 || baseFormData.value.idcType == 3) {
420 // // var regex = /^[a-zA-Z]/ 420 // //转换为大写并判断位数12
421 // if (baseFormData.value.idcCode.length > 12) { 421 // baseFormData.value.idcCode = baseFormData.value.idcCode.toUpperCase()
422 // uni.showToast({ 422 // // var regex = /^[a-zA-Z]/
423 // icon: 'none', 423 // if (baseFormData.value.idcCode.length > 12) {
424 // title: '请输入正确的证件号', 424 // uni.showToast({
425 // duration: 2000 425 // icon: 'none',
426 // }) 426 // title: '请输入正确的证件号',
427 // return 427 // duration: 2000
428 // } 428 // })
429 // } 429 // return
430 } 430 // }
431 431 // }
432 432 }
433 function openpopup() { 433
434 popup.value.open() 434
435 } 435 function openpopup() {
436 436 popup.value.open()
437 437 }
438 function closepopup() { 438
439 agree.value = true 439
440 popup.value.close() 440 function closepopup() {
441 } 441 agree.value = true
442 442 popup.value.close()
443 443 }
444 function changeIdcType(e) { 444
445 console.log(e) 445
446 // 切换证件照类型把当前页面数据清空 446 function changeIdcType(e) {
447 cardObj.value = {} 447 console.log(e)
448 photoArr.value = {} 448 // 切换证件照类型把当前页面数据清空
449 baseFormData.value = { 449 cardObj.value = {}
450 photo: '', 450 photoArr.value = {}
451 idcType: e, 451 baseFormData.value = {
452 perType: '1', 452 photo: '',
453 sourceFlag: 1 453 idcType: e,
454 } 454 perType: '1',
455 } 455 sourceFlag: 1
456 456 }
457 457 }
458 function goSubmit() { 458
459 459
460 if (!agree.value) { 460 function goSubmit() {
461 uni.showToast({ 461
462 icon: 'none', 462 if (!agree.value) {
463 title: '请阅知入会须知', 463 uni.showToast({
464 duration: 2000 464 icon: 'none',
465 }); 465 title: '请阅知入会须知',
466 return 466 duration: 2000
467 } 467 });
468 // 验证必填项 468 return
469 if (!baseFormData.value.name) { 469 }
470 uni.showToast({ 470 // 验证必填项
471 title: `请输入姓名`, 471 if (!baseFormData.value.name) {
472 icon: 'none' 472 uni.showToast({
473 }) 473 title: `请输入姓名`,
474 return 474 icon: 'none'
475 } 475 })
476 if (!baseFormData.value.idcCode) { 476 return
477 uni.showToast({ 477 }
478 title: `请输入证件号码`, 478 if (!baseFormData.value.idcCode) {
479 icon: 'none' 479 uni.showToast({
480 }) 480 title: `请输入证件号码`,
481 return 481 icon: 'none'
482 } 482 })
483 console.log(baseFormData.value.photo) 483 return
484 if (baseFormData.value.photo == '' || baseFormData.value.photo == undefined || !baseFormData.value.photo) { 484 }
485 uni.showToast({ 485
486 title: `请上传头像`, 486 if (baseFormData.value.phone) {
487 icon: 'none' 487 const phoneReg = /^1[3-9]\d{9}$/
488 }) 488 if (!phoneReg.test(baseFormData.value.phone)) {
489 return 489 uni.showToast({
490 } 490 title: '请输入正确的联系方式',
491 //信息确认弹出 491 icon: 'none'
492 uni.showModal({ 492 })
493 content: '请确认信息正确', 493 return
494 success: function(res) { 494 }
495 if (res.confirm) { 495 }
496 if(baseFormData.value.idcType=='4'){ 496
497 baseFormData.value.idcType='0' 497 // if (baseFormData.value.photo == '' || baseFormData.value.photo == undefined || !baseFormData.value.photo) {
498 } 498 // uni.showToast({
499 delete baseFormData.value.card 499 // title: `请上传头像`,
500 500 // icon: 'none'
501 const time = new Date().valueOf() + '' 501 // })
502 baseFormData.value.t = time + Math.floor(Math.random() * 10) 502 // return
503 baseFormData.value.signT = aes2.AESEncrypt(baseFormData.value.idcType + time) 503 // }
504 const baseFormDataJson = encodeURIComponent(JSON.stringify(baseFormData.value)) 504
505 uni.navigateTo({ 505 //信息确认弹出
506 url: `/personal/goPay_per?baseFormData=${baseFormDataJson}` 506 uni.showModal({
507 }) 507 content: '请确认信息正确',
508 508 success: function(res) {
509 // uni.showModal({ 509 if (res.confirm) {
510 // content: '保存成功', 510 if (baseFormData.value.idcType == '4') {
511 // title: '提示', 511 baseFormData.value.idcType = '0'
512 // confirmText:'去支付', 512 }
513 // cancelColor:'个人中心', 513 delete baseFormData.value.card
514 // success: function(res) { 514
515 515 const time = new Date().valueOf() + ''
516 // }, 516 baseFormData.value.t = time + Math.floor(Math.random() * 10)
517 // fail:function(){ 517 baseFormData.value.signT = aes2.AESEncrypt(baseFormData.value.idcType + time)
518 // uni.reLaunch({ 518 const baseFormDataJson = encodeURIComponent(JSON.stringify(baseFormData.value))
519 // url:`/personal/home` 519 uni.navigateTo({
520 // }) 520 url: `/personal/goPay_per?baseFormData=${baseFormDataJson}`
521 // } 521 })
522 // }) 522
523 // api.addPersonToMyDept(baseFormData.value).then(Response => { 523 // uni.showModal({
524 // if (Response.data == 0) { 524 // content: '保存成功',
525 // let msg = '该成员,实名认证未通过,注册失败!' 525 // title: '提示',
526 // uni.showModal({ 526 // confirmText:'去支付',
527 // content: msg, 527 // cancelColor:'个人中心',
528 // title: '提示', 528 // success: function(res) {
529 // success: function() {} 529
530 // }) 530 // },
531 // return 531 // fail:function(){
532 // } 532 // uni.reLaunch({
533 // if (Response.data * 1 < 0) { 533 // url:`/personal/home`
534 // // 会员调入弹出 534 // })
535 // uni.showModal({ 535 // }
536 // content: '该会员已存在其他道馆,如需添加,请发起会员调动', 536 // })
537 // title: '提示', 537 // api.addPersonToMyDept(baseFormData.value).then(Response => {
538 // success: function() {} 538 // if (Response.data == 0) {
539 // }) 539 // let msg = '该成员,实名认证未通过,注册失败!'
540 // return 540 // uni.showModal({
541 // } 541 // content: msg,
542 // // let msg = '保存成功' 542 // title: '提示',
543 543 // success: function() {}
544 544 // })
545 // }) 545 // return
546 } 546 // }
547 } 547 // if (Response.data * 1 < 0) {
548 }); 548 // // 会员调入弹出
549 } 549 // uni.showModal({
550 function getUserInfo() { 550 // content: '该会员已存在其他道馆,如需添加,请发起会员调动',
551 api.getInfo(perId.value).then(res => { 551 // title: '提示',
552 baseFormData.value = res.data 552 // success: function() {}
553 if (baseFormData.areaAssName) baseFormData.ancestorNameList = baseFormData.value.ancestorNameList.join( 553 // })
554 ',').replaceAll(',', 554 // return
555 '/') 555 // }
556 }) 556 // // let msg = '保存成功'
557 } 557
558 </script> 558
559 559 // })
560 <style lang="scss"> 560 }
561 561 }
562 /* 字段名左对齐 */ 562 });
563 .uni-forms-item .uni-forms-item__label { 563 }
564 text-align: left !important; 564
565 justify-content: flex-start !important; 565 function getUserInfo() {
566 padding-left: 0 !important; 566 api.getInfo(perId.value).then(res => {
567 width: auto !important; 567 baseFormData.value = res.data
568 } 568 if (baseFormData.areaAssName) baseFormData.ancestorNameList = baseFormData.value.ancestorNameList.join(
569 569 ',').replaceAll(',', '/')
570 /* 内容右对齐 */ 570 })
571 .uni-forms-item .uni-forms-item__content { 571 }
572 display: flex !important; 572 </script>
573 align-items: center !important; 573
574 justify-content: flex-end !important; 574 <style lang="scss">
575 text-align: right !important; 575 /* 字段名左对齐 */
576 flex-wrap: nowrap !important; 576 .uni-forms-item .uni-forms-item__label {
577 } 577 text-align: left !important;
578 578 justify-content: flex-start !important;
579 /* 输入框内容右对齐 */ 579 padding-left: 0 !important;
580 .uni-forms-item .uni-easyinput .uni-easyinput__content-input, 580 width: auto !important;
581 .uni-forms-item .uni-easyinput input, 581 }
582 .uni-forms-item input, 582
583 .uni-forms-item .uni-data-select .uni-select__input-box, 583 /* 内容右对齐 */
584 .uni-forms-item .uni-data-picker .uni-data-picker__input-box { 584 .uni-forms-item .uni-forms-item__content {
585 text-align: right !important; 585 display: flex !important;
586 } 586 align-items: center !important;
587 587 justify-content: flex-end !important;
588 /* 文本内容右对齐 */ 588 text-align: right !important;
589 .uni-forms-item .uni-forms-item__content text, 589 flex-wrap: nowrap !important;
590 .uni-forms-item .uni-forms-item__content > text { 590 }
591 display: inline-block !important; 591
592 white-space: nowrap !important; 592 /* 输入框内容右对齐 */
593 } 593 .uni-forms-item .uni-easyinput .uni-easyinput__content-input,
594 594 .uni-forms-item .uni-easyinput input,
595 </style> 595 .uni-forms-item input,
596 596 .uni-forms-item .uni-data-select .uni-select__input-box,
597 <style lang="scss" scoped> 597 .uni-forms-item .uni-data-picker .uni-data-picker__input-box {
598 :deep(.uni-popup__mask) { 598 text-align: right !important;
599 overflow: hidden !important; 599 }
600 position: fixed !important; 600
601 top: 0; 601 /* 文本内容右对齐 */
602 left: 0; 602 .uni-forms-item .uni-forms-item__content text,
603 right: 0; 603 .uni-forms-item .uni-forms-item__content>text {
604 bottom: 0; 604 display: inline-block !important;
605 } 605 white-space: nowrap !important;
606 :deep(.uni-popup) { 606 }
607 overflow: hidden !important; 607 </style>
608 } 608
609 :deep(.segmented-control) { 609 <style lang="scss" scoped>
610 height: 100rpx; 610 :deep(.uni-popup__mask) {
611 } 611 overflow: hidden !important;
612 612 position: fixed !important;
613 :deep(.segmented-control__text) { 613 top: 0;
614 line-height: 2; 614 left: 0;
615 font-size: 30rpx; 615 right: 0;
616 } 616 bottom: 0;
617 617 }
618 .tt { 618
619 text-align: center; 619 :deep(.uni-popup) {
620 font-size: 30rpx; 620 overflow: hidden !important;
621 padding: 40rpx 0 0; 621 }
622 } 622
623 623 :deep(.segmented-control) {
624 .popBody { 624 height: 100rpx;
625 font-size: 28rpx; 625 }
626 line-height: 1.5; 626
627 height: 70vh; 627 :deep(.segmented-control__text) {
628 overflow-y: auto; 628 line-height: 2;
629 font-family: 华文仿宋; 629 font-size: 30rpx;
630 height: 80vh; 630 }
631 overflow: auto; 631
632 padding: 30rpx; 632 .tt {
633 633 text-align: center;
634 .btn-red { 634 font-size: 30rpx;
635 margin: 50rpx 0 30rpx; 635 padding: 40rpx 0 0;
636 } 636 }
637 } 637
638 638 .popBody {
639 .agreeline { 639 font-size: 28rpx;
640 padding: 20rpx 40rpx; 640 line-height: 1.5;
641 box-sizing: border-box; 641 height: 70vh;
642 display: flex; 642 overflow-y: auto;
643 font-size: 30rpx; 643 font-family: 华文仿宋;
644 644 height: 80vh;
645 645 overflow: auto;
646 text { 646 padding: 30rpx;
647 color: #014A9F; 647
648 } 648 .btn-red {
649 649 margin: 50rpx 0 30rpx;
650 image { 650 }
651 width: 40rpx; 651 }
652 height: 40rpx; 652
653 margin-right: 20rpx; 653 .hasfixedbottom {
654 } 654 padding-bottom: 200rpx;
655 } 655 }
656 656
657 .upCard { 657 .fixed-agreeline {
658 position: relative; 658 position: fixed;
659 width: 500rpx; 659 bottom: 150rpx;
660 height: 316rpx; 660 left: 0;
661 661 right: 0;
662 .uni-file-picker { 662 z-index: 1;
663 position: absolute; 663 }
664 z-index: 1; 664
665 } 665 .agreeline {
666 666 padding: 20rpx 40rpx;
667 .sfz { 667 box-sizing: border-box;
668 width: 476rpx; 668 display: flex;
669 position: absolute; 669 font-size: 30rpx;
670 top: 0; 670
671 left: 0; 671 text {
672 height: 293rpx; 672 color: #014A9F;
673 } 673 }
674 } 674
675 675 image {
676 .op0 { 676 width: 40rpx;
677 opacity: 0; 677 height: 40rpx;
678 } 678 margin-right: 20rpx;
679 679 }
680 680 }
681 :deep(.item-text-overflow) { 681
682 text-align: left; 682 .upCard {
683 } 683 position: relative;
684 :deep(.fixUniFormItemStyle .uni-data-picker__input-box) { 684 width: 500rpx;
685 justify-content: flex-start !important; 685 height: 316rpx;
686 text-align: left !important; 686
687 } 687 .uni-file-picker {
688 688 position: absolute;
689 /* 让地区选择器的文本左对齐 */ 689 z-index: 1;
690 :deep(.fixUniFormItemStyle .uni-data-picker__text) { 690 }
691 text-align: left !important; 691
692 } 692 .sfz {
693 width: 476rpx;
694 position: absolute;
695 top: 0;
696 left: 0;
697 height: 293rpx;
698 }
699 }
700
701 .op0 {
702 opacity: 0;
703 }
704
705
706 :deep(.item-text-overflow) {
707 text-align: left;
708 }
709
710 :deep(.fixUniFormItemStyle .uni-data-picker__input-box) {
711 justify-content: flex-start !important;
712 text-align: left !important;
713 }
714
715 /* 让地区选择器的文本左对齐 */
716 :deep(.fixUniFormItemStyle .uni-data-picker__text) {
717 text-align: left !important;
718 }
693 </style> 719 </style>
...\ No newline at end of file ...\ No newline at end of file
......
1 <template> 1 <template>
2 <view class="container"> 2 <view class="container">
3 <view class="content"> 3 <view class="content">
4 <view class="card"> 4 <view class="card">
5 <view class="yearRow"> 5 <view class="yearRow">
6 <view class="label">缴费年限</view> 6 <view class="label">缴费年限</view>
7 <view class="control"> 7 <view class="control">
8 <image class="icon" @click="minusYear" src="/static/dd_02.png" mode="widthFix" v-if="form.payYear > 1" ></image> 8 <image class="icon" @click="minusYear" src="/static/dd_02.png" mode="widthFix"
9 <image class="icon" src="/static/dd_02_g.png" mode="widthFix" v-else ></image> 9 v-if="form.payYear > 1"></image>
10 <text class="num">{{ form.payYear }}</text> 10 <image class="icon" src="/static/dd_02_g.png" mode="widthFix" v-else></image>
11 <image class="icon" src="/static/btn_03.png" mode="widthFix" @click="plusYear" v-if="form.payYear < 5" ></image> 11 <text class="num">{{ form.payYear }}</text>
12 <image class="icon" src="/static/btn_03_g.png" mode="widthFix" v-else ></image> 12 <image class="icon" src="/static/btn_03.png" mode="widthFix" @click="plusYear"
13 </view> 13 v-if="form.payYear < 5"></image>
14 </view> 14 <image class="icon" src="/static/btn_03_g.png" mode="widthFix" v-else></image>
15 </view> 15 </view>
16 16 </view>
17 <view class="card "> 17 </view>
18 <view class="row "> 18
19 <text class="label">费用合计</text> 19 <view class="card ">
20 <text class="value red">{{ form.payYear * memberFee }}</text> 20 <view class="row ">
21 </view> 21 <text class="label">费用合计</text>
22 22 <text class="value red">{{ form.payYear * memberFee }}</text>
23 </view> 23 </view>
24 24
25 <view class="payRow "> 25 </view>
26 <radio-group @change="onPayTypeChange"> 26
27 <label class="radioItem"> 27 <view class="payRow ">
28 <radio value="1" :checked="payType === '1'" class="custom-radio" /> 28 <radio-group @change="onPayTypeChange">
29 <view class="payInfo"> 29 <label class="radioItem">
30 <image class="icon" src="/static/min.png" mode="widthFix"></image> 30 <radio value="1" :checked="payType === '1'" class="custom-radio" />
31 <text>民生付</text> 31 <view class="payInfo">
32 </view> 32 <image class="icon" src="/static/min.png" mode="widthFix"></image>
33 </label> 33 <text>民生付</text>
34 </radio-group> 34 </view>
35 </view> 35 </label>
36 36 </radio-group>
37 <view class="totalRow "> 37 </view>
38 <text class="label">支付费用合计</text> 38
39 <text class="value redBig">{{ memberTotalFee }}</text> 39 <view class="totalRow ">
40 </view> 40 <text class="label">支付费用合计</text>
41 41 <text class="value redBig">{{ memberTotalFee }}</text>
42 </view> 42 </view>
43 43
44 <view class="bottomBtn"> 44 </view>
45 <button class="payBtn" @click="handelPay" :loading="isPaying">立即支付 ¥{{ memberTotalFee }}</button> 45
46 </view> 46 <view class="bottomBtn">
47 47 <button class="payBtn" @click="handelPay" :loading="isPaying">立即支付 ¥{{ memberTotalFee }}</button>
48 </view> 48 </view>
49 </template> 49
50 50 </view>
51 <script setup> 51 </template>
52 import { ref, computed, onMounted } from 'vue' 52
53 import { onLoad } from '@dcloudio/uni-app'; 53 <script setup>
54 import * as api from '@/common/api.js' 54 import {
55 55 ref,
56 const form = ref({ 56 computed,
57 payYear: 1 57 onMounted
58 }) 58 } from 'vue'
59 59 import {
60 // 支付方式 60 onLoad
61 const payType = ref('1') 61 } from '@dcloudio/uni-app';
62 const isPaying = ref(false) 62 import to from 'await-to-js'
63 63 import * as api from '@/common/api.js'
64 // 费用与优惠 64
65 const memberFee = ref(0) 65 const form = ref({
66 const memberTotalFee = computed(() => { 66 payYear: 1
67 return memberFee.value * form.value.payYear 67 })
68 68
69 }) 69 // 支付方式
70 onLoad((options) => { 70 const payType = ref('1')
71 if (options.baseFormData) { 71 const isPaying = ref(false)
72 const data = JSON.parse(decodeURIComponent(options.baseFormData)) 72
73 form.value = { 73 // 费用与优惠
74 ...data, 74 const memberFee = ref(0)
75 payYear: 1 // 年限默认1 75 const memberTotalFee = computed(() => {
76 } 76 return memberFee.value * form.value.payYear
77 } 77
78 // 初始化接口 78 })
79 getMyMemberCertUnitFeeApi() 79 onLoad((options) => {
80 }) 80 if (options.baseFormData) {
81 81 const data = JSON.parse(decodeURIComponent(options.baseFormData))
82 82 form.value = {
83 83 ...data,
84 // 减年限 84 payYear: 1 // 年限默认1
85 const minusYear = () => { 85 }
86 if (form.value.payYear > 1) { 86 }
87 form.value.payYear-- 87 // 初始化接口
88 } 88 getMyMemberCertUnitFeeApi()
89 } 89 })
90 90
91 // 加年限(最大 5 年) 91
92 const plusYear = () => { 92
93 if (form.value.payYear < 5) { 93 // 减年限
94 form.value.payYear++ 94 const minusYear = () => {
95 } 95 if (form.value.payYear > 1) {
96 } 96 form.value.payYear--
97 97 }
98 // 支付方式切换 98 }
99 const onPayTypeChange = (e) => { 99
100 payType.value = e.detail.value 100 // 加年限(最大 5 年)
101 } 101 const plusYear = () => {
102 102 if (form.value.payYear < 5) {
103 const handelPay = async () => { 103 form.value.payYear++
104 if (memberTotalFee.value <= 0) { 104 }
105 uni.showToast({ title: '支付金额异常', icon: 'none' }) 105 }
106 return 106
107 } 107 // 支付方式切换
108 108 const onPayTypeChange = (e) => {
109 isPaying.value = true 109 payType.value = e.detail.value
110 try { 110 }
111 // 拼接完整参数 111
112 const postData = { 112 const handelPay = async () => {
113 ...form.value, 113 if (memberTotalFee.value <= 0) {
114 payYear: form.value.payYear, 114 uni.showToast({
115 payType: payType.value, 115 title: '支付金额异常',
116 totalFee: memberTotalFee.value 116 icon: 'none'
117 } 117 })
118 118 return
119 const res = await api.insertSinglePay(postData) 119 }
120 console.log(777,res) 120
121 if (res.data?.orderId) { 121 // 显示 loading
122 api.pcallBack2(res.data.orderId) 122 uni.showLoading({
123 uni.navigateTo({ 123 title: '支付中...',
124 url: `/personal/sucPay` 124 mask: true
125 }) 125 })
126 } 126 isPaying.value = true
127 // if (data.payFlag == 0 || data.orderId) { 127
128 // data.orderId && api.callBack2(data.orderId) 128 // 拼接完整参数
129 // uni.navigateTo({ url: `/personal/submitPay?price=${res.data.price}` }) 129 const postData = {
130 // } 130 ...form.value,
131 } catch (err) { 131 payYear: form.value.payYear,
132 uni.showToast({ title: '支付失败', icon: 'none' }) 132 payType: payType.value,
133 } finally { 133 totalFee: memberTotalFee.value
134 isPaying.value = false 134 }
135 } 135
136 } 136 // 创建订单
137 137 const [orderErr, orderRes] = await to(api.insertSinglePay(postData))
138 138 if (orderErr) {
139 139 uni.hideLoading()
140 // 获取会员费 140 isPaying.value = false
141 async function getMyMemberCertUnitFeeApi() { 141 uni.showToast({
142 const res = await api.getZtxFeeConfig() 142 title: '创建订单失败',
143 memberFee.value = Number(res.data.personMemberFee || 1500) 143 icon: 'none'
144 } 144 })
145 145 return
146 </script> 146 }
147 147
148 <style scoped> 148 if (!orderRes.data?.orderId) {
149 .container { 149 uni.hideLoading()
150 min-height: 100vh; 150 isPaying.value = false
151 background-color: #f7f7f7; 151 uni.showToast({
152 } 152 title: '订单创建异常',
153 .content { 153 icon: 'none'
154 padding: 20rpx 20rpx 120rpx; 154 })
155 } 155 return
156 .card { 156 }
157 background: #fff; 157
158 border-radius: 8rpx; 158 // 等待支付回调
159 padding: 25rpx 20rpx; 159 await to(api.pcallBack2(orderRes.data.orderId))
160 margin-bottom: 20rpx; 160 uni.hideLoading()
161 } 161 isPaying.value = false
162 .yearRow { 162
163 display: flex; 163 // 支付成功,跳转页面
164 align-items: center; 164 uni.navigateTo({
165 justify-content: space-between; 165 url: `/personal/sucPay?orderId=${orderRes.data.orderId}`
166 margin-bottom: 20rpx; 166 })
167 } 167 }
168 .yearRow .label { 168
169 font-size: 28rpx; 169
170 color: #333; 170
171 } 171 // 获取会员费
172 .yearRow .control { 172 async function getMyMemberCertUnitFeeApi() {
173 display: flex; 173 const res = await api.getZtxFeeConfig()
174 align-items: center; 174 memberFee.value = Number(res.data.personMemberFee || 1500)
175 } 175 }
176 .control image { 176 </script>
177 width: 50rpx; 177
178 height: 50rpx; 178 <style scoped>
179 } 179 .container {
180 .yearRow .num { 180 min-height: 100vh;
181 font-size: 28rpx; 181 background-color: #f7f7f7;
182 color: #333; 182 }
183 min-width: 80rpx; 183
184 text-align: center; 184 .content {
185 margin: 0 10rpx; 185 padding: 20rpx 20rpx 120rpx;
186 } 186 }
187 .row { 187
188 display: flex; 188 .card {
189 justify-content: space-between; 189 background: #fff;
190 align-items: center; 190 border-radius: 8rpx;
191 } 191 padding: 25rpx 20rpx;
192 .row .label { 192 margin-bottom: 20rpx;
193 font-size: 28rpx; 193 }
194 color: #333; 194
195 } 195 .yearRow {
196 .row .value { 196 display: flex;
197 font-size: 30rpx; 197 align-items: center;
198 color: #C4121B; 198 justify-content: space-between;
199 font-weight: 500; 199 margin-bottom: 20rpx;
200 } 200 }
201 .hintRow { 201
202 display: flex; 202 .yearRow .label {
203 align-items: flex-start; 203 font-size: 28rpx;
204 font-size: 24rpx; 204 color: #333;
205 line-height: 1.4; 205 }
206 } 206
207 .hintRow .hintText { 207 .yearRow .control {
208 color: #FF8124; 208 display: flex;
209 flex: 1; 209 align-items: center;
210 margin-top: 10rpx; 210 }
211 } 211
212 .deductRow { 212 .control image {
213 background: #fff; 213 width: 50rpx;
214 padding: 20rpx 20rpx; 214 height: 50rpx;
215 display: flex; 215 }
216 justify-content: space-between; 216
217 align-items: center; 217 .yearRow .num {
218 margin-bottom: 10rpx; 218 font-size: 28rpx;
219 border-radius: 8rpx; 219 color: #333;
220 } 220 min-width: 80rpx;
221 .deductRow .label { 221 text-align: center;
222 font-size: 28rpx; 222 margin: 0 10rpx;
223 color: #333; 223 }
224 } 224
225 .deductRow .value { 225 .row {
226 font-size: 30rpx; 226 display: flex;
227 color: #C4121B; 227 justify-content: space-between;
228 } 228 align-items: center;
229 .payRow { 229 }
230 background: #fff; 230
231 border-radius: 8rpx; 231 .row .label {
232 padding: 20rpx 20rpx; 232 font-size: 28rpx;
233 margin-bottom: 20rpx; 233 color: #333;
234 } 234 }
235 .radioItem { 235
236 display: flex; 236 .row .value {
237 align-items: center; 237 font-size: 30rpx;
238 } 238 color: #C4121B;
239 .payInfo { 239 font-weight: 500;
240 display: flex; 240 }
241 align-items: center; 241
242 margin-left: 15rpx; 242 .hintRow {
243 } 243 display: flex;
244 .payInfo .icon { 244 align-items: flex-start;
245 width: 40rpx; 245 font-size: 24rpx;
246 height: 40rpx; 246 line-height: 1.4;
247 margin-right: 10rpx; 247 }
248 } 248
249 .payInfo text { 249 .hintRow .hintText {
250 font-size: 28rpx; 250 color: #FF8124;
251 color: #333; 251 flex: 1;
252 } 252 margin-top: 10rpx;
253 .totalRow { 253 }
254 background: #fff; 254
255 border-radius: 8rpx; 255 .deductRow {
256 padding: 20rpx 20rpx; 256 background: #fff;
257 display: flex; 257 padding: 20rpx 20rpx;
258 justify-content: space-between; 258 display: flex;
259 align-items: center; 259 justify-content: space-between;
260 margin-top: 10rpx; 260 align-items: center;
261 } 261 margin-bottom: 10rpx;
262 .totalRow .label { 262 border-radius: 8rpx;
263 font-size: 28rpx; 263 }
264 color: #333; 264
265 } 265 .deductRow .label {
266 .redBig { 266 font-size: 28rpx;
267 font-size: 32rpx; 267 color: #333;
268 color: #C4121B; 268 }
269 font-weight: bold; 269
270 } 270 .deductRow .value {
271 .bottomBtn { 271 font-size: 30rpx;
272 position: fixed; 272 color: #C4121B;
273 bottom: 0; 273 }
274 left: 0; 274
275 right: 0; 275 .payRow {
276 padding: 20rpx 20rpx; 276 background: #fff;
277 background: #fff; 277 border-radius: 8rpx;
278 border-top: 1rpx solid #eee; 278 padding: 20rpx 20rpx;
279 } 279 margin-bottom: 20rpx;
280 .payBtn { 280 }
281 width: 100%; 281
282 height: 88rpx; 282 .radioItem {
283 line-height: 88rpx; 283 display: flex;
284 background-color: #C4121B; 284 align-items: center;
285 color: #fff; 285 }
286 border-radius: 8rpx; 286
287 font-size: 32rpx; 287 .payInfo {
288 text-align: center; 288 display: flex;
289 border: none; 289 align-items: center;
290 } 290 margin-left: 15rpx;
291 .payBtn[disabled] { 291 }
292 background-color: #ccc; 292
293 color: #999; 293 .payInfo .icon {
294 } 294 width: 40rpx;
295 .red { 295 height: 40rpx;
296 color: #C4121B; 296 margin-right: 10rpx;
297 } 297 }
298 .icon{ 298
299 width:30px; 299 .payInfo text {
300 } 300 font-size: 28rpx;
301 ::v-deep .custom-radio .wx-radio-input { 301 color: #333;
302 width: 30rpx; 302 }
303 height: 30rpx; 303
304 border-radius: 50%; 304 .totalRow {
305 border: 2rpx solid #ccc; 305 background: #fff;
306 } 306 border-radius: 8rpx;
307 ::v-deep .custom-radio .wx-radio-input.wx-radio-input-checked { 307 padding: 20rpx 20rpx;
308 border-color: #C4121B !important; 308 display: flex;
309 background: #C4121B !important; 309 justify-content: space-between;
310 } 310 align-items: center;
311 margin-top: 10rpx;
312 }
313
314 .totalRow .label {
315 font-size: 28rpx;
316 color: #333;
317 }
318
319 .redBig {
320 font-size: 32rpx;
321 color: #C4121B;
322 font-weight: bold;
323 }
324
325 .bottomBtn {
326 position: fixed;
327 bottom: 0;
328 left: 0;
329 right: 0;
330 padding: 20rpx 20rpx;
331 background: #fff;
332 border-top: 1rpx solid #eee;
333 }
334
335 .payBtn {
336 width: 100%;
337 height: 88rpx;
338 line-height: 88rpx;
339 background-color: #C4121B;
340 color: #fff;
341 border-radius: 8rpx;
342 font-size: 32rpx;
343 text-align: center;
344 border: none;
345 }
346
347 .payBtn[disabled] {
348 background-color: #ccc;
349 color: #999;
350 }
351
352 .red {
353 color: #C4121B;
354 }
355
356 .icon {
357 width: 30px;
358 }
359
360 ::v-deep .custom-radio .wx-radio-input {
361 width: 30rpx;
362 height: 30rpx;
363 border-radius: 50%;
364 border: 2rpx solid #ccc;
365 }
366
367 ::v-deep .custom-radio .wx-radio-input.wx-radio-input-checked {
368 border-color: #C4121B !important;
369 background: #C4121B !important;
370 }
311 </style> 371 </style>
...\ No newline at end of file ...\ No newline at end of file
......
1 <template> 1 <template>
2 <view class="success-container"> 2 <view class="success-container">
3 <!-- 成功图标(渐变圆形+动效) --> 3 <!-- 成功图标(渐变圆形+动效) -->
4 <view class="success-icon"> 4 <view class="success-icon">
5 <view class="icon-circle"> 5 <view class="icon-circle">
6 <text class="check-icon"></text> 6 <text class="check-icon"></text>
7 </view> 7 </view>
8 </view> 8 </view>
9 9
10 <!-- 支付成功标题(动画) --> 10 <!-- 支付成功标题(动画) -->
11 <view class="success-title">支付成功</view> 11 <view class="success-title">支付成功</view>
12 <view class="success-subtitle">支付成功,请等待审核</view> 12 <view class="success-subtitle">支付成功,请等待审核</view>
13 13
14 <!-- 订单信息卡片(带阴影) --> 14 <!-- 订单信息卡片(带阴影) -->
15 <view class="info-card"> 15 <view class="info-card">
16 <view class="info-item"> 16 <view class="info-item">
17 <text class="label">付款账户</text> 17 <text class="label">交易流水号</text>
18 <text class="value">(5437)</text> 18 <text class="value">{{ orderInfo.tradeNo }}</text>
19 </view> 19 </view>
20 <view class="info-item"> 20 <view class="info-item">
21 <text class="label">交易流水号</text> 21 <text class="label">商户名称</text>
22 <text class="value">2205051351076117833</text> 22 <text class="value">{{ orderInfo.merchantName || '中国跆拳道协会' }}</text>
23 </view> 23 </view>
24 <view class="info-item"> 24 <view class="info-item">
25 <text class="label">商户名称</text> 25 <text class="label">订单金额</text>
26 <text class="value">中国跆拳道协会</text> 26 <text class="value amount">{{ orderInfo.price ? orderInfo.price + '元' : '--' }}</text>
27 </view> 27 </view>
28 <view class="info-item"> 28 </view>
29 <text class="label">订单金额</text> 29
30 <text class="value amount">1500.00元</text> 30 <!-- 确定按钮(渐变+动效) -->
31 </view> 31 <view class="confirm-btn-area">
32 <view class="info-item"> 32 <button class="confirm-btn" @click="goBack">确定</button>
33 <text class="label">会员编号</text> 33 </view>
34 <text class="value">CTA00004</text> 34 </view>
35 </view> 35 </template>
36 <view class="info-item"> 36
37 <text class="label">会员有效期</text> 37 <script setup>
38 <text class="value">2028年1月25日</text> 38 import {
39 </view> 39 ref
40 </view> 40 } from 'vue'
41 41 import {
42 <!-- 确定按钮(渐变+动效) --> 42 onLoad
43 <view class="confirm-btn-area"> 43 } from '@dcloudio/uni-app'
44 <button class="confirm-btn" @click="goBack">确定</button> 44 import to from 'await-to-js'
45 </view> 45 import * as api from '@/common/api.js'
46 </view> 46
47 </template> 47 const orderInfo = ref({
48 48 id: '',
49 <script setup> 49 tradeNo: '',
50 import { onLoad } from '@dcloudio/uni-app' 50 merchantName: '中国跆拳道协会',
51 const goBack = () => { 51 price: ''
52 uni.navigateTo({ 52 })
53 url: `/personal/home` 53
54 }) 54 const goBack = () => {
55 } 55 uni.reLaunch({
56 56 url: '/login/login'
57 onLoad((option) => { 57 })
58 }) 58 }
59 </script> 59
60 60 onLoad(async (option) => {
61 <style scoped> 61 if (option.orderId) {
62 /* 全局容器 */ 62 const [err, res] = await to(api.getOrderInfo(option.orderId))
63 .success-container { 63 if (!err && res.data) {
64 display: flex; 64 orderInfo.value = res.data
65 flex-direction: column; 65 } else {
66 align-items: center; 66 orderInfo.value.id = option.orderId
67 padding: 100rpx 40rpx 60rpx; 67 }
68 min-height: 100vh; 68 }
69 background-color: #f8f9fa; 69 })
70 box-sizing: border-box; 70 </script>
71 } 71
72 72 <style scoped>
73 /* 成功图标容器 */ 73 /* 全局容器 */
74 .success-icon { 74 .success-container {
75 margin-bottom: 40rpx; 75 display: flex;
76 animation: fadeIn 0.6s ease-out; 76 flex-direction: column;
77 } 77 align-items: center;
78 78 padding: 100rpx 40rpx 60rpx;
79 /* 渐变圆形背景 */ 79 min-height: 100vh;
80 .icon-circle { 80 background-color: #f8f9fa;
81 width: 180rpx; 81 box-sizing: border-box;
82 height: 180rpx; 82 }
83 border-radius: 50%; 83
84 /* 青绿色渐变 */ 84 /* 成功图标容器 */
85 background: linear-gradient(135deg, #06c1ae, #04a896); 85 .success-icon {
86 display: flex; 86 margin-bottom: 40rpx;
87 align-items: center; 87 animation: fadeIn 0.6s ease-out;
88 justify-content: center; 88 }
89 box-shadow: 0 8rpx 30rpx rgba(6, 193, 174, 0.3); 89
90 /* 轻微上浮动效 */ 90 /* 渐变圆形背景 */
91 animation: scaleIn 0.8s ease-out; 91 .icon-circle {
92 } 92 width: 180rpx;
93 93 height: 180rpx;
94 /* 对勾图标 */ 94 border-radius: 50%;
95 .check-icon { 95 /* 青绿色渐变 */
96 font-size: 90rpx; 96 background: linear-gradient(135deg, #06c1ae, #04a896);
97 color: #ffffff; 97 display: flex;
98 font-weight: bold; 98 align-items: center;
99 } 99 justify-content: center;
100 100 box-shadow: 0 8rpx 30rpx rgba(6, 193, 174, 0.3);
101 /* 支付成功标题 */ 101 /* 轻微上浮动效 */
102 .success-title { 102 animation: scaleIn 0.8s ease-out;
103 font-size: 48rpx; 103 }
104 font-weight: 700; 104
105 color: #333333; 105 /* 对勾图标 */
106 margin-bottom: 12rpx; 106 .check-icon {
107 animation: slideUp 0.6s ease-out; 107 font-size: 90rpx;
108 } 108 color: #ffffff;
109 109 font-weight: bold;
110 /* 副标题 */ 110 }
111 .success-subtitle { 111
112 font-size: 28rpx; 112 /* 支付成功标题 */
113 color: #666666; 113 .success-title {
114 margin-bottom: 60rpx; 114 font-size: 48rpx;
115 animation: slideUp 0.8s ease-out; 115 font-weight: 700;
116 } 116 color: #333333;
117 117 margin-bottom: 12rpx;
118 /* 订单信息卡片 */ 118 animation: slideUp 0.6s ease-out;
119 .info-card { 119 }
120 width: 100%; 120
121 background: #ffffff; 121 /* 副标题 */
122 border-radius: 20rpx; 122 .success-subtitle {
123 padding: 40rpx 30rpx; 123 font-size: 28rpx;
124 box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05); 124 color: #666666;
125 margin-bottom: 80rpx; 125 margin-bottom: 60rpx;
126 animation: fadeIn 1s ease-out; 126 animation: slideUp 0.8s ease-out;
127 } 127 }
128 128
129 /* 单个信息项 */ 129 /* 订单信息卡片 */
130 .info-item { 130 .info-card {
131 display: flex; 131 width: 100%;
132 justify-content: space-between; 132 background: #ffffff;
133 align-items: center; 133 border-radius: 20rpx;
134 padding: 24rpx 0; 134 padding: 40rpx 30rpx;
135 border-bottom: 1rpx solid #f5f5f5; 135 box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05);
136 } 136 margin-bottom: 80rpx;
137 /* 最后一项去掉下划线 */ 137 animation: fadeIn 1s ease-out;
138 .info-item:last-child { 138 }
139 border-bottom: none; 139
140 } 140 /* 单个信息项 */
141 141 .info-item {
142 /* 标签样式 */ 142 display: flex;
143 .label { 143 justify-content: space-between;
144 font-size: 32rpx; 144 align-items: center;
145 color: #666666; 145 padding: 24rpx 0;
146 } 146 border-bottom: 1rpx solid #f5f5f5;
147 147 }
148 /* 值样式 */ 148
149 .value { 149 /* 最后一项去掉下划线 */
150 font-size: 32rpx; 150 .info-item:last-child {
151 color: #333333; 151 border-bottom: none;
152 text-align: right; 152 }
153 } 153
154 /* 金额特殊样式 */ 154 /* 标签样式 */
155 .amount { 155 .label {
156 color: #cd1e27; 156 font-size: 32rpx;
157 font-weight: 600; 157 color: #666666;
158 } 158 white-space: nowrap;
159 159 margin-right: 20rpx;
160 /* 确定按钮区域 */ 160 flex-shrink: 0;
161 .confirm-btn-area { 161 }
162 width: 100%; 162
163 padding: 0 20rpx; 163 /* 值样式 */
164 box-sizing: border-box; 164 .value {
165 } 165 font-size: 32rpx;
166 166 color: #333333;
167 /* 确定按钮(渐变+动效) */ 167 text-align: right;
168 .confirm-btn { 168 word-break: break-all;
169 width: 100%; 169 word-wrap: break-word;
170 height: 90rpx; 170 }
171 line-height: 90rpx; 171
172 /* 按钮渐变背景 */ 172 /* 金额特殊样式 */
173 background: #fff; 173 .amount {
174 color: #C4121B; 174 color: #cd1e27;
175 font-size: 36rpx; 175 font-weight: 600;
176 font-weight: 600; 176 }
177 border-radius: 45rpx; 177
178 border: 1px solid #C4121B; 178 /* 确定按钮区域 */
179 animation: slideUp 1s ease-out; 179 .confirm-btn-area {
180 /* 禁止默认样式 */ 180 width: 100%;
181 position: relative; 181 padding: 0 20rpx;
182 overflow: hidden; 182 box-sizing: border-box;
183 } 183 }
184 /* 按钮点击反馈 */ 184
185 .confirm-btn::after { 185 /* 确定按钮(渐变+动效) */
186 border: none; 186 .confirm-btn {
187 } 187 width: 100%;
188 .confirm-btn:active { 188 height: 90rpx;
189 transform: scale(0.98); 189 line-height: 90rpx;
190 box-shadow: 0 4rpx 10rpx rgba(6, 193, 174, 0.2); 190 /* 按钮渐变背景 */
191 } 191 background: #fff;
192 192 color: #C4121B;
193 /* 动画定义 */ 193 font-size: 36rpx;
194 @keyframes fadeIn { 194 font-weight: 600;
195 0% { opacity: 0; } 195 border-radius: 45rpx;
196 100% { opacity: 1; } 196 border: 1px solid #C4121B;
197 } 197 animation: slideUp 1s ease-out;
198 @keyframes scaleIn { 198 /* 禁止默认样式 */
199 0% { transform: scale(0); } 199 position: relative;
200 70% { transform: scale(1.1); } 200 overflow: hidden;
201 100% { transform: scale(1); } 201 }
202 } 202
203 @keyframes slideUp { 203 /* 按钮点击反馈 */
204 0% { opacity: 0; transform: translateY(30rpx); } 204 .confirm-btn::after {
205 100% { opacity: 1; transform: translateY(0); } 205 border: none;
206 } 206 }
207
208 .confirm-btn:active {
209 transform: scale(0.98);
210 box-shadow: 0 4rpx 10rpx rgba(6, 193, 174, 0.2);
211 }
212
213 /* 动画定义 */
214 @keyframes fadeIn {
215 0% {
216 opacity: 0;
217 }
218
219 100% {
220 opacity: 1;
221 }
222 }
223
224 @keyframes scaleIn {
225 0% {
226 transform: scale(0);
227 }
228
229 70% {
230 transform: scale(1.1);
231 }
232
233 100% {
234 transform: scale(1);
235 }
236 }
237
238 @keyframes slideUp {
239 0% {
240 opacity: 0;
241 transform: translateY(30rpx);
242 }
243
244 100% {
245 opacity: 1;
246 transform: translateY(0);
247 }
248 }
207 </style> 249 </style>
...\ No newline at end of file ...\ No newline at end of file
......
1 ## 2.1.6(2023-04-16)
2 * 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
3 ## 2.1.5(2023-04-16)
4 * 新增 兼容APP平台
5
6 ## 2.1.4(2023-03-13)
7 * 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
8 * 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
9 * 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制(1365*1365)则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
10 * 优化 旋转图标指示方向与实际旋转方向不符
11
12 ## 2.1.3(2023-02-06)
13 * 优化 vue3支持
14
15 ## 2.1.2(2023-02-03)
16 * 新增 navigation 属性,H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
17 * 修复 H5平台部分设备(已知iPhone11以下机型)拍照的图片缩放时会闪动的问题
18
19 ## 2.1.1(2022-12-06)
20 * 修复 横屏适配问题
21
22 ## 2.1.0(2022-12-06)
23 * 新增 兼容H5平台,使用 renderjs 响应手势事件
24
25 ## 2.0.0(2022-12-05)
26 * 重构 插件,使用 WXS 响应手势事件
27 * 新增 图片翻转
28 * 新增 拉伸裁剪框放大图片
29 * 新增 监听PC鼠标滚轮触发缩放
30 * 新增 圆形、圆角矩形的图片裁剪
31 * 优化 图片缩放,移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
32 * 优化 裁剪框样式
33 * 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
34 * 优化 生成图片使用新版 Canvas 2D 接口
1 /**
2 * 图片编辑器-手势监听
3 * 1. 支持编译到app-vue(uni-app 2.5.5及以上版本)、H5上
4 */
5 /** 图片偏移量 */
6 var offset = { x: 0, y: 0 };
7 /** 图片缩放比例 */
8 var scale = 1;
9 /** 图片最小缩放比例 */
10 var minScale = 1;
11 /** 图片旋转角度 */
12 var rotate = 0;
13 /** 触摸点 */
14 var touches = [];
15 /** 图片布局信息 */
16 var img = {};
17 /** 系统信息 */
18 var sys = {};
19 /** 裁剪区域布局信息 */
20 var area = {};
21 /** 触摸行为类型 */
22 var touchType = '';
23 /** 操作角的位置 */
24 var activeAngle = 0;
25 /** 裁剪区域布局信息偏移量 */
26 var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
27 /** 元素ID */
28 var elIds = {
29 'imageStyles': 'crop-image',
30 'maskStylesList': 'crop-mask-block',
31 'borderStyles': 'crop-border',
32 'circleBoxStyles': 'crop-circle-box',
33 'circleStyles': 'crop-circle',
34 'gridStylesList': 'crop-grid',
35 'angleStylesList': 'crop-angle',
36 }
37 /** 记录上次初始化时间戳,排除APP重复更新 */
38 var timestamp = 0;
39 /**
40 * 样式对象转字符串
41 * @param {Object} style 样式对象
42 */
43 function styleToString(style) {
44 if(typeof style === 'string') return style;
45 var str = '';
46 for (let k in style) {
47 str += k + ':' + style[k] + ';';
48 }
49 return str;
50 }
51 /**
52 *
53 * @param {Object} instance 页面实例对象
54 * @param {Object} key 要修改样式的key
55 * @param {Object|Array} style 样式
56 */
57 function setStyle(instance, key, style) {
58 // console.log('setStyle', instance, key, JSON.stringify(style))
59 // #ifdef APP-PLUS
60 if(Object.prototype.toString.call(style) === '[object Array]') {
61 for (var i = 0, len = style.length; i < len; i++) {
62 var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
63 el && (el.style = styleToString(style[i]));
64 }
65 } else {
66 var el = window.document.getElementById(elIds[key]);
67 el && (el.style = styleToString(style));
68 }
69 // #endif
70 // #ifdef H5
71 instance[key] = style;
72 // #endif
73 }
74 /**
75 * 触发页面实例指定方法
76 * @param {Object} instance 页面实例对象
77 * @param {Object} name 方法名称
78 * @param {Object} obj 传递参数
79 */
80 function callMethod(instance, name, obj) {
81 // #ifdef APP-PLUS
82 instance.callMethod(name, obj);
83 // #endif
84 // #ifdef H5
85 instance[name](obj);
86 // #endif
87 }
88 /**
89 * 计算两点间距
90 * @param {Object} touches 触摸点信息
91 */
92 function getDistanceByTouches(touches) {
93 // 根据勾股定理求两点间距离
94 var a = touches[1].pageX - touches[0].pageX;
95 var b = touches[1].pageY - touches[0].pageY;
96 var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
97 // 求两点间的中点坐标
98 // 1. a、b可能为负值
99 // 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
100 // 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
101 var x = touches[1].pageX - a / 2;
102 var y = touches[1].pageY - b / 2;
103 return { c, x, y };
104 };
105 /**
106 * 检查边界:限制 x、y 拖动范围,禁止滑出边界
107 * @param {Object} e 点坐标
108 */
109 function checkRange(e) {
110 var r = rotate / 90 % 2;
111 if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
112 var o = (img.height - img.width) / 2; // 宽高差值一半
113 return {
114 x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
115 y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
116 }
117 }
118 return {
119 x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
120 y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
121 }
122 };
123 /**
124 * 变更图片布局信息
125 * @param {Object} e 布局信息
126 */
127 function changeImageRect(e) {
128 // console.log('changeImageRect', e)
129 offset.x += e.x || 0;
130 offset.y += e.y || 0;
131 if(e.check) { // 检查边界
132 var point = checkRange(offset);
133 if(offset.x !== point.x || offset.y !== point.y) {
134 offset = point;
135 }
136 }
137
138 // 因频繁修改 width/height 会造成大量的内存消耗,改为scale
139 // e.instance.imageStyles = {
140 // width: img.width + 'px',
141 // height: img.height + 'px',
142 // transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
143 // };
144 var ox = (img.width - img.oldWidth) / 2;
145 var oy = (img.height - img.oldHeight) / 2;
146 // e.instance.imageStyles = {
147 // width: img.oldWidth + 'px',
148 // height: img.oldHeight + 'px',
149 // transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
150 // };
151 setStyle(e.instance, 'imageStyles', {
152 width: img.oldWidth + 'px',
153 height: img.oldHeight + 'px',
154 transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
155 });
156 callMethod(e.instance, 'dataChange', {
157 width: img.width,
158 height: img.height,
159 x: offset.x,
160 y: offset.y,
161 rotate: rotate
162 });
163 };
164 /**
165 * 变更裁剪区域布局信息
166 * @param {Object} e 布局信息
167 */
168 function changeAreaRect(e) {
169 // console.log('changeAreaRect', e)
170 // 变更蒙版样式
171 setStyle(e.instance, 'maskStylesList', [
172 {
173 left: 0,
174 width: (area.left + areaOffset.left) + 'px',
175 top: 0,
176 bottom: 0,
177 },
178 {
179 left: (area.right + areaOffset.right) + 'px',
180 right: 0,
181 top: 0,
182 bottom: 0,
183 },
184 {
185 left: (area.left + areaOffset.left) + 'px',
186 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
187 top: 0,
188 height: (area.top + areaOffset.top) + 'px',
189 },
190 {
191 left: (area.left + areaOffset.left) + 'px',
192 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
193 top: (area.bottom + areaOffset.bottom) + 'px',
194 // height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
195 bottom: 0,
196 }
197 ]);
198 // 变更边框样式
199 if(area.showBorder) {
200 setStyle(e.instance, 'borderStyles', {
201 left: (area.left + areaOffset.left) + 'px',
202 top: (area.top + areaOffset.top) + 'px',
203 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
204 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
205 });
206 }
207
208 // 变更参考线样式
209 if(area.showGrid) {
210 setStyle(e.instance, 'gridStylesList', [
211 {
212 'border-width': '1px 0 0 0',
213 left: (area.left + areaOffset.left) + 'px',
214 right: (area.right + areaOffset.right) + 'px',
215 top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
216 width: (area.width + areaOffset.right - areaOffset.left) + 'px'
217 },
218 {
219 'border-width': '1px 0 0 0',
220 left: (area.left + areaOffset.left) + 'px',
221 right: (area.right + areaOffset.right) + 'px',
222 top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
223 width: (area.width + areaOffset.right - areaOffset.left) + 'px'
224 },
225 {
226 'border-width': '0 1px 0 0',
227 top: (area.top + areaOffset.top) + 'px',
228 bottom: (area.bottom + areaOffset.bottom) + 'px',
229 left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
230 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
231 },
232 {
233 'border-width': '0 1px 0 0',
234 top: (area.top + areaOffset.top) + 'px',
235 bottom: (area.bottom + areaOffset.bottom) + 'px',
236 left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
237 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
238 }
239 ]);
240 }
241
242 // 变更四个伸缩角样式
243 if(area.showAngle) {
244 setStyle(e.instance, 'angleStylesList', [
245 {
246 'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
247 left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
248 top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
249 },
250 {
251 'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
252 left: (area.right + areaOffset.right - area.angleSize) + 'px',
253 top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
254 },
255 {
256 'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
257 left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
258 top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
259 },
260 {
261 'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
262 left: (area.right + areaOffset.right - area.angleSize) + 'px',
263 top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
264 }
265 ]);
266 }
267
268 // 变更圆角样式
269 if(area.radius > 0) {
270 var radius = area.radius;
271 if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
272 radius = (area.width / 2);
273 } else { // 圆角矩形
274 if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
275 radius = Math.min(area.width / 2, area.height / 2, radius);
276 }
277 }
278 setStyle(e.instance, 'circleBoxStyles', {
279 left: (area.left + areaOffset.left) + 'px',
280 top: (area.top + areaOffset.top) + 'px',
281 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
282 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
283 });
284 setStyle(e.instance, 'circleStyles', {
285 'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
286 'border-radius': radius + 'px'
287 });
288 }
289 };
290 /**
291 * 缩放图片
292 * @param {Object} e 布局信息
293 */
294 function scaleImage(e) {
295 // console.log('scaleImage', e)
296 var last = scale;
297 scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
298 if(last !== scale) {
299 img.width = img.oldWidth * scale;
300 img.height = img.oldHeight * scale;
301 // 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
302 // 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
303 // 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
304 // 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
305 // 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
306 e.x = (e.x - offset.x) * (1 - scale / last);
307 e.y = (e.y - offset.y) * (1 - scale / last);
308 changeImageRect(e);
309 return true;
310 }
311 return false;
312 };
313 /**
314 * 获取触摸点在哪个角
315 * @param {number} x 触摸点x轴坐标
316 * @param {number} y 触摸点y轴坐标
317 * @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
318 */
319 function getToucheAngle(x, y) {
320 // console.log('getToucheAngle', x, y, JSON.stringify(area))
321 var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
322 var oy = sys.navigation ? 0 : sys.windowTop;
323 if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
324 if(x >= area.left - o && x <= area.left + area.angleSize + o) {
325 return 1; // 左上角
326 } else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
327 return 2; // 右上角
328 }
329 } else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
330 if(x >= area.left - o && x <= area.left + area.angleSize + o) {
331 return 3; // 左下角
332 } else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
333 return 4; // 右下角
334 }
335 }
336 return 0; // 无触摸到角
337 };
338 /**
339 * 重置数据
340 */
341 function resetData() {
342 offset = { x: 0, y: 0 };
343 scale = 1;
344 minScale = 1;
345 rotate = 0;
346 };
347 function getTouchs(touches) {
348 var result = [];
349 var len = touches ? touches.length : 0
350 for (var i = 0; i < len; i++) {
351 result[i] = {
352 pageX: touches[i].pageX,
353 // h5无标题栏时,窗口顶部距离仍为标题栏高度,且触摸点y轴坐标还是有标题栏的值,即减去标题栏高度的值
354 pageY: touches[i].pageY + sys.windowTop
355 };
356 }
357 return result;
358 };
359 export default {
360 data() {
361 return {
362 imageStyles: {},
363 maskStylesList: [{}, {}, {}, {}],
364 borderStyles: {},
365 gridStylesList: [{}, {}, {}, {}],
366 angleStylesList: [{}, {}, {}, {}],
367 circleBoxStyles: {},
368 circleStyles: {}
369 }
370 },
371 created() {
372 // 监听 PC 端鼠标滚轮
373 // #ifdef H5
374 window.addEventListener('mousewheel', (e) => {
375 var touchs = getTouchs([e])
376 img.src && scaleImage({
377 instance: this.getInstance(),
378 check: true,
379 // 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
380 scale: e.deltaY > 0 ? -0.05 : 0.05,
381 x: touchs[0].pageX,
382 y: touchs[0].pageY
383 });
384 });
385 // #endif
386 },
387 methods: {
388 getInstance() {
389 // #ifdef APP-PLUS
390 return this.$ownerInstance;
391 // #endif
392 // #ifdef H5
393 return this;
394 // #endif
395 },
396 /**
397 * 初始化:观察数据变更
398 * @param {Object} newVal 新数据
399 * @param {Object} oldVal 旧数据
400 * @param {Object} o 组件实例对象
401 */
402 initObserver: function(newVal, oldVal, o, i) {
403 console.log('initObserver', newVal, oldVal, o, i)
404 if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
405 timestamp = newVal.timestamp;
406 img = newVal.img;
407 sys = newVal.sys;
408 area = newVal.area;
409 resetData();
410 img.src && changeImageRect({
411 instance: this.getInstance(),
412 x: (sys.windowWidth - img.width) / 2,
413 y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
414 });
415 changeAreaRect({
416 instance: this.getInstance()
417 });
418 }
419 },
420 /**
421 * 鼠标滚轮滚动
422 * @param {Object} e 事件对象
423 * @param {Object} o 组件实例对象
424 */
425 mousewheel: function(e, o) {
426 // h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
427 },
428 /**
429 * 触摸开始
430 * @param {Object} e 事件对象
431 * @param {Object} o 组件实例对象
432 */
433 touchstart: function(e, o) {
434 if(!img.src) return;
435 touches = getTouchs(e.touches);
436 activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
437 if(touches.length === 1 && activeAngle !== 0) {
438 touchType = 'stretch'; // 伸缩裁剪区域
439 } else {
440 touchType = '';
441 }
442 // console.log('touchstart', e, activeAngle)
443 },
444 /**
445 * 触摸移动
446 * @param {Object} e 事件对象
447 * @param {Object} o 组件实例对象
448 */
449 touchmove: function(e, o) {
450 if(!img.src) return;
451 // console.log('touchmove', e, o)
452 e.touches = getTouchs(e.touches);
453 if(touchType === 'stretch') { // 触摸四个角进行拉伸
454 var point = e.touches[0];
455 var start = touches[0];
456 var x = point.pageX - start.pageX;
457 var y = point.pageY - start.pageY;
458 if(x !== 0 || y !== 0) {
459 var maxX = area.width * (1 - area.minScale);
460 var maxY = area.height * (1 - area.minScale);
461 // console.log(x, y, maxX, maxY)
462 touches[0] = point;
463 switch(activeAngle) {
464 case 1: // 左上角
465 x += areaOffset.left;
466 y += areaOffset.top;
467 if(x >= 0 && y >= 0) { // 有效滑动
468 if(x > y) { // 以x轴滑动距离为缩放基准
469 if(x > maxX) x = maxX;
470 y = x * area.height / area.width;
471 } else { // 以y轴滑动距离为缩放基准
472 if(y > maxY) y = maxY;
473 x = y * area.width / area.height;
474 }
475 areaOffset.left = x;
476 areaOffset.top = y;
477 }
478 break;
479 case 2: // 右上角
480 x += areaOffset.right;
481 y += areaOffset.top;
482 if(x <= 0 && y >= 0) { // 有效滑动
483 if(-x > y) { // 以x轴滑动距离为缩放基准
484 if(-x > maxX) x = -maxX;
485 y = -x * area.height / area.width;
486 } else { // 以y轴滑动距离为缩放基准
487 if(y > maxY) y = maxY;
488 x = -y * area.width / area.height;
489 }
490 areaOffset.right = x;
491 areaOffset.top = y;
492 }
493 break;
494 case 3: // 左下角
495 x += areaOffset.left;
496 y += areaOffset.bottom;
497 if(x >= 0 && y <= 0) { // 有效滑动
498 if(x > -y) { // 以x轴滑动距离为缩放基准
499 if(x > maxX) x = maxX;
500 y = -x * area.height / area.width;
501 } else { // 以y轴滑动距离为缩放基准
502 if(-y > maxY) y = -maxY;
503 x = -y * area.width / area.height;
504 }
505 areaOffset.left = x;
506 areaOffset.bottom = y;
507 }
508 break;
509 case 4: // 右下角
510 x += areaOffset.right;
511 y += areaOffset.bottom;
512 if(x <= 0 && y <= 0) { // 有效滑动
513 if(-x > -y) { // 以x轴滑动距离为缩放基准
514 if(-x > maxX) x = -maxX;
515 y = x * area.height / area.width;
516 } else { // 以y轴滑动距离为缩放基准
517 if(-y > maxY) y = -maxY;
518 x = y * area.width / area.height;
519 }
520 areaOffset.right = x;
521 areaOffset.bottom = y;
522 }
523 break;
524 }
525 // console.log(x, y, JSON.stringify(areaOffset))
526 changeAreaRect({
527 instance: this.getInstance(),
528 });
529 // this.draw();
530 }
531 } else if (e.touches.length == 2) { // 双点触摸缩放
532 var start = getDistanceByTouches(touches);
533 var end = getDistanceByTouches(e.touches);
534 scaleImage({
535 instance: this.getInstance(),
536 check: !area.bounce,
537 scale: (end.c - start.c) / 100,
538 x: end.x,
539 y: end.y
540 });
541 touchType = 'scale';
542 } else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
543 touchType = 'move';
544 } else {
545 changeImageRect({
546 instance: this.getInstance(),
547 check: !area.bounce,
548 x: e.touches[0].pageX - touches[0].pageX,
549 y: e.touches[0].pageY - touches[0].pageY
550 });
551 touchType = 'move';
552 }
553 touches = e.touches;
554 },
555 /**
556 * 触摸结束
557 * @param {Object} e 事件对象
558 * @param {Object} o 组件实例对象
559 */
560 touchend: function(e, o) {
561 if(!img.src) return;
562 if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
563 // 裁剪区域宽度被缩放到多少
564 var left = areaOffset.left;
565 var right = areaOffset.right;
566 var top = areaOffset.top;
567 var bottom = areaOffset.bottom;
568 var w = area.width + right - left;
569 var h = area.height + bottom - top;
570 // 图像放大倍数
571 var p = scale * (area.width / w) - scale;
572 // 复原裁剪区域
573 areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
574 changeAreaRect({
575 instance: this.getInstance(),
576 });
577 scaleImage({
578 instance: this.getInstance(),
579 scale: p,
580 x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
581 y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
582 });
583 } else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
584 changeImageRect({
585 instance: this.getInstance(),
586 check: true
587 });
588 }
589 },
590 /**
591 * 顺时针翻转图片90°
592 * @param {Object} e 事件对象
593 * @param {Object} o 组件实例对象
594 */
595 rotateImage: function(e, o) {
596 rotate = (rotate + 90) % 360;
597
598 // 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
599 var r = rotate / 90 % 2;
600 minScale = 1;
601 if(img.width < area.height) {
602 minScale = area.height / img.oldWidth;
603 } else if(img.height < area.width) {
604 minScale = (area.width / img.oldHeight)
605 }
606 if(minScale !== 1) {
607 scaleImage({
608 instance: this.getInstance(),
609 scale: minScale - scale,
610 x: sys.windowWidth / 2,
611 y: (sys.windowHeight - sys.offsetBottom) / 2
612 });
613 }
614
615 // 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
616 // 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
617 // 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
618 var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
619 var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
620 changeImageRect({
621 instance: this.getInstance(),
622 check: true,
623 x: -ox - oy,
624 y: -oy + ox
625 });
626 }
627 }
628 }
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <view class="image-cropper" @wheel="cropper.mousewheel">
3 <canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
4 width: `${canvansWidth}px`,
5 height: `${canvansHeight}px`
6 }"></canvas>
7 <canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
8 width: `${canvansWidth}px`,
9 height: `${canvansHeight}px`
10 }"></canvas>
11 <view class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
12 <image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
13 <view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
14 <view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
15 <view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
16 <view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
17 </view>
18 <block v-if="showGrid">
19 <view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
20 </block>
21 <block v-if="showAngle">
22 <view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
23 <view :style="[{
24 width: `${angleSize}px`,
25 height: `${angleSize}px`
26 }]"></view>
27 </view>
28 </block>
29 </view>
30 <view class="fixed-bottom safe-area-inset-bottom">
31 <view v-if="rotatable && !!imgSrc" class="rotate-icon" @click="cropper.rotateImage"></view>
32 <view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
33 <block v-else-if="!!imgSrc">
34 <view class="rechoose" @click="chooseImage">重选</view>
35 <button class="button" size="mini" @click="cropClick">确定</button>
36 </block>
37 <view v-else class="choose-btn" @click="chooseImage">选择图片</view>
38 </view>
39 </view>
40 </template>
41
42 <!-- #ifdef APP-VUE || H5 -->
43 <script module="cropper" lang="renderjs">
44 import cropper from './qf-image-cropper.render.js';
45 export default {
46 mixins: [ cropper ]
47 }
48 </script>
49 <!-- #endif -->
50 <!-- #ifdef MP-WEIXIN || MP-QQ -->
51 <script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
52 <!-- #endif -->
53 <script>
54 /** 裁剪区域最大宽高所占屏幕宽度百分比 */
55 const AREA_SIZE = 75;
56 /** 图片默认宽高 */
57 const IMG_SIZE = 300;
58
59 export default {
60 name:"qf-image-cropper",
61 // #ifdef MP-WEIXIN
62 options: {
63 // 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
64 styleIsolation: "isolated"
65 },
66 // #endif
67 props: {
68 /** 图片资源地址 */
69 src: {
70 type: String,
71 default: ''
72 },
73 /** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
74 width: {
75 type: Number,
76 default: IMG_SIZE
77 },
78 /** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
79 height: {
80 type: Number,
81 default: IMG_SIZE
82 },
83 /** 是否绘制裁剪区域边框 */
84 showBorder: {
85 type: Boolean,
86 default: true
87 },
88 /** 是否绘制裁剪区域网格参考线 */
89 showGrid: {
90 type: Boolean,
91 default: true
92 },
93 /** 是否展示四个支持伸缩的角 */
94 showAngle: {
95 type: Boolean,
96 default: true
97 },
98 /** 裁剪区域最小缩放倍数 */
99 areaScale: {
100 type: Number,
101 default: 0.3
102 },
103 /** 图片最大缩放倍数 */
104 maxScale: {
105 type: Number,
106 default: 5
107 },
108 /** 是否有回弹效果:拖动时可以拖出边界,释放时会弹回边界 */
109 bounce: {
110 type: Boolean,
111 default: true
112 },
113 /** 是否支持翻转 */
114 rotatable: {
115 type: Boolean,
116 default: true
117 },
118 /** 是否支持从本地选择素材 */
119 choosable: {
120 type: Boolean,
121 default: true
122 },
123 /** 四个角尺寸,单位px */
124 angleSize: {
125 type: Number,
126 default: 20
127 },
128 /** 四个角边框宽度,单位px */
129 angleBorderWidth: {
130 type: Number,
131 default: 2
132 },
133 /** 裁剪图片圆角半径,单位px */
134 radius: {
135 type: Number,
136 default: 0
137 },
138 /** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
139 fileType: {
140 type: String,
141 default: 'png'
142 },
143 /**
144 * 图片从绘制到生成所需时间,单位ms
145 * 微信小程序平台使用 `Canvas 2D` 绘制时有效
146 * 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
147 */
148 delay: {
149 type: Number,
150 default: 1000
151 },
152 // #ifdef H5
153 /**
154 * 页面是否是原生标题栏
155 * H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
156 * 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
157 */
158 navigation: {
159 type: Boolean,
160 default: true
161 }
162 // #endif
163 },
164 emits: ["crop"],
165 data() {
166 return {
167 // 用不同 id 使 v-for key 不重复
168 maskList: [
169 { id: 'crop-mask-block-1' },
170 { id: 'crop-mask-block-2' },
171 { id: 'crop-mask-block-3' },
172 { id: 'crop-mask-block-4' },
173 ],
174 gridList: [
175 { id: 'crop-grid-1' },
176 { id: 'crop-grid-2' },
177 { id: 'crop-grid-3' },
178 { id: 'crop-grid-4' },
179 ],
180 angleList: [
181 { id: 'crop-angle-1' },
182 { id: 'crop-angle-2' },
183 { id: 'crop-angle-3' },
184 { id: 'crop-angle-4' },
185 ],
186 /** 本地缓存的图片路径 */
187 imgSrc: '',
188 /** 图片的裁剪宽度 */
189 imgWidth: IMG_SIZE,
190 /** 图片的裁剪高度 */
191 imgHeight: IMG_SIZE,
192 /** 裁剪区域最大宽度所占屏幕宽度百分比 */
193 widthPercent: AREA_SIZE,
194 /** 裁剪区域最大高度所占屏幕宽度百分比 */
195 heightPercent: AREA_SIZE,
196 /** 裁剪区域布局信息 */
197 area: {},
198 /** 未被缩放过的图片宽 */
199 oldWidth: 0,
200 /** 未被缩放过的图片高 */
201 oldHeight: 0,
202 /** 系统信息 */
203 sys: uni.getSystemInfoSync(),
204 scaleWidth: 0,
205 scaleHeight: 0,
206 rotate: 0,
207 offsetX: 0,
208 offsetY: 0,
209 use2d: false,
210 canvansWidth: 0,
211 canvansHeight: 0,
212 // imageStyles: {},
213 // maskStylesList: [{}, {}, {}, {}],
214 // borderStyles: {},
215 // gridStylesList: [{}, {}, {}, {}],
216 // angleStylesList: [{}, {}, {}, {}],
217 // circleBoxStyles: {},
218 // circleStyles: {},
219 }
220 },
221 computed: {
222 initData() {
223 // console.log('initData')
224 return {
225 timestamp: new Date().getTime(),
226 area: {
227 ...this.area,
228 bounce: this.bounce,
229 showBorder: this.showBorder,
230 showGrid: this.showGrid,
231 showAngle: this.showAngle,
232 angleSize: this.angleSize,
233 angleBorderWidth: this.angleBorderWidth,
234 minScale: this.areaScale,
235 widthPercent: this.widthPercent,
236 heightPercent: this.heightPercent,
237 radius: this.radius
238 },
239 sys: this.sys,
240 img: {
241 maxScale: this.maxScale,
242 src: this.imgSrc,
243 width: this.oldWidth,
244 height: this.oldHeight,
245 oldWidth: this.oldWidth,
246 oldHeight: this.oldHeight,
247 }
248 }
249 },
250 imgProps() {
251 return {
252 width: this.width,
253 height: this.height,
254 src: this.src,
255 }
256 }
257 },
258 watch: {
259 imgProps: {
260 handler(val) {
261 // console.log('imgProps', val)
262 // 自定义裁剪尺,示例如下:
263 this.imgWidth = Number(val.width) || IMG_SIZE;
264 this.imgHeight = Number(val.height) || IMG_SIZE;
265 let use2d = true;
266 // #ifndef MP-WEIXIN
267 use2d = false;
268 // #endif
269 // if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
270 // use2d = false;
271 // }
272 let canvansWidth = this.imgWidth;
273 let canvansHeight = this.imgHeight;
274 let size = Math.max(canvansWidth, canvansHeight)
275 let scalc = 1;
276 if(size > 1365) {
277 scalc = 1365 / size;
278 }
279 this.canvansWidth = canvansWidth * scalc;
280 this.canvansHeight = canvansHeight * scalc;
281 this.use2d = use2d;
282 this.initArea();
283 val.src && this.initImage(val.src);
284 },
285 immediate: true
286 },
287 },
288 methods: {
289 /** 提供给wxs调用,用来接收图片变更数据 */
290 dataChange(e) {
291 // console.log('dataChange', e)
292 this.scaleWidth = e.width;
293 this.scaleHeight = e.height;
294 this.rotate = e.rotate;
295 this.offsetX = e.x;
296 this.offsetY = e.y;
297 },
298 /** 初始化裁剪区域布局信息 */
299 initArea() {
300 // 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
301 this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
302 // #ifndef H5
303 this.sys.windowTop = 0;
304 this.sys.navigation = true;
305 // #endif
306 // #ifdef H5
307 // h5平台的窗口高度是包含标题栏的
308 this.sys.windowTop = this.sys.windowTop || 44;
309 this.sys.navigation = this.navigation;
310 // #endif
311 let wp = this.widthPercent;
312 let hp = this.heightPercent;
313 if (this.imgWidth > this.imgHeight) {
314 hp = hp * this.imgHeight / this.imgWidth;
315 } else if (this.imgWidth < this.imgHeight) {
316 wp = wp * this.imgWidth / this.imgHeight;
317 }
318 const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
319 const width = size * wp / 100;
320 const height = size * hp / 100;
321 const left = (this.sys.windowWidth - width) / 2;
322 const right = left + width;
323 const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
324 const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
325 this.area = { width, height, left, right, top, bottom };
326 this.scaleWidth = width;
327 this.scaleHeight = height;
328 },
329 /** 从本地选取图片 */
330 chooseImage() {
331 // #ifdef MP-WEIXIN || MP-JD
332 if(uni.chooseMedia) {
333 uni.chooseMedia({
334 count: 1,
335 mediaType: ['image'],
336 success: (res) => {
337 this.resetData();
338 this.initImage(res.tempFiles[0].tempFilePath);
339 }
340 });
341 return;
342 }
343 // #endif
344 uni.chooseImage({
345 count: 1,
346 success: (res) => {
347 this.resetData();
348 this.initImage(res.tempFiles[0].path);
349 }
350 });
351 },
352 /** 重置数据 */
353 resetData() {
354 this.imgSrc = '';
355 this.rotate = 0;
356 this.offsetX = 0;
357 this.offsetY = 0;
358 this.initArea();
359 },
360 /**
361 * 初始化图片信息
362 * @param {String} url 图片链接
363 */
364 initImage(url) {
365 uni.getImageInfo({
366 src: url,
367 success: (res) => {
368 this.imgSrc = res.path;
369 let scale = res.width / res.height;
370 let areaScale = this.area.width / this.area.height;
371 if (scale > 1) { // 横向图片
372 if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
373 this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
374 } else { // 否则宽固定、高自适应
375 this.scaleHeight = res.height * this.scaleWidth / res.width;
376 }
377 } else { // 纵向图片
378 if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
379 this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
380 } else { // 否则高固定,宽自适应
381 this.scaleWidth = res.width * this.scaleHeight / res.height;
382 }
383 }
384 // 记录原始宽高,为缩放比列做限制
385 this.oldWidth = this.scaleWidth;
386 this.oldHeight = this.scaleHeight;
387 },
388 fail: (err) => {
389 console.error(err)
390 }
391 });
392 },
393 /**
394 * 剪切图片圆角
395 * @param {Object} ctx canvas 的绘图上下文对象
396 * @param {Number} radius 圆角半径
397 * @param {Number} scale 生成图片的实际尺寸与截取区域比
398 * @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
399 */
400 drawClipImage(ctx, radius, scale, drawImage) {
401 if(radius > 0) {
402 ctx.save();
403 ctx.beginPath();
404 const w = this.canvansWidth;
405 const h = this.canvansHeight;
406 if(w === h && radius >= w / 2) { // 圆形
407 ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
408 } else { // 圆角矩形
409 if(w !== h) { // 限制圆角半径不能超过短边的一半
410 radius = Math.min(w / 2, h / 2, radius);
411 // radius = Math.min(Math.max(w, h) / 2, radius);
412 }
413 ctx.moveTo(radius, 0);
414 ctx.arcTo(w, 0, w, h, radius);
415 ctx.arcTo(w, h, 0, h, radius);
416 ctx.arcTo(0, h, 0, 0, radius);
417 ctx.arcTo(0, 0, w, 0, radius);
418 ctx.closePath();
419 }
420 ctx.clip();
421 drawImage && drawImage(true);
422 ctx.restore();
423 } else {
424 drawImage && drawImage(false);
425 }
426 },
427 /**
428 * 旋转图片
429 * @param {Object} ctx canvas 的绘图上下文对象
430 * @param {Number} rotate 旋转角度
431 * @param {Number} scale 生成图片的实际尺寸与截取区域比
432 */
433 drawRotateImage(ctx, rotate, scale) {
434 if(rotate !== 0) {
435 // 1. 以图片中心点为旋转中心点
436 const x = this.scaleWidth * scale / 2;
437 const y = this.scaleHeight * scale / 2;
438 ctx.translate(x, y);
439 // 2. 旋转画布
440 ctx.rotate(rotate * Math.PI / 180);
441 // 3. 旋转完画布后恢复设置旋转中心时所做的偏移
442 ctx.translate(-x, -y);
443 }
444 },
445 drawImage(ctx, image, callback) {
446 // 生成图片的实际尺寸与截取区域比
447 const scale = this.canvansWidth / this.area.width;
448 this.drawClipImage(ctx, this.radius, scale, () => {
449 this.drawRotateImage(ctx, this.rotate, scale);
450 const r = this.rotate / 90;
451 ctx.drawImage(
452 image,
453 [
454 (this.offsetX - this.area.left),
455 (this.offsetY - this.area.top),
456 -(this.offsetX - this.area.left),
457 -(this.offsetY - this.area.top)
458 ][r] * scale,
459 [
460 (this.offsetY - this.area.top),
461 -(this.offsetX - this.area.left),
462 -(this.offsetY - this.area.top),
463 (this.offsetX - this.area.left)
464 ][r] * scale,
465 this.scaleWidth * scale,
466 this.scaleHeight * scale
467 );
468 });
469 },
470 /**
471 * 绘图
472 * @param {Object} canvas
473 * @param {Object} ctx canvas 的绘图上下文对象
474 * @param {String} src 图片路径
475 * @param {Function} callback 开始绘制时回调
476 */
477 draw2DImage(canvas, ctx, src, callback) {
478 // console.log('draw2DImage', canvas, ctx, src, callback)
479 if(canvas) {
480 const image = canvas.createImage();
481 image.onload = () => {
482 this.drawImage(ctx, image);
483 // 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
484 callback && setTimeout(callback, this.delay);
485 };
486 image.onerror = (err) => {
487 console.error(err)
488 uni.hideLoading();
489 };
490 image.src = src;
491 } else {
492 this.drawImage(ctx, src);
493 setTimeout(() => {
494 ctx.draw(false, callback);
495 }, 200);
496 }
497 },
498 /**
499 * 画布转图片到本地缓存
500 * @param {Object} canvas
501 * @param {String} canvasId
502 */
503 canvasToTempFilePath(canvas, canvasId) {
504 // console.log('canvasToTempFilePath', canvas, canvasId)
505 uni.canvasToTempFilePath({
506 canvas,
507 canvasId,
508 x: 0,
509 y: 0,
510 width: this.canvansWidth,
511 height: this.canvansHeight,
512 destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
513 destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
514 fileType: this.fileType, // 目标文件的类型,默认png
515 success: (res) => {
516 // 生成的图片临时文件路径
517 this.handleImage(res.tempFilePath);
518 },
519 fail: (err) => {
520 uni.hideLoading();
521 uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
522 }
523 }, this);
524 },
525 /** 确认裁剪 */
526 cropClick() {
527 uni.showLoading({ title: '裁剪中...', mask: true });
528 if(!this.use2d) {
529 const ctx = uni.createCanvasContext('imgCanvas', this);
530 ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
531 this.draw2DImage(null, ctx, this.imgSrc, () => {
532 this.canvasToTempFilePath(null, 'imgCanvas');
533 });
534 return;
535 }
536 // #ifdef MP-WEIXIN
537 const query = uni.createSelectorQuery().in(this);
538 query.select('#imgCanvas')
539 .fields({ node: true, size: true })
540 .exec((res) => {
541 const canvas = res[0].node;
542
543 const dpr = uni.getSystemInfoSync().pixelRatio;
544 canvas.width = res[0].width * dpr;
545 canvas.height = res[0].height * dpr;
546 const ctx = canvas.getContext('2d');
547 ctx.scale(dpr, dpr);
548 ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
549
550 this.draw2DImage(canvas, ctx, this.imgSrc, () => {
551 this.canvasToTempFilePath(canvas);
552 });
553 });
554 // #endif
555 },
556 handleImage(tempFilePath){
557 // 在H5平台下,tempFilePath 为 base64
558 // console.log(tempFilePath)
559 uni.hideLoading();
560 this.$emit('crop', { tempFilePath });
561 }
562 }
563 }
564 </script>
565
566 <style lang="scss" scoped>
567 .image-cropper {
568 position: fixed;
569 left: 0;
570 right: 0;
571 top: 0;
572 bottom: 0;
573 overflow: hidden;
574 display: flex;
575 flex-direction: column;
576 background-color: #000;
577 .img-canvas {
578 position: absolute !important;
579 transform: translateX(-100%);
580 }
581 .pic-preview {
582 width: 100%;
583 flex: 1;
584 position: relative;
585
586 .crop-mask-block {
587 background-color: rgba(51, 51, 51, 0.8);
588 z-index: 2;
589 position: fixed;
590 box-sizing: border-box;
591 pointer-events: none;
592 }
593 .crop-circle-box {
594 position: fixed;
595 box-sizing: border-box;
596 z-index: 2;
597 pointer-events: none;
598 overflow: hidden;
599 .crop-circle {
600 width: 100%;
601 height: 100%;
602 }
603 }
604 .crop-image {
605 padding: 0 !important;
606 margin: 0 !important;
607 border-radius: 0 !important;
608 display: block !important;
609 }
610 .crop-border {
611 position: fixed;
612 border: 1px solid #fff;
613 box-sizing: border-box;
614 z-index: 3;
615 pointer-events: none;
616 }
617 .crop-grid {
618 position: fixed;
619 z-index: 3;
620 border-style: dashed;
621 border-color: #fff;
622 pointer-events: none;
623 opacity: 0.5;
624 }
625 .crop-angle {
626 position: fixed;
627 z-index: 3;
628 border-style: solid;
629 border-color: #fff;
630 pointer-events: none;
631 }
632 }
633
634 .fixed-bottom {
635 position: fixed;
636 left: 0;
637 right: 0;
638 bottom: 0;
639 z-index: 99;
640 display: flex;
641 flex-direction: row;
642 background-color: $uni-bg-color-grey;
643
644 .rotate-icon {
645 background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
646 background-size: 60% 60%;
647 background-repeat: no-repeat;
648 background-position:center;
649 width: 80rpx;
650 height: 80rpx;
651 position: absolute;
652 top: -90rpx;
653 left: 10rpx;
654 transform: rotateY(180deg);
655 }
656
657 .rechoose {
658 color: $uni-color-primary;
659 padding: 0 $uni-spacing-row-lg;
660 line-height: 100rpx;
661 }
662
663 .choose-btn {
664 color: $uni-color-primary;
665 text-align: center;
666 line-height: 100rpx;
667 flex: 1;
668 }
669
670 .button {
671 margin: auto $uni-spacing-row-lg auto auto;
672 background-color: $uni-color-primary;
673 color: #fff;
674 }
675 }
676
677 .safe-area-inset-bottom {
678 padding-bottom: 0;
679 padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
680 padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
681 }
682
683 }
684 </style>
...\ No newline at end of file ...\ No newline at end of file
1 /**
2 * 图片编辑器-手势监听
3 * 1. wxs 暂不支持 es6 语法
4 * 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上(uni-app 2.2.5及以上版本)
5 */
6 /** 图片偏移量 */
7 var offset = { x: 0, y: 0 };
8 /** 图片缩放比例 */
9 var scale = 1;
10 /** 图片最小缩放比例 */
11 var minScale = 1;
12 /** 图片旋转角度 */
13 var rotate = 0;
14 /** 触摸点 */
15 var touches = [];
16 /** 图片布局信息 */
17 var img = {};
18 /** 系统信息 */
19 var sys = {};
20 /** 裁剪区域布局信息 */
21 var area = {};
22 /** 触摸行为类型 */
23 var touchType = '';
24 /** 操作角的位置 */
25 var activeAngle = 0;
26 /** 裁剪区域布局信息偏移量 */
27 var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
28 /**
29 * 计算两点间距
30 * @param {Object} touches 触摸点信息
31 */
32 function getDistanceByTouches(touches) {
33 // 根据勾股定理求两点间距离
34 var a = touches[1].pageX - touches[0].pageX;
35 var b = touches[1].pageY - touches[0].pageY;
36 var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
37 // 求两点间的中点坐标
38 // 1. a、b可能为负值
39 // 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
40 // 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
41 var x = touches[1].pageX - a / 2;
42 var y = touches[1].pageY - b / 2;
43 return { c, x, y };
44 };
45 /**
46 * 检查边界:限制 x、y 拖动范围,禁止滑出边界
47 * @param {Object} e 点坐标
48 */
49 function checkRange(e) {
50 var r = rotate / 90 % 2;
51 if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
52 var o = (img.height - img.width) / 2; // 宽高差值一半
53 return {
54 x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
55 y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
56 }
57 }
58 return {
59 x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
60 y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
61 }
62 };
63 /**
64 * 变更图片布局信息
65 * @param {Object} e 布局信息
66 */
67 function changeImageRect(e) {
68 offset.x += e.x || 0;
69 offset.y += e.y || 0;
70 var image = e.instance.selectComponent('.crop-image');
71 if(e.check) { // 检查边界
72 var point = checkRange(offset);
73 if(offset.x !== point.x || offset.y !== point.y) {
74 offset = point;
75 }
76 }
77 // image.setStyle({
78 // width: img.width + 'px',
79 // height: img.height + 'px',
80 // transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
81 // });
82 var ox = (img.width - img.oldWidth) / 2;
83 var oy = (img.height - img.oldHeight) / 2;
84 image.setStyle({
85 width: img.oldWidth + 'px',
86 height: img.oldHeight + 'px',
87 transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
88 });
89
90 e.instance.callMethod('dataChange', {
91 width: img.width,
92 height: img.height,
93 x: offset.x,
94 y: offset.y,
95 rotate: rotate
96 });
97 };
98 /**
99 * 变更裁剪区域布局信息
100 * @param {Object} e 布局信息
101 */
102 function changeAreaRect(e) {
103 // 变更蒙版样式
104 var masks = e.instance.selectAllComponents('.crop-mask-block');
105 var maskStyles = [
106 {
107 left: 0,
108 width: (area.left + areaOffset.left) + 'px',
109 top: 0,
110 bottom: 0,
111 },
112 {
113 left: (area.right + areaOffset.right) + 'px',
114 right: 0,
115 top: 0,
116 bottom: 0,
117 },
118 {
119 left: (area.left + areaOffset.left) + 'px',
120 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
121 top: 0,
122 height: (area.top + areaOffset.top) + 'px',
123 },
124 {
125 left: (area.left + areaOffset.left) + 'px',
126 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
127 top: (area.bottom + areaOffset.bottom) + 'px',
128 // height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
129 bottom: 0,
130 }
131 ];
132 var len = masks.length;
133 for (var i = 0; i < len; i++) {
134 masks[i].setStyle(maskStyles[i]);
135 }
136
137 // 变更边框样式
138 if(area.showBorder) {
139 var border = e.instance.selectComponent('.crop-border');
140 border.setStyle({
141 left: (area.left + areaOffset.left) + 'px',
142 top: (area.top + areaOffset.top) + 'px',
143 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
144 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
145 });
146 }
147
148 // 变更参考线样式
149 if(area.showGrid) {
150 var grids = e.instance.selectAllComponents('.crop-grid');
151 var gridStyles = [
152 {
153 'border-width': '1px 0 0 0',
154 left: (area.left + areaOffset.left) + 'px',
155 right: (area.right + areaOffset.right) + 'px',
156 top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
157 width: (area.width + areaOffset.right - areaOffset.left) + 'px'
158 },
159 {
160 'border-width': '1px 0 0 0',
161 left: (area.left + areaOffset.left) + 'px',
162 right: (area.right + areaOffset.right) + 'px',
163 top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
164 width: (area.width + areaOffset.right - areaOffset.left) + 'px'
165 },
166 {
167 'border-width': '0 1px 0 0',
168 top: (area.top + areaOffset.top) + 'px',
169 bottom: (area.bottom + areaOffset.bottom) + 'px',
170 left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
171 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
172 },
173 {
174 'border-width': '0 1px 0 0',
175 top: (area.top + areaOffset.top) + 'px',
176 bottom: (area.bottom + areaOffset.bottom) + 'px',
177 left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
178 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
179 }
180 ];
181 var len = grids.length;
182 for (var i = 0; i < len; i++) {
183 grids[i].setStyle(gridStyles[i]);
184 }
185 }
186
187 // 变更四个伸缩角样式
188 if(area.showAngle) {
189 var angles = e.instance.selectAllComponents('.crop-angle');
190 var angleStyles = [
191 {
192 'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
193 left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
194 top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
195 },
196 {
197 'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
198 left: (area.right + areaOffset.right - area.angleSize) + 'px',
199 top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
200 },
201 {
202 'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
203 left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
204 top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
205 },
206 {
207 'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
208 left: (area.right + areaOffset.right - area.angleSize) + 'px',
209 top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
210 }
211 ];
212 var len = angles.length;
213 for (var i = 0; i < len; i++) {
214 angles[i].setStyle(angleStyles[i]);
215 }
216 }
217
218 // 变更圆角样式
219 if(area.radius > 0) {
220 var circleBox = e.instance.selectComponent('.crop-circle-box');
221 var circle = e.instance.selectComponent('.crop-circle');
222 var radius = area.radius;
223 if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
224 radius = (area.width / 2);
225 } else { // 圆角矩形
226 if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
227 radius = Math.min(area.width / 2, area.height / 2, radius);
228 }
229 }
230 circleBox.setStyle({
231 left: (area.left + areaOffset.left) + 'px',
232 top: (area.top + areaOffset.top) + 'px',
233 width: (area.width + areaOffset.right - areaOffset.left) + 'px',
234 height: (area.height + areaOffset.bottom - areaOffset.top) + 'px'
235 });
236 circle.setStyle({
237 'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
238 'border-radius': radius + 'px'
239 });
240 }
241 };
242 /**
243 * 缩放图片
244 * @param {Object} e 布局信息
245 */
246 function scaleImage(e) {
247 var last = scale;
248 scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
249 if(last !== scale) {
250 img.width = img.oldWidth * scale;
251 img.height = img.oldHeight * scale;
252 // 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
253 // 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
254 // 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
255 // 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
256 // 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
257 e.x = (e.x - offset.x) * (1 - scale / last);
258 e.y = (e.y - offset.y) * (1 - scale / last);
259 changeImageRect(e);
260 return true;
261 }
262 return false;
263 };
264 /**
265 * 获取触摸点在哪个角
266 * @param {number} x 触摸点x轴坐标
267 * @param {number} y 触摸点y轴坐标
268 * @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
269 */
270 function getToucheAngle(x, y) {
271 // console.log('getToucheAngle', x, y, JSON.stringify(area))
272 var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
273 if(y >= area.top - o && y <= area.top + area.angleSize + o) {
274 if(x >= area.left - o && x <= area.left + area.angleSize + o) {
275 return 1; // 左上角
276 } else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
277 return 2; // 右上角
278 }
279 } else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
280 if(x >= area.left - o && x <= area.left + area.angleSize + o) {
281 return 3; // 左下角
282 } else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
283 return 4; // 右下角
284 }
285 }
286 return 0; // 无触摸到角
287 };
288 /**
289 * 重置数据
290 */
291 function resetData() {
292 offset = { x: 0, y: 0 };
293 scale = 1;
294 minScale = 1;
295 rotate = 0;
296 };
297 module.exports = {
298 /**
299 * 初始化:观察数据变更
300 * @param {Object} newVal 新数据
301 * @param {Object} oldVal 旧数据
302 * @param {Object} o 组件实例对象
303 */
304 initObserver: function(newVal, oldVal, o, i) {
305 if(newVal) {
306 img = newVal.img;
307 sys = newVal.sys;
308 area = newVal.area;
309 resetData();
310 img.src && changeImageRect({
311 instance: o,
312 x: (sys.windowWidth - img.width) / 2,
313 y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
314 });
315 changeAreaRect({
316 instance: o
317 });
318 // console.log('initRect', JSON.stringify(newVal))
319 }
320 },
321 /**
322 * 鼠标滚轮滚动
323 * @param {Object} e 事件对象
324 * @param {Object} o 组件实例对象
325 */
326 mousewheel: function(e, o) {
327 if(!img.src) return;
328 scaleImage({
329 instance: o,
330 check: true,
331 // 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
332 scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
333 x: e.touches[0].pageX,
334 y: e.touches[0].pageY
335 });
336 },
337 /**
338 * 触摸开始
339 * @param {Object} e 事件对象
340 * @param {Object} o 组件实例对象
341 */
342 touchstart: function(e, o) {
343 if(!img.src) return;
344 touches = e.touches;
345 activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
346 if(touches.length === 1 && activeAngle !== 0) {
347 touchType = 'stretch'; // 伸缩裁剪区域
348 } else {
349 touchType = '';
350 }
351 // console.log('touchstart', JSON.stringify(e), activeAngle)
352 },
353 /**
354 * 触摸移动
355 * @param {Object} e 事件对象
356 * @param {Object} o 组件实例对象
357 */
358 touchmove: function(e, o) {
359 if(!img.src) return;
360 // console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
361 if(touchType === 'stretch') { // 触摸四个角进行拉伸
362 var point = e.touches[0];
363 var start = touches[0];
364 var x = point.pageX - start.pageX;
365 var y = point.pageY - start.pageY;
366 if(x !== 0 || y !== 0) {
367 var maxX = area.width * (1 - area.minScale);
368 var maxY = area.height * (1 - area.minScale);
369 // console.log(x, y, maxX, maxY)
370 touches[0] = point;
371 switch(activeAngle) {
372 case 1: // 左上角
373 x += areaOffset.left;
374 y += areaOffset.top;
375 if(x >= 0 && y >= 0) { // 有效滑动
376 if(x > y) { // 以x轴滑动距离为缩放基准
377 if(x > maxX) x = maxX;
378 y = x * area.height / area.width;
379 } else { // 以y轴滑动距离为缩放基准
380 if(y > maxY) y = maxY;
381 x = y * area.width / area.height;
382 }
383 areaOffset.left = x;
384 areaOffset.top = y;
385 }
386 break;
387 case 2: // 右上角
388 x += areaOffset.right;
389 y += areaOffset.top;
390 if(x <= 0 && y >= 0) { // 有效滑动
391 if(-x > y) { // 以x轴滑动距离为缩放基准
392 if(-x > maxX) x = -maxX;
393 y = -x * area.height / area.width;
394 } else { // 以y轴滑动距离为缩放基准
395 if(y > maxY) y = maxY;
396 x = -y * area.width / area.height;
397 }
398 areaOffset.right = x;
399 areaOffset.top = y;
400 }
401 break;
402 case 3: // 左下角
403 x += areaOffset.left;
404 y += areaOffset.bottom;
405 if(x >= 0 && y <= 0) { // 有效滑动
406 if(x > -y) { // 以x轴滑动距离为缩放基准
407 if(x > maxX) x = maxX;
408 y = -x * area.height / area.width;
409 } else { // 以y轴滑动距离为缩放基准
410 if(-y > maxY) y = -maxY;
411 x = -y * area.width / area.height;
412 }
413 areaOffset.left = x;
414 areaOffset.bottom = y;
415 }
416 break;
417 case 4: // 右下角
418 x += areaOffset.right;
419 y += areaOffset.bottom;
420 if(x <= 0 && y <= 0) { // 有效滑动
421 if(-x > -y) { // 以x轴滑动距离为缩放基准
422 if(-x > maxX) x = -maxX;
423 y = x * area.height / area.width;
424 } else { // 以y轴滑动距离为缩放基准
425 if(-y > maxY) y = -maxY;
426 x = y * area.width / area.height;
427 }
428 areaOffset.right = x;
429 areaOffset.bottom = y;
430 }
431 break;
432 }
433 // console.log(x, y, JSON.stringify(areaOffset))
434 changeAreaRect({
435 instance: o,
436 });
437 // this.draw();
438 }
439 } else if (e.touches.length == 2) { // 双点触摸缩放
440 var start = getDistanceByTouches(touches);
441 var end = getDistanceByTouches(e.touches);
442 scaleImage({
443 instance: o,
444 check: !area.bounce,
445 scale: (end.c - start.c) / 100,
446 x: end.x,
447 y: end.y
448 });
449 touchType = 'scale';
450 } else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
451 touchType = 'move';
452 } else {
453 changeImageRect({
454 instance: o,
455 check: !area.bounce,
456 x: e.touches[0].pageX - touches[0].pageX,
457 y: e.touches[0].pageY - touches[0].pageY
458 });
459 touchType = 'move';
460 }
461 touches = e.touches;
462 },
463 /**
464 * 触摸结束
465 * @param {Object} e 事件对象
466 * @param {Object} o 组件实例对象
467 */
468 touchend: function(e, o) {
469 if(!img.src) return;
470 if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
471 // 裁剪区域宽度被缩放到多少
472 var left = areaOffset.left;
473 var right = areaOffset.right;
474 var top = areaOffset.top;
475 var bottom = areaOffset.bottom;
476 var w = area.width + right - left;
477 var h = area.height + bottom - top;
478 // 图像放大倍数
479 var p = scale * (area.width / w) - scale;
480 // 复原裁剪区域
481 areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
482 changeAreaRect({
483 instance: o,
484 });
485 scaleImage({
486 instance: o,
487 scale: p,
488 x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
489 y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
490 });
491 } else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
492 changeImageRect({
493 instance: o,
494 check: true
495 });
496 }
497 },
498 /**
499 * 顺时针翻转图片90°
500 * @param {Object} e 事件对象
501 * @param {Object} o 组件实例对象
502 */
503 rotateImage: function(e, o) {
504 rotate = (rotate + 90) % 360;
505
506 // 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
507 var r = rotate / 90 % 2;
508 minScale = 1;
509 if(img.width < area.height) {
510 minScale = area.height / img.oldWidth;
511 } else if(img.height < area.width) {
512 minScale = (area.width / img.oldHeight)
513 }
514 if(minScale !== 1) {
515 scaleImage({
516 instance: o,
517 scale: minScale - scale,
518 x: sys.windowWidth / 2,
519 y: (sys.windowHeight - sys.offsetBottom) / 2
520 });
521 }
522
523 // 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
524 // 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
525 // 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
526 var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
527 var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
528 changeImageRect({
529 instance: o,
530 check: true,
531 x: -ox - oy,
532 y: -oy + ox
533 });
534 },
535 // 此处只用于对齐其他平台端的样式参数,防止异常,无作用
536 imageStyles: {},
537 maskStylesList: [{}, {}, {}, {}],
538 borderStyles: {},
539 gridStylesList: [{}, {}, {}, {}],
540 angleStylesList: [{}, {}, {}, {}],
541 circleBoxStyles: {},
542 circleStyles: {},
543 }
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "id": "qf-image-cropper",
3 "displayName": "图片裁剪插件",
4 "version": "2.1.6",
5 "description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
6 "keywords": [
7 "qf-image-cropper",
8 "图片裁剪",
9 "图片编辑",
10 "头像裁剪",
11 "小程序"
12 ],
13 "repository": "",
14 "engines": {
15 "HBuilderX": "^3.1.0"
16 },
17 "dcloudext": {
18 "type": "component-vue",
19 "sale": {
20 "regular": {
21 "price": "0.00"
22 },
23 "sourcecode": {
24 "price": "0.00"
25 }
26 },
27 "contact": {
28 "qq": ""
29 },
30 "declaration": {
31 "ads": "无",
32 "data": "插件不采集任何数据",
33 "permissions": "无"
34 },
35 "npmurl": ""
36 },
37 "uni_modules": {
38 "dependencies": [],
39 "encrypt": [],
40 "platforms": {
41 "client": {
42 "Vue": {
43 "vue2": "y",
44 "vue3": "y"
45 },
46 "App": {
47 "app-vue": "y",
48 "app-nvue": "n"
49 },
50 "H5-mobile": {
51 "Safari": "y",
52 "Android Browser": "y",
53 "微信浏览器(Android)": "y",
54 "QQ浏览器(Android)": "u"
55 },
56 "H5-pc": {
57 "Chrome": "u",
58 "IE": "u",
59 "Edge": "u",
60 "Firefox": "u",
61 "Safari": "u"
62 },
63 "小程序": {
64 "微信": "y",
65 "阿里": "n",
66 "百度": "n",
67 "字节跳动": "n",
68 "QQ": "u",
69 "钉钉": "n",
70 "快手": "n",
71 "飞书": "n",
72 "京东": "n"
73 },
74 "快应用": {
75 "华为": "n",
76 "联盟": "n"
77 }
78 }
79 }
80 }
81 }
...\ No newline at end of file ...\ No newline at end of file
1 # qf-image-cropper
2 ## 图片裁剪插件
3 uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
4
5 ### 平台支持:
6 1. 支持微信小程序:移动端、PC端、开发者工具
7 2. 支持H5平台(2.1.0版本起)
8 3. 支持APP平台(2.1.5版本起):Android、IOS
9 4. 其他平台暂未测试兼容性未知
10
11 ### 支持功能:
12 1. 自定义裁剪尺寸
13 2. 定点等比例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
14 3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
15 4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
16 5. 裁剪生成新图片
17 6. 本地选择图片
18 7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
19 8. 裁剪圆角图片:圆形、圆角矩形
20
21 ### 属性说明
22 | 属性名 | 类型 | 默认值 | 说明 |
23 |:---|:---|:---|:---|
24 | src | String | | 图片资源地址 |
25 | width | Number | 300 | 裁剪宽度 |
26 | height | Number | 300 | 裁剪高度 |
27 | showBorder | Boolean | true | 是否绘制裁剪区域边框 |
28 | showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
29 | showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
30 | areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
31 | maxScale | Number | 5 | 图片最大缩放倍数 |
32 | bounce | Boolean | true | 是否有回弹效果:拖动时可以拖出边界,释放时会弹回边界 |
33 | rotatable | Boolean | true | 是否支持翻转 |
34 | choosable | Boolean | true | 是否支持从本地选择素材 |
35 | angleSize | Number | 20 | 四个角尺寸,单位px |
36 | angleBorderWidth | Number | 2 | 四个角边框宽度,单位px |
37 | radius | Number | | 裁剪图片圆角半径,单位px |
38 | fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
39 | delay | Number | 1000 | 图片从绘制到生成所需时间,单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
40 | navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
41 | @crop | EventHandle | | 剪裁完成后触发,event = { tempFilePath }。在H5平台下,tempFilePath 为 base64 |
42
43 ### 基本用法
44 ```
45 <template>
46 <div>
47 <qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
48 </div>
49 </template>
50
51 <script>
52 import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
53 export default {
54 components: {
55 QfImageCropper
56 },
57 methods: {
58 handleCrop(e) {
59 uni.previewImage({
60 urls: [e.tempFilePath],
61 current: 0
62 });
63 }
64 }
65 }
66 </script>
67 ```
68 ### 使用说明
69 1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
70 ```
71 {
72 "enablePullDownRefresh": false,
73 "disableScroll": true
74 }
75 ```
76 2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
77 ```
78 1.界面卡顿,内存占用过高
79 2.生成图片失真(模糊)
80 3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
81 ```
82 3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
83 4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
...\ No newline at end of file ...\ 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!