完成选择热门icon页,支付宝账单新增80%

This commit is contained in:
tangxinyue 2026-01-07 18:44:31 +08:00
parent 183e4a7181
commit dee2f1c3b7
87 changed files with 3964 additions and 285 deletions

13
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,13 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "custom",
"type" : "uni-app:app-ios"
},
{
"playground" : "custom",
"type" : "uni-app:app-android"
}
]
}

View File

@ -24,8 +24,8 @@
</view> </view>
<view v-if="item.isRefund" class="refund">已全额退款</view> <view v-if="item.isRefund" class="refund">已全额退款</view>
<view v-if="isBalance" class="balance secondary">余额 <text class="balance-text">{{ <view v-if="isBalance" class="balance secondary">余额 <text class="balance-text">{{
Number(item.balance).toFixed(2) Number(item.balance).toFixed(2)
}}</text></view> }}</text></view>
</view> </view>
</view> </view>
</view> </view>
@ -35,110 +35,108 @@
</template> </template>
<script setup> <script setup>
import { import {
defineProps, onMounted,
defineEmits, reactive
onMounted, } from 'vue'
reactive
} from 'vue'
// //
const props = defineProps({ const props = defineProps({
list: { list: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
isBalance: { isBalance: {
type: Boolean, type: Boolean,
default: true default: true
} }
}) })
// //
const emit = defineEmits(['onBill']) const emit = defineEmits(['onBill'])
const data = reactive({}) const data = reactive({})
onMounted(() => {}) onMounted(() => { })
</script> </script>
<style> <style>
/* @import '../../common/main.css'; */ /* @import '../../common/main.css'; */
</style> </style>
<style lang="less"> <style lang="less">
.balance-list-container { .balance-list-container {
width: 100%; width: 100%;
}
.balance-item {
display: flex;
width: 100%;
flex-direction: row;
padding: 0;
.balance-item-text-container {
flex: 1;
display: flex;
justify-content: space-between;
margin-left: 12px;
box-shadow: 0 0.3px 0 0 #F0F0EE;
padding: 12px 12px 12px 0;
} }
.balance-item { .img {
margin: 12px 0;
width: 30px;
height: 30px;
border-radius: 50%;
margin-left: 12px;
}
.balance-item-text {
display: flex; display: flex;
width: 100%; flex-direction: column;
flex-direction: row; // justify-content: space-between;
padding: 0;
.balance-item-text-container { .name {
flex: 1; color: #343434;
display: flex;
justify-content: space-between;
margin-left: 12px;
box-shadow: 0 0.3px 0 0 #F0F0EE;
padding: 12px 12px 12px 0;
} }
.img { .secondary {
margin: 12px 0; color: var(--text-secondary);
width: 30px;
height: 30px;
border-radius: 50%;
margin-left: 12px;
}
.balance-item-text {
display: flex;
flex-direction: column;
// justify-content: space-between;
.name {
color: #343434;
}
.secondary {
color: var(--text-secondary);
font-size: 12px;
}
.money {
font-size: 17px;
font-weight: 500;
}
.add-color {
color: #F6610F;
}
.red-add-color {
color: #F53646;
}
.minus-color {
color: #333333;
}
.balance-text {
margin-right: 2px;
}
}
.refund {
color: #EA6B48;
font-size: 12px; font-size: 12px;
} }
.text-right { .money {
align-items: flex-end; font-size: 17px;
font-weight: 500;
}
.add-color {
color: #F6610F;
}
.red-add-color {
color: #F53646;
}
.minus-color {
color: #333333;
}
.balance-text {
margin-right: 2px;
} }
} }
.refund {
color: #EA6B48;
font-size: 12px;
}
.text-right {
align-items: flex-end;
}
}
</style> </style>

View File

@ -46,8 +46,6 @@
<script setup> <script setup>
import popup from '../popup/popup.vue' import popup from '../popup/popup.vue'
import { import {
defineProps,
defineEmits,
onMounted, onMounted,
reactive, reactive,
ref ref

View File

@ -1,6 +1,6 @@
{ {
"name" : "alipay-emulator", "name" : "alipay-emulator",
"appid" : "__UNI__D535736", "appid" : "__UNI__B05EDBF",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
@ -41,7 +41,9 @@
] ]
}, },
/* ios */ /* ios */
"ios" : {}, "ios" : {
"dSYMs" : false
},
/* SDK */ /* SDK */
"sdkConfigs" : {} "sdkConfigs" : {}
} }

View File

@ -28,6 +28,13 @@
"navigationBarTitleText": "新增账单", "navigationBarTitleText": "新增账单",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/common/hot-icon/hot-icon",
"style": {
"navigationBarTitleText": "热门图标",
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@ -5,55 +5,64 @@
"selectId": "1", "selectId": "1",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": false, "isAdd": false,
"imageUrl": "",
"name": "",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "productDescription",
"required": true
}, },
{ {
"label": "收单机构", "label": "收单机构",
"value": "支付宝支付科技有限公司", "value": "支付宝支付科技有限公司",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "collectionOrganization" "key": "collectionOrganization",
"required": true
}, },
{ {
"label": "收款方全称", "label": "收款方全称",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "receiverFullName" "key": "receiverFullName",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "日用百货", "value": "日用百货",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -68,48 +77,56 @@
"selectId": "2", "selectId": "2",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": false, "isAdd": false,
"imageUrl": "/static/image/common/hot-icon/shangpin.png",
"name": "**x",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "productDescription",
"required": true
}, },
{ {
"label": "收款方全称", "label": "收款方全称",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "receiverFullName" "key": "receiverFullName",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "日用百货", "value": "日用百货",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -125,55 +142,64 @@
"selectId": "3", "selectId": "3",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": false, "isAdd": false,
"name": "",
"imageUrl": "/static/image/common/hot-icon/shangpin.png",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "productDescription",
"required": true
}, },
{ {
"label": "收单机构", "label": "收单机构",
"value": "支付宝支付科技有限公司", "value": "支付宝支付科技有限公司",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "collectionOrganization" "key": "collectionOrganization",
"required": true
}, },
{ {
"label": "清算机构", "label": "清算机构",
"value": "中国中国银联股份有限公司", "value": "中国中国银联股份有限公司",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "clearingOrganization" "key": "clearingOrganization",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "餐饮美食", "value": "餐饮美食",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -187,41 +213,48 @@
"selectId": "4", "selectId": "4",
"orderStatus": "还款成功", "orderStatus": "还款成功",
"isAdd": false, "isAdd": false,
"imageUrl": "/static/image/common/hot-icon/huabei.png",
"name": "花呗",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "还款到", "label": "还款到",
"value": "花呗", "value": "花呗",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "repaymentTo" "key": "repaymentTo",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "信用借还", "value": "信用借还",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -232,49 +265,57 @@
"type": "转账1", "type": "转账1",
"selectId": "5", "selectId": "5",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": false, "isAdd": true,
"imageUrl": "/static/image/common/hot-icon/default.png",
"name": "xxxx有限公司",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "转账备注", "label": "转账备注",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "transferNote" "key": "transferNote",
"required": true
}, },
{ {
"label": "对方账户", "label": "对方账户",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "counterAccount" "key": "counterAccount",
"required": true
}, },
{ {
"label": "订单号", "label": "订单号",
"value": "", "value": "",
"type": "number", "type": "number",
"focus": false, "focus": false,
"key": "orderNumber" "key": "orderNumber",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "转账红包", "value": "转账红包",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -288,41 +329,48 @@
"selectId": "6", "selectId": "6",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": true, "isAdd": true,
"imageUrl": "/static/image/common/hot-icon/default.png",
"name": "xxx",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "转账备注", "label": "转账备注",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "transferNote" "key": "transferNote",
"required": true
}, },
{ {
"label": "对方账户", "label": "对方账户",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "counterAccount" "key": "counterAccount",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "转账红包", "value": "转账红包",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -337,41 +385,48 @@
"isAdd": false, "isAdd": false,
"orderStatus": "交易成功", "orderStatus": "交易成功",
"selectId": "7", "selectId": "7",
"imageUrl": "/static/image/common/hot-icon/default.png",
"name": "xxx",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "对方账户", "label": "对方账户",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "counterAccount" "key": "counterAccount",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "转账红包", "value": "转账红包",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -387,42 +442,49 @@
"selectId": "8", "selectId": "8",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"isAdd": true, "isAdd": true,
"imageUrl": "/static/image/common/hot-icon/default.png",
"name": "**x",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "关联记录", "label": "关联记录",
"value": "", "value": "查看关联记录",
"type": "text", "type": "text",
"textColor": "#12447A", "textColor": "#12447A",
"focus": false, "focus": false,
"key": "relatedRecord" "key": "relatedRecord",
"required": true
}, },
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "收钱码收款",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "productDescription",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "收入", "value": "收入",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -438,49 +500,57 @@
"selectId": "9", "selectId": "9",
"orderStatus": "退款成功", "orderStatus": "退款成功",
"isAdd": true, "isAdd": true,
"imageUrl": "/static/image/common/hot-icon/tuikuan.png",
"name": "",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "关联记录", "label": "关联记录",
"value": "", "value": "查看原账单",
"type": "text", "type": "text",
"textColor": "#12447A", "textColor": "#12447A",
"focus": false, "focus": false,
"key": "relatedRecord" "key": "relatedRecord",
"required": true
}, },
{ {
"label": "创建时间", "label": "创建时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "退款方式", "label": "退款方式",
"value": "招商银行储蓄卡(0123)",
"type": "text",
"focus": false,
"key": "refundMethod"
},
{
"label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "refundMethod",
"required": true
},
{
"label": "商品说明",
"value": "退款-xxx",
"type": "text",
"focus": false,
"key": "productDescription",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "退款", "value": "退款",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -493,49 +563,58 @@
{ {
"type": "充值缴费1", "type": "充值缴费1",
"selectId": "10", "selectId": "10",
"isAdd": false,
"orderStatus": "交易成功", "orderStatus": "交易成功",
"imageUrl": "",
"name": "中国移动",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "支付时间", "label": "支付时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)",
"type": "text",
"focus": false,
"key": "payMethod"
},
{
"label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "payMethod",
"required": true
},
{
"label": "商品说明",
"value": "为123****5623充值",
"type": "text",
"focus": false,
"key": "productDescription",
"required": true
}, },
{ {
"label": "收款方全称", "label": "收款方全称",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "counterAccount" "key": "receiverFullName",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "充值缴费", "value": "充值缴费",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -550,6 +629,9 @@
{ {
"type": "充值缴费2", "type": "充值缴费2",
"selectId": "11", "selectId": "11",
"isAdd": false,
"imageUrl": "",
"name": "石油天然气公司",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"itemInfoList": [ "itemInfoList": [
{ {
@ -557,49 +639,56 @@
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)",
"type": "text",
"focus": false,
"key": "payMethod"
},
{
"label": "缴费说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "payMethod",
"required": true
},
{
"label": "缴费说明",
"value": "xxxxxxxx缴费",
"type": "text",
"focus": false,
"key": "productDescription",
"required": true
}, },
{ {
"label": "户号", "label": "户号",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "householdNumber" "key": "householdNumber",
"required": true
}, },
{ {
"label": "订单号", "label": "订单号",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "orderNumber" "key": "orderNumber",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "充值缴费", "value": "充值缴费",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -611,43 +700,62 @@
{ {
"type": "网购1", "type": "网购1",
"selectId": "12", "selectId": "12",
"isAdd": false,
"imageUrl": "",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"itemInfoList": [ "itemInfoList": [
{ {
"label": "关联记录", "label": "关联记录",
"value": "", "value": "查看关联记录",
"type": "text", "type": "text",
"textColor": "#12447A", "textColor": "#12447A",
"focus": false, "focus": false,
"key": "relatedRecord" "key": "relatedRecord",
"required": true
}, },
{ {
"label": "支付时间", "label": "支付时间",
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "日用百货", "value": "日用百货",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
},
{
"label": "交易详情",
"value": {
"imgUrl": "",
"text": "",
"quantity": "共1件"
},
"type": "link",
"focus": false,
"key": "tradeDetail",
"required": true
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [
@ -661,6 +769,8 @@
{ {
"type": "网购2", "type": "网购2",
"selectId": "13", "selectId": "13",
"isAdd": false,
"imageUrl": "",
"orderStatus": "交易成功", "orderStatus": "交易成功",
"itemInfoList": [ "itemInfoList": [
{ {
@ -668,42 +778,48 @@
"value": "", "value": "",
"type": "time", "type": "time",
"focus": false, "focus": false,
"key": "createTime" "key": "createTime",
"required": true
}, },
{ {
"label": "付款方式", "label": "付款方式",
"value": "招商银行储蓄卡(0123)", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "payMethod" "key": "payMethod",
"required": true
}, },
{ {
"label": "商品说明", "label": "商品说明",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "productDescription" "key": "productDescription",
"required": true
}, },
{ {
"label": "收款方全称", "label": "收款方全称",
"value": "", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "counterAccount" "key": "receiverFullName",
"required": true
}, },
{ {
"label": "账单分类", "label": "账单分类",
"value": "日用百货", "value": "日用百货",
"type": "select", "type": "select",
"focus": false, "focus": false,
"key": "billCategory" "key": "billCategory",
"required": true
}, },
{ {
"label": "标签和备注", "label": "标签和备注",
"value": "添加", "value": "",
"type": "text", "type": "text",
"focus": false, "focus": false,
"key": "tagAndNote" "key": "tagAndNote",
"required": false
} }
], ],
"defaultBottomIcons": [ "defaultBottomIcons": [

View File

@ -13,11 +13,12 @@
<view class="add-bill-container"> <view class="add-bill-container">
<!-- 随机骰子 --> <!-- 随机骰子 -->
<view class="random-dice"> <view class="random-dice">
<image class="random-dice-image" src="/static/image/common/random-dice.png"></image> <image class="random-dice-image" src="/static/image/common/random-dice.png" @click="randomBillInfo">
</image>
</view> </view>
<!-- 头像 --> <!-- 头像 -->
<view class="avatar-box"> <view class="avatar-box">
<image class="avatar-image" src="/static/image/bill/add-bill/add-avatar.png"></image> <image class="avatar-image" :src="billData.imgUrl || defaultImage" @click="changeAvatar"></image>
</view> </view>
<!-- 主要信息 --> <!-- 主要信息 -->
<view class="main-info flex-align-center flex-column"> <view class="main-info flex-align-center flex-column">
@ -30,14 +31,19 @@
<image class="edit-image" src="/static/image/bill/add-bill/edit.png"></image> <image class="edit-image" src="/static/image/bill/add-bill/edit.png"></image>
</view> </view>
<!-- 金额 --> <!-- 金额 -->
<view class=" money info-item-input" style="height: 77rpx;"> <view class="money-box flex-align-center">
<!-- 隐藏的text用于测量宽度 --> <view class="add money alipay-font">{{ billData.isAdd ? '+' : '-' }}</view>
<text class="text-measure font-w500" style="font-size: 64rpx;">{{ billData.money }}</text> <!-- 金额 -->
<view class=" money info-item-input alipay-font" style="height: 77rpx;">
<!-- 隐藏的text用于测量宽度 -->
<text class="text-measure font-w500" style="font-size: 64rpx;">{{ billData.money }}</text>
<input class="text-input font-w500" style="font-size: 64rpx;" type="digit" <input class="text-input font-w500" style="font-size: 64rpx;" type="digit"
v-model="billData.money" /> v-model="billData.money" />
<image class="edit-image" src="/static/image/bill/add-bill/edit.png"></image> <image class="edit-image" src="/static/image/bill/add-bill/edit.png"></image>
</view>
</view> </view>
<!-- 订单状态 --> <!-- 订单状态 -->
<view class="order-status-info" :class="{ isRefund: billData.merchantOption.refund }"> <view class="order-status-info" :class="{ isRefund: billData.merchantOption.refund }">
{{ billData.orderStatus }} {{ billData.orderStatus }}
@ -45,30 +51,48 @@
</view> </view>
<!-- 详情信息列表 --> <!-- 详情信息列表 -->
<view class="detail-info-container"> <view class="detail-info-container">
<view class="info-item-box" v-for="item in billData.itemInfoList" :key="item.type"> <template v-for="item in billData.itemInfoList" :key="item.id">
<view class="item-label"> <view class="info-item-box">
{{ item.label }} <view class="item-label">
</view> {{ item.label }}
<view class="info-item-input" @click="onClickItemInfo(item)"> </view>
<!-- 隐藏的text用于测量宽度 --> <view v-if="item.type != 'link'" class="info-item-input" @click="onClickItemInfo(item)">
<text class="text-measure" <!-- 隐藏的text用于测量宽度 -->
:class="{ visibility: item.type == 'time' || item.type == 'select' }">{{ item.value <text class="text-measure"
}}</text> :class="{ visibility: item.type == 'time' || item.type == 'select' }">{{ item.value
}}</text>
<input v-if="item.type == 'text' || item.type == 'digit' || item.type == 'number'" <input v-if="item.type == 'text' || item.type == 'digit' || item.type == 'number'"
:style="{ color: item.textColor ? item.textColor : '#1a1a1a' }" class="text-input" :style="{ color: item.textColor ? item.textColor : '#1a1a1a' }" class="text-input"
:type="item.type" :focus="item.focus" v-model="item.value" @click.stop /> :type="item.type" :focus="item.focus" v-model="item.value" @click.stop />
</view>
<image v-if="item.type != 'link'" class="edit-image" src="/static/image/bill/add-bill/edit.png"
@click="onClickItemInfo(item)">
</image>
</view> </view>
<image class="edit-image" src="/static/image/bill/add-bill/edit.png" @click="onClickItemInfo(item)"> <view v-if="item.type == 'link'" class="info-item-link">
</image> <view class="img-box">
</view> <image class="img w100 h100" :src="item.value.imgUrl || defaultImage"></image>
</view>
<view class="textarea-box flex-1">
<textarea class="textarea w100 h100" name="" v-model="item.value.text" id=""
placeholder="请输入交易商品名称"></textarea>
</view>
<view class="right-input-box flex-align-center">
<!-- <text class="right-text">{{ item.value.number }}</text> -->
<input class="right-text" type="text" v-model="item.value.quantity" @click.stop />
<image class="edit-image" src="/static/image/bill/add-bill/edit.png">
</image>
</view>
</view>
</template>
</view> </view>
</view> </view>
<!-- switch选项列表 --> <!-- switch选项列表 -->
<view class="switch-option-container"> <view class="switch-option-container">
<template v-for="option in data.switchOptions" :key="option.id"> <template v-for="option in data.switchOptions" :key="option.id">
<view v-if="option.isShow()" class="border-bottom" <view v-if="option.isShow()" class="border-bottom"
:class="{ 'no-border-bottom': billData.merchantOption[option.key] && option.isShow() }"> :class="{ 'no-border-bottom': isNoBorderBottom(option) }">
<view class="switch-option"> <view class="switch-option">
<view class="switch-option-text">{{ option.name }}</view> <view class="switch-option-text">{{ option.name }}</view>
<switch v-if="option.isSwitch" color="#1676FE" :checked="billData.merchantOption[option.key]" <switch v-if="option.isSwitch" color="#1676FE" :checked="billData.merchantOption[option.key]"
@ -78,7 +102,7 @@
<view class="service-detail" <view class="service-detail"
v-if="option.key == 'serviceDetail' && billData.merchantOption.serviceDetail && option.isShow()"> v-if="option.key == 'serviceDetail' && billData.merchantOption.serviceDetail && option.isShow()">
<image class="service-detail-image" <image class="service-detail-image"
src="/static/image/bill/add-bill/service-detail-image.png" /> :src="billData.merchantOption.serverDetailInfo.imgUrl || defaultImage" />
<view class="flex-1 over-hidden"> <view class="flex-1 over-hidden">
<view class="service-detail-info info-item-input"> <view class="service-detail-info info-item-input">
<!-- 隐藏的text用于测量宽度 --> <!-- 隐藏的text用于测量宽度 -->
@ -191,7 +215,8 @@
import navBar from '@/components/nav-bar/nav-bar.vue' import navBar from '@/components/nav-bar/nav-bar.vue'
import addBillJson from './add-bill.json' import addBillJson from './add-bill.json'
import DateTimePicker from '@/components/dengrq-datetime-picker/dateTimePicker/index.vue'; import DateTimePicker from '@/components/dengrq-datetime-picker/dateTimePicker/index.vue';
import { stringUtil } from '@/utils/common.js'; import { stringUtil, randomUtil, util, uiUtil } from '@/utils/common.js';
import hotIcon from "@/static/json/hot-icon.json"
import { import {
reactive, reactive,
@ -202,9 +227,11 @@ import {
nextTick nextTick
} from 'vue' } from 'vue'
import { import {
onLoad,
onShow, onShow,
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
// //
const timepopup = ref(null) const timepopup = ref(null)
@ -308,17 +335,23 @@ const keySet = new Set()
classifyTabBar.value.forEach(item => { classifyTabBar.value.forEach(item => {
if (item.itemInfoList) { if (item.itemInfoList) {
item.itemInfoList.forEach(info => { item.itemInfoList.forEach(info => {
if (info.key == "payMethod") {
info.value = "招商银行储蓄卡(0123)"
} else if (info.key == "tagAndNote") {
info.value = "添加"
}
if (info.key && !keySet.has(info.key)) { if (info.key && !keySet.has(info.key)) {
keySet.add(info.key) keySet.add(info.key)
allFieldsList.value.push({ allFieldsList.value.push({
key: info.key, key: info.key,
value: info.value ? info.value : "" value: info.value ? info.value : "",
type: info.type
}) })
} }
}) })
} }
}) })
const defaultImage = "/static/image/bill/add-bill/add-avatar.png"
const data = reactive({ const data = reactive({
navBar: { navBar: {
title: '新增账单', title: '新增账单',
@ -339,11 +372,17 @@ const data = reactive({
startDate: "", startDate: "",
endDate: "", endDate: "",
}, },
storeData: {
imgUrl: "",
name: ""
},
// //
billData: { billData: {
id: "", id: "",
selectId: -1, selectId: -1,
imgUrl: "",
name: '', name: '',
isAdd: false,
money: "", money: "",
orderStatus: '交易成功', orderStatus: '交易成功',
itemInfoList: classifyTabBar.value[0].itemInfoList, itemInfoList: classifyTabBar.value[0].itemInfoList,
@ -376,12 +415,23 @@ let { billData, datePickerData, selectItemInfo } = toRefs(data)
onMounted(() => { onMounted(() => {
billData.value.selectId = 1 billData.value.selectId = 1
randomBillInfo()
}) })
onShow(() => { }) onShow(() => { })
onLoad((option) => {
console.log(option)
uni.$on('addBill', (res) => {
console.log("addBill", res);
billData.value.imgUrl = res.url
billData.value.name = res.name
console.log(billData.value)
});
})
// selectId // selectId
watch(() => billData.value.selectId, (newVal) => { watch(() => billData.value.selectId, (newVal, oldVal) => {
// //
allFieldsList.value.forEach(item => { allFieldsList.value.forEach(item => {
billData.value.itemInfoList.forEach(oldItem => { billData.value.itemInfoList.forEach(oldItem => {
@ -402,7 +452,7 @@ watch(() => billData.value.selectId, (newVal) => {
currentItemInfoList.forEach(item => { currentItemInfoList.forEach(item => {
allFieldsList.value.forEach(oldItem => { allFieldsList.value.forEach(oldItem => {
if (oldItem.key == item.key) { if (oldItem.key == item.key) {
item.value = oldItem.value item.value = item.value || oldItem.value
} }
}) })
}) })
@ -420,7 +470,19 @@ watch(() => billData.value.selectId, (newVal) => {
billData.value.bottomIcons = classifyTabBar.value.find(item => item.selectId == newVal).defaultBottomIcons billData.value.bottomIcons = classifyTabBar.value.find(item => item.selectId == newVal).defaultBottomIcons
console.log("当前分类的底部字段", billData.value.bottomIcons) console.log("当前分类的底部字段", billData.value.bottomIcons)
//
const currentItem = classifyTabBar.value.find(item => item.selectId == newVal)
//
billData.value.isAdd = currentItem.isAdd
//
billData.value.name = currentItem.name || data.storeData.name
if (newVal == 10 || newVal == 11) {
billData.value.imgUrl = currentItem.imageUrl
} else {
billData.value.imgUrl = currentItem.imageUrl || data.storeData.imgUrl
}
setItemInfoValue(newVal) setItemInfoValue(newVal)
}) })
@ -428,73 +490,120 @@ watch(() => billData.value.selectId, (newVal) => {
* 设置每个字段信息值 * 设置每个字段信息值
*/ */
const setItemInfoValue = (id) => { const setItemInfoValue = (id) => {
//
billData.value.itemInfoList.forEach(item => { billData.value.itemInfoList.forEach(item => {
if (item.key == 'orderNumber') {
item.value = randomUtil.randomOrderNumber(28)
} else if (item.type == 'time') {
item.value = randomUtil.randomTime()
console.log("item.type == 'time'", item.value)
} else if (item.key == 'householdNumber') {
item.value = randomUtil.randomOrderNumber(10)
} else if (item.key == 'receiverFullName') {
item.value = `${billData.value.name}有限公司`
} else if (item.key == 'productDescription') {
item.value = `xxxxxxx消费`
}
switch (id) { switch (id) {
case 1:
if (item.key == 'receiverFullName') {
item.value = `${billData.value.name}有限公司`
} else if (item.key == 'productDescription') {
item.value = `xxxxxxx消费`
}
break;
case 2: case 2:
if (item.key == 'productDescription') { if (item.key == 'receiverFullName') {
item.value = `收钱码收款` item.value = `${billData.value.name}(个人)`
} else if (item.key == 'receiverFullName') {
item.value = `***(个人)`
} }
break; break;
case 3:
if (item.key == 'productDescription') {
item.value = `xxxxxxx消费`
}
break;
case 4:
break;
case 5: case 5:
if (item.key == 'transferNote') { if (item.key == 'counterAccount') {
item.value = `报销`
} else if (item.key == 'counterAccount') {
item.value = `${billData.value.name}` item.value = `${billData.value.name}`
} }
break; break;
case 6: case 6:
if (item.key == 'transferNote') { if (item.key == 'counterAccount' && item.value == '') {
item.value = `转账`
} else if (item.key == 'counterAccount') {
item.value = `xxx123654789` item.value = `xxx123654789`
} }
break; break;
case 7: case 7:
break; if (item.key == 'counterAccount' && item.value == '') {
case 8: item.value = `xxx123654789`
if (item.key == 'relatedRecord') {
item.value = `查看关联记录`
} }
break; break;
case 9:
if (item.key == 'relatedRecord') {
item.value = `查看原账单`
}
break;
case 10:
break;
case 11:
break;
case 12:
break;
case 13:
break;
default: default:
break; break;
} }
}) })
} }
/**
* 切换头像
*/
const changeAvatar = () => {
util.goPage("/pages/common/hot-icon/hot-icon" + "?page=addBill")
}
/**
* 随机生成账单信息
*/
const randomBillInfo = () => {
//
billData.value.money = randomUtil.randomMoney()
console.log("allFieldsList", allFieldsList.value)
//
const hotIconList = hotIcon.moneyHotIcon.filter(item => item.classify != '系统图标' && item.classify != '自定义' && item.classify != '银行卡')
if (hotIconList.length > 0) {
const randomIcon = hotIconList[randomUtil.random(0, hotIconList.length - 1)]
billData.value.imgUrl = `/static/image/common/hot-icon/${randomIcon.label}.png`
billData.value.name = randomIcon.name
}
data.storeData = {
imgUrl: billData.value.imgUrl,
name: billData.value.name
}
//
billData.value.itemInfoList.forEach(item => {
if (item.key == 'orderNumber') {
item.value = randomUtil.randomOrderNumber(28)
} else if (item.type == 'time') {
item.value = randomUtil.randomTime()
console.log("item.type == 'time'", item.value)
} else if (item.key == 'householdNumber') {
item.value = randomUtil.randomOrderNumber(10)
} else if (item.key == 'receiverFullName') {
item.value = `${billData.value.name}有限公司`
}
})
console.log("随机生成账单信息", billData.value)
}
/**
* 校验表单数据
*/
const validateBillData = () => {
if (!billData.value.name) {
uiUtil.showError("请输入名称")
return false
}
if (!billData.value.money) {
uiUtil.showError("请输入金额")
return false
}
// itemInfoList
for (let i = 0; i < billData.value.itemInfoList.length; i++) {
const item = billData.value.itemInfoList[i]
if (item.required && !item.value) {
uiUtil.showError(`请输入${item.label}`)
return false
}
}
return true
}
/** /**
* 保存账单 * 保存账单
*/ */
const onRightClick = () => { const onRightClick = () => {
if (!validateBillData()) return
billData.value.id = stringUtil.uuid() billData.value.id = stringUtil.uuid()
console.log("保存", billData.value) console.log("保存", billData.value)
} }
@ -543,6 +652,14 @@ const onSwitchChange = (e, option) => {
} }
} }
/**
* 判断是否需要移除下边框
*/
const isNoBorderBottom = (option) => {
const keys = ['serviceDetail', 'recommendService', 'serviceRecommend'];
return billData.value.merchantOption[option.key] && keys.includes(option.key);
}
/** /**
* 格式化时间 * 格式化时间
*/ */
@ -618,6 +735,14 @@ page {
overflow: hidden; overflow: hidden;
overflow-x: scroll; overflow-x: scroll;
&::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
.tab-item { .tab-item {
display: inline-block; display: inline-block;
padding: 12rpx 24rpx; padding: 12rpx 24rpx;
@ -664,11 +789,13 @@ page {
.avatar-image { .avatar-image {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%;
} }
} }
.main-info { .main-info {
color: var(--text-color); color: #1a1a1a;
font-size: 64rpx;
.order-status-info { .order-status-info {
font-size: 28rpx; font-size: 28rpx;
@ -694,6 +821,41 @@ page {
font-size: 26rpx; font-size: 26rpx;
} }
} }
.info-item-link {
display: flex;
align-items: center;
padding: 12rpx 16rpx;
background-color: #F6F7FB;
border-radius: 20rpx 20rpx 20rpx 20rpx;
.img-box {
width: 88rpx;
height: 88rpx;
border-radius: 8rpx;
margin-right: 22rpx;
background-color: #E8EDF2;
}
.textarea-box {
height: 40px;
.textarea {
font-size: 26rpx;
color: #1a1a1a;
}
}
.right-input-box {
.right-text {
color: #969696;
font-size: 22rpx;
max-width: 60rpx;
text-align: right;
margin-left: 8rpx;
}
}
}
} }
.info-item-box { .info-item-box {
@ -751,7 +913,7 @@ page {
} }
.money { .money {
margin-top: 14rpx; // margin-top: 14rpx;
} }
.switch-option-container { .switch-option-container {

View File

@ -0,0 +1,369 @@
<template>
<view class="container" v-if="!isOpenCropper">
<!-- 自定义头部导航栏 -->
<navBar :title="data.navbar.title" :bgColor="data.navbar.bgColor" :isBack="true" isRightButton
@right-click="submitBtn">
</navBar>
<view class="icon-box">
<view class="list group" v-for="(group, index) in data.showHotListIcon" :key="index">
<view class="title">
{{ group.classify }}
</view>
<view class="title-box"></view>
<view class="item" v-for="(item, idx) in group.data" :key="idx"
:class="{ active: data.activeId === item.selectId }" @click="onSelect(item)">
<image class="" :src="item.url" mode=""></image>
<view class="" v-if="item.name">
{{ item.name }}
</view>
</view>
</view>
</view>
</view>
<view v-if="isOpenCropper">
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</view>
</template>
<script setup>
//
import navBar from '@/components/nav-bar/nav-bar.vue'
import hotIcon from "@/static/json/hot-icon.json"
import {
reactive,
toRefs
} from "vue";
import {
onLoad,
onShow,
onReady,
onPullDownRefresh,
onReachBottom
} from "@dcloudio/uni-app";
const data = reactive({
navbar: {
title: "热门图标",
bgColor: '#EDEDED',
},
isOpenCropper: false,
activeId: '',
hotIconList: [],
fromPage: "addBill",
activeData: "",
showHotListIcon: []
})
let {
isOpenCropper
} = toRefs(data)
onLoad((option) => {
if (option.page) {
data.fromPage = option.page
}
data.hotIconList = []
console.log(data.hotIconList, hotIcon.moneyHotIcon)
hotIcon.moneyHotIcon.forEach((i, index) => {
data.hotIconList.push({
selectId: index,
name: i.name,
url: `/static/image/common/hot-icon/${i.label}.png`,
isUpLoad: i.isUpLoad,
classify: i.classify
})
})
console.log(data.hotIconList)
getImage()
})
/**
* @param {Object} item
* 选择图片
*/
function onSelect(item) {
if (item.isUpLoad) {
data.activeId = ''
data.activeData = ''
isOpenCropper.value = true
// chooseImage()
} else {
console.log(item);
data.activeId = item.selectId
data.activeData = item
}
}
/**
* @param {Object} e
* 图片裁剪完成
*/
async function handleCrop(e) {
console.log(e);
const savedFilePath = await saveAndDisplayImage(e.tempFilePath)
let arr = [{
selectId: generateAlphanumericCode(10),
name: "",
url: savedFilePath,
isUpLoad: false,
classify: "自定义"
}]
console.log("图片储存成功", arr)
//
data.hotIconList = insertAfterFirstInPlace(data.hotIconList, arr);
await addImg(arr[0])
formatJson()
isOpenCropper.value = false
}
const formatJson = () => {
const groupedMap = new Map();
data.hotIconList.forEach(item => {
if (!groupedMap.has(item.classify)) {
groupedMap.set(item.classify, {
classify: item.classify,
data: []
});
}
groupedMap.get(item.classify).data.push(item);
});
const list = Array.from(groupedMap.values()).map((group, index) => {
return {
classify: group.classify,
data: group.data.map((item, idx) => {
return item
})
}
})
data.showHotListIcon = list
console.log("分组后的json", list);
}
/**
* 获取图片
*/
const getImage = () => {
//
// console.log("", res)
// const imgArr = res
// console.log(imgArr)
// imgArr.forEach(i => {
// i.selectId = generateAlphanumericCode()
// i.classify = i.classify ? i.classify : ''
// })
// insertAfterFirstInPlace(data.hotIconList, imgArr);
formatJson()
}
//
function generateAlphanumericCode(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({
length
}, () =>
chars.charAt(Math.floor(Math.random() * chars.length))
).join('');
}
/**
* 新增图片
*/
const addImg = (data) => {
// //
// let sql = `INSERT INTO image (url,type,name) VALUES ('${data.url}','moneyHotIcon','${data.name}')`;
// proxy.$sqliteApi.addTable(sql).then((res) => {
// console.log("", res)
// }).catch(e => {
// console.log("", e)
// })
}
/**
* 从本地选择图片
*/
function chooseImage() {
uni.chooseImage({
count: 9, // 9
sizeType: ['original', 'compressed'], //
sourceType: ['album'], //
success: (res) => {
res.tempFilePaths.forEach(async item => {
const savedFilePath = await saveAndDisplayImage(item)
let arr = [{
selectId: generateAlphanumericCode(10),
name: "",
url: savedFilePath,
isUpLoad: item.isUpLoad
}]
console.log("图片储存成功", arr)
//
data.hotIconList = insertAfterFirstInPlace(data.hotIconList, arr);
formatJson()
addImg(arr[0])
return obj
})
},
fail: (err) => {
console.error('选择图片失败:', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
};
/**
* @param {Object} file
* 保存图片到本地
*/
async function saveAndDisplayImage(file) {
try {
//
const {
savedFilePath
} = await uni.saveFile({
tempFilePath: file
})
return savedFilePath
} catch (error) {
console.error('保存图片失败:', error)
return ""
}
}
onReady(() => {
})
onShow(() => { })
onPullDownRefresh(() => {
setTimeout(() => {
uni.stopPullDownRefresh();
}, 1000);
})
onReachBottom(() => {
})
function insertAfterFirstInPlace(mainArray, newArray) {
console.log("mainArray", mainArray)
console.log("newArray", newArray)
mainArray.splice(1, 0, ...newArray);
return mainArray;
}
function submitBtn() {
if (data.activeId == '') {
uni.showToast({
icon: "none",
title: "请选择图标"
})
} else {
console.log("来自页面", data.fromPage);
uni.navigateBack({
delta: 1,
success: () => {
//
setTimeout(() => {
uni.$emit(data.fromPage, data.activeData);
}, 100);
}
})
}
}
</script>
<style lang="scss">
page {
background-color: #ededed;
}
.container {
padding-bottom: calc(8rpx + constant(safe-area-inset-bottom)); // IOS<11.2
padding-bottom: calc(8rpx + env(safe-area-inset-bottom)); // IOS>11.2
.icon-box {
background-color: #fff;
padding: 12px 0;
padding-top: 2px;
margin: 0 24rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
margin-bottom: 8px;
}
}
.list {
display: flex;
flex-wrap: wrap;
padding: 0 2px;
.item {
background-color: #fff;
width: calc(25% - 20px);
height: 75px;
border-radius: 7px 7px 7px 7px;
border: 1px solid #E8E8E8;
margin: 9px 10px;
// margin-top: 16px;
text-align: center;
font-size: 10px;
color: #1A1A1A;
image {
width: 42px;
height: 42px;
margin-top: 10px;
border-radius: 6px;
}
}
.active {
background: #BCEDD3;
border: 1px solid #07C160;
}
}
.group {
position: relative;
margin-top: 12px;
.title {
position: absolute;
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 16px;
color: #2C2C2C;
z-index: 2;
margin-left: 12px;
}
.title-box {
width: 100%;
height: 24px;
}
}
.group::before {
position: absolute;
content: "";
width: 41px;
height: 8px;
left: 6px;
background: linear-gradient(90deg, #9AC8FF 0%, rgba(42, 255, 195, 0) 67.86%);
border-radius: 10px 10px 10px 10px;
z-index: 0;
transform: rotate(345deg);
opacity: 0.7;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

413
static/json/hot-icon.json Normal file
View File

@ -0,0 +1,413 @@
{
"moneyHotIcon": [
{
"name": "本地上传",
"label": "add",
"isUpLoad": true,
"classify": "自定义"
},
{
"name": "默认",
"label": "default",
"classify": "系统图标"
},
{
"name": "商品",
"label": "shangpin",
"classify": "系统图标"
},
{
"name": "花呗",
"label": "huabei",
"classify": "系统图标"
},
{
"name": "余额宝",
"label": "yuebao",
"classify": "系统图标"
},
{
"name": "退款",
"label": "tuikuan",
"classify": "系统图标"
},
{
"name": "燃气费",
"label": "ranqifei",
"classify": "系统图标"
},
{
"name": "电费",
"label": "dianfei",
"classify": "系统图标"
},
{
"name": "水费",
"label": "shuifei",
"classify": "系统图标"
},
{
"name": "古茗",
"label": "guming",
"classify": "奶茶饮品"
},
{
"name": "一点点",
"label": "yidiandian",
"classify": "奶茶饮品"
},
{
"name": "蜜雪冰城",
"label": "mixuebingcheng",
"classify": "奶茶饮品"
},
{
"name": "茶百道",
"label": "chabaidao",
"classify": "奶茶饮品"
},
{
"name": "星巴克",
"label": "xingbake",
"classify": "奶茶饮品"
},
{
"name": "瑞幸咖啡",
"label": "ruixingkafei",
"classify": "奶茶饮品"
},
{
"name": "奈雪的茶",
"label": "naixuedecha",
"classify": "奶茶饮品"
},
{
"name": "麦当劳",
"label": "maidanglao",
"classify": "美食"
},
{
"name": "塔斯汀",
"label": "tasiting",
"classify": "美食"
},
{
"name": "肯德基",
"label": "kendeji",
"classify": "美食"
},
{
"name": "华莱士",
"label": "hualaishi",
"classify": "美食"
},
{
"name": "必胜客",
"label": "bishengke",
"classify": "美食"
},
{
"name": "汉堡王",
"label": "hanbaowang",
"classify": "美食"
},
{
"name": "海底捞",
"label": "haidilao",
"classify": "美食"
},
{
"name": "正新鸡排",
"label": "zhengxinjipai",
"classify": "美食"
},
{
"name": "米村拌饭",
"label": "micunbanfan",
"classify": "美食"
},
{
"name": "煎饼道",
"label": "jianbingdao",
"classify": "美食"
},
{
"name": "周黑鸭",
"label": "zhouheiya",
"classify": "美食"
},
{
"name": "沙县小吃",
"label": "shaxianxiaochi",
"classify": "美食"
},
{
"name": "淘宝",
"label": "taobao",
"classify": "购物"
},
{
"name": "抖音商城",
"label": "douyinshangcheng",
"classify": "购物"
},
{
"name": "拼多多",
"label": "pinduoduo",
"classify": "购物"
},
{
"name": "京东",
"label": "jingdong",
"classify": "购物"
},
{
"name": "饿了么",
"label": "eleme",
"classify": "购物"
},
{
"name": "闲鱼",
"label": "xianyu",
"classify": "购物"
},
{
"name": "转转",
"label": "zhuanzhuan",
"classify": "购物"
},
{
"name": "阿里巴巴",
"label": "1688",
"classify": "购物"
},
{
"name": "美团",
"label": "meituan",
"classify": "购物"
},
{
"name": "美团外卖",
"label": "meituanwaimai",
"classify": "购物"
},
{
"name": "大众点评",
"label": "dazhongdianping",
"classify": "购物"
},
{
"name": "唯品会",
"label": "weipinhui",
"classify": "购物"
},
{
"name": "天猫",
"label": "tianmao",
"classify": "购物"
},
{
"name": "盒马鲜生",
"label": "hemaxiansheng",
"classify": "购物"
},
{
"name": "百果园",
"label": "baiguoyuan",
"classify": "购物"
},
{
"name": "711",
"label": "711",
"classify": "购物"
},
{
"name": "罗森",
"label": "luosen",
"classify": "购物"
},
{
"name": "卖德龙",
"label": "maidelong",
"classify": "购物"
},
{
"name": "永辉超市",
"label": "yonghuichaoshi",
"classify": "购物"
},
{
"name": "叮咚买菜",
"label": "dingdongmaicai",
"classify": "购物"
},
{
"name": "山姆超市",
"label": "shanmuchaoshi",
"classify": "购物"
},
{
"name": "高德地图",
"label": "gaodeditu",
"classify": "出行"
},
{
"name": "哈啰",
"label": "haluo",
"classify": "出行"
},
{
"name": "滴滴出行",
"label": "didichuxing",
"classify": "出行"
},
{
"name": "花小猪",
"label": "huaxiaozhu",
"classify": "出行"
},
{
"name": "猫眼电影",
"label": "maoyandianying",
"classify": "娱乐"
},
{
"name": "淘票票",
"label": "taopiaopiao",
"classify": "娱乐"
},
{
"name": "电竞网吧",
"label": "dianjingwangba",
"classify": "娱乐"
},
{
"name": "虎牙直播",
"label": "huyazhibo",
"classify": "娱乐"
},
{
"name": "王者荣耀",
"label": "wangzherongyao",
"classify": "游戏"
},
{
"name": "金铲铲",
"label": "jinchanchan",
"classify": "游戏"
},
{
"name": "原神",
"label": "yuanshen",
"classify": "游戏"
},
{
"name": "燕云十六声",
"label": "yanyunshiliusheng",
"classify": "游戏"
},
{
"name": "无畏契约",
"label": "wuweiqiyue",
"classify": "游戏"
},
{
"name": "三角洲行动",
"label": "sanjiaozhou",
"classify": "游戏"
},
{
"name": "哔哩哔哩",
"label": "bilibili",
"classify": "娱乐"
},
{
"name": "腾讯视频",
"label": "tengxunshipin",
"classify": "娱乐"
},
{
"name": "爱奇艺",
"label": "aiqiyi",
"classify": "娱乐"
},
{
"name": "优酷",
"label": "youku",
"classify": "娱乐"
},
{
"name": "芒果TV",
"label": "mangguoTV",
"classify": "娱乐"
},
{
"name": "抖音",
"label": "douyin",
"classify": "娱乐"
},
{
"name": "招商银行",
"label": "zhaoshangyinhang",
"classify": "银行卡"
},
{
"name": "建设银行",
"label": "jiansheyinhang",
"classify": "银行卡"
},
{
"name": "工商银行",
"label": "gongshangyinhang",
"classify": "银行卡"
},
{
"name": "农业银行",
"label": "nongyeyinhang",
"classify": "银行卡"
},
{
"name": "储蓄银行",
"label": "chuxvyinhang",
"classify": "银行卡"
},
{
"name": "交通银行",
"label": "jiaotongyinhang",
"classify": "银行卡"
},
{
"name": "中国银行",
"label": "zhongguoyinhang",
"classify": "银行卡"
},
{
"name": "浦发银行",
"label": "pufayinhang",
"classify": "银行卡"
},
{
"name": "云上贵州",
"label": "yunshangguizhou",
"classify": "其它"
}
],
"IncomeBillItem": [
{
"name": "微信红包",
"label": "hongbao"
},
{
"name": "退款",
"label": "tuikuan"
},
{
"name": "二维码收款",
"label": "qianbao"
},
{
"name": "转账",
"label": "zhuanzhang"
}
]
}

View File

@ -0,0 +1,72 @@
## 2.2.52024-07-30
* 修复 当 checkRange=true 时,拖动四个伸缩角放大图片时还可能会超出或未到边界的问题
* 修复 当 checkRange=false 时,图片旋转时会放大图片适应裁剪尺寸的问题
* 修复 当 checkRange=true 时,图片旋转 90° 或 270° 进行缩放可能会无法拖动图片的问题
## 2.2.42024-06-21
* 新增 reverseRotatable 属性,是否支持逆向翻转
* 修复 `2.1.7` 版本导致旋转后图片没有自动适配裁剪框的问题
## 2.2.32024-06-21
* 新增 gpu 属性,是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题
* 修复 组件使用 `v-if` 并设置 `src` 属性时可能会出现图片渲染位置存在偏差的问题
## 2.2.22024-06-21
* 优化 组件实例 chooseImage 方法支持传参
* 修复 组件使用 `v-if` 时组件无非正常渲染的问题
## 2.2.12024-06-15
* 修复 H5平台不支持手势拖动图片的问题
## 2.2.02024-05-31
* 修复 APP平台 `vue2` 项目因 `2.1.9` 版本修复 `vue3` 项目bug而引发的问题
## 2.1.92024-05-29
* 修复 APP平台 `vue3` 项目因 uniapp `renderjs` 中未支持条件编译导致运行了H5平台代码报错的问题
## 2.1.82024-05-29
* 新增 zIndex 属性,调整组件层级
* 新增 组件内容插槽
* 优化 微信小程序平台动态修改元素style时的多余内容
## 2.1.72024-05-28
* 新增 checkRange 属性,当 checkRange=false 时允许图片位置超出裁剪边界
* 新增 minScale 属性,图片最小缩放倍数,当 minScale<0 时可使图片宽高不再受裁剪区域宽高限制
* 新增 backgroundColor 属性,生成图片背景色,如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块
* 优化 动态修改图片宽高但没有传入src时尺寸适应问题
* 修复 APP平台通过 `this.$ownerInstance` 获取组件实例时机过早,其值为 `undefined` 导致报错界面没有正常渲染的问题
## 2.1.62023-04-16
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
## 2.1.52023-04-15
* 新增 兼容APP平台
## 2.1.42023-03-13
* 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
* 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制1365*1365则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
* 优化 旋转图标指示方向与实际旋转方向不符
## 2.1.32023-02-06
* 优化 vue3支持
## 2.1.22023-02-03
* 新增 navigation 属性H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
* 修复 H5平台部分设备已知iPhone11以下机型拍照的图片缩放时会闪动的问题
## 2.1.12022-12-06
* 修复 横屏适配问题
## 2.1.02022-12-06
* 新增 兼容H5平台使用 renderjs 响应手势事件
## 2.0.02022-12-05
* 重构 插件,使用 WXS 响应手势事件
* 新增 图片翻转
* 新增 拉伸裁剪框放大图片
* 新增 监听PC鼠标滚轮触发缩放
* 新增 圆形、圆角矩形的图片裁剪
* 优化 图片缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
* 优化 裁剪框样式
* 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
* 优化 生成图片使用新版 Canvas 2D 接口

View File

@ -0,0 +1,855 @@
/**
* 图片编辑器-手势监听
* 1. 支持编译到app-vueuni-app 2.5.5及以上版本H5上
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/** 元素ID */
var elIds = {
'imageStyles': 'crop-image',
'maskStylesList': 'crop-mask-block',
'borderStyles': 'crop-border',
'circleBoxStyles': 'crop-circle-box',
'circleStyles': 'crop-circle',
'gridStylesList': 'crop-grid',
'angleStylesList': 'crop-angle',
}
/** 记录上次初始化时间戳排除APP重复更新 */
var timestamp = 0;
/** vue3 renderjs 条件编译无效,以此方式区别 APP 和 H5 */
// #ifdef H5
var platform = 'H5';
// #endif
// #ifdef APP
var platform = 'APP';
// #endif
/** 容错值 */
var fault = 0.000001;
/**
* 获取ab两数中的最小正数
* @param a
* @param b
*/
function minimum(a, b) {
if (a > 0 && b < 0) return a;
if (a < 0 && b > 0) return b;
if (a > 0 && b > 0) return Math.min(a, b);
return 0;
}
/**
* 在容错访问内获取n近似值
* @param n
*/
function num(n) {
var m = parseFloat((n).toFixed(6));
return m === fault || m === -fault ? 0 : m;
}
/**
* 比较a值在容错值范围内是否等于b值
* @param a
* @param b
*/
function equalsByFault(a, b) {
return Math.abs(a - b) <= fault;
}
/**
* 比较a值在容错值范围内是否小于b值
* @param a
* @param b
*/
function lessThanByFault(a, b) {
var c = a - b;
return c < 0 ? c < -fault : c < fault;
}
/**
* 验证并获取有效最大值
* @param v
* @param max
* @param isInclude
* @param x
* @param y
* @param rate
* @returns
*/
function validMax(v, max, isInclude, x, y, rate) {
if(typeof max === 'number') {
if(isInclude && equalsByFault(max, y)) { // 宽高不等时x轴用y轴值要做等比例转换
var n = num(max * rate);
if (n <= x) return n; // 转化后值在x轴最大值范围内
return x; // 转化后值超出x轴最大值范围则用最大值
}
return max;
}
return v;
}
/**
* 样式对象转字符串
* @param {Object} style 样式对象
*/
function styleToString(style) {
if(typeof style === 'string') return style;
var str = '';
for (let k in style) {
str += k + ':' + style[k] + ';';
}
return str;
}
/**
*
* @param {Object} instance 页面实例对象
* @param {Object} key 要修改样式的key
* @param {Object|Array} style 样式
*/
function setStyle(instance, key, style) {
// console.log('setStyle', instance, key, JSON.stringify(style))
// #ifdef APP-PLUS
if(platform === 'APP') {
if(Object.prototype.toString.call(style) === '[object Array]') {
for (var i = 0, len = style.length; i < len; i++) {
var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
el && (el.style = styleToString(style[i]));
}
} else {
var el = window.document.getElementById(elIds[key]);
el && (el.style = styleToString(style));
}
}
// #endif
// #ifdef H5
if(platform === 'H5') instance[key] = style;
// #endif
}
/**
* 触发页面实例指定方法
* @param {Object} instance 页面实例对象
* @param {Object} name 方法名称
* @param {Object} obj 传递参数
*/
function callMethod(instance, name, obj) {
// #ifdef APP-PLUS
if(platform === 'APP') instance.callMethod(name, obj);
// #endif
// #ifdef H5
if(platform === 'H5') instance[name](obj);
// #endif
}
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
}
/**
* 旋转90°或270°时检查边界限制 xy 拖动范围禁止滑出边界
* @param {Object} e 点坐标
* @param {Object} xReverse x是否反向
* @param {Object} yReverse y是否反向
*/
function checkRotateRange(e, xReverse, yReverse) {
var o = num((img.height - img.width) / 2); // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
};
}
/**
* 检查边界限制 xy 拖动范围禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
if (area.width === area.height) {
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
if (img.width < area.height || img.height < area.width) {
if (area.width < area.height && img.width < img.height) {
return isInclude
? checkRotateRange(e, area.width < area.height, area.width < area.height)
: checkRotateRange(e, false, true);
}
if (area.height < area.width && img.height < img.width) {
return isInclude
? checkRotateRange(e, area.height < area.width, area.height < area.width)
: checkRotateRange(e, true, false);
}
}
if (img.height >= area.width && img.width >= area.height) {
return checkRotateRange(e, false, false);
}
if (isInclude) {
return area.height < area.width
? checkRotateRange(e, true, true)
: checkRotateRange(e, area.width < area.height, area.width < area.height);
}
if (img.height < area.width && !img.width < area.height) {
return checkRotateRange(e, true, false);
}
if (!img.height < area.width && img.width < area.height) {
return checkRotateRange(e, false, true);
}
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
}
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
// console.log('changeImageRect', e)
offset.x += e.x || 0;
offset.y += e.y || 0;
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// 因频繁修改 width/height 会造成大量的内存消耗改为scale
// e.instance.imageStyles = {
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
// };
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
// e.instance.imageStyles = {
// width: img.oldWidth + 'px',
// height: img.oldHeight + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
// };
setStyle(e.instance, 'imageStyles', {
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px' + ') rotate(' + rotate +'deg) scale(' + scale + ')'
});
callMethod(e.instance, 'dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// console.log('changeAreaRect', e)
// 变更蒙版样式
setStyle(e.instance, 'maskStylesList', [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
]);
// 变更边框样式
if(area.showBorder) {
setStyle(e.instance, 'borderStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
setStyle(e.instance, 'gridStylesList', [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更四个伸缩角样式
if(area.showAngle) {
setStyle(e.instance, 'angleStylesList', [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更圆角样式
if(area.radius > 0) {
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
setStyle(e.instance, 'circleBoxStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
setStyle(e.instance, 'circleStyles', {
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
// console.log('scaleImage', e)
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = num(img.oldWidth * scale);
img.height = num(img.oldHeight * scale);
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = num((e.x - offset.x) * (1 - scale / last));
e.y = num((e.y - offset.y) * (1 - scale / last));
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=1=左上2=右上3=左下4=右下
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
var oy = sys.navigation ? 0 : sys.windowTop;
if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
function getTouchs(touches) {
var result = [];
var len = touches ? touches.length : 0
for (var i = 0; i < len; i++) {
result[i] = {
pageX: touches[i].pageX,
// h5无标题栏时窗口顶部距离仍为标题栏高度且触摸点y轴坐标还是有标题栏的值即减去标题栏高度的值
pageY: touches[i].pageY + sys.windowTop
};
}
return result;
};
var mouseEvent = false;
export default {
data() {
return {
imageStyles: {},
maskStylesList: [{}, {}, {}, {}],
borderStyles: {},
gridStylesList: [{}, {}, {}, {}],
angleStylesList: [{}, {}, {}, {}],
circleBoxStyles: {},
circleStyles: {}
}
},
created() {
// 监听 PC 端鼠标滚轮
// #ifdef H5
platform === 'H5' && window.addEventListener('mousewheel', async (e) => {
var touchs = getTouchs([e])
img.src && scaleImage({
instance: await this.getInstance(),
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.deltaY > 0 ? -0.05 : 0.05,
x: touchs[0].pageX,
y: touchs[0].pageY
});
});
// #endif
},
// #ifdef H5
mounted() {
platform === 'H5' && this.initH5Events();
},
// #endif
setPlatform(p) {
platform = p;
},
methods: {
// #ifdef H5
getTouchEvent(e) {
e.touches = [
{ pageX: e.pageX, pageY: e.pageY }
];
return e;
},
initH5Events() {
const preview = document.getElementById('pic-preview');
preview?.addEventListener('mousedown', (e, ev) => {
mouseEvent = true;
this.touchstart(this.getTouchEvent(e));
});
preview?.addEventListener('mousemove', (e) => {
if (!mouseEvent) return;
this.touchmove(this.getTouchEvent(e));
});
preview?.addEventListener('mouseup', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
preview?.addEventListener('mouseleave', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
},
// #endif
async getInstance() {
// #ifdef APP-PLUS
if(platform === 'APP')
return this.$ownerInstance
? Promise.resolve(this.$ownerInstance)
: new Promise((resolve) => {
setTimeout(async () => {
resolve(await this.getInstance());
});
});
// #endif
// #ifdef H5
if(platform === 'H5')
return Promise.resolve(this);
// #endif
},
/**
* 初始化观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: async function(newVal, oldVal, o, i) {
// console.log('initObserver', newVal, oldVal, o, i)
if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
timestamp = newVal.timestamp;
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
const instance = await this.getInstance()
img.src && changeImageRect({
instance,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance
});
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = getTouchs(e.touches);
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', e, activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: async function(e, o) {
if(!img.src) return;
// console.log('touchmove', e, o)
e.touches = getTouchs(e.touches);
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = num(area.width * (1 - area.minScale));
var maxY = num(area.height * (1 - area.minScale));
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
var r = rotate / 90 % 2;
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
var isInclude = xCompare && yCompare;
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
switch(activeAngle) {
case 1: // 左上角
x = num(x + areaOffset.left);
y = num(y + areaOffset.top);
if(x >= 0 && y >= 0) { // 有效滑动
var t = num(offset.y + m - area.top);
var l = num(offset.x - m - area.left);
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x = num(x + areaOffset.right);
y = num(y + areaOffset.top);
if(x <= 0 && y >= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var t = num(offset.y + m - area.top);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((t >= 0) || (l >= 0))
? minimum(t, l)
: false;
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(-y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += num(x + areaOffset.left);
y += num(y + areaOffset.bottom);
if(x >= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.width : img.height);
var t = num(area.bottom - m - offset.y - w);
var l = num(offset.x - m - area.left);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(-y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x = num(x + areaOffset.right);
y = num(y + areaOffset.bottom);
if(x <= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var h = (r === 1 ? img.width : img.height);
var t = num(area.bottom - offset.y - h - m);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: await this.getInstance(),
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: await this.getInstance(),
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: await this.getInstance(),
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: async function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: await this.getInstance(),
});
scaleImage({
instance: await this.getInstance(),
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: await this.getInstance(),
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: async function(r) {
rotate = (rotate + (r || 90)) % 360;
if(img.minScale >= 1 && area.checkRange) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = area.width / img.oldHeight;
}
if(minScale !== 1) {
scaleImage({
instance: await this.getInstance(),
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: await this.getInstance(),
check: true,
x: -ox - oy,
y: -oy + ox
});
},
rotateImage90: function() {
this.rotateImage(90)
},
rotateImage270: function() {
this.rotateImage(270)
},
}
}

View File

@ -0,0 +1,743 @@
<template>
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`
}"></canvas>
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`
}"></canvas>
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
</view>
<block v-if="showGrid">
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
</block>
<block v-if="showAngle">
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
<view :style="[{
width: `${angleSize}px`,
height: `${angleSize}px`
}]"></view>
</view>
</block>
</view>
<slot />
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
</view>
<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
<block v-else-if="!!imgSrc">
<view class="rechoose" @click="chooseImage">重选</view>
<button class="button" size="mini" @click="cropClick">确定</button>
</block>
<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
</view>
</view>
</template>
<!-- #ifdef APP-VUE -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
// vue3 app renderjs
cropper.setPlatform('APP');
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef H5 -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-QQ -->
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
<!-- #endif -->
<script>
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
const AREA_SIZE = 75;
/** 图片默认宽高 */
const IMG_SIZE = 300;
export default {
name:"qf-image-cropper",
// #ifdef MP-WEIXIN
options: {
// 使 class
styleIsolation: "isolated"
},
// #endif
props: {
/** 图片资源地址 */
src: {
type: String,
default: ''
},
/** 裁剪宽度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
width: {
type: Number,
default: IMG_SIZE
},
/** 裁剪高度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
height: {
type: Number,
default: IMG_SIZE
},
/** 是否绘制裁剪区域边框 */
showBorder: {
type: Boolean,
default: true
},
/** 是否绘制裁剪区域网格参考线 */
showGrid: {
type: Boolean,
default: true
},
/** 是否展示四个支持伸缩的角 */
showAngle: {
type: Boolean,
default: true
},
/** 裁剪区域最小缩放倍数 */
areaScale: {
type: Number,
default: 0.3
},
/** 图片最小缩放倍数 */
minScale: {
type: Number,
default: 1
},
/** 图片最大缩放倍数 */
maxScale: {
type: Number,
default: 5
},
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
checkRange: {
type: Boolean,
default: true
},
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
backgroundColor: {
type: String
},
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
bounce: {
type: Boolean,
default: true
},
/** 是否支持翻转 */
rotatable: {
type: Boolean,
default: true
},
/** 是否支持逆向翻转 */
reverseRotatable: {
type: Boolean,
default: false
},
/** 是否支持从本地选择素材 */
choosable: {
type: Boolean,
default: true
},
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
gpu: {
type: Boolean,
default: false
},
/** 四个角尺寸单位px */
angleSize: {
type: Number,
default: 20
},
/** 四个角边框宽度单位px */
angleBorderWidth: {
type: Number,
default: 2
},
zIndex: {
type: [Number, String]
},
/** 裁剪图片圆角半径单位px */
radius: {
type: Number,
default: 0
},
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
fileType: {
type: String,
default: 'png'
},
/**
* 图片从绘制到生成所需时间单位ms
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值 `Canvas 2d` 采用同步绘制需自己把控绘制完成时间
*/
delay: {
type: Number,
default: 1000
},
// #ifdef H5
/**
* 页面是否是原生标题栏
* H5平台当 showAngle true 使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 必须将此值设为 false 否则四个可拉伸角的触发位置会有偏差
* 因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的
*/
navigation: {
type: Boolean,
default: true
}
// #endif
},
emits: ["crop"],
data() {
return {
// id 使 v-for key
maskList: [
{ id: 'crop-mask-block-1' },
{ id: 'crop-mask-block-2' },
{ id: 'crop-mask-block-3' },
{ id: 'crop-mask-block-4' },
],
gridList: [
{ id: 'crop-grid-1' },
{ id: 'crop-grid-2' },
{ id: 'crop-grid-3' },
{ id: 'crop-grid-4' },
],
angleList: [
{ id: 'crop-angle-1' },
{ id: 'crop-angle-2' },
{ id: 'crop-angle-3' },
{ id: 'crop-angle-4' },
],
/** 本地缓存的图片路径 */
imgSrc: '',
/** 图片的裁剪宽度 */
imgWidth: IMG_SIZE,
/** 图片的裁剪高度 */
imgHeight: IMG_SIZE,
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
widthPercent: AREA_SIZE,
/** 裁剪区域最大高度所占屏幕宽度百分比 */
heightPercent: AREA_SIZE,
/** 裁剪区域布局信息 */
area: {},
/** 未被缩放过的图片宽 */
oldWidth: 0,
/** 未被缩放过的图片高 */
oldHeight: 0,
/** 系统信息 */
sys: uni.getSystemInfoSync(),
scaleWidth: 0,
scaleHeight: 0,
rotate: 0,
offsetX: 0,
offsetY: 0,
use2d: false,
canvansWidth: 0,
canvansHeight: 0,
// imageStyles: {},
// maskStylesList: [{}, {}, {}, {}],
// borderStyles: {},
// gridStylesList: [{}, {}, {}, {}],
// angleStylesList: [{}, {}, {}, {}],
// circleBoxStyles: {},
// circleStyles: {},
}
},
computed: {
initData() {
// console.log('initData')
return {
timestamp: new Date().getTime(),
area: {
...this.area,
bounce: this.bounce,
showBorder: this.showBorder,
showGrid: this.showGrid,
showAngle: this.showAngle,
angleSize: this.angleSize,
angleBorderWidth: this.angleBorderWidth,
minScale: this.areaScale,
widthPercent: this.widthPercent,
heightPercent: this.heightPercent,
radius: this.radius,
checkRange: this.checkRange,
zIndex: +this.zIndex || 0,
},
sys: this.sys,
img: {
minScale: this.minScale,
maxScale: this.maxScale,
src: this.imgSrc,
width: this.oldWidth,
height: this.oldHeight,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
gpu: this.gpu,
}
}
},
imgProps() {
return {
width: this.width,
height: this.height,
src: this.src,
}
}
},
watch: {
imgProps: {
handler(val, oldVal) {
//
this.imgWidth = Number(val.width) || IMG_SIZE;
this.imgHeight = Number(val.height) || IMG_SIZE;
let use2d = true;
// #ifndef MP-WEIXIN
use2d = false;
// #endif
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
// use2d = false;
// }
let canvansWidth = this.imgWidth;
let canvansHeight = this.imgHeight;
let size = Math.max(canvansWidth, canvansHeight)
let scalc = 1;
if(size > 1365) {
scalc = 1365 / size;
}
this.canvansWidth = canvansWidth * scalc;
this.canvansHeight = canvansHeight * scalc;
this.use2d = use2d;
this.initArea();
const src = val.src || this.imgSrc;
src && this.initImage(src, oldVal === undefined);
},
immediate: true
},
},
methods: {
/** 提供给wxs调用用来接收图片变更数据 */
dataChange(e) {
// console.log('dataChange', e)
this.scaleWidth = e.width;
this.scaleHeight = e.height;
this.rotate = e.rotate;
this.offsetX = e.x;
this.offsetY = e.y;
},
/** 初始化裁剪区域布局信息 */
initArea() {
// = +
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
// #ifndef H5
this.sys.windowTop = 0;
this.sys.navigation = true;
// #endif
// #ifdef H5
// h5
this.sys.windowTop = this.sys.windowTop || 44;
this.sys.navigation = this.navigation;
// #endif
let wp = this.widthPercent;
let hp = this.heightPercent;
if (this.imgWidth > this.imgHeight) {
hp = hp * this.imgHeight / this.imgWidth;
} else if (this.imgWidth < this.imgHeight) {
wp = wp * this.imgWidth / this.imgHeight;
}
const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
const width = size * wp / 100;
const height = size * hp / 100;
const left = (this.sys.windowWidth - width) / 2;
const right = left + width;
const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
this.area = { width, height, left, right, top, bottom };
this.scaleWidth = width;
this.scaleHeight = height;
},
/** 从本地选取图片 */
chooseImage(options) {
// #ifdef MP-WEIXIN || MP-JD
if(uni.chooseMedia) {
uni.chooseMedia({
...options,
count: 1,
mediaType: ['image'],
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].tempFilePath);
}
});
return;
}
// #endif
uni.chooseImage({
...options,
count: 1,
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].path);
}
});
},
/** 重置数据 */
resetData() {
this.imgSrc = '';
this.rotate = 0;
this.offsetX = 0;
this.offsetY = 0;
this.initArea();
},
/**
* 初始化图片信息
* @param {String} url 图片链接
*/
initImage(url, isFirst) {
uni.getImageInfo({
src: url,
success: async (res) => {
if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve, 50)));
this.imgSrc = res.path;
let scale = res.width / res.height;
let areaScale = this.area.width / this.area.height;
if (scale > 1) { //
if (scale >= areaScale) { //
this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
} else { //
this.scaleHeight = res.height * this.scaleWidth / res.width;
}
} else { //
if (scale <= areaScale) { //
this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
} else { //
this.scaleWidth = res.width * this.scaleHeight / res.height;
}
}
//
this.oldWidth = +this.scaleWidth.toFixed(2);
this.oldHeight = +this.scaleHeight.toFixed(2);
},
fail: (err) => {
console.error(err)
}
});
},
/**
* 剪切图片圆角
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} radius 圆角半径
* @param {Number} scale 生成图片的实际尺寸与截取区域比
* @param {Function} drawImage 执行剪切时所调用的绘图方法入参为是否执行了剪切
*/
drawClipImage(ctx, radius, scale, drawImage) {
if(radius > 0) {
ctx.save();
ctx.beginPath();
const w = this.canvansWidth;
const h = this.canvansHeight;
if(w === h && radius >= w / 2) { //
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
} else { //
if(w !== h) { //
radius = Math.min(w / 2, h / 2, radius);
// radius = Math.min(Math.max(w, h) / 2, radius);
}
ctx.moveTo(radius, 0);
ctx.arcTo(w, 0, w, h, radius);
ctx.arcTo(w, h, 0, h, radius);
ctx.arcTo(0, h, 0, 0, radius);
ctx.arcTo(0, 0, w, 0, radius);
ctx.closePath();
}
ctx.clip();
drawImage && drawImage(true);
ctx.restore();
} else {
drawImage && drawImage(false);
}
},
/**
* 旋转图片
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} rotate 旋转角度
* @param {Number} scale 生成图片的实际尺寸与截取区域比
*/
drawRotateImage(ctx, rotate, scale) {
if(rotate !== 0) {
// 1.
const x = this.scaleWidth * scale / 2;
const y = this.scaleHeight * scale / 2;
ctx.translate(x, y);
// 2.
ctx.rotate(rotate * Math.PI / 180);
// 3.
ctx.translate(-x, -y);
}
},
drawImage(ctx, image, callback) {
//
const scale = this.canvansWidth / this.area.width;
if(this.backgroundColor) {
if(ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
else ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
}
this.drawClipImage(ctx, this.radius, scale, () => {
this.drawRotateImage(ctx, this.rotate, scale);
const r = this.rotate / 90;
ctx.drawImage(
image,
[
(this.offsetX - this.area.left),
(this.offsetY - this.area.top),
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top)
][r] * scale,
[
(this.offsetY - this.area.top),
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top),
(this.offsetX - this.area.left)
][r] * scale,
this.scaleWidth * scale,
this.scaleHeight * scale
);
});
},
/**
* 绘图
* @param {Object} canvas
* @param {Object} ctx canvas 的绘图上下文对象
* @param {String} src 图片路径
* @param {Function} callback 开始绘制时回调
*/
draw2DImage(canvas, ctx, src, callback) {
// console.log('draw2DImage', canvas, ctx, src, callback)
if(canvas) {
const image = canvas.createImage();
image.onload = () => {
this.drawImage(ctx, image);
// ````
callback && setTimeout(callback, this.delay);
};
image.onerror = (err) => {
console.error(err)
uni.hideLoading();
};
image.src = src;
} else {
this.drawImage(ctx, src);
setTimeout(() => {
ctx.draw(false, callback);
}, 200);
}
},
/**
* 画布转图片到本地缓存
* @param {Object} canvas
* @param {String} canvasId
*/
canvasToTempFilePath(canvas, canvasId) {
// console.log('canvasToTempFilePath', canvas, canvasId)
uni.canvasToTempFilePath({
canvas,
canvasId,
x: 0,
y: 0,
width: this.canvansWidth,
height: this.canvansHeight,
destWidth: this.imgWidth, //
destHeight: this.imgHeight, //
fileType: this.fileType, // png
success: (res) => {
//
this.handleImage(res.tempFilePath);
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
}
}, this);
},
/** 确认裁剪 */
cropClick() {
uni.showLoading({ title: '裁剪中...', mask: true });
if(!this.use2d) {
const ctx = uni.createCanvasContext('imgCanvas', this);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(null, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(null, 'imgCanvas');
});
return;
}
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(this);
query.select('#imgCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const dpr = uni.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(canvas);
});
});
// #endif
},
handleImage(tempFilePath){
// H5tempFilePath base64
// console.log(tempFilePath)
uni.hideLoading();
this.$emit('crop', { tempFilePath });
}
}
}
</script>
<style lang="scss" scoped>
.image-cropper {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: #000;
.img-canvas {
position: absolute !important;
transform: translateX(-100%);
}
.pic-preview {
width: 100%;
flex: 1;
position: relative;
.crop-mask-block {
background-color: rgba(51, 51, 51, 0.8);
z-index: 2;
position: fixed;
box-sizing: border-box;
pointer-events: none;
}
.crop-circle-box {
position: fixed;
box-sizing: border-box;
z-index: 2;
pointer-events: none;
overflow: hidden;
.crop-circle {
width: 100%;
height: 100%;
}
}
.crop-image {
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
display: block !important;
backface-visibility: hidden;
}
.crop-border {
position: fixed;
border: 1px solid #fff;
box-sizing: border-box;
z-index: 3;
pointer-events: none;
}
.crop-grid {
position: fixed;
z-index: 3;
border-style: dashed;
border-color: #fff;
pointer-events: none;
opacity: 0.5;
}
.crop-angle {
position: fixed;
z-index: 3;
border-style: solid;
border-color: #fff;
pointer-events: none;
}
}
.fixed-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
flex-direction: row;
background-color: $uni-bg-color-grey;
.action-bar {
position: absolute;
top: -90rpx;
left: 10rpx;
display: flex;
.rotate-icon {
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=');
background-size: 60% 60%;
background-repeat: no-repeat;
background-position: center;
width: 80rpx;
height: 80rpx;
&.is-reverse {
transform: rotateY(180deg);
}
}
}
.rechoose {
color: $uni-color-primary;
padding: 0 $uni-spacing-row-lg;
line-height: 100rpx;
}
.choose-btn {
color: $uni-color-primary;
text-align: center;
line-height: 100rpx;
flex: 1;
}
.button {
margin: auto $uni-spacing-row-lg auto auto;
background-color: $uni-color-primary;
color: #fff;
}
}
.safe-area-inset-bottom {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom); // IOS<11.2
padding-bottom: env(safe-area-inset-bottom); // IOS>=11.2
}
}
</style>

View File

@ -0,0 +1,727 @@
/**
* 图片编辑器-手势监听
* 1. wxs 暂不支持 es6 语法
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上uni-app 2.2.5及以上版本)
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/** 容错值 */
var fault = 0.000001;
/**
* 获取a、b两数中的最小正数
* @param a
* @param b
*/
function minimum(a, b) {
if (a > 0 && b < 0) return a;
if (a < 0 && b > 0) return b;
if (a > 0 && b > 0) return Math.min(a, b);
return 0;
}
/**
* 在容错访问内获取n近似值
* @param n
*/
function num(n) {
var m = parseFloat((n).toFixed(6));
return m === fault || m === -fault ? 0 : m;
}
/**
* 比较a值在容错值范围内是否等于b值
* @param a
* @param b
*/
function equalsByFault(a, b) {
return Math.abs(a - b) <= fault;
}
/**
* 比较a值在容错值范围内是否小于b值
* @param a
* @param b
*/
function lessThanByFault(a, b) {
var c = a - b;
return c < 0 ? c < -fault : c < fault;
}
/**
* 验证并获取有效最大值
* @param v
* @param max
* @param isInclude
* @param x
* @param y
* @param rate
* @returns
*/
function validMax(v, max, isInclude, x, y, rate) {
if(typeof max === 'number') {
if(isInclude && equalsByFault(max, y)) { // 宽高不等时x轴用y轴值要做等比例转换
var n = num(max * rate);
if (n <= x) return n; // 转化后值在x轴最大值范围内
return x; // 转化后值超出x轴最大值范围则用最大值
}
return max;
}
return v;
}
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
}
/**
* 旋转90°或270°时检查边界限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
* @param {Object} xReverse x是否反向
* @param {Object} yReverse y是否反向
*/
function checkRotateRange(e, xReverse, yReverse) {
var o = num((img.height - img.width) / 2); // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
};
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
if (area.width === area.height) {
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
if (img.width < area.height || img.height < area.width) {
if (area.width < area.height && img.width < img.height) {
return isInclude
? checkRotateRange(e, area.width < area.height, area.width < area.height)
: checkRotateRange(e, false, true);
}
if (area.height < area.width && img.height < img.width) {
return isInclude
? checkRotateRange(e, area.height < area.width, area.height < area.width)
: checkRotateRange(e, true, false);
}
}
if (img.height >= area.width && img.width >= area.height) {
return checkRotateRange(e, false, false);
}
if (isInclude) {
return area.height < area.width
? checkRotateRange(e, true, true)
: checkRotateRange(e, area.width < area.height, area.width < area.height);
}
if (img.height < area.width && !img.width < area.height) {
return checkRotateRange(e, true, false);
}
if (!img.height < area.width && img.width < area.height) {
return checkRotateRange(e, false, true);
}
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
};
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
offset.x += e.x || 0;
offset.y += e.y || 0;
var image = e.instance.selectComponent('.crop-image');
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// image.setStyle({
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
// });
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
image.setStyle({
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
});
e.instance.callMethod('dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// 变更蒙版样式
var masks = e.instance.selectAllComponents('.crop-mask-block');
var maskStyles = [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
];
var len = masks.length;
for (var i = 0; i < len; i++) {
masks[i].setStyle(maskStyles[i]);
}
// 变更边框样式
if(area.showBorder) {
var border = e.instance.selectComponent('.crop-border');
border.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
var grids = e.instance.selectAllComponents('.crop-grid');
var gridStyles = [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
];
var len = grids.length;
for (var i = 0; i < len; i++) {
grids[i].setStyle(gridStyles[i]);
}
}
// 变更四个伸缩角样式
if(area.showAngle) {
var angles = e.instance.selectAllComponents('.crop-angle');
var angleStyles = [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
];
var len = angles.length;
for (var i = 0; i < len; i++) {
angles[i].setStyle(angleStyles[i]);
}
}
// 变更圆角样式
if(area.radius > 0) {
var circleBox = e.instance.selectComponent('.crop-circle-box');
var circle = e.instance.selectComponent('.crop-circle');
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
circleBox.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
circle.setStyle({
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = num(img.oldWidth * scale);
img.height = num(img.oldHeight * scale);
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = num((e.x - offset.x) * (1 - scale / last));
e.y = num((e.y - offset.y) * (1 - scale / last));
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=无1=左上2=右上3=左下4=右下;
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
function rotateImage(e, o, r) {
rotate = (rotate + r) % 360;
if(img.minScale >= 1 && area.checkRange) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = area.width / img.oldHeight;
}
if(minScale !== 1) {
scaleImage({
instance: o,
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: o,
check: true,
x: -ox - oy,
y: -oy + ox
});
};
module.exports = {
/**
* 初始化:观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: function(newVal, oldVal, o, i) {
if(newVal) {
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
img.src && changeImageRect({
instance: o,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance: o
});
// console.log('initRect', JSON.stringify(newVal))
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
if(!img.src) return;
scaleImage({
instance: o,
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
x: e.touches[0].pageX,
y: e.touches[0].pageY
});
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = e.touches;
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', JSON.stringify(e), activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: function(e, o) {
if(!img.src) return;
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = num(area.width * (1 - area.minScale));
var maxY = num(area.height * (1 - area.minScale));
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
var r = rotate / 90 % 2;
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
var isInclude = xCompare && yCompare;
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
switch(activeAngle) {
case 1: // 左上角
x = num(x + areaOffset.left);
y = num(y + areaOffset.top);
if(x >= 0 && y >= 0) { // 有效滑动
var t = num(offset.y + m - area.top);
var l = num(offset.x - m - area.left);
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x = num(x + areaOffset.right);
y = num(y + areaOffset.top);
if(x <= 0 && y >= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var t = num(offset.y + m - area.top);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((t >= 0) || (l >= 0))
? minimum(t, l)
: false;
// var max = isInclude && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
// ? minimum(offset.y - area.top, area.right - offset.x - img.width)
// : false;
// console.log(offset.x, offset.y, img.width, img.height, area.top, area.right, m, max)
// console.log(offset.y + m - area.top, area.right + m - offset.x - w)
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(-y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += num(x + areaOffset.left);
y += num(y + areaOffset.bottom);
if(x >= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.width : img.height);
var t = num(area.bottom - m - offset.y - w);
var l = num(offset.x - m - area.left);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(-y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x = num(x + areaOffset.right);
y = num(y + areaOffset.bottom);
if(x <= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var h = (r === 1 ? img.width : img.height);
var t = num(area.bottom - offset.y - h - m);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: o,
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: o,
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: o,
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: o,
});
scaleImage({
instance: o,
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: o,
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: function(e, o) {
rotateImage(e, o, 90);
},
rotateImage90: function(e, o) {
rotateImage(e, o, 90)
},
rotateImage270: function(e, o) {
rotateImage(e, o, 270)
},
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
imageStyles: '',
maskStylesList: ['', '', '', ''],
borderStyles: '',
gridStylesList: ['', '', '', ''],
angleStylesList: ['', '', '', ''],
circleBoxStyles: '',
circleStyles: '',
}

View File

@ -0,0 +1,81 @@
{
"id": "qf-image-cropper",
"displayName": "图片裁剪插件",
"version": "2.2.5",
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
"keywords": [
"qf-image-cropper",
"图片裁剪",
"图片编辑",
"头像裁剪",
"小程序"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "u",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

View File

@ -0,0 +1,97 @@
# qf-image-cropper
## 图片裁剪插件
uniapp微信小程序图片裁剪插件支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### 平台支持:
1. 支持微信小程序移动端、PC端、开发者工具
2. 支持H5平台2.1.0版本起)
3. 支持APP平台2.1.5版本起Android、IOS
4. 其他平台暂未测试兼容性未知
### 支持功能:
1. 自定义裁剪尺寸
2. 定点等比例缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. 裁剪生成新图片
6. 本地选择图片
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
8. 裁剪圆角图片:圆形、圆角矩形
### 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| src | String | | 图片资源地址 |
| width | Number | 300 | 裁剪宽度 |
| height | Number | 300 | 裁剪高度 |
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
| minScale | Number | 1 | 图片最小缩放倍数 |
| maxScale | Number | 5 | 图片最大缩放倍数 |
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
| rotatable | Boolean | true | 是否支持翻转 |
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
| choosable | Boolean | true | 是否支持从本地选择素材 |
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
| angleSize | Number | 20 | 四个角尺寸单位px |
| angleBorderWidth | Number | 2 | 四个角边框宽度单位px |
| zIndex | Number/String | | 调整组件层级 |
| radius | Number | | 裁剪图片圆角半径单位px |
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
| delay | Number | 1000 | 图片从绘制到生成所需时间单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的 |
| @crop | EventHandle | | 剪裁完成后触发event = { tempFilePath }。在H5平台下tempFilePath 为 base64 |
### 基本用法
```
<template>
<div>
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</div>
</template>
<script>
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
methods: {
handleCrop(e) {
uni.previewImage({
urls: [e.tempFilePath],
current: 0
});
}
}
}
</script>
```
通过ref组件实例可在进入页面后直接打开相册选择图片
```
mounted() {
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
}
```
### 使用说明
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
"enablePullDownRefresh": false,
"disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸建议1365x1365以内否则可能会导致如下问题
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
5.如果组件无法正常渲染且使用了 `v-if` 时,可尝试将 `v-if` 替换为 `v-show`
6.如果App端导入组件后无法正常渲染请尝试重新运行

View File

@ -403,12 +403,38 @@ export const randomUtil = {
* @returns {string} 格式化后的时间 * @returns {string} 格式化后的时间
*/ */
randomTime(startTime, endTime, format = 'YYYY-MM-DD HH:mm:ss') { randomTime(startTime, endTime, format = 'YYYY-MM-DD HH:mm:ss') {
const start = new Date(startTime).getTime(); let end;
const end = new Date(endTime).getTime(); if (endTime) {
end = new Date(endTime).getTime();
} else {
end = Date.now();
}
let start;
if (startTime) {
start = new Date(startTime).getTime();
} else {
// 默认为结束时间往前推3个月
const date = new Date(end);
date.setMonth(date.getMonth() - 3);
start = date.getTime();
}
const randomTimestamp = Math.floor(Math.random() * (end - start + 1)) + start; const randomTimestamp = Math.floor(Math.random() * (end - start + 1)) + start;
return dateUtil.format(randomTimestamp, format); return dateUtil.format(randomTimestamp, format);
}, },
/**
* 生成随机金额
* @param {number} min - 最小值
* @param {number} max - 最大值
* @returns {string} 随机金额
*/
randomMoney(min = 0, max = 5000) {
const num = Math.random() * (max - min) + min;
return num.toFixed(2);
},
/** /**
* 随机生成指定位数订单号 * 随机生成指定位数订单号
* @param {number} length - 订单号位数 * @param {number} length - 订单号位数