MrShi
2024-03-08 cf1972fe1e1ec70717d3a9edd63d906090d33e09
mrshi
已添加16个文件
已修改18个文件
5702 ■■■■■ 文件已修改
admin/src/api/business/member.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/business/OperaDeviceRoleWindow.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/components/business/OperaVisitsDesWindow.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/deletePersonnel.vue 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/device.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/deviceRole.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/empower.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/interfaceLog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/internalMember.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/views/business/visits.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/vue.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/App.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/components/cropping/cropping.vue 700 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/components/keyboard-input/keyboard-input.vue 463 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/components/tly-picture-cut/tlyPictureCut.vue 413 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/main.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/package.json 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/pages/userinfo/userinfo.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/pages/visitorApplication/visitorApplication.vue 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/redirect.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/store/index.js 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/changelog.md 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/components/bt-cropper/bt-cropper.vue 1095 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/components/bt-cropper/iconfont.css 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/components/bt-cropper/js/touchs.js 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/components/bt-cropper/utils/tools.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/package.json 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/bt-cropper/readme.md 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/changelog.md 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.render.js 628 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue 692 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.wxs 543 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/package.json 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
h5/uni_modules/qf-image-cropper/readme.md 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin/src/api/business/member.js
@@ -36,6 +36,11 @@
  return request.get(`/business/member/batchBlock?ids=${ids}`)
}
// é‡æ–°æŽˆæƒ
export function roleAuth (id) {
  return request.get(`/business/member/roleAuth/${id}`)
}
// åˆ é™¤
export function deleteById (id) {
  return request.get(`/business/member/delete/${id}`)
admin/src/components/business/OperaDeviceRoleWindow.vue
@@ -2,15 +2,15 @@
    <GlobalWindow
        :title="title"
        :visible.sync="visible"
        width="50%"
        width="900px"
        :confirm-working="isWorking"
        @confirm="confirm"
    >
        <el-form :model="form" ref="form" :rules="rules">
            <el-form-item label="门禁组名称" prop="name">
            <el-form-item label="门禁点分组名称" prop="name">
                <el-input v-model="form.name" placeholder="请输入名称" v-trim/>
            </el-form-item>
            <el-form-item label="默认门禁组" prop="isDefault">
            <el-form-item label="默认门禁点分组" prop="isDefault">
                <el-switch
                    v-model="form.isDefault"
                    active-color="#13ce66"
@@ -27,9 +27,10 @@
                <el-transfer
                    v-if="form.radio === 1"
                    style="margin-top: 15px;"
                    :titles="['未选门禁组', '已选门禁组']"
                    :titles="['未选门禁点', '已选门禁点']"
                    filterable
                    filter-placeholder="请输入门禁组名称"
                    :filter-method="filterMethod"
                    filter-placeholder="请输入门禁点或区域名称"
                    v-model="form.doorIds"
                    :data="device">
                </el-transfer>
@@ -54,7 +55,7 @@
        }
      }
      callback()
    }
    };
    return {
      // è¡¨å•数据
      form: {
@@ -66,6 +67,10 @@
        radio: 0
      },
      device: [],
      filterMethod(query, item) {
        if (!query) return item;
        return item.label.indexOf(query) > -1 || item.area.indexOf(query) > -1;
      },
      // éªŒè¯è§„则
      rules: {
        name: [
@@ -157,7 +162,8 @@
        .then(res => {
          this.device = res.map(item => {
            return {
              label: item.name,
              label: item.doorName,
              area: item.regionPathName,
              key: item.id
            }
          })
@@ -192,6 +198,6 @@
<style>
    .el-transfer-panel {
        width: 350px;
        width: 350px !important;
    }
</style>
admin/src/components/business/OperaVisitsDesWindow.vue
@@ -12,10 +12,12 @@
                <div class="list_item_label">拜访信息</div>
                <div class="list_item_val" v-if="info" style="display: inline-block">
                    <div class="list_item_val_item">拜访对方:{{info.receptMemberName}} - {{info.receptMemberDepartment}}</div>
                    <div class="list_item_val_item">拜访时间:{{info.starttime}} è‡³ {{info.endtime}}</div>
                    <div class="list_item_val_item">预约时间:{{info.starttime}} è‡³ {{info.endtime}}</div>
                    <div class="list_item_val_item" v-if="info.inDate && info.outDate">签到时间:{{info.inDate}} è‡³ {{info.outDate}}</div>
                    <div class="list_item_val_item" v-else>签到时间:-</div>
                    <div class="list_item_val_item">拜访事由:{{info.reason}}</div>
                    <div class="list_item_val_item">申请人员:{{info.name}} {{info.companyName}}</div>
                    <div class="list_item_val_item">申请门禁:{{info.deviceRoleList ? info.deviceRoleList.map(item => item.name).join(' | ') : ''}}</div>
                    <div class="list_item_val_item">申请门禁:{{info.deviceRoleList ? info.deviceRoleList.map(item => item.name).join(' | ') : '-'}}</div>
                    <div class="list_item_val_item">创建时间:{{info.createDate}}</div>
                </div>
                <div class="list_item_val" v-if="info" style="display: inline-block;float: right">
admin/src/views/business/deletePersonnel.vue
@@ -19,10 +19,10 @@
            <!--                <li><el-button type="primary" v-permissions="['business:member:create']" @click="thaws">离场</el-button></li>-->
            <!--            </ul>-->
            <el-table
                    v-loading="isWorking.search"
                    :data="tableData.list"
                    stripe
                    @selection-change="handleSelectionChange"
                v-loading="isWorking.search"
                :data="tableData.list"
                stripe
                @selection-change="handleSelectionChange"
            >
                <!--                <el-table-column type="selection" width="55"></el-table-column>-->
                <el-table-column prop="name" label="姓名" min-width="100px"></el-table-column>
@@ -35,36 +35,38 @@
                        <span v-if="row.type === 2">内部人员</span>
                    </template>
                </el-table-column>
                <el-table-column prop="inDate" label="入场时间" min-width="100px"></el-table-column>
                <el-table-column prop="outDate" label="授权到期时间" min-width="100px"></el-table-column>
                <el-table-column label="超时时长" min-width="100px">
                    <template slot-scope="{row}">
                        <span v-if="row.outStatus === 1">{{row.timeOut.toString().replace('-', '')}}分钟</span>
                        <span v-else>-</span>
                    </template>
                </el-table-column>
<!--                <el-table-column prop="inDate" label="入场时间" min-width="100px"></el-table-column>-->
<!--                <el-table-column prop="outDate" label="授权到期时间" min-width="100px"></el-table-column>-->
<!--                <el-table-column label="超时时长" min-width="100px">-->
<!--                    <template slot-scope="{row}">-->
<!--                        <span v-if="row.outStatus === 1">{{row.timeOut.toString().replace('-', '')}}分钟</span>-->
<!--                        <span v-else>-</span>-->
<!--                    </template>-->
<!--                </el-table-column>-->
                <el-table-column label="状态" min-width="100px">
                    <template slot-scope="{row}">
                        <span v-if="row.outStatus === 0">未超时</span>
                        <span style="color: red;" v-else-if="row.outStatus === 1">已超时</span>
                        <span v-else-if="row.outStatus === 2">即将超时</span>
                        <span v-if="row.hkStatus === 0">未同步</span>
                        <span v-else-if="row.hkStatus === 1">已同步</span>
                        <span v-else-if="row.hkStatus === 2">同步失败</span>
                        <span v-else-if="row.hkStatus === 3">不符合下发条件</span>
                        <span v-else-if="row.hkStatus === 4">等待删除权限</span>
                    </template>
                </el-table-column>
                <el-table-column
                        v-if="containPermissions(['business:member:update', 'business:member:delete'])"
                        label="操作"
                        min-width="120"
                        fixed="right"
                >
                    <template slot-scope="{row}">
                        <el-button type="text" icon="el-icon-edit" v-permissions="['business:member:update']" @click="departure(row.id)">离厂</el-button>
                    </template>
                </el-table-column>
<!--                <el-table-column-->
<!--                        v-if="containPermissions(['business:member:update', 'business:member:delete'])"-->
<!--                        label="操作"-->
<!--                        min-width="120"-->
<!--                        fixed="right"-->
<!--                >-->
<!--                    <template slot-scope="{row}">-->
<!--                        <el-button type="text" icon="el-icon-edit" v-permissions="['business:member:update']" @click="departure(row.id)">离厂</el-button>-->
<!--                    </template>-->
<!--                </el-table-column>-->
            </el-table>
            <pagination
                    @size-change="handleSizeChange"
                    @current-change="handlePageChange"
                    :pagination="tableData.pagination"
                @size-change="handleSizeChange"
                @current-change="handlePageChange"
                :pagination="tableData.pagination"
            >
            </pagination>
        </template>
@@ -95,7 +97,7 @@
    created () {
      this.config({
        module: '人员信息表',
        api: '/business/strandedPersonnel',
        api: '/business/member',
        'field.id': 'id',
        'field.main': 'id'
      })
admin/src/views/business/device.vue
@@ -2,15 +2,11 @@
    <TableLayout :permissions="['business:device:query']">
        <!-- æœç´¢è¡¨å• -->
        <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
            <el-form-item label="门禁名称" prop="name">
                <el-input v-model="searchForm.name" placeholder="请输入门禁名称" @keypress.enter.native="search"></el-input>
            <el-form-item label="门禁点名称" prop="doorName">
                <el-input v-model="searchForm.doorName" placeholder="请输入门禁点名称" @keypress.enter.native="search"></el-input>
            </el-form-item>
            <el-form-item label="门禁等级" prop="level">
                <el-select v-model="searchForm.level" placeholder="请选择门禁等级" @keypress.enter.native="search">
                    <el-option label="一级门禁" value="1"></el-option>
                    <el-option label="二级门禁" value="2"></el-option>
                    <el-option label="三级门禁" value="3"></el-option>
                </el-select>
            <el-form-item label="区域名称" prop="regionPathName">
                <el-input v-model="searchForm.regionPathName" placeholder="请输入区域名称" @keypress.enter.native="search"></el-input>
            </el-form-item>
            <section>
                <el-button type="primary" @click="search">搜索</el-button>
@@ -27,16 +23,12 @@
                :data="tableData.list"
                stripe
            >
                <el-table-column prop="name" label="设备名称" min-width="100px"></el-table-column>
                <el-table-column prop="doorName" label="门禁点名称" min-width="100px"></el-table-column>
                <el-table-column prop="regionPathName" label="区域名称" min-width="100px"></el-table-column>
                <el-table-column prop="doorNo" label="门禁点编号" min-width="100px"></el-table-column>
                <el-table-column prop="manufature" label="厂商" min-width="100px"></el-table-column>
                <el-table-column prop="no" label="设备编号" min-width="100px"></el-table-column>
                <el-table-column prop="name" label="门禁名称" min-width="100px"></el-table-column>
                <el-table-column prop="level" label="门禁等级" min-width="100px"></el-table-column>
                <el-table-column prop="status" label="设备状态" min-width="100px">
                    <template slot-scope="{row}">
                        <span v-if="row.status === 0">禁用</span>
                        <span v-if="row.status === 1">启用</span>
                    </template>
                </el-table-column>
                <el-table-column prop="no" label="设备号" min-width="100px"></el-table-column>
<!--                <el-table-column-->
<!--                    v-if="containPermissions(['business:device:update'])"-->
<!--                    label="操作"-->
@@ -75,8 +67,8 @@
    return {
      // æœç´¢
      searchForm: {
        name: '',
        level: ''
        doorName: '',
        regionPathName: ''
      },
      options: []
    }
admin/src/views/business/deviceRole.vue
@@ -1,9 +1,9 @@
<template>
    <TableLayout :permissions="['business:devicerole:query']">
        <!-- æœç´¢è¡¨å• -->
        <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
            <el-form-item label="门禁组名称" prop="name">
                <el-input v-model="searchForm.name" placeholder="请输入门禁组名称" @keypress.enter.native="search"></el-input>
        <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="110px" inline>
            <el-form-item label="门禁点分组名称" prop="name">
                <el-input v-model="searchForm.name" placeholder="请输入门禁点分组名称" @keypress.enter.native="search"></el-input>
            </el-form-item>
            <section>
                <el-button type="primary" @click="search">搜索</el-button>
@@ -13,7 +13,7 @@
        <!-- è¡¨æ ¼å’Œåˆ†é¡µ -->
        <template v-slot:table-wrap>
            <ul class="toolbar" v-permissions="['business:devicerole:create', 'business:devicerole:delete']">
                <li><el-button type="primary" @click="$refs.operaDeviceRoleWindow.open('新建门禁角色')" icon="el-icon-plus" v-permissions="['business:devicerole:create']">新建</el-button></li>
                <li><el-button type="primary" @click="$refs.operaDeviceRoleWindow.open('新建门禁点分组')" icon="el-icon-plus" v-permissions="['business:devicerole:create']">新建</el-button></li>
                <li><el-button @click="deleteByIdInBatch" icon="el-icon-delete" v-permissions="['business:devicerole:delete']">删除</el-button></li>
            </ul>
            <el-table
@@ -23,7 +23,7 @@
                @selection-change="handleSelectionChange"
            >
                <el-table-column type="selection" width="55"></el-table-column>
                <el-table-column prop="name" label="门禁组名称" min-width="100px"></el-table-column>
                <el-table-column prop="name" label="门禁点分组" min-width="100px"></el-table-column>
                <el-table-column prop="memberNum" label="使用人数" min-width="100px"></el-table-column>
                <el-table-column prop="isDefault" label="是否默认" min-width="100px">
                    <template slot-scope="{row}">
@@ -83,7 +83,7 @@
  },
  created () {
    this.config({
      module: '门禁角色',
      module: '门禁点分组',
      api: '/business/deviceRole',
      'field.id': 'id',
      'field.main': 'name'
@@ -108,7 +108,7 @@
        row.radio = 0
        row.doorIds = []
      }
      this.$refs.operaDeviceRoleWindow.open('编辑门禁角色', row)
      this.$refs.operaDeviceRoleWindow.open('编辑门禁点分组', row)
    }
  }
}
admin/src/views/business/empower.vue
@@ -60,10 +60,14 @@
                <el-table-column prop="memberPhone" label="手机号" min-width="120px"></el-table-column>
                <el-table-column prop="memberidCard" label="身份证号码" min-width="130px"></el-table-column>
                <el-table-column prop="companyName" label="所属公司" min-width="150px"></el-table-column>
                <el-table-column prop="deviceName" label="设备名称" min-width="150px"></el-table-column>
                <el-table-column label="门禁有效期" min-width="170px">
                    <template slot-scope="{row}">
                        <span>起:{{row.startTime}}</span><br />
                        <span>止:{{row.endTime}}</span>
                        <span v-if="!row.startTime || !row.endTime">长期</span>
                        <div v-else>
                            <span>起:{{row.startTime}}</span><br />
                            <span>止:{{row.endTime}}</span>
                        </div>
                    </template>
                </el-table-column>
                <el-table-column prop="sendDate" label="创建时间" min-width="150px"></el-table-column>
admin/src/views/business/interfaceLog.vue
@@ -40,7 +40,7 @@
                <el-table-column label="类型" min-width="100px">
                    <template slot-scope="{row}">
                        <span v-if="row.type == 0">调用</span>
                        <span v-if="row.type == 1">推送接受</span>
                        <span v-if="row.type == 1">推送接收</span>
                    </template>
                </el-table-column>
                <el-table-column label="平台" min-width="100px">
admin/src/views/business/internalMember.vue
@@ -107,17 +107,16 @@
                        <el-button @click="$refs.cardOpeningRecord.open('开卡记录', row.id)" type="text">{{row.memberCardCount || '0'}}</el-button>
                    </template>
                </el-table-column>
<!--                <el-table-column-->
<!--                    v-if="containPermissions(['business:member:update', 'business:member:delete'])"-->
<!--                    label="操作"-->
<!--                    min-width="120"-->
<!--                    fixed="right"-->
<!--                >-->
<!--                    <template slot-scope="{row}">-->
<!--                        <el-button type="text" @click="$refs.operaMemberWindow.open('编辑人员信息表', row)" icon="el-icon-edit" v-permissions="['business:member:update']">编辑</el-button>-->
<!--                        <el-button type="text" @click="deleteById(row)" icon="el-icon-delete" v-permissions="['business:member:delete']">删除</el-button>-->
<!--                    </template>-->
<!--                </el-table-column>-->
                <el-table-column
                    v-if="containPermissions(['business:member:update', 'business:member:delete'])"
                    label="操作"
                    min-width="120"
                    fixed="right"
                >
                    <template slot-scope="{row}">
                        <el-button type="text" @click="empower(row.id)" v-permissions="['business:empower:create']">重新授权</el-button>
                    </template>
                </el-table-column>
            </el-table>
            <pagination
                @size-change="handleSizeChange"
@@ -138,7 +137,7 @@
import cardOpeningRecord from '@/components/business/cardOpeningRecord'
import Tree from '@/components/common/Tree'
import { fetchList } from '@/api/business/company'
import { memberSync } from '@/api/business/member'
import { memberSync, roleAuth } from '@/api/business/member'
export default {
  name: 'internalMember',
  extends: BaseTable,
@@ -173,6 +172,21 @@
    this.getfindCompanyTreePage()
  },
  methods: {
    empower(id) {
      var that = this
      this.$confirm('确定重新授权吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        roleAuth(id)
          .then(res => {
            that.search()
          })
      }).catch(() => {
      });
    },
    // èŽ·å–ç»„ç»‡æ ‘
    getfindCompanyTreePage () {
      fetchList(1)
admin/src/views/business/visits.vue
@@ -49,12 +49,21 @@
                <el-table-column prop="companyName" label="公司名称" min-width="100px"></el-table-column>
                <el-table-column prop="receptMemberName" label="被访人" min-width="100px"></el-table-column>
                <el-table-column prop="reason" label="拜访事由" min-width="100px"></el-table-column>
                <el-table-column label="拜访时间" min-width="170px">
                <el-table-column label="预约时间" min-width="170px">
                    <template slot-scope="{row}">
                        <span>起:{{row.starttime}}</span><br/>
                        <span>止:{{row.endtime}}</span>
                    </template>
                </el-table-column>
                <el-table-column label="签到时间" min-width="170px">
                    <template slot-scope="{row}">
                        <div v-if="row.inDate && row.outDate">
                            <span>起:{{row.inDate}}</span><br/>
                            <span>止:{{row.outDate}}</span>
                        </div>
                        <span v-else>-</span>
                    </template>
                </el-table-column>
                <el-table-column label="随访人员" min-width="100px">
                    <template slot-scope="{row}">
                        <span>{{row.memberNum || '-'}}</span>
admin/vue.config.js
@@ -13,7 +13,7 @@
        // http://192.168.0.134:10028   ä»»åº·
        // http://192.168.0.110:10013   ç£Šç£Š
        // http://192.168.0.132:10013   å¸…å“¥
        // http://192.168.0.126:10033   èå§
        // http://192.168.0.176:10028   èå§
        // http://192.168.0.186:10028   ä»»åº·
        // https://dmtest.ahapp.net/admin_api   æµ‹è¯•服
        // http://10.10.99.63/admin_interface/  æœ€æ–°æµ‹è¯•服(内网)
h5/App.vue
@@ -18,6 +18,9 @@
                    }).then(res => {
                        if (res.code === 200) {
                            that.$store.commit('setOpenId', res.data.openid)
                            if (res.data.member) {
                                that.$store.commit('setMember', res.data.member)
                            }
                        }
                    })
                }
h5/components/cropping/cropping.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,700 @@
<template name="yq-avatar">
    <view class="croppage">
        <canvas canvas-id="avatar-canvas" id="avatar-canvas" class="my-canvas" :style="{top: sT, height: csH}"
            :disable-scroll="false"></canvas>
        <canvas canvas-id="oper-canvas" id="oper-canvas" class="oper-canvas" :style="{top: sT1, height: csH1}"
            :disable-scroll="false" @touchstart="fStart" @touchmove="fMove" @touchend="fEnd">
        </canvas>
        <canvas canvas-id="prv-canvas" id="prv-canvas" class="prv-canvas" :disable-scroll="false" @touchstart="fHideImg"
            :style="{ height: csH, top: pT }">
        </canvas>
        <view class="oper-wrapper" :style="{top:tp}">
            <view class="oper">
                <view class="btn-wrapper">
                    <view @click="rechoose" hover-class="hover" :style="{width: bW}"><text>重选</text></view>
                    <view @click="fUpload" hover-class="hover" :style="{width: bW}"><text>确定</text></view>
                </view>
            </view>
        </view>
    </view>
</template>
<script>
    const tH = 50;
    export default {
        data() {
            return {
                sT1num: 0,
                csH1: '0px',
                sT1: 0,
                csH: '0px',
                sD: 'none',
                sT: '-10000px',
                pT: '-10000px',
                iS: {},
                sS: {},
                bW: '19%',
                bD: 'flex',
                tp: 0,
            };
        },
        props: {
            avatarSrc: '',
            avatarStyle: '',
            selWidth: '',
            selHeight: '',
            expWidth: '',
            expHeight: '',
            minScale: '',
            maxScale: '',
            canScale: '',
            canRotate: '',
            lockWidth: '',
            lockHeight: '',
            stretch: '',
            lock: '',
            fileType: '',
            noTab: '',
            inner: '',
            quality: '',
            index: '',
            bgImage: '',
        },
        created() {
            this.cc = uni.createCanvasContext('avatar-canvas', this);
            this.cco = uni.createCanvasContext('oper-canvas', this);
            this.ccp = uni.createCanvasContext('prv-canvas', this);
            this.qlty = parseFloat(this.quality) || 1; //画质
            this.letRotate = (this.canRotate === false || this.inner === true || this.inner === 'true' || this
                .canRotate === 'false') ? 0 : 1; //是否旋转
            this.letScale = (this.canScale === false || this.canScale === 'false') ? 0 : 1; //是否放大
            this.isin = (this.inner === true || this.inner === 'true') ? 1 : 0;
            this.indx = this.index || undefined; //禁止旋转并在图片范围内移动
            this.mnScale = parseFloat(this.minScale) || 0.3; //缩小比例
            this.mxScale = parseFloat(this.maxScale) || 4; //放大比列
            this.noBar = (this.noTab === true || this.noTab === 'true') ? 1 : 0; //是否存在nobar
            this.stc = this.stretch; //自动铺满
            this.lck = this.lock; //锁定图方向 x y long short longSel shortSel
            this.fType = this.fileType === 'jpg' ? 'jpg' : 'png';
            if (this.isin || !this.letRotate) {
                this.bW = '24%';
                this.bD = 'none';
            } else {
                this.bW = '19%';
                this.bD = 'flex';
            }
            if (this.noBar) {
                this.fWindowResize();
            } else {
                uni.showTabBar({
                    fail: () => {
                        this.noBar = 1;
                    },
                    success: () => {
                        this.noBar = 0;
                    },
                    complete: (res) => {
                        this.fWindowResize();
                    }
                });
            }
        },
        methods: {
            //重选
            rechoose() {
                const that = this
                uni.chooseImage({
                    count: 1,
                    sizeType: ['original', 'compressed'],
                    sourceType: ["album"],
                    success: (res) => {
                        that.sT1 = that.sT
                        that.csH1 = that.csH
                        that.fSelect(res.tempFilePaths[0])
                    }
                });
            },
            //初始化各种数据
            fWindowResize() {
                let sysInfo = uni.getSystemInfoSync();
                this.platform = sysInfo.platform;
                this.wW = sysInfo.windowWidth;
                // #ifndef H5
                let phone = uni.getSystemInfoSync().platform
                console.log(uni.getSystemInfoSync().platform)
                if (phone == 'ios') {
                    let h = uni.upx2px(60); //将rpx单位值转换成px è£å‰ªæ¡†è‡ªå·±
                    let b = uni.upx2px(100); //底部按钮
                    this.sT1 = (sysInfo.windowHeight - h - b) / 2 + 'px' //裁剪框距离顶部+px
                    this.sT1num = (sysInfo.windowHeight - h - b) / 2 //裁剪框距离顶部不+px
                } else {
                    this.drawTop = 0;
                }
                // #endif
                // #ifndef MP-ALIPAY
                let phone = uni.getSystemInfoSync().platform
                this.wH = sysInfo.windowHeight; //设备高
                if (!this.noBar) this.wH += tH; //th=50
                this.csH = this.wH - tH + 'px'; //高=设备高-50(导航)
                if (phone == 'ios') { //适配ios
                    this.csH1 = (this.wH - 50) / 2 + 'px'
                } else {
                    this.csH1 = this.wH - tH + 'px';
                }
                // #endif
                this.tp = this.csH;
                this.pxRatio = this.wW / 750; //设备宽/750  æ¯”列
                this.expWidth && (this.eW = this.expWidth.toString().indexOf('upx') >= 0 ? parseInt(this.expWidth) * this
                    .pxRatio : parseInt(this.expWidth));
                this.expHeight && (this.eH = this.expHeight.toString().indexOf('upx') >= 0 ? parseInt(this.expHeight) *
                    this.pxRatio : parseInt(this.expHeight));
                this.fHideImg();
            },
            fSelect(r) {
                if (this.fSelecting) return;
                this.fSelecting = true;
                setTimeout(() => {
                    this.fSelecting = false;
                }, 500); //防抖
                let path = this.imgPath = r; //需要剪裁的图片路径
                // èŽ·å–å›¾ç‰‡ä¿¡æ¯
                uni.getImageInfo({
                    src: path,
                    success: r => {
                        this.imgWidth = r.width;
                        this.imgHeight = r.height;
                        this.path = path;
                        if (!this.hasSel) {
                            let style = this.sS || {};
                            if (this.selWidth && this.selHeight) { //设置的剪裁区域宽高
                                let sW = this.selWidth.toString().indexOf('upx') >= 0 ?
                                    parseInt(this.selWidth) * this.pxRatio : parseInt(
                                        this.selWidth),
                                    sH = this.selHeight.toString().indexOf('upx') >= 0 ?
                                    parseInt(this.selHeight) * this.pxRatio : parseInt(
                                        this.selHeight);
                                style.width = sW + 'px';
                                style.height = sH + 'px';
                                style.top = ((this.wH - sH - tH) | 0) / 2 + 'px';
                                style.left = ((this.wW - sW) | 0) / 2 + 'px';
                                // }
                            } else {
                                uni.showModal({
                                    title: '裁剪框的宽或高没有设置',
                                    showCancel: false
                                })
                                return;
                            }
                            this.sS = style;
                        }
                        if (this.noBar) {
                            setTimeout(() => {
                                this.fDrawInit(true);
                            }, 200)
                        } else {
                            uni.hideTabBar({
                                complete: () => {
                                    setTimeout(() => {
                                        this.fDrawInit(true);
                                    }, 200)
                                }
                            });
                        }
                    },
                    fail: () => {
                        uni.showToast({
                            title: "请选择正确图片",
                            duration: 2000,
                        })
                    },
                    complete() {
                        uni.hideLoading();
                    }
                });
            },
            //剪裁确定
            fUpload() {
                uni.showLoading({
                    title: '图片上传中...',
                    mask: true
                });
                if (this.fUploading) return;
                this.fUploading = true;
                setTimeout(() => {
                    this.fUploading = false;
                }, 1000)
                let style = this.sS,
                    x = parseInt(style.left),
                    y = parseInt(style.top),
                    width = parseInt(style.width),
                    height = parseInt(style.height),
                    expWidth = this.eW || (width * this.pixelRatio),
                    expHeight = this.eH || (height * this.pixelRatio);
                this.fHideImg();
                // #ifndef MP-ALIPAY
                let phone = uni.getSystemInfoSync().platform
                if (phone == 'ios') {
                    y = this.sT1num
                }
                uni.canvasToTempFilePath({
                    x: x,
                    y: y,
                    width: width,
                    height: height,
                    destWidth: expWidth,
                    destHeight: expHeight,
                    canvasId: 'avatar-canvas',
                    fileType: this.fType,
                    quality: this.qlty,
                    success: (r) => {
                        r = r.tempFilePath;
                        // #ifndef H5
                        this.$emit("upload", {
                            avatar: this.imgSrc,
                            path: r,
                            index: this.indx,
                            data: this.rtn,
                            base64: this.base64 || null
                        });
                        // #endif
                    },
                    fail: (res) => {
                        uni.showToast({
                            title: "error1",
                            duration: 2000,
                        })
                    },
                    complete: () => {
                        uni.hideLoading();
                        this.noBar || uni.showTabBar();
                        this.$emit("end");
                    }
                }, this);
                // #endif
            },
            fDrawInit(ini = false) {
                let allWidth = this.wW, //设备宽
                    allHeight = this.wH, //设备高
                    imgWidth = this.imgWidth, //图宽
                    imgHeight = this.imgHeight, //图高
                    imgRadio = imgWidth / imgHeight, //图比
                    useWidth = allWidth - 40, //设备宽-40
                    useHeight = allHeight - tH - 80, //设备高-80
                    useRadio = useWidth / useHeight,
                    sW = parseInt(this.sS.width), //图片信息
                    sH = parseInt(this.sS.height);
                this.fixWidth = 0;
                this.fixHeight = 0;
                this.lckWidth = 0;
                this.lckHeight = 0;
                switch (this.stc) { //以下是自动铺满的算法
                    case 'x':
                        this.fixWidth = 1;
                        break;
                    case 'y':
                        this.fixHeight = 1;
                        break;
                    case 'long':
                        if (imgRadio > 1) this.fixWidth = 1;
                        else this.fixHeight = 1;
                        break;
                    case 'short':
                        if (imgRadio > 1) this.fixHeight = 1;
                        else this.fixWidth = 1;
                        break;
                    case 'longSel':
                        if (sW > sH) this.fixWidth = 1;
                        else this.fixHeight = 1;
                        break;
                    case 'shortSel':
                        if (sW > sH) this.fixHeight = 1;
                        else this.fixWidth = 1;
                        break;
                }
                switch (this.lck) { //以下锁定屏幕的移动方向
                    case 'x':
                        this.lckWidth = 1;
                        break;
                    case 'y':
                        this.lckHeight = 1;
                        break;
                    case 'long':
                        if (imgRadio > 1) this.lckWidth = 1;
                        else this.lckHeight = 1;
                        break;
                    case 'short':
                        if (imgRadio > 1) this.lckHeight = 1;
                        else this.lckWidth = 1;
                        break;
                    case 'longSel':
                        if (sW > sH) this.lckWidth = 1;
                        else this.lckHeight = 1;
                        break;
                    case 'shortSel':
                        if (sW > sH) this.lckHeight = 1;
                        else this.lckWidth = 1;
                        break;
                }
                if (this.fixWidth) {
                    useWidth = sW;
                    useHeight = useWidth / imgRadio;
                } else if (this.fixHeight) {
                    useHeight = sH;
                    useWidth = useHeight * imgRadio;
                } else if (imgRadio < useRadio) {
                    if (imgHeight < useHeight) {
                        useWidth = imgWidth;
                        useHeight = imgHeight;
                    } else {
                        useWidth = useHeight * imgRadio;
                    }
                } else {
                    if (imgWidth < useWidth) {
                        useWidth = imgWidth;
                        useHeight = imgHeight;
                    } else {
                        useHeight = useWidth / imgRadio;
                    }
                }
                if (this.isin) {
                    if (useWidth < sW) {
                        useWidth = sW;
                        useHeight = useWidth / imgRadio;
                        this.lckHeight = 0;
                    }
                    if (useHeight < sH) {
                        useHeight = sH;
                        useWidth = useHeight * imgRadio;
                        this.lckWidth = 0;
                    }
                }
                this.scaleSize = 1;
                this.rotateDeg = 0;
                this.posWidth = (allWidth - useWidth) / 2 | 0;
                this.posHeight = (allHeight - useHeight - tH) / 2 | 0;
                this.useWidth = useWidth | 0;
                this.useHeight = useHeight | 0;
                this.centerX = this.posWidth + useWidth / 2;
                this.centerY = this.posHeight + useHeight / 2;
                this.focusX = 0;
                this.focusY = 0;
                let style = this.sS,
                    left = parseInt(style.left),
                    top = parseInt(style.top),
                    width = parseInt(style.width),
                    height = parseInt(style.height),
                    canvas = this.canvas,
                    canvasOper = this.canvasOper,
                    cc = this.cc, //avatar-canvas
                    cco = this.cco; //oper-canvas  è£å‰ª
                cco.beginPath(); //开始创建一个路径
                cco.setLineWidth(3); //设置线条的宽度  px
                cco.setGlobalAlpha(1); //设置全局画笔透明度。
                cco.setStrokeStyle('white'); //设置边框颜色。如果没有设置 fillStyle,默认颜色为 black。
                cco.strokeRect(left, top, width, height); //画一个矩形(非填充)。用 setFillStroke() è®¾ç½®è¾¹æ¡†é¢œè‰²ï¼Œå¦‚果没设置默认是黑色。
                cco.setFillStyle('black');
                cco.setGlobalAlpha(0.5);
                cco.fillRect(0, 0, this.wW, top); //填充一个矩形
                cco.fillRect(0, top, left, height);
                cco.fillRect(0, top + height, this.wW, this.wH - height - top - tH);
                cco.fillRect(left + width, top, this.wW - width - left, height);
                cco.setGlobalAlpha(1);
                cco.setStrokeStyle('red');
                cco.moveTo(left + 15, top); //把路径移动到画布中的指定点,不创建线条。用 stroke() æ–¹æ³•来画线条。
                cco.lineTo(left, top); //增加一个新点,然后创建一条从上次指定点到目标点的线。
                cco.lineTo(left, top + 15);
                cco.moveTo(left + width - 15, top);
                cco.lineTo(left + width, top);
                cco.lineTo(left + width, top + 15);
                cco.moveTo(left + 15, top + height);
                cco.lineTo(left, top + height);
                cco.lineTo(left, top + height - 15);
                cco.moveTo(left + width - 15, top + height);
                cco.lineTo(left + width, top + height);
                cco.lineTo(left + width, top + height - 15);
                cco.stroke(); //画线条
                cco.draw(false, () => { //将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas ä¸­ã€‚
                    if (ini) {
                        this.sT = this.drawTop + 'px';
                        this.fDrawImage(true); //绘制背景色
                    }
                });
                this.$emit("init");
            },
            //绘制背景色
            fDrawImage(ini = false) {
                let tm_now = Date.now(); //当前时间
                if (tm_now - this.drawTm < 20) return;
                this.drawTm = tm_now;
                let cc = this.cc,
                    imgWidth = this.useWidth * this.scaleSize,
                    imgHeight = this.useHeight * this.scaleSize;
                if (this.bgImage) { //如果背景图
                    // #ifndef MP-ALIPAY
                    cc.drawImage(this.bgImage, 0, 0, this.wW, this.wH - tH); //绘制图像到画布。
                    // #endif
                } else {
                    cc.fillRect(0, 0, this.wW, this.wH - tH); //填充一个矩形
                }
                if (this.isin) { //禁止旋转并在图片范围内移动
                    let cx = this.focusX * (this.scaleSize - 1),
                        cy = this.focusY * (this.scaleSize - 1);
                    cc.translate(this.centerX, this.centerY);
                    cc.rotate(this.rotateDeg * Math.PI / 180);
                    cc.drawImage(this.imgPath, this.posWidth - this.centerX - cx, this.posHeight - this.centerY - cy,
                        imgWidth, imgHeight);
                } else {
                    cc.translate(this.posWidth + imgWidth / 2, this.posHeight + imgHeight / 2);
                    cc.rotate(this.rotateDeg * Math.PI / 180);
                    cc.drawImage(this.imgPath, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
                }
                cc.draw(false);
            },
            //旋转
            fRotate() {
                this.rotateDeg += 90 - this.rotateDeg % 90;
                this.fDrawImage();
            },
            //手指触摸开始
            fStart(e) {
                let touches = e.touches,
                    touch0 = touches[0],
                    touch1 = touches[1];
                this.touch0 = touch0;
                this.touch1 = touch1;
                if (touch1) {
                    let x = touch1.x - touch0.x,
                        y = touch1.y - touch0.y;
                    this.fgDistance = Math.sqrt(x * x + y * y);
                }
            },
            //手指触摸后移动
            fMove(e) {
                let touches = e.touches,
                    touch0 = touches[0],
                    touch1 = touches[1];
                if (touch1) {
                    let x = touch1.x - touch0.x,
                        y = touch1.y - touch0.y,
                        fgDistance = Math.sqrt(x * x + y * y),
                        scaleSize = 0.005 * (fgDistance - this.fgDistance),
                        beScaleSize = this.scaleSize + scaleSize;
                    do {
                        if (!this.letScale) break;
                        if (beScaleSize < this.mnScale) break;
                        if (beScaleSize > this.mxScale) break;
                        let growX = this.useWidth * scaleSize / 2,
                            growY = this.useHeight * scaleSize / 2;
                        if (this.isin) {
                            let imgWidth = this.useWidth * beScaleSize,
                                imgHeight = this.useHeight * beScaleSize,
                                l = this.posWidth - growX,
                                t = this.posHeight - growY,
                                r = l + imgWidth,
                                b = t + imgHeight,
                                left = parseInt(this.sS.left),
                                top = parseInt(this.sS.top),
                                width = parseInt(this.sS.width),
                                height = parseInt(this.sS.height),
                                right = left + width,
                                bottom = top + height,
                                cx, cy;
                            if (imgWidth <= width || imgHeight <= height) break;
                            this.cx = cx = this.focusX * beScaleSize - this.focusX,
                                this.cy = cy = this.focusY * beScaleSize - this.focusY;
                            this.posWidth -= growX;
                            this.posHeight -= growY;
                            if (this.posWidth - cx > left) {
                                this.posWidth = left + cx;
                            }
                            if (this.posWidth + imgWidth - cx < right) {
                                this.posWidth = right - imgWidth + cx;
                            }
                            if (this.posHeight - cy > top) {
                                this.posHeight = top + cy;
                            }
                            if (this.posHeight + imgHeight - cy < bottom) {
                                this.posHeight = bottom - imgHeight + cy;
                            }
                        } else {
                            this.posWidth -= growX;
                            this.posHeight -= growY;
                        }
                        this.scaleSize = beScaleSize;
                    } while (0);
                    this.fgDistance = fgDistance;
                    if (touch1.x !== touch0.x && this.letRotate) {
                        x = (this.touch1.y - this.touch0.y) / (this.touch1.x - this.touch0.x);
                        y = (touch1.y - touch0.y) / (touch1.x - touch0.x);
                        this.rotateDeg += Math.atan((y - x) / (1 + x * y)) * 180 / Math.PI;
                        this.touch0 = touch0;
                        this.touch1 = touch1;
                    }
                    this.fDrawImage();
                } else if (this.touch0) {
                    let x = touch0.x - this.touch0.x,
                        y = touch0.y - this.touch0.y,
                        beX = this.posWidth + x,
                        beY = this.posHeight + y;
                    if (this.isin) {
                        let imgWidth = this.useWidth * this.scaleSize,
                            imgHeight = this.useHeight * this.scaleSize,
                            l = beX,
                            t = beY,
                            r = l + imgWidth,
                            b = t + imgHeight,
                            left = parseInt(this.sS.left),
                            top = parseInt(this.sS.top),
                            right = left + parseInt(this.sS.width),
                            bottom = top + parseInt(this.sS.height),
                            cx, cy;
                        this.cx = cx = this.focusX * this.scaleSize - this.focusX;
                        this.cy = cy = this.focusY * this.scaleSize - this.focusY;
                        if (!this.lckWidth && Math.abs(x) < 100) {
                            if (left < l - cx) {
                                this.posWidth = left + cx;
                            } else if (right > r - cx) {
                                this.posWidth = right - imgWidth + cx;
                            } else {
                                this.posWidth = beX;
                                this.focusX -= x;
                            }
                        }
                        if (!this.lckHeight && Math.abs(y) < 100) {
                            if (top < t - cy) {
                                this.focusY -= (top + cy - this.posHeight);
                                this.posHeight = top + cy;
                            } else if (bottom > b - cy) {
                                this.focusY -= (bottom + cy - (this.posHeight + imgHeight));
                                this.posHeight = bottom - imgHeight + cy;
                            } else {
                                this.posHeight = beY;
                                this.focusY -= y;
                            }
                        }
                    } else {
                        if (Math.abs(x) < 100 && !this.lckWidth) this.posWidth = beX;
                        if (Math.abs(y) < 100 && !this.lckHeight) this.posHeight = beY;
                        this.focusX -= x;
                        this.focusY -= y;
                    }
                    this.touch0 = touch0;
                    this.fDrawImage();
                }
            },
            //手指触摸动作结束
            fEnd(e) {
                let touches = e.touches,
                    touch0 = touches && touches[0],
                    touch1 = touches && touches[1];
                if (touch0) {
                    this.touch0 = touch0;
                } else {
                    this.touch0 = null;
                    this.touch1 = null;
                }
            },
            fHideImg() {
                this.prvImg = '';
                this.pT = '-10000px';
                this.prvImgData = null;
                this.target = null;
            },
        }
    }
</script>
<style>
    .croppage {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        background-color: #000000;
        position: relative;
    }
    .my-canvas {
        /* display: flex; */
        position: absolute !important;
        left: 0;
        z-index: 100000;
        width: 100%;
    }
    .my-avatar {
        width: 150upx;
        height: 150upx;
        border-radius: 100%;
    }
    .oper-canvas {
        /* display: flex; */
        position: absolute !important;
        left: 0;
        z-index: 100000;
        width: 100%;
    }
    .prv-canvas {
        display: flex;
        position: fixed !important;
        background: #000000;
        left: 0;
        z-index: 200000;
        width: 100%;
    }
    .oper-wrapper {
        height: 100rpx;
        position: fixed !important;
        box-sizing: border-box;
        border: 1px solid #F1F1F1;
        background: #ffffff;
        width: 100%;
        left: 0;
        bottom: 0;
        z-index: 99999999;
        flex-direction: row;
    }
    .oper {
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding: 10upx 20upx;
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        align-self: center;
    }
    .btn-wrapper {
        display: flex;
        flex-direction: row;
        flex-grow: 1;
        height: 50px;
        justify-content: space-between;
    }
    .btn-wrapper view {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 16px;
        color: #3b7ffb;
        border-radius: 6%;
    }
    .hover {
        background: #f1f1f1;
        border-radius: 6%;
    }
    .clr-wrapper {
        display: flex;
        flex-direction: row;
        flex-grow: 1;
    }
    .clr-wrapper view {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 16px;
        color: #333;
        border: 1px solid #f1f1f1;
        border-radius: 6%;
    }
    .my-slider {
        flex-grow: 1;
    }
</style>
h5/components/keyboard-input/keyboard-input.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,463 @@
 <template>
    <view class="so-mask" v-if="show">
        <view style="height: 100%" @tap="$emit('close')" />
        <view class="so-plate animation-scale-up">
            <view class="so-plate-head">
                <view class="so-plate-type">
                    <radio-group @change="typeChange">
                        <label>
                            <radio value="1" :checked="type===1" />
                            æ™®é€šè½¦ç‰Œ
                        </label>
                        <label>
                            <radio value="2" :checked="type===2" />
                            æ–°èƒ½æºè½¦ç‰Œ
                        </label>
                    </radio-group>
                </view>
            </view>
            <view class="so-plate-body">
                <view class="so-plate-word" :class="{ active: currentInputIndex == 0 }" @tap="inputSwitch"
                    data-index="0">
                    <text>{{ currentInputValue[0] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 1 }" @tap="inputSwitch"
                    data-index="1">
                    <text>{{ currentInputValue[1] }}</text>
                </view>
                <view class="so-plate-dot"></view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 2 }" @tap="inputSwitch"
                    data-index="2">
                    <text>{{ currentInputValue[2] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 3 }" @tap="inputSwitch"
                    data-index="3">
                    <text>{{ currentInputValue[3] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 4 }" @tap="inputSwitch"
                    data-index="4">
                    <text>{{ currentInputValue[4] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 5 }" @tap="inputSwitch"
                    data-index="5">
                    <text>{{ currentInputValue[5] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 6 }" @tap="inputSwitch"
                    data-index="6">
                    <text>{{ currentInputValue[6] }}</text>
                </view>
                <view class="so-plate-word" :class="{ active: currentInputIndex == 7 }" @tap="inputSwitch"
                    v-if="type == 2" data-index="7">
                    <text>{{ currentInputValue[7] }}</text>
                </view>
            </view>
            <view class="so-plate-foot">
                <view class="so-plate-keyboard" :style="{height:keyboardHeight}">
                    <view id="keyboard">
                        <block v-if="inputType == 1">
                            <view hover-class="hover" class="so-plate-key" v-for="el of provinceText" :key="el"
                                :data-value="el" @tap="chooseKey">{{ el }}</view>
                        </block>
                        <block v-if="inputType == 1">
                            <text class="so-plate-key fill-block"></text>
                        </block>
                        <block v-if="inputType >= 3">
                            <view hover-class="hover" class="so-plate-key" v-for="el of numberText" :key="el"
                                :data-value="el" @tap="chooseKey">{{ el }}</view>
                        </block>
                        <block v-if="inputType >= 2">
                            <view hover-class="hover" class="so-plate-key" v-for="el of wordText" :key="el"
                                :data-value="el" @tap="chooseKey">{{ el }}</view>
                        </block>
                        <block v-if="inputType == 3">
                            <text v-for="el of fillBlock" :key="el.num" class="so-plate-key fill-block"></text>
                        </block>
                        <block v-if="inputType == 4">
                            <view hover-class="hover" class="so-plate-key" v-for="el of lastWordText" :key="el"
                                :data-value="el" @tap="chooseKey">{{ el }}</view>
                        </block>
                        <text v-if="inputType == 4" class="so-plate-key fill-block"></text>
                    </view>
                </view>
                <view class="so-plate-btn-group">
                    <view>
                        <button class="so-plate-btn so-plate-btn--cancel" @tap="$emit('close')">取消</button>
                    </view>
                    <view>
                        <button class="so-plate-btn so-plate-btn--delete" @tap="deleteKey">删除</button>
                        <button class="so-plate-btn so-plate-btn--submit" @tap="exportPlate">完成</button>
                    </view>
                </view>
            </view>
        </view>
    </view>
    <view v-else></view>
</template>
<script>
    export default {
        name: 'uni-plate-input',
        data() {
            return {
                show: false,
                type: 1, //车牌类型
                currentInputIndex: 0, //当前编辑的输入框
                currentInputValue: ['', '', '', '', '', '', ''],
                fillBlock: [{
                    num: 11
                }, {
                    num: 12
                }, {
                    num: 13
                }, {
                    num: 14
                }, {
                    num: 15
                }, {
                    num: 16
                }], //避免:key报错
                keyboardHeightInit: false,
                keyboardHeight: 'auto',
                provinceText: [
                    '粤',
                    '京',
                    '冀',
                    '沪',
                    'æ´¥',
                    '晋',
                    '蒙',
                    'è¾½',
                    '吉',
                    '黑',
                    '苏',
                    '浙',
                    '皖',
                    '闽',
                    'èµ£',
                    '鲁',
                    '豫',
                    '鄂',
                    '湘',
                    '桂',
                    '琼',
                    '渝',
                    '川',
                    'è´µ',
                    '云',
                    '藏',
                    '陕',
                    '甘',
                    '青',
                    '宁',
                    '新'
                ],
                numberText: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
                wordText: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U',
                    'V', 'W', 'X', 'Y', 'Z'
                ],
                lastWordText: ['挂', '港', 'å­¦', '领', 'è­¦']
            };
        },
        props: {
            plate: {
                type: String
            }
        },
        computed: {
            //输入框类型
            inputType() {
                switch (this.currentInputIndex) {
                    case 0:
                        return 1;
                        break;
                    case 1:
                        return 2;
                        break;
                    case 2:
                        return 3;
                        break;
                    case 3:
                        return 3;
                        break;
                    case 4:
                        return 3;
                        break;
                    case 5:
                        return 3;
                        break;
                    case 6:
                        return this.type == 2 ? 3 : 4;
                        break;
                    case 7:
                        return 4;
                        break;
                    default:
                        return 1;
                        break;
                }
            }
        },
        watch: {
            currentInputIndex: function(n, o) {
                if (!this.keyboardHeightInit) return
                this.$nextTick(() => {
                    this.changeKeyboardHeight()
                })
            }
        },
        methods: {
            open() {
                this.type = 1
                this.currentInputIndex = 0
                this.currentInputValue = ['', '', '', '', '', '', '']
                this.show = true
                setTimeout(() => { //在动画结束之后才开始获取
                    this.$nextTick(() => {
                        this.changeKeyboardHeight()
                    })
                }, 500);
            },
            close() {
                this.show = false
            },
            //车牌类型切换
            typeChange(e) {
                this.$emit("typeChange", e.detail.value);
                const {
                    value
                } = e.detail;
                this.type = parseInt(value)
                this.currentInputIndex = 0
                if (value == 1) {
                    this.currentInputValue = ['', '', '', '', '', '', '']
                } else {
                    this.currentInputValue = ['', '', '', '', '', '', '', '']
                }
            },
            inputSwitch(e) {
                const {
                    index
                } = e.currentTarget.dataset;
                this.currentInputIndex = parseInt(index);
            },
            chooseKey(e) {
                const {
                    value
                } = e.currentTarget.dataset;
                this.$set(this.currentInputValue, this.currentInputIndex, value);
                if (this.type == 1 && this.currentInputIndex < 6) {
                    this.currentInputIndex++
                }
                if (this.type == 2 && this.currentInputIndex < 7) {
                    this.currentInputIndex++
                }
            },
            deleteKey() {
                this.$set(this.currentInputValue, this.currentInputIndex, '')
                if (this.currentInputIndex != 0) this.currentInputIndex--
            },
            exportPlate() {
                const plate = this.currentInputValue.join('')
                let err = false
                if (this.type === 1 && plate.length != 7) {
                    err = true
                } else if (this.type === 2 && plate.length != 8) {
                    err = true
                }
                if (err) return uni.showToast({
                    title: '请输入完整的车牌号码',
                    icon: 'none'
                })
                this.$emit('export', plate)
            },
            changeKeyboardHeight() {
                const that = this
                const query = uni.createSelectorQuery().in(this);
                query.select('#keyboard').boundingClientRect();
                query.exec(function(res) {
                    if (res && res[0]) {
                        that.keyboardHeight = res[0].height + uni.upx2px(30) + 'px'
                        that.keyboardHeightInit = true
                    }
                });
            }
        },
        mounted() {
            // console.log(this.plate);
    //         const plateKey = this.plate.split('')
    //         if (plateKey.length === 7) {
    //             this.type = 1
    //         } else if (plateKey.length === 8) {
    //             this.type = 2
    //         }
    //         if (plateKey.length === 7 || plateKey.length === 8) {
    //             this.currentInputValue = plateKey
    //             this.currentInputIndex = plateKey.length - 1
    //         }
    //         setTimeout(() => { //在动画结束之后才开始获取
    //             this.$nextTick(() => {
    //                 this.changeKeyboardHeight()
    //             })
    //         }, 500);
        }
    };
</script>
<style scoped lang="less">
    .so-mask {
        position: fixed;
        top: 0;
        bottom: 0;
        right: 0;
        left: 0;
        background: rgba(0, 0, 0, 0.5);
        z-index: 99999;
    }
    .so-plate {
        box-sizing: border-box;
        position: absolute;
        bottom: 0;
        width: 100%;
        left: 0;
        background: #fff;
        padding: 25upx 25upx 0 25upx;
        &-head {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        &-type {
            flex:1;
            display:block;
            label {
                display: inline-block;
                min-height: 32upx;
                font-size: 26upx;
                margin-right: 10upx;
            }
        }
        &-body {
            box-sizing: border-box;
            padding: 30upx 0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        &-word {
            border: 1upx solid #ccc;
            border-radius: 10upx;
            height: 0;
            margin: 0 5upx;
            box-sizing: border-box;
            padding-bottom: calc((100% - 70upx) / 7);
            width: calc((100% - 70upx) / 7);
            position: relative;
            &.active {
                border-color: #007aff;
                box-shadow: 0 0 15upx 0 #007aff;
            }
            text {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translateX(-50%) translateY(-50%);
                font-weight: 700;
                font-size: 32upx;
            }
        }
        &-dot {
            width: 15upx;
            height: 15upx;
            background: #ccc;
            border-radius: 50%;
            margin: 0 5upx;
        }
        &-keyboard {
            background: #eee;
            margin-left: -25upx;
            margin-right: -25upx;
            padding: 20upx 25upx 10upx 25upx;
            box-sizing: border-box;
            transition: all .3s;
            &>view{
                display: flex;
                flex-wrap: wrap;
                justify-content: space-between;
            }
        }
        &-key {
            display: block;
            background: #fff;
            border-radius: 10upx;
            box-shadow: 0 0 8upx 0 #bbb;
            width: 80upx;
            height: 80upx;
            margin: 5upx 0;
            font-size: 32upx;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            &.hover {
                background: #efefef;
            }
            &.fill-block {
                width: 80upx;
                height: 80upx;
                background: none;
                box-shadow: none;
            }
        }
        &-btn {
            display: inline-block;
            background: #fff;
            border-radius: 10upx;
            box-shadow: 0 0 10upx 0 #bbb;
            font-size: 28upx;
            text-align: center;
            margin:0 0 0 10upx;
            padding:0 25upx;
            &-group{
                display: flex;
                justify-content: space-between;
                background: #eee;
                margin-left: -25upx;
                margin-right: -25upx;
                box-sizing: border-box;
                padding: 0 25upx 10upx 25upx;
            }
            &--cancel{
                margin:0;
            }
            &--submit{
                background:#5773f9;
                color:#fff;
            }
            &--delete{
                color:#fd6b6d;
            }
        }
    }
    .animation-scale-up {
        animation-duration: .2s;
        animation-timing-function: ease-out;
        animation-fill-mode: both;
        animation-name: scale-up
    }
    @keyframes scale-up {
        0% {
            opacity: .8;
            transform: scale(.8)
        }
        100% {
            opacity: 1;
            transform: scale(1)
        }
    }
</style>
h5/components/tly-picture-cut/tlyPictureCut.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,413 @@
<template>
    <view class="picture-cut" :class="{'picture-cut-show':isShow}">
         <!-- :style="{width:choosePictureWidth+'px',height:choosePictureHeight+'px',top:choosePictureTop+'px',left:choosePictureLeft+'px'}" -->
        <view class="select-box" :style="{width: '70%', height: '250px', top:choosePictureTop+'px',left: '15%'}">
            <view class="horn">
                <!-- <view class="lt" data-drag="leftTop"  @touchstart.stop="dragStart" @touchmove.stop.prevent="dragMove" ></view>
                <view class="rt"  data-drag="topTight" @touchstart.stop="dragStart" @touchmove.stop.prevent="dragMove" ></view>
                <view class="rb" data-drag="rightBottom" @touchstart.stop="dragStart" @touchmove.stop.prevent="dragMove"></view>
                <view class="lb" data-drag="bottomLeft"  @touchstart.stop="dragStart" @touchmove.stop.prevent="dragMove"></view> -->
            </view>
        </view>
        <movable-area class="picture-area" :style="{width:areaWidth+'px',height:areaHeight+'px',top:areaHeightTop+'px',left:areaWidthLeft+'px'}">
            <movable-view class="picture-view" :style="{width:img_width/img_scaling+'px',height:img_height/img_scaling+'px'}"
             direction="all" :x="offsetX" :y="offsetY" scale="true" :scale-min="scaleMin" @change="movableChange" @scale="movableScale">
                <image :style="{width:img_width/img_scaling+'px',height:img_height/img_scaling+'px'}" :src="pictureSrc"></image>
            </movable-view>
        </movable-area>
        <view class="area-bottom">
            <view class="area-bottom-box">
                <view @click="cancelArea">取消</view>
                <!-- <view @click="changeImg">换个图片</view> -->
                <view @click="createImg">完成</view>
            </view>
        </view>
        <canvas canvas-id="picture-canvas" :style="'position:absolute;border: 1px solid red; width:'+canvasWidth+'px;height:'+canvasHeight+'px;top:-9999px;left:-9999px;'" class="canvas-view"></canvas>
    </view>
</template>
<script>
    let tailorSize = 240; // éœ€è¦æˆªå–的尺寸240x240px,此变量要和样式中的240px和120px相对应,120px为此变量的一半,若要修改成其他值一定要一一对应
    let tailorWidth = 0;
    let tailorHeight = 0;
    let    newOffsetX = 0; // æ‹–动缩放完成后的X轴偏移量
    let newOffsetY = 0; // æ‹–动缩放完成后的Y轴偏移量
    let T_PAGE_X, // æ‰‹ç§»åŠ¨çš„æ—¶å€™x的位置
        T_PAGE_Y, // æ‰‹ç§»åŠ¨çš„æ—¶å€™Y的位置
        area_x,
        area_y
    let [choosePictureMinWidth,choosePictureMinHeight] = [50,50];
    export default {
        name: "tly-picture-cut",
        props: {
            // å›¾ç‰‡è·¯å¾„
            pictureSrc: {
                type: String,
                required: true
            }
        },
        data() {
            return {
                offsetX: 0, // å›¾åƒåˆå§‹åŒ–çš„X轴偏移量
                offsetY: 0, // å›¾åƒåˆå§‹åŒ–çš„Y轴偏移量
                img_width: 0, // å›¾ç‰‡çœŸå®žå®½åº¦
                img_height: 0, // å›¾ç‰‡çœŸå®žé«˜åº¦
                img_scaling: 1, //图片初始化缩放比例
                scale: 1, // æ‹–动缩放完成后的图片缩放比例
                scaleMin: 0.5, // æœ€å°ç¼©æ”¾å€¼
                isShow: false,
                choosePictureWidth:0,
                choosePictureHeight:0,
                choosePictureTop:0,
                choosePictureLeft:0,
                chooseVisible:0.5,
                areaWidth:0,
                areaHeight:0,
                movableViewTop:0,
                movableViewLeft:0,
                areaWidthLeft:0,
                areaHeightTop:0,
                canvasWidth:0,
                canvasHeight:0,
            };
        },
        watch: {
            pictureSrc() {
                this.getImgInfo();
            }
        },
        methods: {
            changeImg(){
                this.$emit('changeImg')
            },
            cancelArea(){
                this.hidePop();
            },
            // è®¾ç½®å¤§å°çš„æ—¶å€™è§¦å‘çš„touchStart事件
            dragStart(e) {
                T_PAGE_X = e.touches[0].pageX
                T_PAGE_Y = e.touches[0].pageY
            },
            // è®¾ç½®å¤§å°çš„æ—¶å€™è§¦å‘çš„touchMove事件
            dragMove(e) {
                var _this = this
                var dragType = e.target.dataset.drag
                var dragLengthX = (T_PAGE_X - e.touches[0].pageX);
                var dragLengthY = (T_PAGE_Y - e.touches[0].pageY);
                    switch (dragType) {
                        case 'leftTop':
                            this.choosePictureWidth += dragLengthX;
                            this.choosePictureLeft  -= dragLengthX;
                            this.choosePictureTop  -= dragLengthY;
                            this.choosePictureHeight += dragLengthY;
                            this.canvasY -= dragLengthY
                            this.canvasX -= dragLengthX
                            break;
                        case 'topTight':
                            this.choosePictureWidth -= dragLengthX;
                            this.choosePictureHeight += dragLengthY;
                            this.choosePictureTop  -= dragLengthY;
                            this.canvasY -= dragLengthY
                            break;
                        case 'bottomLeft':
                            this.choosePictureWidth += dragLengthX;
                            this.choosePictureLeft  -= dragLengthX;
                            this.choosePictureHeight -= dragLengthY;
                            this.canvasX  -= dragLengthX
                            break;
                        case 'rightBottom':
                            this.choosePictureWidth -= dragLengthX;
                            this.choosePictureHeight -= dragLengthY;
                            break;
                        default:
                            break;
                    }
                    T_PAGE_Y -= dragLengthY;
                    T_PAGE_X -= dragLengthX;
            },
            // æ˜¾ç¤ºç»„ä»¶
            showPop() {
                this.isShow = true;
            },
            // éšè—ç»„ä»¶
            hidePop() {
                this.isShow = false;
            },
            // åˆå§‹åŒ–图片
            getImgInfo() {
                uni.getImageInfo({
                    src: this.pictureSrc,
                    success: (res) => {
                        // å±å¹•可用宽高
                        let sysInfo = uni.getSystemInfoSync();
                        let windowWidth = sysInfo.windowWidth;
                        let windowHeight = sysInfo.windowHeight;
                        // å›¾ç‰‡å®½é«˜
                        // ç­‰æ¯”缩放
                        this.img_width = windowWidth*0.8;
                        this.img_height = res.height*(this.img_width/res.width);
                        this.canvasWidth = this.img_width;
                        this.canvasHeight = this.img_height;
                        //截图宽
                        this.choosePictureWidth = this.img_width*this.chooseVisible;
                        //截图高
                        this.choosePictureHeight = this.img_height*this.chooseVisible;
                        this.canvasX = (this.img_width - this.choosePictureWidth) /2;
                        this.canvasY = (this.img_height - this.choosePictureHeight) /2;
                        area_x = this.canvasX ;
                        area_y = this.canvasY ;
                        this.choosePictureTop = windowHeight/2-this.choosePictureHeight/2;
                        this.choosePictureLeft = windowWidth/2-this.choosePictureWidth/2;
                        // è®¡ç®—初始缩放比和最小缩放值
                        if (this.img_width < this.choosePictureWidth || this.img_height < this.choosePictureHeight) { // å½“图片宽或高小于240px时
                            let count = this.img_width <= this.img_height ? this.img_width : this.img_height;
                            let check = this.choosePictureWidth <= this.choosePictureHeight ? this.choosePictureWidth : this.choosePictureHeight;
                            this.img_scaling = count / check;
                            this.scaleMin = 1;
                        } else if (this.img_width > windowWidth && this.img_width <= this.img_height) { // å½“图片宽度大于屏幕宽度并且图片宽度小于图片高度时
                            this.img_scaling = this.img_width / windowWidth;
                            this.scaleMin = tailorSize / windowWidth;
                        } else {
                            this.img_scaling  = 1;
                            //this.img_scaling = count / check;
                            let count = this.img_width <= this.img_height ? this.img_width : this.img_height;
                            let check = this.choosePictureWidth <= this.choosePictureHeight ? this.choosePictureWidth : this.choosePictureHeight;
                            this.scaleMin = check / count;
                        }
                        //外块宽高
                        this.areaWidth = this.choosePictureWidth*3;
                        this.areaHeight = this.choosePictureHeight*3;
                        //外块偏移量
                        this.areaHeightTop = -((this.areaHeight - windowHeight)/2);
                        this.areaWidthLeft = -((this.areaWidth - windowWidth)/2);
                        this.offsetX = this.areaWidth/2-(this.img_width/this.img_scaling)/2;
                        this.offsetY = this.areaHeight/2-(this.img_height/this.img_scaling)/2;
                        // èŽ·å–æ–°çš„åç§»é‡
                        newOffsetX = this.offsetX;
                        newOffsetY = this.offsetY;
                    }
                })
            },
            // è®¡ç®—拖动偏移量
            movableChange(e) {
                this.canvasX = this.canvasX-(e.detail.x-area_x);
                area_x = e.detail.x;
                this.canvasY = this.canvasY-(e.detail.y-area_y)
                if(this.canvasY<0){
                    this.canvasY = 0;
                }
                area_y = e.detail.y;
            },
            // è®¡ç®—缩放比例和偏移量
            movableScale(e) {
                newOffsetX = e.detail.x
                newOffsetY = e.detail.y
                this.canvasWidth = this.canvasWidth*e.detail.scale;
                this.canvasHeight = this.canvasHeight*e.detail.scale;
                this.scale = e.detail.scale;
            },
            // æˆªå–图片
            createImg() {
                var that = this;
                let ctx = uni.createCanvasContext("picture-canvas",this);
                ctx.drawImage(this.pictureSrc, 0, 0, this.img_width*this.scale,this.img_height*this.scale );
                    // ctx.draw();
                ctx.draw(false, () => {
                    uni.canvasToTempFilePath({
                        x: this.canvasX,
                        y: this.canvasY,
                        width: this.choosePictureWidth,
                        height: this.choosePictureHeight,
                        destWidth: this.choosePictureWidth,
                        destHeight: this.choosePictureHeight,
                        quality: 0.9,
                        canvasId: 'picture-canvas',
                        success: function (res) {
                              //#ifdef H5
                              that.hidePop();
                              that.$emit("createImg",res.tempFilePath)
                              //#endif
                              //#ifdef MP-WEIXIN || APP
                               uni.getFileSystemManager().readFile({
                                   filePath: res.tempFilePath,
                                   encoding: 'base64',
                                   success: res => {
                                       let base64 = 'data:image/png;base64,' + res.data;
                                       that.hidePop();
                                       that.$emit("createImg",base64)
                                   }
                               })
                                //#endif
                        }
                    },this);
                });
            }
        }
    }
</script>
<style>
    .picture-cut {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        width: 100%;
        height: 100%;
        z-index: 999999;
        transform: translateX(100%);
        transition: all 200ms ease;
        visibility: hidden;
    }
    .picture-cut-show {
        transform: translateY(0) !important;
        visibility: visible;
    }
    /* æ‹–动域的位置和大小 */
    .picture-cut .picture-area {
        position: absolute;
        /* ä½¿å…¶å±…中定位 */
    }
    /* æ‹–动控件的大小 */
    .picture-area {
        pointer-events: none;
        z-index: 998;
    }
    .picture-view {
        pointer-events: auto;
    }
    /* ä¸­é—´é€‰æ‹©æ¡†çš„大小,本意是视觉上模拟拖动域 */
    .select-box {
        position: absolute;
        /* top: calc(50% - 120px);
        left: calc(50% - 120px); */
/*         width: 240px;
        height: 240px; */
        box-sizing: border-box;
    /*     border-radius: 50%; */
        box-shadow: 0px 0px 0px 2005px rgba(0, 0, 0, 0.7) ;
        z-index: 999;
        pointer-events: none;
    /*     min-width: 50px;
        min-height: 50px; */
    }
    .horn{
        position: absolute;
        width: 100%;
        height: 100%;
        /* border:1rpx solid #ff0000; */
    }
    .horn>view{
        width: 15px;
        height: 15px;
        position:absolute;
    }
    .horn .lt{
        pointer-events: auto;
        /* border-top: 3px solid #ff0000;
        border-left: 3px solid #ff0000; */
        left: -4px;
        top: -4px;
    }
    .horn .rt{
        pointer-events: auto;
        /* border-top: 3px solid #ff0000;
        border-right: 3px solid #ff0000; */
        right: -2px;
        top: -2px;
    }
    .horn .rb{
        pointer-events: auto;
        /* border-bottom:3px solid #ff0000;
        border-right: 3px solid #ff0000; */
        right: -2px;
        bottom: -2px;
    }
    .horn .lb{
        pointer-events: auto;
        /* border-bottom:3px solid #ff0000;
        border-left: 3px solid #ff0000; */
        left: -2px;
        bottom: -2px;
    }
    .area-bottom{
        position: absolute;
        width:100%;
        height:75px;
        bottom: 0;
        z-index:999;
        box-shadow: 0 -8px 12px -5px  #ffffff;
        background-color: rgba(66, 66, 66, 0.4);
    }
    .area-bottom-box{
        display: flex;
        justify-content: space-between;
        width:86%;
        height:50px;
        line-height: 60px;
        color:#FFFFFF;
        margin:0 auto;
    }
    .button-view {
        position: absolute;
        bottom: 20px;
        right: 20px;
        width: 60px;
        background-color: #007AFF;
        font-size: 14px;
        color: #FFFFFF;
        z-index:101;
    }
    /* ç”»å¸ƒå¤§å°ï¼Œç”»å¸ƒå¤§å°å°±æ˜¯æˆªå–的原始大小 */
    .canvas-view {
        position: absolute;
    /*     visibility: hidden; */
    }
</style>
h5/main.js
@@ -9,7 +9,7 @@
// Vue.prototype.$baseUrl = 'http://192.168.0.186:10027/';
// Vue.prototype.$baseUrl = 'https://dmtest.ahapp.net/h5_api/';
Vue.prototype.$baseUrl = 'http://218.23.218.228:8018/web_interface/';
Vue.prototype.$baseUrl = 'http://facepay.huasunsolar.com/web_interface/';
Vue.prototype.$store = store;
App.mpType = 'app'
h5/package.json
@@ -1,24 +1,16 @@
{
    "id": "stephenzhou-picker",
    "name": "支持分页Picker",
    "displayName": "支持分页Picker",
    "version": "1.0",
    "description": "Popup弹出框,可滚动分页,支持单选、多选功能的picker筛选组件",
    "id": "tly-picture-cut",
    "name": "图片裁剪 å››è§’拖动 å›¾ç‰‡ç¼©æ”¾ç§»åЍ",
    "displayName": "图片裁剪 å››è§’拖动 å›¾ç‰‡ç¼©æ”¾ç§»åЍ",
    "version": "1.0.0",
    "description": "图片裁剪组件 æ”¯æŒæ‹–动四角 å·²ä½¿ç”¨å¾®ä¿¡å°ç¨‹åºã€H5 å¹³å°",
    "keywords": [
        "筛选",
        "分页",
        "搜索",
        "多选",
        "单选"
        "图片裁剪"
    ],
    "dcloudext": {
        "category": [
            "前端组件",
            "通用组件"
        ]
    },
    "dependencies": {
        "qrcodejs2": "0.0.2",
        "uniapp-qrcode": "^1.0.2"
    }
}
}
h5/pages/userinfo/userinfo.vue
@@ -63,14 +63,20 @@
        <view class="footer-box">
            <view class="submit-button" @click="submit">下一步</view>
        </view>
        <u-picker keyName="name" :show="show" :columns="columns" @confirm="confirm" @cancel="show = false"></u-picker>
        <!-- <tly-picture-cut ref="tlyPictureCut" :pictureSrc="photoSrc" @createImg="uploadImg"></tly-picture-cut> -->
        <u-picker :show="show" :columns="columns" keyName="name" @cancel="show = false" @confirm="confirm"></u-picker>
        <qf-image-cropper ref="cropper" :width="280" :height="280" :radius="30" @crop="uploadImg"></qf-image-cropper>
    </view>
</template>
<script>
    import tlyPictureCut from "@/components/tly-picture-cut/tlyPictureCut.vue";
    import QfImageCropper from '@/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue';
    import { mapState } from 'vuex'
    export default {
        data() {
            return {
                photoSrc: "",
                show: false,
                visit: '',
                type: '',
@@ -86,17 +92,46 @@
                    faceImg: '',
                    faceImgUrl: '',
                    imgurl: '',
                    imgurlUrl: ''
                    imgurlUrl: '',
                    companyName: ''
                }
            }
        },
        
        computed: {
            ...mapState(['member'])
        },
        components: { tlyPictureCut, QfImageCropper },
        onLoad(option) {
            if (this.member) {
                this.visitorData.name = this.member.name
                this.visitorData.phone = this.member.phone
                this.visitorData.idcardNo = this.member.idcardDecode
                this.visitorData.companyName = this.member.visitCompanyName
                this.visitorData.faceImg = this.member.faceImg
                if (this.member.faceImg) {
                    this.visitorData.faceImgUrl = this.member.prefixUrl + this.member.faceImg
                }
                this.visitorData.imgurl = this.member.imgurl
                if (this.member.imgurl) {
                    this.visitorData.imgurlUrl = this.member.prefixUrl + this.member.imgurl
                }
                this.visitorData.idcardType = this.member.idcardType
                if (this.member.idcardType === 0) {
                    this.visitorData.idcardTypeName = '身份证'
                } else if (this.member.idcardType === 1) {
                    this.visitorData.idcardTypeName = '港澳证件'
                } else if (this.member.idcardType === 2) {
                    this.visitorData.idcardTypeName = '护照'
                }
            }
            this.visitorData.userAnswerId = option.userAnswerId
            this.getVisit()
            uni.$on('update', (data) => {
                this.uploadImg(data.tempFilePath)
            })
            // uni.$on('update', (data) => {
            //     this.uploadImg(data.tempFilePath)
            // })
        },
        methods: {
@@ -159,23 +194,23 @@
                })
            },
            uploadImg(file) {
                var that = this
                that.$refs.cropper.close()
                uni.showLoading({ title: '上传中', mask: true });
                uni.uploadFile({
                    url: `${this.$baseUrl}public/api/uploadFtp.do`,
                    filePath: file,
                    filePath: file.tempFilePath,
                    name: 'file',
                    formData: {
                        folderCode: 'MEMBER_IMG'
                    },
                    success: (uploadFileRes) => {
                        let res = JSON.parse(uploadFileRes.data)
                        if (this.type === 'faceImg') {
                            this.visitorData.faceImg = res.data.halfPath
                            this.visitorData.faceImgUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                        } else {
                            this.visitorData.imgurl = res.data.halfPath
                            this.visitorData.imgurlUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                        }
                        this.visitorData.faceImg = res.data.halfPath
                        this.visitorData.faceImgUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                    },
                    fail(err) {
                        alert('失败')
                    },
                    complete() {
                        uni.hideLoading();
@@ -183,42 +218,42 @@
                });
            },
            upload(type) {
                this.type = type
                var that = this
                that.type = type
                if (type === 'faceImg') {
                    that.$refs.cropper.open()
                    return
                }
                uni.chooseImage({
                    count: 1,
                    success: (chooseImageRes) => {
                        if (type === 'faceImg') {
                            uni.navigateTo({
                                url: `/pages/cropping/cropping?item=${JSON.stringify({ tempFilePath: chooseImageRes.tempFilePaths[0] })}`
                            })
                            return
                        }
                        uni.showLoading({ title: '上传中', mask: true });
                        for (let i = 0; i < chooseImageRes.tempFilePaths.length; i++) {
                        // if (type === 'faceImg') {
                        //     that.photoSrc = chooseImageRes.tempFilePaths[0];
                        //     that.$refs.tlyPictureCut.showPop();
                        // }
                        if (type === 'imgurl') {
                            uni.showLoading({ title: '上传中', mask: true });
                            uni.uploadFile({
                                url: `${this.$baseUrl}public/api/uploadFtp.do`,
                                filePath: chooseImageRes.tempFilePaths[i],
                                url: `${that.$baseUrl}public/api/uploadFtp.do`,
                                filePath: chooseImageRes.tempFilePaths[0],
                                name: 'file',
                                formData: {
                                    folderCode: 'MEMBER_IMG'
                                },
                                success: (uploadFileRes) => {
                                timeout: 60000,
                                success: (uploadFileRes) => {
                                    let res = JSON.parse(uploadFileRes.data)
                                    if (type === 'faceImg') {
                                        this.visitorData.faceImg = res.data.halfPath
                                        this.visitorData.faceImgUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                                    } else {
                                        this.visitorData.imgurl = res.data.halfPath
                                        this.visitorData.imgurlUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                                    }
                                    that.visitorData.imgurl = res.data.halfPath
                                    that.visitorData.imgurlUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                                },
                                complete() {
                                    if (i === chooseImageRes.tempFilePaths.length - 1) {
                                        uni.hideLoading();
                                    }
                                    uni.hideLoading();
                                }
                            });
                        }
                    },
                    fail(err) {
                        alert('api报错')
                    }
                });
            },
@@ -231,7 +266,7 @@
    }
</script>
<style>
<style lang="scss">
    page {
        background-color: #F7F7F7 !important;
    }
@@ -268,7 +303,7 @@
        display: flex;
        flex-direction: column;
        .title1_a {
            font-size: 30rpx;
            font-size: 30rpx !important;
            font-weight: 400;
            color: #222222;
            display: flex;
@@ -280,7 +315,7 @@
            }
        }
        .title1_b {
            font-size: 24rpx;
            font-size: 24rpx !important;
            font-weight: 400;
            color: #999999;
        }
h5/pages/visitorApplication/visitorApplication.vue
@@ -45,6 +45,7 @@
                    <text>访问门禁</text>
                    <text>*</text>
                </view>
                 <!-- @click="show6 = true" -->
                <view class="list_item_content">
                    <text :style="{color: form1.doorSelectName ? '#000000' : ''}">{{form1.doorSelectName ? form1.doorSelectName : '请选择'}}</text>
                    <u-icon name="arrow-right" color="#CCCCCC" size="20"></u-icon>
@@ -64,8 +65,9 @@
                    <text>随行车辆</text>
                    <text></text>
                </view>
                <view class="list_item_content">
                    <input type="text" placeholder="请输入车牌号" v-model="form1.carNos" maxlength="8" placeholder-style="color: #999999;" />
                <view class="list_item_content" @click="openInput(1)">
                    <text :style="{color: form1.carNos ? '#000000' : ''}">{{form1.carNos ? form1.carNos : '请输入车牌号码'}}</text>
                    <!-- <input type="text" placeholder="请输入车牌号" v-model="form1.carNos" maxlength="8" placeholder-style="color: #999999;" /> -->
                </view>
            </view>
        </view>
@@ -226,7 +228,7 @@
                    <view class="adduser_list_item">
                        <view class="adduser_list_item_label">
                            <text>公司</text>
                            <text></text>
                            <text>*</text>
                        </view>
                        <view class="adduser_list_item_ipt">
                            <input type="text" v-model="withUserList.companyName" placeholder-style="color: #999999;font-size: 28rpx;" placeholder="请输入公司名称" />
@@ -237,8 +239,9 @@
                            <text>随行车辆</text>
                            <text></text>
                        </view>
                        <view class="adduser_list_item_ipt">
                            <input type="text" v-model="withUserList.carNos" placeholder-style="color: #999999;font-size: 28rpx;" placeholder="请输入车牌号" />
                        <view class="adduser_list_item_ipt" @click="openInput(2)">
                            <text :style="{color: withUserList.carNos ? '#000000' : ''}">{{withUserList.carNos ? withUserList.carNos : '请输入车牌号码'}}</text>
                            <!-- <input type="text" v-model="withUserList.carNos" disabled placeholder-style="color: #999999;font-size: 28rpx;" placeholder="请输入车牌号" /> -->
                        </view>
                    </view>
                    <view class="adduser_list_item">
@@ -278,13 +281,22 @@
            </view>
        </u-popup>
        <u-picker keyName="name" :show="show6" :columns="columns1" @confirm="seleIdcard" @cancel="show6 = false"></u-picker>
        <!-- <tly-picture-cut ref="tlyPictureCut" :pictureSrc="photoSrc" @createImg="uploadImg"></tly-picture-cut> -->
        <keyboardInput ref="keyboard" @export="setPlate" @close="closeInput" />
        <qf-image-cropper ref="cropper" :width="280" :height="280" :radius="30" @crop="uploadImg"></qf-image-cropper>
    </view>
</template>
<script>
    import tlyPictureCut from "@/components/tly-picture-cut/tlyPictureCut.vue";
    import keyboardInput from "@/components/keyboard-input/keyboard-input.vue";
    import QfImageCropper from '@/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue';
    export default {
        data() {
            return {
                photoSrc: '',
                type: '',
                inputType: '',
                show: false,
                show1: false,
                show2: false,
@@ -292,6 +304,7 @@
                show4: false,
                show5: false,
                show6: false,
                show7: false,
                fileList: [],
                columns1: [[{name: '身份证', id: 0}, {name: '港澳证件', id: 1},{name: '护照', id: 2}]],
                columns: [],
@@ -328,13 +341,52 @@
                verify: ''
            };
        },
        components: { tlyPictureCut, keyboardInput, QfImageCropper },
        onLoad(options) {
            this.form = JSON.parse(options.data)
            if (options.data) {
                this.form = JSON.parse(options.data)
            }
            this.getvisit()
            this.getVisit1()
            this.getUserValid()
        },
        methods: {
            openInput(type) {
                this.inputType = type
                this.$refs.keyboard.open()
            },
            setPlate(e) {
                if (this.inputType === 1) {
                    this.form1.carNos = e
                } else if (this.inputType === 2) {
                    this.withUserList.carNos = e
                }
                this.$forceUpdate()
                this.closeInput()
            },
            closeInput() {
                this.$refs.keyboard.close()
            },
            uploadImg(file) {
                this.$refs.cropper.close()
                uni.showLoading({ title: '上传中', mask: true });
                uni.uploadFile({
                    url: `${this.$baseUrl}public/api/uploadFtp.do`,
                    filePath: file.tempFilePath,
                    name: 'file',
                    formData: {
                        folderCode: 'MEMBER_IMG'
                    },
                    success: (uploadFileRes) => {
                        let res = JSON.parse(uploadFileRes.data)
                        this.withUserList.faceImg = res.data.halfPath
                        this.withUserList.faceImgUrl = res.data.prefixPath + res.data.folder + res.data.halfPath
                    },
                    complete() {
                        uni.hideLoading();
                    }
                });
            },
            closeMJ() {
                this.show = false
                this.columns.forEach(item => {
@@ -354,7 +406,7 @@
                    title: '离场时间不能为空',
                    icon: 'none'
                })
                if (!this.form1.doorSelectName) return uni.showToast({
                if (!this.form1.doorSelectName && this.accessControl == 1) return uni.showToast({
                    title: '访问门禁不能为空',
                    icon: 'none'
                })
@@ -374,7 +426,6 @@
                    withUserList: this.personnel
                }).then(res => {
                    if (res.code === 200) {
                        console.log(res)
                        uni.reLaunch({
                            url: `/pages/appointmentDetails/appointmentDetails?id=${res.data}`
                        })
@@ -424,6 +475,10 @@
                        icon: 'none'
                    })
                }
                if (!this.withUserList.companyName) return uni.showToast({
                    title: '公司不能为空',
                    icon: 'none'
                })
                if (!this.withUserList.faceImg) return uni.showToast({
                    title: '人脸照片不能为空',
                    icon: 'none'
@@ -449,8 +504,18 @@
                this.withUserList.imgurlUrl = ''
            },
            upload(type) {
                this.type = type
                if (type === 'faceImg') {
                    this.$refs.cropper.open()
                    return
                }
                uni.chooseImage({
                    success: (chooseImageRes) => {
                        // if (type === 'faceImg') {
                        //     this.photoSrc = chooseImageRes.tempFilePaths[0];
                        //     this.$refs.tlyPictureCut.showPop();
                        //     return
                        // }
                        uni.showLoading({ title: '上传中', mask: true });
                        for (let i = 0; i < chooseImageRes.tempFilePaths.length; i++) {
                            uni.uploadFile({
@@ -533,9 +598,6 @@
            },
            // æŸ¥è¯¢ç”¨æˆ·
            getUser() {
                console.log(this.verify)
                console.log(this.form1.phone1)
                console.log(this.form1.receptMemberName)
                if (this.verify === '0') {
                    if (this.form1.phone1) {
                        this.$u.api.getVisitedMember({
@@ -568,7 +630,6 @@
                    label: 'BEVISITED_USER_VALID'
                }).then(res => {
                    if (res.code === 200) {
                        console.log(res)
                        this.verify = res.data.code
                    }
                })
h5/redirect.html
@@ -6,7 +6,7 @@
  <script>
    var p = location.href.split("?")
    // window.location.href = 'https://dmtest.ahapp.net/hsvisit_h5/#/pages/notice/notice?' + p[1]
    window.location.href = 'http://218.23.218.228:8018/h5/#/pages/notice/notice?' + p[1]
    window.location.href = 'http://facepay.huasunsolar.com/h5/#/pages/notice/notice?' + p[1]
  </script>
</head>
<body>
h5/store/index.js
@@ -4,18 +4,25 @@
Vue.use(Vuex)
const openId = uni.getStorageSync('openId');
const member = uni.getStorageSync('member');
const store = new Vuex.Store({
    
    state: {
        openId: openId || ''
        openId: openId || '',
        member: member || null
    },
    
    mutations: {
        // è®¾ç½®åŸŽå¸‚
        // è®¾ç½®openId
        setOpenId(state, val) {
            state.openId = val
            uni.setStorageSync('openId', val);
        },
        // è®¾ç½®ç”¨æˆ·ä¿¡æ¯
        setMember(state, val) {
            state.member = val
            uni.setStorageSync('member', val);
        }
    }
    
h5/uni_modules/bt-cropper/changelog.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
## 3.0.2(2023-06-06)
更新了文档
## 3.0.1(2022-11-03)
修复 æ’¤é”€å’Œé‡åšä¸ç”Ÿæ•ˆçš„问题
## 3.0.0(2022-11-03)
使用wxs重构代码,性能大提升
新增 æ”¯æŒè’™ç‰ˆè£å‰ªï¼Œå¯ä»¥è£å‰ªä»»ä½•形状的图形(详情见demo示例)
新增 æ”¯æŒåœ¨å¼¹çª—中使用(详情见demo示例)
移除 ç”±äºŽæ’槽会导致许多问题,实际上开发者自己封装组件反而更简单,所以3.0版本以后移除插槽,2.0迁移教程见 demo:全屏裁剪
## 2.0.3(2022-08-21)
修复 åœ¨vue3 ç¨‹åºä¸­æŠ¥é”™çš„问题
新增 æ–°å¢žäº†å›¾ç‰‡åˆå§‹åŒ–完成和加载失败的事件
## 2.0.2(2022-08-18)
新增 å¢žåŠ äº†åŽŸåƒç´ è£å‰ªåŠŸèƒ½ï¼Œå³ä½¿ç”¨ç”¨æˆ·åœ¨è£å‰ªæ¡†å–æ™¯çš„å¤§å°ä½œä¸ºè¾“å‡ºåƒç´ ï¼Œæ¢å¥è¯è¯´ï¼Œè¾“å‡ºçš„å›¾ç‰‡åˆ†è¾¨çŽ‡ä¸Žè¾“å…¥å›¾ç‰‡åˆ†è¾¨çŽ‡ä¸€æ ·
新增 å¢žåŠ äº†change事件,会在图像和裁剪框位置变化后触发
新增 å¢žåŠ äº†compress参数 åŽ‹ç¼©å›¾ç‰‡ï¼ŒåŽ‹ç¼©å›¾ç‰‡æ˜¯ä¸ºäº†æå‡æµç•…åº¦ï¼Œæ‰€ä»¥åªä¼šé’ˆå¯¹ç”¨æˆ·æ‹–åŠ¨çš„é‚£å¼ å›¾ç‰‡è¿›è¡ŒåŽ‹ç¼©ï¼Œæœ€ç»ˆè¾“å‡ºçš„å›¾åƒå“è´¨å¹¶ä¸ä¼šå—åˆ°å½±å“
修复 ç”¨æˆ·åœ¨æ²¡æœ‰ä¼ å…¥å›¾åƒæ—¶æŠ¥é”™çš„问题
修复 ios在某些机型上拖动出现残留的问题
## 2.0.1(2022-07-20)
修复:ios打包成app的时候有几率裁剪不成功的问题
## 2.0.0(2022-07-13)
更新了2.0版本,增加了图片放大功能
## bt-cropper å›¾ç‰‡è£åˆ‡
========
### 2022å¹´7月13日 å‘布2.0版本
* 1.完全重构了代码,并且优化了性能
* 2.支持app
* 3.修复demo在H5的情况下高度错误的问题
h5/uni_modules/bt-cropper/components/bt-cropper/bt-cropper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1095 @@
<template>
    <view class="bt-container" :style="[containerStyle]">
        <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
        <view class="mainContent" data-type="image" @touchstart="wxsModule.touchStart" @touchmove="wxsModule.touchMove" @touchend="wxsModule.touchEnd">
        <!-- #endif -->
        <!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
        <view class="mainContent" data-type="image" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd">
        <!-- #endif -->
            <template v-if="imageRect && cropperRect">
                <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                <image mode="aspectFit" :src="imageSrc" class="image" :rotateAngle="rotateAngle" :change:rotateAngle="wxsModule.changeRotateAngle" :change:imageRect="wxsModule.changeImageRect" :imageRect="imageRect" :class="{ anim }">
                <!-- #endif -->
                <!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                <image mode="aspectFit" :src="imageSrc" class="image" :style="[imageStyle]" :class="{ anim }">
                <!-- #endif -->
                </image>
                <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                <view class="cropper" :class="{ anim }" :change:cropperRect="wxsModule.changeCropper" :cropperRect="cropperRect" :change:ratio="wxsModule.changeRatio" :ratio="ratio" >
                <!-- #endif -->
                <!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
                <view class="cropper" :class="{ anim }"  :style="[cropperStyle]">
                <!-- #endif -->
                    <image class="mask" :src="mask"></image>
                    <template v-if="showGrid">
                        <view class="line row row1"></view>
                        <view class="line row row2"></view>
                        <view class="line col col1"></view>
                        <view class="line col col2"></view>
                    </template>
                    <!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5  -->
                    <view class="controller vertical left" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller vertical right" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller horizon top" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller horizon bottom" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller left top" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller left bottom" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller right top" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <view class="controller right bottom" @touchstart="wxsModule.touchStart" data-type="controller" />
                    <!-- #endif -->
                    <!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5  -->
                    <view class="controller vertical left" @touchstart.stop="touchStart(-1, 0, $event)" />
                    <view class="controller vertical right" @touchstart.stop="touchStart(1, 0, $event)" />
                    <view class="controller horizon top" @touchstart.stop="touchStart(0, -1, $event)" />
                    <view class="controller horizon bottom" @touchstart.stop="touchStart(0, 1, $event)" />
                    <view class="controller left top" @touchstart.stop="touchStart(-1, -1, $event)" />
                    <view class="controller left bottom" @touchstart.stop="touchStart(-1, 1, $event)" />
                    <view class="controller right top" @touchstart.stop="touchStart(1, -1, $event)" />
                    <view class="controller right bottom" @touchstart.stop="touchStart(1, 1, $event)" />
                    <!-- #endif -->
                </view>
            </template>
        </view>
            <canvas v-if="type2d" type="2d" class="bt-canvas" :width="target.width" :height="target.height"></canvas>
            <canvas
                v-else
                :canvas-id="canvasId"
                class="bt-canvas"
                :style="{
                    width: target.width + 'px',
                    height: target.height + 'px'
                }"
                :width="target.width * pixel"
                :height="target.height * pixel"
            ></canvas>
    </view>
</template>
<script>
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
import touches from './js/touchs.js';
// #endif
import { parseUnit,sleep } from './utils/tools.js';
/**
 * better-cropper å›¾ç‰‡è£åˆ‡æ’ä»¶
 */
export default {
    name: 'bt-cropper',
    props: {
        // å›¾ç‰‡è·¯å¾„,支持网络路径和本地路径
        imageSrc: {
            type: String,
            default: '',
            required: true
        },
        mask: {
            type: String,
            default: ''
        },
        // æ‰‹åŠ¨æŒ‡å®šå®¹å™¨çš„å¤§å°
        containerSize: {
            type: Object,
            default: null
        },
        // è¾“出图片的格式,默认jpg
        fileType: {
            type: String,
            default: 'png'
        },
        // ç”Ÿæˆçš„图片的宽度,不传或者传0表示按照原始分辨率裁切
        dWidth: Number,
        maxWidth: {
            type: Number,
            default: 2000
        },
        // è£åˆ‡æ¯”例,0表示自由
        ratio: {
            type: Number,
            default: 0,
            validator(value) {
                if (typeof value === 'number') {
                    if (value < 0) {
                        return false;
                    }
                } else {
                    return false;
                }
                return true;
            }
        },
        // æ—‹è½¬è§’度
        rotate: Number,
        // æ˜¯å¦å±•示网格
        showGrid: {
            type: Boolean,
            default: false
        },
        // å›¾ç‰‡è´¨é‡ï¼Œ0-1 è¶Šå¤§è´¨é‡è¶Šå¥½
        quality: {
            type: Number,
            default: 1
        },
        canvas2d: {
            type: Boolean,
            default: false
        },
        // åˆå§‹çš„图片位置
        initPosition: {
            type: Object,
            default() {
                return null;
            }
        },
        // æ˜¯å¦å¼€å¯æ“ä½œç»“束后自动放大
        autoZoom: {
            type: Boolean,
            default: true
        }
    },
    // #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
    mixins: [touches],
    // #endif
    data() {
        return {
            canvasId: 'bt-cropper',
            containerRect: null,
            imageInfo: null,
            operationHistory: [],
            operationIndex: 0,
            anim: false,
            timer: null,
            // æ˜¯å¦ä½¿ç”¨2D canvas
            type2d: false,
            pixel: 1,
            imageRect: null,
            cropperRect: null,
            target:{
                width:0,
                height:0
            }
        };
    },
    watch: {
        imageSrc: {
            handler(src) {
                if (typeof src === 'string' && src !== '') {
                    this.imageInit(src);
                } else {
                    this.imageInfo = null;
                }
            },
            immediate: true
        },
        ratio() {
            if (this.ratio != 0) {
                this.startAnim();
                this.init();
            }
        }
    },
    computed: {
        containerStyle() {
            if (this.containerSize && this.containerRect) {
                return {
                    width: this.containerRect.width + 'px',
                    height: this.containerRect.height + 'px'
                };
            }
            return {};
        },
        rotateAngle(){
            const angel = Number(this.rotate)
            if(isNaN(angel)){
                return 0
            }else{
                return angel % 360
            }
        }
    },
    methods: {
        startAnim() {
            this.stopAnim();
            this.anim = true;
            this.timer = setTimeout(() => {
                this.anim = false;
            }, 200);
        },
        stopAnim() {
            this.anim = false;
            clearTimeout(this.timer);
        },
        imageInit(src) {
            uni.showLoading({
                title: '载入中...'
            });
            uni.getImageInfo({
                src,
                success: res => {
                    this.imageInfo = res;
                    this.$nextTick(() => {
                        this.getContainer().then(rect => {
                            this.containerRect = rect;
                            this.init();
                        });
                    });
                },
                fail: (err) => {
                    this.$emit('loadFail',err);
                    uni.showToast({
                        title: '图片下载失败!',
                        icon: 'none'
                    });
                },
                complete(res) {
                    uni.hideLoading();
                }
            });
        },
        initCropper() {
            const imageRate = this.imageInfo.width / this.imageInfo.height;
            const containerRate = this.containerRect.width / this.containerRect.height;
            const imageRect = {};
            let cropperRate = this.ratio;
            if (cropperRate == 0) {
                if (this.cropperRect) {
                    cropperRate = this.cropperRect.width / this.cropperRect.height;
                } else {
                    cropperRate = 1;
                }
            }
            const cropperRect = {};
            if (containerRate > cropperRate) {
                cropperRect.height = this.containerRect.height * 0.85;
                cropperRect.width = cropperRect.height * cropperRate;
            } else {
                cropperRect.width = this.containerRect.width * 0.85;
                cropperRect.height = cropperRect.width / cropperRate;
            }
            if (cropperRate > imageRate) {
                imageRect.width = cropperRect.width;
                imageRect.height = imageRect.width / imageRate;
            } else {
                imageRect.height = cropperRect.height;
                imageRect.width = imageRect.height * imageRate;
            }
            imageRect.left = (this.containerRect.width - imageRect.width) / 2;
            imageRect.top = (this.containerRect.height - imageRect.height) / 2;
            cropperRect.left = imageRect.left + (imageRect.width - cropperRect.width) / 2;
            cropperRect.top = imageRect.top + (imageRect.height - cropperRect.height) / 2;
            return {
                imageRect,
                cropperRect
            };
        },
        init() {
            const {imageRect,cropperRect} = this.initCropper()
            if(this.initPosition){
                const scale = this.imageInfo.width/imageRect.width;
                const {left,top,width,height} = this.initPosition;
                if(left!==undefined&&top!==undefined&&width!==undefined&&height!==undefined){
                    cropperRect.width = width/scale;
                    cropperRect.height = height/scale;
                    cropperRect.left = left/scale;
                    cropperRect.top = top/scale;
                    this.$nextTick(this.zoomToFill);
                }
            }
            this.imageRect = imageRect
            this.cropperRect = cropperRect
            this.operationHistory = [{imageRect,cropperRect}];
            this.operationIndex = 0;
            this.setTarget();
            // #ifdef MP-WEIXIN
            const systemInfo = uni.getSystemInfoSync();
            if (this.canvas2d === false || systemInfo.platform === 'windows' || systemInfo.platform === 'mac') {
                this.type2d = false;
            } else {
                this.type2d = true;
                this.pixel = systemInfo.pixelRatio;
            }
            // #endif
            //非微信小程序端强制关闭canvas2d模式
            // #ifndef MP-WEIXIN
            this.type2d = false;
            // #endif
            // #ifdef  MP-TOUTIAO || MP-LARK || MP-ALIPAY
            this.type2d = this.canvas2d;
            // #endif
        },
        // è®¾ç½®ç›®æ ‡å›¾åƒçš„大小
        setTarget(){
            const ratio = this.cropperRect.width / this.cropperRect.height;
            if (!!this.dWidth) {
                this.target = {
                    width: this.dWidth,
                    height: this.dWidth / (ratio || 1)
                }
            } else {
                const width = Math.min(this.maxWidth, this.cropperRect.width * (this.imageInfo.width / this.imageRect.width));
                this.target = {
                    width,
                    height: width / (ratio || 1)
                }
            }
        },
        addHistory({imageRect,cropperRect}){
            if(this.operationIndex!==this.operationHistory.length-1){
                this.operationHistory = this.operationHistory.slice(0,this.operationIndex)
            }
            this.operationHistory.push({
                imageRect,
                cropperRect
            });
            if (this.operationHistory.length > 10) {
                this.operationHistory.shift();
            }
            this.operationIndex = this.operationHistory.length - 1;
        },
        updateData(data) {
            this.imageRect = data.imageRect
            this.cropperRect = data.cropperRect
            this.addHistory(data);
            this.setTarget();
            if (this.autoZoom) {
                this.timer = setTimeout(() => {
                    this.zoomToFill();
                }, 600);
            }
            const {imageRect,cropperRect} = data
            const scale = imageRect.width/this.imageInfo.width
            this.$emit('change', {
                left: (cropperRect.left - imageRect.left) /scale,
                top: (cropperRect.top - imageRect.top) / scale,
                width: cropperRect.width / scale,
                height: cropperRect.height / scale
            });
        },
        getContainer() {
            if (this.containerSize !== null && typeof this.containerSize == 'object') {
                const { width, height } = this.containerSize;
                return Promise.resolve({
                    width: parseUnit(width),
                    height: parseUnit(height)
                });
            } else {
                return new Promise(resolve => {
                    const query = uni.createSelectorQuery().in(this);
                    query
                        .select('.mainContent')
                        .boundingClientRect(rect => {
                            resolve(rect);
                        })
                        .exec();
                });
            }
        },
        zoomToFill() {
            this.startAnim();
            const beforeCropper = {
                ...this.cropperRect
            };
            const operation = {
                imageRect:this.imageRect,
                cropperRect:this.cropperRect
            };
            this.cropperRect = this.initCropper().cropperRect;
            const scale = this.cropperRect.width / beforeCropper.width;
            const ox = beforeCropper.left - this.imageRect.left;
            const oy = beforeCropper.top - this.imageRect.top;
            this.imageRect = {
                width: this.imageRect.width * scale,
                height: this.imageRect.height * scale,
                left: this.imageRect.left + (this.cropperRect.left - beforeCropper.left) - (scale - 1) * ox,
                top: this.imageRect.top + (this.cropperRect.top - beforeCropper.top) - (scale - 1) * oy
            };
        },
        onTouchStart() {
            this.stopAnim();
        },
        // æ’¤é”€
        undo() {
            if (this.operationIndex > 0) {
                this.operationIndex--;
                this.imageRect = this.operationHistory[this.operationIndex].imageRect;
                this.cropperRect = this.operationHistory[this.operationIndex].cropperRect;
                return true;
            }
            return false;
        },
        // é‡åš
        resume() {
            if (this.operationIndex < this.operationHistory.length - 1) {
                this.operationIndex++;
                this.imageRect = this.operationHistory[this.operationIndex].imageRect;
                this.cropperRect = this.operationHistory[this.operationIndex].cropperRect;
                return true;
            }
            return false;
        },
        async drawImage(ctx,image,x,y,w,h){
            if (this.type2d) {
                await new Promise(resolve => (image.onload = resolve));
                ctx.drawImage(image, x * this.pixel, y * this.pixel, w * this.pixel, h * this.pixel);
            } else {
                const path = await new Promise((resolve)=>{
                    uni.getImageInfo({
                        src:image,
                        success({path}){
                            resolve(path)
                        }
                    })
                })
                ctx.drawImage(path, x * this.pixel, y * this.pixel, w * this.pixel, h * this.pixel);
                await new Promise((resolve) => ctx.draw(false,resolve));
            }
        },
        async crop() {
            let ctx;
            let canvas;
            this.$emit('cropStart')
            this.setTarget()
            if (this.type2d) {
                const query = uni.createSelectorQuery().in(this);
                canvas = await new Promise(resolve =>
                    query
                        .select('.bt-canvas')
                        .node(({ node }) => resolve(node))
                        .exec()
                );
                canvas.width = this.target.width * this.pixel;
                canvas.height = this.target.height * this.pixel;
                ctx = canvas.getContext('2d');
                // #ifdef MP-TOUTIAO
                if(this.type2d){
                    console.warn("请注意:目前头条系小程序暂时无法使用2d canvas保存图片,建议换成V1版本")
                }
                // #endif
            } else {
                ctx = uni.createCanvasContext(this.canvasId,this);
            }
            const scale = this.cropperRect.width / this.target.width;
            const dx = (this.cropperRect.left - this.imageRect.left) / scale;
            const dy = (this.cropperRect.top - this.imageRect.top) / scale;
            let image;
            if(this.type2d){
                image = canvas.createImage()
                image.src = this.imageSrc;
            }else{
                image = this.imageSrc;
            }
            const x = -dx,
                y = -dy,
                w = this.imageRect.width / scale,
                h = this.imageRect.height / scale;
            ctx.save();
            ctx.translate(x + w / 2, y + h / 2);
            ctx.rotate((this.rotateAngle * Math.PI) / 180);
            ctx.translate(-(x + w / 2), -(y + h / 2));
            await this.drawImage(ctx,image, x, y, w, h);
            ctx.restore();
            if (this.mask !== '') {
                let imageData;
                if (this.type2d) {
                    imageData = ctx.getImageData(0, 0, this.target.width, this.target.height);
                } else {
                    imageData = await new Promise(resolve => {
                        uni.canvasGetImageData({
                            canvasId: this.canvasId,
                            x: 0,
                            y: 0,
                            width: this.target.width,
                            height: this.target.height,
                            success(res) {
                                resolve(res);
                            }
                        },this);
                    });
                }
                ctx.clearRect(0, 0, this.target.width, this.target.height);
                if(this.type2d){
                    image.src = this.mask;
                }else{
                    image = this.mask;
                }
                await this.drawImage(ctx,image, 0, 0, this.target.width, this.target.height);
                let maskData;
                if (this.type2d) {
                    maskData = ctx.getImageData(0, 0, this.target.width, this.target.height);
                } else {
                    maskData = await new Promise((resolve,reject) => {
                        uni.canvasGetImageData({
                            canvasId: this.canvasId,
                            x: 0,
                            y: 0,
                            width: this.target.width,
                            height: this.target.height,
                            success(res) {
                                resolve(res);
                            }
                        },this);
                    });
                }
                ctx.clearRect(0, 0, this.target.width, this.target.height);
                for (let index = 3; index < maskData.data.length; index += 4) {
                    const alpha = maskData.data[index];
                    if (alpha !== 0) {
                        imageData.data[index] = 0;
                    }
                }
                if (this.type2d) {
                    ctx.putImageData(imageData, 0, 0);
                } else {
                    await new Promise(resolve => {
                        uni.canvasPutImageData({
                            canvasId: this.canvasId,
                            x: 0,
                            y: 0,
                            width: imageData.width,
                            height: imageData.height,
                            data: imageData.data,
                            complete: res => {
                                resolve(res);
                            }
                        },this);
                    });
                }
            }
            return new Promise((resolve,reject) => {
                const params = {};
                if (this.type2d) {
                    params.canvas = canvas;
                } else {
                    params.canvasId = this.canvasId;
                }
                uni.canvasToTempFilePath({
                    ...params,
                    destWidth: this.target.width,
                    destHeight: this.target.height,
                    quality: Number(this.quality) || 1,
                    fileType: this.fileType,
                    success: ({ tempFilePath }) => {
                        // #ifdef H5
                        var arr = tempFilePath.split(',');
                        var mime = arr[0].match(/:(.*?);/)[1];
                        var bstr = atob(arr[1]);
                        var n = bstr.length;
                        var u8arr = new Uint8Array(n);
                        for (var i = 0; i < n; i++) {
                            u8arr[i] = bstr.charCodeAt(i);
                        }
                        var url = URL || webkitURL;
                        resolve(
                            url.createObjectURL(
                                new Blob([u8arr], {
                                    type: mime
                                })
                            )
                        );
                        // #endif
                        resolve(tempFilePath);
                    },
                    fail(err) {
                        console.log('保存失败,错误信息:',err);
                        reject(err);
                    }
                },this);
            });
        }
    }
};
</script>
<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
<script module="wxsModule" lang="wxs">
var startTouchs = [];
var startDistance = 0;
var touchCenter = [];
var ratio = 0;
var imageInstance = null;
var cropperInstance = null;
var touchType = "";
var touchInstance = null;
var cropperRect = null;
var imageRect = null;
// æ—‹è½¬è§’度
var rotateAngle = 0;
// æ“ä½œæ—¶æ”¹å˜çš„对象
var changes = {
    imageRect: null,
    cropperRect: null
}
function updateImageStyle() {
    var imageRect = changes.imageRect
    imageInstance.setStyle({
        left: imageRect.left + 'px',
        top: imageRect.top + 'px',
        width: imageRect.width + 'px',
        height: imageRect.height + 'px',
        transform: 'rotate(' + rotateAngle + 'deg)'
    })
}
function updateCopperStyle() {
    var cropperRect = changes.cropperRect
    cropperInstance.setStyle({
        left: cropperRect.left + "px",
        top: cropperRect.top + "px",
        width: cropperRect.width + "px",
        height: cropperRect.height + "px"
    })
}
function imageScale(scaleRate) {
    var cw = imageRect.width * (scaleRate - 1)
    var ch = imageRect.height * (scaleRate - 1)
    changes.imageRect = {
        width: imageRect.width + cw,
        height: imageRect.height + ch,
        left: imageRect.left - cw * (touchCenter[0]),
        top: imageRect.top - ch * (touchCenter[1])
    }
}
function getImageRotateSizeChange(w,h){
    var cw = h*Math.sin(rotateAngle/180 * Math.PI)
    var ch = w*Math.sin(rotateAngle/180 * Math.PI)
    return {cw,ch}
}
// è§’度转弧度
function ang2deg(ang){
    return ang/180*Math.PI
}
// è®¡ç®—旋转后真实的图片大小
function getRealSize(){
    var w = changes.imageRect.width
    var h = changes.imageRect.height
    var l =  changes.imageRect.left
    var t =  changes.imageRect.top
    // å†…斜边
    var R = Math.sqrt(w*w+h*h)
    var angle = Math.atan(h/w) / Math.PI * 180
    var rorate = rotateAngle%90
    var direct = Math.floor(rotateAngle/90)
    var width = R*Math.cos(ang2deg(angle-rorate))
    var height = R*Math.sin(ang2deg(angle+rorate))
    if(direct % 2 === 1){
        var temp = width
        width = height
        height = temp
    }
    return {
        width: width,
        height: height,
        left: l - (width - w)/2,
        top: t - (height - h)/2,
        dw: width - w,
        dh: height - h
    }
}
module.exports = {
    touchStart: function (ev, oi) {
        // #ifdef APP-PLUS || H5
        ev.preventDefault();
        ev.stopPropagation();
        // #endif
        touchInstance = ev.instance;
        var dataSet = ev.instance.getDataset()
        touchType = dataSet.type;
        startTouchs = ev.touches;
        oi.callMethod('onTouchStart')
        if (startTouchs.length == 2) {
            touchType = "image"
            var x1 = startTouchs[0].clientX
            var y1 = startTouchs[0].clientY
            var x2 = startTouchs[1].clientX
            var y2 = startTouchs[1].clientY
            var distance = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)
            startDistance = Math.sqrt(distance)
            var leftPercent = ((x1 + x2) / 2 - imageRect.left) / imageRect.width
            var topPercent = ((y1 + y2) / 2 - imageRect.top) / imageRect.height
            touchCenter = [leftPercent, topPercent]
        }
        return false;
    },
    touchMove: function (ev, io) {
        if (touchType == "") return false
        // #ifdef H5
        ev.preventDefault();
        ev.stopPropagation();
        // #endif
        var touches = ev.touches;
        var changeX1 = touches[0].clientX - startTouchs[0].clientX;
        var changeY1 = touches[0].clientY - startTouchs[0].clientY;
        if (startTouchs.length == 1) {
            if (touchType === 'image') {
                changes.imageRect.left = imageRect.left + changeX1;
                changes.imageRect.top = imageRect.top + changeY1;
                updateImageStyle()
            } else if (touchType === 'controller') {
                var directionX = 0;
                if (touchInstance.hasClass('left')) {
                    directionX = -1;
                }
                if (touchInstance.hasClass('right')) {
                    directionX = 1;
                }
                var directionY = 0;
                if (touchInstance.hasClass('top')) {
                    directionY = -1
                }
                if (touchInstance.hasClass('bottom')) {
                    directionY = 1
                }
                var changeX = changeX1 * directionX;
                var changeY = changeY1 * directionY;
                // æ¯”例缩放控制
                if (ratio !== 0) {
                    if (directionX * directionY !== 0) {
                        if (changeX / ratio > changeY) {
                            changeY = changeX / ratio
                            changeX = changeY * ratio
                        } else {
                            changeX = changeY * ratio
                            changeY = changeX / ratio
                        }
                    } else {
                        if (directionX == 0) {
                            changeX = changeY * ratio
                        } else {
                            changeY = changeX / ratio
                        }
                    }
                }
                var realSize = getRealSize()
                var width = cropperRect.width + changeX
                var height = cropperRect.height + changeY
                // var imageRight = imageRect.left + imageRect.width
                // var imageBottom = imageRect.top + imageRect.height
                var imageRight = realSize.left+realSize.width
                var imageBottom = realSize.top+realSize.height
                if (directionX != -1) {
                    if (cropperRect.left + width > imageRight) {
                        width = imageRight - cropperRect.left
                        if (ratio !== 0) {
                            height = width / ratio
                        }
                    }
                } else {
                    var cLeft = cropperRect.left - changeX
                    if (cLeft < realSize.left) {
                        width = cropperRect.left + cropperRect.width - realSize.left
                        if (ratio !== 0) {
                            height = width / ratio
                        }
                    }
                }
                // åˆ¤æ–­æ˜¯å¦è§¦åº•
                if (directionY != -1) {
                    if (cropperRect.top + height > imageBottom) {
                        height = imageBottom - cropperRect.top
                        if (ratio !== 0) {
                            width = height * ratio
                        }
                    }
                } else {
                    var cTop = cropperRect.top - changeY
                    if (cTop < realSize.top) {
                        height = cropperRect.top + cropperRect.height - realSize.top
                        if (ratio !== 0) {
                            width = height * ratio
                        }
                    }
                }
                if (directionX == -1) {
                    changes.cropperRect.left = cropperRect.left + cropperRect.width - width
                }
                if (directionY == -1) {
                    changes.cropperRect.top = cropperRect.top + cropperRect.height - height
                }
                // è¾¹ç•ŒæŽ§åˆ¶
                changes.cropperRect.width = width
                changes.cropperRect.height = height
                updateCopperStyle()
            }
        } else if (touches.length == 2 && startTouchs.length == 2) {
            var changeX2 = touches[0].clientX - touches[1].clientX;
            var changeY2 = touches[0].clientY - touches[1].clientY;
            var distance = Math.pow(changeX2, 2) + Math.pow(changeY2, 2)
            distance = Math.sqrt(distance)
            // æ”¾å¤§æ¯”例
            var scaleRate = distance / startDistance
            imageScale(scaleRate)
            updateImageStyle()
        }
        return false;
    },
    touchEnd: function (ev, oi) {
        if (touchType === "image") {
            var cropperLeft = cropperRect.left
            var cropperRight = cropperRect.left + cropperRect.width
            var cropperTop = cropperRect.top
            var cropperBottom = cropperTop + cropperRect.height
            var cropperRate = cropperRect.width / cropperRect.height
            var realSize = getRealSize()
            var rate = realSize.width / realSize.height
            if (realSize.width < cropperRect.width || realSize.height < cropperRect.height) {
                var scale = 1
                if (rate < cropperRate) {
                    scale = cropperRect.width / realSize.width
                } else {
                    scale = cropperRect.height / realSize.height
                }
                imageRect.width = changes.imageRect.width
                imageRect.height = changes.imageRect.height
                imageScale(scale)
            }
            // è¾¹ç•ŒæŽ§åˆ¶start
            if (cropperLeft < realSize.left) {
                changes.imageRect.left = cropperLeft + realSize.dw/2
            }
            if (cropperRight > realSize.left + realSize.width) {
                changes.imageRect.left = cropperRight - realSize.width + realSize.dw/2
            }
            if (cropperTop < realSize.top) {
                changes.imageRect.top = cropperTop + realSize.dh/2
            }
            if (cropperBottom > realSize.top + realSize.height) {
                changes.imageRect.top = cropperBottom - realSize.height + realSize.dh/2
            }
            // è¾¹ç•ŒæŽ§åˆ¶end
            updateImageStyle()
        }
        oi.callMethod('updateData', {
            cropperRect: changes.cropperRect,
            imageRect: changes.imageRect,
        })
        touchType = ""
        startTouchs = []
        return false;
    },
    // å°†é€»è¾‘层的图像变换同步过来
    // è£å‰ªæ¯”例变化
    changeRatio: function (value) {
        ratio = value
    },
    changeRotateAngle:function (value){
        rotateAngle = value;
        if(imageInstance){
            updateImageStyle()
        }
        var realSize = getRealSize()
    },
    changeImageRect: function (value, oldValue, oi) {
        if (value) {
            imageRect = value;
            changes.imageRect = {
                left: value.left,
                top: value.top,
                width: value.width,
                height: value.height
            };
            // #ifndef MP-WEIXIN || MP-QQ
            setTimeout(function() {
                imageInstance = oi.selectComponent('.mainContent > .image')
                updateImageStyle();
            });
            // #endif
            // #ifdef MP-WEIXIN || MP-QQ
            imageInstance = oi.selectComponent('.mainContent > .image')
            updateImageStyle();
            // #endif
        }
    },
    changeCropper: function (value, oldValue, oi) {
        if (value) {
            cropperRect = value
            changes.cropperRect = {
                left: value.left,
                top: value.top,
                width: value.width,
                height: value.height
            }
            // #ifdef H5 || APP-VUE
            setTimeout(function() {
            // #endif
                cropperInstance = oi.selectComponent('.mainContent > .cropper')
                updateCopperStyle()
            // #ifdef H5 || APP-VUE
            });
            // #endif
        }
    }
}
</script>
<!-- #endif -->
<style lang="scss" scoped>
.bt-container {
    // display: flex;
    // flex-direction: column;
    // justify-content: space-between;
    height: 100%;
    box-sizing: border-box;
    // background-color: #0e1319;
    position: relative;
    overflow: hidden;
    .bt-canvas {
        position: fixed;
        left: 100%;
        top: 0;
    }
    .mainContent {
        // flex: 1;
        // flex-shrink:0;
        // margin: 60rpx;
        width: 100%;
        height: 100%;
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
        // touch-action: none;
        .image {
            position: absolute;
            // will-change: transform;
            // transform-origin: center center;
            width: 85%;
            height: 85%;
            will-change: left, top, width, height;
        }
        .controller {
            position: absolute;
            z-index: 99;
            padding: 10rpx;
            $offset: -20rpx;
            &::after {
                display: block;
                content: '';
                filter: drop-shadow(0 0px 10rpx rgba(0, 0, 0, 0.3));
            }
            &.vertical {
                top: calc(50% - 30rpx);
            }
            &.horizon {
                left: calc(50% - 30rpx);
            }
            &.left {
                &::after {
                    height: 40rpx;
                    border-left: 10rpx solid #fff;
                }
                left: $offset;
            }
            &.right {
                &::after {
                    height: 40rpx;
                    border-right: 10rpx solid #fff;
                }
                right: $offset;
            }
            &.top {
                top: $offset;
                &::after {
                    width: 40rpx;
                    border-top: 10rpx solid #fff;
                }
            }
            &.bottom {
                bottom: $offset;
                &::after {
                    width: 40rpx;
                    border-bottom: 10rpx solid #fff;
                }
            }
            &.left.bottom,
            &.right.bottom,
            &.left.top,
            &.left.bottom {
                &::after {
                    width: 30rpx;
                    height: 30rpx;
                    background-color: transparent;
                }
            }
        }
        .cropper {
            position: absolute;
            border: 1px solid #eee;
            box-sizing: content-box;
            // transform-origin: center center;
            outline: 999px solid rgba(0, 0, 0, 0.5);
            will-change: left, top, width, height;
            // display: contain;
            // pointer-events: none;
            .mask {
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                opacity: 0.5;
            }
            .line {
                position: absolute;
                // background-color: #eee;
            }
            .row {
                width: 100%;
                height: 0px;
                left: 0;
                border-top: 1px dashed #007aff;
            }
            .col {
                height: 100%;
                width: 0px;
                border-left: 1px dashed #007aff;
            }
            .row1 {
                top: 33%;
            }
            .row2 {
                top: 66%;
            }
            .col1 {
                left: 33%;
            }
            .col2 {
                left: 66%;
            }
        }
    }
    // .slot {
    //     position: relative;
    //     padding-top: 20rpx;
    // }
}
.anim {
    transition: 0.2s;
}
</style>
h5/uni_modules/bt-cropper/components/bt-cropper/iconfont.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
@font-face {
  font-family: "iconfont"; /* Project id 3311610 */
  src: url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff2?t=1649382821379') format('woff2'),
       url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff?t=1649382821379') format('woff'),
       url('//at.alicdn.com/t/font_3311610_7wh8injedpd.ttf?t=1649382821379') format('truetype');
}
.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.icon-reset:before {
  content: "\e611";
}
.icon-move:before {
  content: "\e67b";
}
h5/uni_modules/bt-cropper/components/bt-cropper/js/touchs.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,253 @@
var startTouchs = [];
var touchType = ''
var startDistance = 0;
var touchCenter = [];
var cropperRect = null;
var imageRect = null;
var directionX = 0;
var directionY = 0;
var ratio = 0;
// æ“ä½œæ—¶æ”¹å˜çš„对象
var changes = {
    imageRect: null,
    cropperRect: null
}
// è®¡ç®—旋转后真实的图片大小
function getRealSize(){
    var w = changes.imageRect.width
    var h = changes.imageRect.height
    var l =  changes.imageRect.left
    var t =  changes.imageRect.top
    // å†…斜边
    var R = Math.sqrt(w*w+h*h)
    var angle = Math.atan(h/w) / Math.PI * 180
    var rorate = rotateAngle%90
    var direct = Math.floor(rotateAngle/90)
    var width = R*Math.cos(ang2deg(angle-rorate))
    var height = R*Math.sin(ang2deg(angle+rorate))
    if(direct % 2 === 1){
        var temp = width
        width = height
        height = temp
    }
    return {
        width: width,
        height: height,
        left: l - (width - w)/2,
        top: t - (height - h)/2,
        dw: width - w,
        dh: height - h
    }
}
export default {
    computed: {
        imageStyle() {
            const imageRect = this.imageRect
            if (imageRect) {
                return {
                    left: imageRect.left + 'px',
                    top: imageRect.top + 'px',
                    width: imageRect.width + 'px',
                    height: imageRect.height + 'px'
                }
            } else {
                return {}
            }
        },
        cropperStyle() {
            const cropperRect = this.cropperRect
            if (cropperRect) {
                return {
                    left: cropperRect.left + 'px',
                    top: cropperRect.top + 'px',
                    width: cropperRect.width + 'px',
                    height: cropperRect.height + 'px'
                }
            } else {
                return {}
            }
        }
    },
    methods: {
        touchStart() {
            let ev;
            if (arguments.length == 3) {
                directionX = arguments[0];
                directionY = arguments[1];
                ev = arguments[2];
                touchType = "controller";
            } else {
                touchType = "image";
                ev = arguments[0];
            }
            startTouchs = ev.touches;
            changes = {
                imageRect: this.imageRect,
                cropperRect: this.cropperRect
            };
            ratio = this.ratio;
            cropperRect = {
                ...changes.cropperRect
            }
            imageRect = {
                ...changes.imageRect
            }
            if (startTouchs.length == 2) {
                const imageRect = this.imageRect
                var x1 = startTouchs[0].clientX
                var y1 = startTouchs[0].clientY
                var x2 = startTouchs[1].clientX
                var y2 = startTouchs[1].clientY
                var distance = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)
                startDistance = Math.sqrt(distance)
                var leftPercent = ((x1 + x2) / 2 - imageRect.left) / imageRect.width
                var topPercent = ((y1 + y2) / 2 - imageRect.top) / imageRect.height
                touchCenter = [leftPercent, topPercent]
            }
        },
        touchMove(ev) {
            if(startTouchs.length!==ev.touches.length) return
            var touches = ev.touches;
            var changeX1 = touches[0].clientX - startTouchs[0].clientX;
            var changeY1 = touches[0].clientY - startTouchs[0].clientY;
            if (startTouchs.length == 1) {
                if (touchType === 'image') {
                    changes.imageRect.left = imageRect.left + changeX1;
                    changes.imageRect.top = imageRect.top + changeY1;
                    // console.log(startTouchs.length,ev.touches.length)
                } else if (touchType === 'controller') {
                    var changeX = changeX1 * directionX;
                    var changeY = changeY1 * directionY;
                    // æ¯”例缩放控制
                    if (ratio !== 0) {
                        if (directionX * directionY !== 0) {
                            if (changeX / ratio > changeY) {
                                changeY = changeX / ratio
                                changeX = changeY * ratio
                            } else {
                                changeX = changeY * ratio
                                changeY = changeX / ratio
                            }
                        } else {
                            if (directionX == 0) {
                                changeX = changeY * ratio
                            } else {
                                changeY = changeX / ratio
                            }
                        }
                    }
                    var realSize = getRealSize()
                    var width = cropperRect.width + changeX
                    var height = cropperRect.height + changeY
                    var imageRight = realSize.left+realSize.width
                    var imageBottom = realSize.top+realSize.height
                    if (directionX != -1) {
                        if (cropperRect.left + width > imageRight) {
                            width = imageRight - cropperRect.left
                            if (ratio !== 0) {
                                height = width / ratio
                            }
                        }
                    } else {
                        var cLeft = cropperRect.left - changeX
                        if (cLeft < realSize.left) {
                            width = cropperRect.left + cropperRect.width - realSize.left
                            if (ratio !== 0) {
                                height = width / ratio
                            }
                        }
                    }
                    // åˆ¤æ–­æ˜¯å¦è§¦åº•
                    if (directionY != -1) {
                        if (cropperRect.top + height > imageBottom) {
                            height = imageBottom - cropperRect.top
                            if (ratio !== 0) {
                                width = height * ratio
                            }
                        }
                    } else {
                        var cTop = cropperRect.top - changeY
                        if (cTop < realSize.top) {
                            height = cropperRect.top + cropperRect.height - realSize.top
                            if (ratio !== 0) {
                                width = height * ratio
                            }
                        }
                    }
                    if (directionX == -1) {
                        changes.cropperRect.left = cropperRect.left + cropperRect.width - width
                    }
                    if (directionY == -1) {
                        changes.cropperRect.top = cropperRect.top + cropperRect.height - height
                    }
                    // è¾¹ç•ŒæŽ§åˆ¶
                    changes.cropperRect.width = width
                    changes.cropperRect.height = height
                }
            } else if (touches.length == 2 && startTouchs.length == 2) {
                var changeX2 = touches[0].clientX - touches[1].clientX;
                var changeY2 = touches[0].clientY - touches[1].clientY;
                var distance = Math.pow(changeX2, 2) + Math.pow(changeY2, 2)
                distance = Math.sqrt(distance)
                // æ”¾å¤§æ¯”例
                var scaleRate = distance / startDistance
                this.imageScale(scaleRate)
            }
        },
        touchEnd(ev) {
            // console.log('end',ev)
            if(ev.touches.length!==0) return
            if (touchType === "image") {
                var cropperLeft = cropperRect.left
                var cropperRight = cropperRect.left + cropperRect.width
                var cropperTop = cropperRect.top
                var cropperBottom = cropperTop + cropperRect.height
                var cropperRate = cropperRect.width / cropperRect.height
                var realSize = getRealSize()
                var rate = realSize.width / realSize.height
                if (realSize.width < cropperRect.width || realSize.height < cropperRect.height) {
                    var scale = 1
                    if (rate < cropperRate) {
                        scale = cropperRect.width / realSize.width
                    } else {
                        scale = cropperRect.height / realSize.height
                    }
                    imageRect.width = changes.imageRect.width
                    imageRect.height = changes.imageRect.height
                    this.imageScale(scale)
                }
                // è¾¹ç•ŒæŽ§åˆ¶start
                if (cropperLeft < realSize.left) {
                    changes.imageRect.left = cropperLeft + realSize.dw/2
                }
                if (cropperRight > realSize.left + realSize.width) {
                    changes.imageRect.left = cropperRight - realSize.width + realSize.dw/2
                }
                if (cropperTop < realSize.top) {
                    changes.imageRect.top = cropperTop + realSize.dh/2
                }
                if (cropperBottom > realSize.top + realSize.height) {
                    changes.imageRect.top = cropperBottom - realSize.height + realSize.dh/2
                }
                // è¾¹ç•ŒæŽ§åˆ¶end
            }
            this.updateData({
                cropperRect: changes.cropperRect,
                imageRect: changes.imageRect,
            })
            touchType = ""
            startTouchs = []
            return false;
        },
        imageScale(scaleRate) {
            var cw = imageRect.width * (scaleRate - 1)
            var ch = imageRect.height * (scaleRate - 1)
            changes.imageRect = {
                width: imageRect.width + cw,
                height: imageRect.height + ch,
                left: imageRect.left - cw * (touchCenter[0]),
                top: imageRect.top - ch * (touchCenter[1])
            }
        }
    }
}
h5/uni_modules/bt-cropper/components/bt-cropper/utils/tools.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
export function getTouchPoints(touchs) {
    return Array.from(touchs).map(ev => {
        return [ev.clientX, ev.clientY]
    })
}
// å‡½æ•°é˜²æŠ–
export function debounce(fn, wait = 200) {
    var timer = null;
    return function (){
        if (timer !== null) {
            clearTimeout(timer);
        }
        timer = setTimeout(fn.bind(this), wait);
    }
}
/**
 * @description ç¡çœ 
 * @param {number} time ç­‰å¾…时间毫秒数
 */
export function sleep(time = 200) {
    return new Promise(resolve => {
        setTimeout(resolve, time)
    })
}
const systemInfo = uni.getSystemInfoSync();
export function parseUnit(size){
    if(typeof size == 'number' || !isNaN(Number(size))){
        return uni.upx2px(size)
    }else if(typeof size === 'string') {
        if(size.endsWith('rpx')){
            return parseUnit(size.replace('rpx',''))
        }else if(size.endsWith('px')){
            return Number(size.replace('px',''))
        }else if(size.endsWith('vw')){
            return Number(size.replace('vw',''))*systemInfo.screenWidth/100
        }else if(size.endsWith('vh')){
            return Number(size.replace('vh',''))*systemInfo.screenHeight/100
        }
    }
    return 0
}
h5/uni_modules/bt-cropper/package.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
{
    "id": "bt-cropper",
    "displayName": "bt-cropper图片裁剪插件",
    "version": "3.0.2",
    "description": "一款好用的图片裁剪插件",
    "keywords": [
        "图片",
        "图片裁剪",
        "图片裁剪",
        "头像裁剪",
        "cropper"
    ],
    "repository": "",
    "engines": {
        "HBuilderX": "^3.4.9"
    },
    "dcloudext": {
        "sale": {
            "regular": {
                "price": "0.00"
            },
            "sourcecode": {
                "price": "0.00"
            }
        },
        "contact": {
            "qq": "1097122362"
        },
        "declaration": {
            "ads": "无",
            "data": "插件不采集任何数据",
            "permissions": "无"
        },
        "npmurl": "",
        "type": "component-vue"
    },
    "uni_modules": {
        "dependencies": [],
        "encrypt": [],
        "platforms": {
            "cloud": {
                "tcb": "y",
                "aliyun": "y"
            },
            "client": {
                "Vue": {
                    "vue2": "y",
                    "vue3": "y"
                },
                "App": {
                    "app-vue": "y",
                    "app-nvue": "n"
                },
                "H5-mobile": {
                    "Safari": "y",
                    "Android Browser": "y",
                    "微信浏览器(Android)": "y",
                    "QQ浏览器(Android)": "y"
                },
                "H5-pc": {
                    "Chrome": "n",
                    "IE": "n",
                    "Edge": "n",
                    "Firefox": "n",
                    "Safari": "n"
                },
                "小程序": {
                    "微信": "y",
                    "阿里": "u",
                    "百度": "u",
                    "字节跳动": "y",
                    "QQ": "y"
                },
                "快应用": {
                    "华为": "n",
                    "联盟": "n"
                }
            }
        }
    }
}
h5/uni_modules/bt-cropper/readme.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
## bt-cropper å›¾ç‰‡è£åˆ‡
> **组件名:bt-cropper**
图片裁切组件,在页面中裁切图片,输出裁切后的图片,支持app,小程序,H5
### [在线体验](https://static-mp-8cc5bb7c-7831-45bf-a4e1-a71b10d3319c.next.bspapp.com/h5)
## å°†demo编译为微信小程序的时候,一定要填写appid!!!不然无法使用!!
> **注意事项**
> ä¸ºäº†é¿å…é”™è¯¯ä½¿ç”¨ï¼Œç»™å¤§å®¶å¸¦æ¥ä¸å¥½çš„开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。
> - ç»„件需要依赖 `sass` æ’ä»¶ ï¼Œè¯·è‡ªè¡Œæ‰‹åŠ¨å®‰è£…
> - åªæµ‹è¯•了头条小程序,app-vue å®‰å“,微信小程序和H5 å¤§éƒ¨åˆ†å¹³å°åº”该都没问题了
> - åŒ…裹层或裁剪器需要手动指定高度和宽度,推荐手动指定裁剪器的大小,尤其是头条小程序,js有时候获取不到容器的大小
> - å¦‚使用过程中有任何问题,或者您有一些好的建议,欢迎联系作者微信:1097122362
### å®‰è£…方式
本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
### åŸºæœ¬ç”¨æ³•
**示例**
```html
<template>
    <view class="container">
        <bt-cropper ref="cropper" :imageSrc="imageSrc"></bt-cropper>
        <button @click="crop">裁切</button>
    </view>
</template>
<style>
    .container{
        /** å¤–层一定要指定大小 */
        height:100vh;
    }
</style>
```
```javascript
export default {
   methods:{
      crop(){
        // é€šè¿‡ç»„件定义的ref调用cropper方法,返回一个promise对象
        this.$refs.cropper.crop().then((res)=>{
            console.log(res)
        })
      }
   }
}
```
### é™å®šè£åˆ‡æ¯”例
bt-cropper,指定ratio即可设置裁切框的宽高比,如果你想让用户自由缩放,将ratio设置为0即可
**示例**
```html
<bt-cropper ref="cropper" :ratio="16/9":imageSrc="imageSrc"></bt-cropper>
```
## API
### cropper Props
|属性名|类型|默认值|说明|
|:-:|:-:|:-:|:-:|
|ratio|number|0|裁切图像的宽高比,0表示自由比例|
|dWidth|number|0|生成的图片的宽度,单位:px,如果传入0的话就是按原像素的比例裁剪,也就是说,输出图片的清晰度和输入图片的清晰度一样|
|imageSrc|String|''|原图的路径,支持本地路径和网络路径,如果是网络路径,小程序要注意配置下载域名,H5要注意跨域问题|
|mask|String|''| è£å‰ªçš„蒙版url,配合蒙版可以裁剪出任何形状的图形 (示例见全屏裁剪demo) |
|fileType|String|'jpg'|目标文件的类型,只支持 'jpg' æˆ– 'png'。默认为 'jpg'|
|quality|Number|1|图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理|
|showGrid|Boolean|false|是否显示中心网格线,默认不显示|
|initPosition|object|null|图片自定义的初始的位置,内容格式见change事件|
|autoZoom|Boolean|true|是否开启操作结束后自动放大到窗口大小|
|containerSize|object|null|手动指定容器大小,如果裁剪器放在大小会移动或缩放的dom中,则必须手动指定大小,可以带上单位,如果不带单位默认px,支持的单位有:rpx,px,vw,vm,示例:{width:100,height:1100}或者:{width:'100rpx',height:'100rpx'}|
|canvas2d|Boolean|false| æ˜¯å¼€å¯ 2d canvas |
### cropper Methods
|方法名称|说明|参数|
|:-:|:-:|:-:|
|crop|裁剪图片|开始绘制并开始裁剪图片,返回Promise对象|
|init|初始化,图片路径变化的时候自动调用此方法,如果包裹的dom大小变化了,你需要手动调用此方法|-|
|initCropper|重置裁剪框和图片的位置和大小到初始的位置和大小|-|
|undo|撤销,最多可以回退10步|-|
|resume|重做|-|
### cropper Events
|方法名称|说明|返回值|
|:-:|:-:|:-:|
|change|当裁剪框和图片的相对位置发生变化的时候触发,返回裁剪框与图片的相对位置|ev={left:number,top:number,width:number,height:number}|
|loadFail|当图片加载失败时触发| - |
|cropStart|当裁开始时触发| - |
## å¸®åŠ©
在使用中如遇到无法解决的问题,请提 [Issues](https://gitee.com/xiaojiang1996/better-uni-cropper/issues) æˆ–者加我 å¾®ä¿¡:1097122362。
h5/uni_modules/qf-image-cropper/changelog.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
## 2.1.6(2023-04-16)
* ä¿®å¤ ç»„件使用 v-show æŒ‡ä»¤ä¼šå¯¼è‡´é€‰æ‹©å›¾ç‰‡åŽåˆå§‹ä½ç½®ä¸¥é‡åä½çš„问题
## 2.1.5(2023-04-16)
* æ–°å¢ž å…¼å®¹APP平台
## 2.1.4(2023-03-13)
* æ–°å¢ž fileType å±žæ€§ï¼Œç”¨äºŽæŒ‡å®šç”Ÿæˆæ–‡ä»¶çš„类型,只支持 'jpg' æˆ– 'png',默认为 'png'
* æ–°å¢ž delay å±žæ€§ï¼Œå¾®ä¿¡å°ç¨‹åºå¹³å°ä½¿ç”¨ `Canvas 2D` ç»˜åˆ¶æ—¶æŽ§åˆ¶å›¾ç‰‡ä»Žç»˜åˆ¶åˆ°ç”Ÿæˆæ‰€éœ€æ—¶é—´
* ä¼˜åŒ– å½“生成图片的尺寸宽/高超过 Canvas 2D æœ€å¤§é™åˆ¶ï¼ˆ1365*1365)则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
* ä¼˜åŒ– æ—‹è½¬å›¾æ ‡æŒ‡ç¤ºæ–¹å‘与实际旋转方向不符
## 2.1.3(2023-02-06)
* ä¼˜åŒ– vue3支持
## 2.1.2(2023-02-03)
* æ–°å¢ž navigation å±žæ€§ï¼ŒH5平台当 showAngle ä¸º true æ—¶ï¼Œä½¿ç”¨æ’件的页面在 `page.json` ä¸­é…ç½®äº† "navigationStyle": "custom" æ—¶ï¼Œå¿…须将此值设为 false ï¼Œå¦åˆ™å››ä¸ªå¯æ‹‰ä¼¸è§’的触发位置会有偏差
* ä¿®å¤ H5平台部分设备(已知iPhone11以下机型)拍照的图片缩放时会闪动的问题
## 2.1.1(2022-12-06)
* ä¿®å¤ æ¨ªå±é€‚配问题
## 2.1.0(2022-12-06)
* æ–°å¢ž å…¼å®¹H5平台,使用 renderjs å“åº”手势事件
## 2.0.0(2022-12-05)
* é‡æž„ æ’件,使用 WXS å“åº”手势事件
* æ–°å¢ž å›¾ç‰‡ç¿»è½¬
* æ–°å¢ž æ‹‰ä¼¸è£å‰ªæ¡†æ”¾å¤§å›¾ç‰‡
* æ–°å¢ž ç›‘听PC鼠标滚轮触发缩放
* æ–°å¢ž åœ†å½¢ã€åœ†è§’矩形的图片裁剪
* ä¼˜åŒ– å›¾ç‰‡ç¼©æ”¾ï¼Œç§»åŠ¨ç«¯ä»¥åŒæŒ‡è§¦æ‘¸ä¸­å¿ƒç‚¹ä¸ºç¼©æ”¾ä¸­å¿ƒç‚¹ï¼ŒPC端以鼠标所在点为缩放中心点
* ä¼˜åŒ– è£å‰ªæ¡†æ ·å¼
* ä¼˜åŒ– å›¾ç‰‡ä½ç½®æ‹–动 æ”¯æŒè¾¹ç•Œå›žå¼¹æ•ˆæžœï¼ˆæ»‘动时可滑出边界,释放时回弹到边界)
* ä¼˜åŒ– ç”Ÿæˆå›¾ç‰‡ä½¿ç”¨æ–°ç‰ˆ Canvas 2D æŽ¥å£
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.render.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,628 @@
/**
 * å›¾ç‰‡ç¼–辑器-手势监听
 * 1. æ”¯æŒç¼–译到app-vue(uni-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;
/**
 * æ ·å¼å¯¹è±¡è½¬å­—符串
 * @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(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
    instance[key] = style;
    // #endif
}
/**
 * è§¦å‘页面实例指定方法
 * @param {Object} instance é¡µé¢å®žä¾‹å¯¹è±¡
 * @param {Object} name æ–¹æ³•名称
 * @param {Object} obj ä¼ é€’参数
 */
function callMethod(instance, name, obj) {
    // #ifdef APP-PLUS
    instance.callMethod(name, obj);
    // #endif
    // #ifdef 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 };
};
/**
 * æ£€æŸ¥è¾¹ç•Œï¼šé™åˆ¶ x、y æ‹–动范围,禁止滑出边界
 * @param {Object} e ç‚¹åæ ‡
 */
function checkRange(e) {
    var r = rotate / 90 % 2;
    if(r === 1) { // å› å›¾ç‰‡å®½é«˜å¯èƒ½ä¸ç­‰ï¼Œç¿»è½¬ 90° æˆ– 270° åŽå›¾ç‰‡å®½é«˜éœ€åç€è®¡ç®—,且左右和上下边界要根据差值做偏移
        var o = (img.height - img.width) / 2; // å®½é«˜å·®å€¼ä¸€åŠ
        return {
            x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
            y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
        }
    }
    return {
        x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
        y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
    }
};
/**
 * å˜æ›´å›¾ç‰‡å¸ƒå±€ä¿¡æ¯
 * @param {Object} e å¸ƒå±€ä¿¡æ¯
 */
function changeImageRect(e) {
    // console.log('changeImageRect', e)
    offset.x += e.x || 0;
    offset.y += e.y || 0;
    if(e.check) { // æ£€æŸ¥è¾¹ç•Œ
        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: '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,
        },
        {
            left: (area.right + areaOffset.right) + 'px',
            right: 0,
            top: 0,
            bottom: 0,
        },
        {
            left: (area.left + areaOffset.left) + 'px',
            width: (area.width + areaOffset.right - areaOffset.left) + 'px',
            top: 0,
            height: (area.top + areaOffset.top) + 'px',
        },
        {
            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,
        }
    ]);
    // å˜æ›´è¾¹æ¡†æ ·å¼
    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',
        });
    }
    // å˜æ›´å‚考线样式
    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'
            },
            {
                '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'
            },
            {
                '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'
            },
            {
                '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'
            }
        ]);
    }
    // å˜æ›´å››ä¸ªä¼¸ç¼©è§’样式
    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',
            },
            {
                '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',
            },
            {
                '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',
            },
            {
                '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',
            }
        ]);
    }
    // å˜æ›´åœ†è§’样式
    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'
        });
        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 = img.oldWidth * scale;
        img.height = img.oldHeight * scale;
        // å‚考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
        //             è¯¥å››è¾¹å½¢ä¸Šæœ‰ä¸€ä¸ªç‚¹E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
        //             æ–°å››è¾¹å½¢çš„A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
        // é¢„期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
        // è®¡ç®—方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
        e.x = (e.x - offset.x) * (1 - scale / last);
        e.y = (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 = 1;
    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;
};
export default {
    data() {
        return {
            imageStyles: {},
            maskStylesList: [{}, {}, {}, {}],
            borderStyles: {},
            gridStylesList: [{}, {}, {}, {}],
            angleStylesList: [{}, {}, {}, {}],
            circleBoxStyles: {},
            circleStyles: {}
        }
    },
    created() {
        // ç›‘听 PC ç«¯é¼ æ ‡æ»šè½®
        // #ifdef H5
        window.addEventListener('mousewheel', (e) => {
            var touchs = getTouchs([e])
            img.src && scaleImage({
                instance: 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
    },
    methods: {
        getInstance() {
            // #ifdef APP-PLUS
            return this.$ownerInstance;
            // #endif
            // #ifdef H5
            return this;
            // #endif
        },
        /**
         * åˆå§‹åŒ–:观察数据变更
         * @param {Object} newVal æ–°æ•°æ®
         * @param {Object} oldVal æ—§æ•°æ®
         * @param {Object} o ç»„件实例对象
         */
        initObserver: 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;
                resetData();
                img.src && changeImageRect({
                    instance: this.getInstance(),
                    x: (sys.windowWidth - img.width) / 2,
                    y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
                });
                changeAreaRect({
                    instance: this.getInstance()
                });
            }
        },
        /**
         * é¼ æ ‡æ»šè½®æ»šåЍ
         * @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: 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 = area.width * (1 - area.minScale);
                    var maxY = area.height * (1 - area.minScale);
                    // console.log(x, y, maxX, maxY)
                    touches[0] = point;
                    switch(activeAngle) {
                        case 1: // å·¦ä¸Šè§’
                            x += areaOffset.left;
                            y += areaOffset.top;
                            if(x >= 0 && y >= 0) { // æœ‰æ•ˆæ»‘动
                                if(x > y) { // ä»¥x轴滑动距离为缩放基准
                                    if(x > maxX) x = maxX;
                                    y = x * area.height / area.width;
                                } else { // ä»¥y轴滑动距离为缩放基准
                                    if(y > maxY) y = maxY;
                                    x = y * area.width / area.height;
                                }
                                areaOffset.left = x;
                                areaOffset.top = y;
                            }
                            break;
                        case 2: // å³ä¸Šè§’
                            x += areaOffset.right;
                            y += areaOffset.top;
                            if(x <= 0 && y >= 0) { // æœ‰æ•ˆæ»‘动
                                if(-x > y) { // ä»¥x轴滑动距离为缩放基准
                                    if(-x > maxX) x = -maxX;
                                    y = -x * area.height / area.width;
                                } else { // ä»¥y轴滑动距离为缩放基准
                                    if(y > maxY) y = maxY;
                                    x = -y * area.width / area.height;
                                }
                                areaOffset.right = x;
                                areaOffset.top = y;
                            }
                            break;
                        case 3: // å·¦ä¸‹è§’
                            x += areaOffset.left;
                            y += areaOffset.bottom;
                            if(x >= 0 && y <= 0) { // æœ‰æ•ˆæ»‘动
                                if(x > -y) { // ä»¥x轴滑动距离为缩放基准
                                    if(x > maxX) x = maxX;
                                    y = -x * area.height / area.width;
                                } else { // ä»¥y轴滑动距离为缩放基准
                                    if(-y > maxY) y = -maxY;
                                    x = -y * area.width / area.height;
                                }
                                areaOffset.left = x;
                                areaOffset.bottom = y;
                            }
                            break;
                        case 4: // å³ä¸‹è§’
                            x += areaOffset.right;
                            y += areaOffset.bottom;
                            if(x <= 0 && y <= 0) { // æœ‰æ•ˆæ»‘动
                                if(-x > -y) { // ä»¥x轴滑动距离为缩放基准
                                    if(-x > maxX) x = -maxX;
                                    y = x * area.height / area.width;
                                } else { // ä»¥y轴滑动距离为缩放基准
                                    if(-y > maxY) y = -maxY;
                                    x = y * area.width / area.height;
                                }
                                areaOffset.right = x;
                                areaOffset.bottom = y;
                            }
                            break;
                    }
                    // console.log(x, y, JSON.stringify(areaOffset))
                    changeAreaRect({
                        instance: this.getInstance(),
                    });
                    // this.draw();
                }
            } else if (e.touches.length == 2) { // åŒç‚¹è§¦æ‘¸ç¼©æ”¾
                var start = getDistanceByTouches(touches);
                var end = getDistanceByTouches(e.touches);
                scaleImage({
                    instance: 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: 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: 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: this.getInstance(),
                });
                scaleImage({
                    instance: 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: this.getInstance(),
                    check: true
                });
            }
        },
        /**
         * é¡ºæ—¶é’ˆç¿»è½¬å›¾ç‰‡90°
         * @param {Object} e äº‹ä»¶å¯¹è±¡
         * @param {Object} o ç»„件实例对象
         */
        rotateImage: function(e, o) {
            rotate = (rotate + 90) % 360;
            // å› å›¾ç‰‡å®½é«˜å¯èƒ½ä¸ç­‰ï¼Œç¿»è½¬åŽå›¾ç‰‡å®½é«˜éœ€è¶³å¤Ÿå¡«æ»¡è£å‰ªåŒºåŸŸ
            var r = rotate / 90 % 2;
            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: 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: this.getInstance(),
                check: true,
                x: -ox - oy,
                y: -oy + ox
            });
        }
    }
}
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,692 @@
<template>
    <view class="image-cropper" @wheel="cropper.mousewheel" v-if="show">
        <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 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>
        <view class="fixed-bottom safe-area-inset-bottom">
            <view v-if="rotatable && !!imgSrc" class="rotate-icon" @click="cropper.rotateImage"></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 || 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
            },
            /** å›¾ç‰‡æœ€å¤§ç¼©æ”¾å€æ•° */
            maxScale: {
                type: Number,
                default: 5
            },
            /** æ˜¯å¦æœ‰å›žå¼¹æ•ˆæžœï¼šæ‹–动时可以拖出边界,释放时会弹回边界 */
            bounce: {
                type: Boolean,
                default: true
            },
            /** æ˜¯å¦æ”¯æŒç¿»è½¬ */
            rotatable: {
                type: Boolean,
                default: true
            },
            /** æ˜¯å¦æ”¯æŒä»Žæœ¬åœ°é€‰æ‹©ç´ æ */
            choosable: {
                type: Boolean,
                default: true
            },
            /** å››ä¸ªè§’尺寸,单位px */
            angleSize: {
                type: Number,
                default: 20
            },
            /** å››ä¸ªè§’边框宽度,单位px */
            angleBorderWidth: {
                type: Number,
                default: 2
            },
            /** è£å‰ªå›¾ç‰‡åœ†è§’半径,单位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,
                show: false
                // 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
                    },
                    sys: this.sys,
                    img: {
                        maxScale: this.maxScale,
                        src: this.imgSrc,
                        width: this.oldWidth,
                        height: this.oldHeight,
                        oldWidth: this.oldWidth,
                        oldHeight: this.oldHeight,
                    }
                }
            },
            imgProps() {
                return {
                    width: this.width,
                    height: this.height,
                    src: this.src,
                }
            }
        },
        watch: {
            imgProps: {
                handler(val) {
                    // console.log('imgProps', val)
                    // è‡ªå®šä¹‰è£å‰ªå°ºï¼Œç¤ºä¾‹å¦‚下:
                    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();
                    val.src && this.initImage(val.src);
                },
                immediate: true
            },
        },
        methods: {
            open() {
                this.show = true
            },
            close() {
                this.show = false
            },
            /** æä¾›ç»™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() {
                // #ifdef MP-WEIXIN || MP-JD
                if(uni.chooseMedia) {
                    uni.chooseMedia({
                        count: 1,
                        mediaType: ['image'],
                        success: (res) => {
                            this.resetData();
                            this.initImage(res.tempFiles[0].tempFilePath);
                        }
                    });
                    return;
                }
                // #endif
                uni.chooseImage({
                    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) {
                uni.getImageInfo({
                    src: url,
                    success: (res) => {
                        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;
                        this.oldHeight = this.scaleHeight;
                    },
                    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;
                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){
                // åœ¨H5平台下,tempFilePath ä¸º 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;
        z-index: 999999;
        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;
            }
            .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;
            .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;
                position: absolute;
                top: -90rpx;
                left: 10rpx;
                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>
h5/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.wxs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,543 @@
/**
 * å›¾ç‰‡ç¼–辑器-手势监听
 * 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 };
/**
 * è®¡ç®—两点间距
 * @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 };
};
/**
 * æ£€æŸ¥è¾¹ç•Œï¼šé™åˆ¶ x、y æ‹–动范围,禁止滑出边界
 * @param {Object} e ç‚¹åæ ‡
 */
function checkRange(e) {
    var r = rotate / 90 % 2;
    if(r === 1) { // å› å›¾ç‰‡å®½é«˜å¯èƒ½ä¸ç­‰ï¼Œç¿»è½¬ 90° æˆ– 270° åŽå›¾ç‰‡å®½é«˜éœ€åç€è®¡ç®—,且左右和上下边界要根据差值做偏移
        var o = (img.height - img.width) / 2; // å®½é«˜å·®å€¼ä¸€åŠ
        return {
            x: Math.min(Math.max(e.x, -img.height + o + area.width + area.left), area.left + o),
            y: Math.min(Math.max(e.y, -img.width - o + area.height + area.top), area.top - o)
        }
    }
    return {
        x: Math.min(Math.max(e.x, -img.width + area.width + area.left), area.left),
        y: Math.min(Math.max(e.y, -img.height + area.height + area.top), area.top)
    }
};
/**
 * å˜æ›´å›¾ç‰‡å¸ƒå±€ä¿¡æ¯
 * @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) { // æ£€æŸ¥è¾¹ç•Œ
        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: '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,
        },
        {
            left: (area.right + areaOffset.right) + 'px',
            right: 0,
            top: 0,
            bottom: 0,
        },
        {
            left: (area.left + areaOffset.left) + 'px',
            width: (area.width + areaOffset.right - areaOffset.left) + 'px',
            top: 0,
            height: (area.top + areaOffset.top) + 'px',
        },
        {
            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,
        }
    ];
    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',
        });
    }
    // å˜æ›´å‚考线样式
    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'
            },
            {
                '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'
            },
            {
                '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'
            },
            {
                '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'
            }
        ];
        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',
            },
            {
                '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',
            },
            {
                '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',
            },
            {
                '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',
            }
        ];
        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'
        });
        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 = img.oldWidth * scale;
        img.height = img.oldHeight * scale;
        // å‚考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
        //             è¯¥å››è¾¹å½¢ä¸Šæœ‰ä¸€ä¸ªç‚¹E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
        //             æ–°å››è¾¹å½¢çš„A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
        // é¢„期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
        // è®¡ç®—方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
        e.x = (e.x - offset.x) * (1 - scale / last);
        e.y = (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 = 1;
    rotate = 0;
};
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;
            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 = area.width * (1 - area.minScale);
                var maxY = area.height * (1 - area.minScale);
                // console.log(x, y, maxX, maxY)
                touches[0] = point;
                switch(activeAngle) {
                    case 1: // å·¦ä¸Šè§’
                        x += areaOffset.left;
                        y += areaOffset.top;
                        if(x >= 0 && y >= 0) { // æœ‰æ•ˆæ»‘动
                            if(x > y) { // ä»¥x轴滑动距离为缩放基准
                                if(x > maxX) x = maxX;
                                y = x * area.height / area.width;
                            } else { // ä»¥y轴滑动距离为缩放基准
                                if(y > maxY) y = maxY;
                                x = y * area.width / area.height;
                            }
                            areaOffset.left = x;
                            areaOffset.top = y;
                        }
                        break;
                    case 2: // å³ä¸Šè§’
                        x += areaOffset.right;
                        y += areaOffset.top;
                        if(x <= 0 && y >= 0) { // æœ‰æ•ˆæ»‘动
                            if(-x > y) { // ä»¥x轴滑动距离为缩放基准
                                if(-x > maxX) x = -maxX;
                                y = -x * area.height / area.width;
                            } else { // ä»¥y轴滑动距离为缩放基准
                                if(y > maxY) y = maxY;
                                x = -y * area.width / area.height;
                            }
                            areaOffset.right = x;
                            areaOffset.top = y;
                        }
                        break;
                    case 3: // å·¦ä¸‹è§’
                        x += areaOffset.left;
                        y += areaOffset.bottom;
                        if(x >= 0 && y <= 0) { // æœ‰æ•ˆæ»‘动
                            if(x > -y) { // ä»¥x轴滑动距离为缩放基准
                                if(x > maxX) x = maxX;
                                y = -x * area.height / area.width;
                            } else { // ä»¥y轴滑动距离为缩放基准
                                if(-y > maxY) y = -maxY;
                                x = -y * area.width / area.height;
                            }
                            areaOffset.left = x;
                            areaOffset.bottom = y;
                        }
                        break;
                    case 4: // å³ä¸‹è§’
                        x += areaOffset.right;
                        y += areaOffset.bottom;
                        if(x <= 0 && y <= 0) { // æœ‰æ•ˆæ»‘动
                            if(-x > -y) { // ä»¥x轴滑动距离为缩放基准
                                if(-x > maxX) x = -maxX;
                                y = x * area.height / area.width;
                            } else { // ä»¥y轴滑动距离为缩放基准
                                if(-y > maxY) y = -maxY;
                                x = 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) {
        rotate = (rotate + 90) % 360;
        // å› å›¾ç‰‡å®½é«˜å¯èƒ½ä¸ç­‰ï¼Œç¿»è½¬åŽå›¾ç‰‡å®½é«˜éœ€è¶³å¤Ÿå¡«æ»¡è£å‰ªåŒºåŸŸ
        var r = rotate / 90 % 2;
        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
        });
    },
    // æ­¤å¤„只用于对齐其他平台端的样式参数,防止异常,无作用
    imageStyles: {},
    maskStylesList: [{}, {}, {}, {}],
    borderStyles: {},
    gridStylesList: [{}, {}, {}, {}],
    angleStylesList: [{}, {}, {}, {}],
    circleBoxStyles: {},
    circleStyles: {},
}
h5/uni_modules/qf-image-cropper/package.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
{
  "id": "qf-image-cropper",
  "displayName": "图片裁剪插件",
  "version": "2.1.6",
  "description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
  "keywords": [
    "qf-image-cropper",
    "图片裁剪",
    "图片编辑",
    "头像裁剪",
    "小程序"
],
  "repository": "",
  "engines": {
    "HBuilderX": "^3.1.0"
  },
"dcloudext": {
    "type": "component-vue",
    "sale": {
      "regular": {
        "price": "0.00"
      },
      "sourcecode": {
        "price": "0.00"
      }
    },
    "contact": {
      "qq": ""
    },
    "declaration": {
      "ads": "无",
      "data": "插件不采集任何数据",
      "permissions": "无"
    },
    "npmurl": ""
  },
  "uni_modules": {
    "dependencies": [],
    "encrypt": [],
    "platforms": {
      "client": {
        "Vue": {
          "vue2": "y",
          "vue3": "y"
        },
        "App": {
          "app-vue": "y",
          "app-nvue": "n"
        },
        "H5-mobile": {
          "Safari": "y",
          "Android Browser": "y",
          "微信浏览器(Android)": "y",
          "QQ浏览器(Android)": "u"
        },
        "H5-pc": {
          "Chrome": "u",
          "IE": "u",
          "Edge": "u",
          "Firefox": "u",
          "Safari": "u"
        },
        "小程序": {
          "微信": "y",
          "阿里": "n",
          "百度": "n",
          "字节跳动": "n",
          "QQ": "u",
          "钉钉": "n",
          "快手": "n",
          "飞书": "n",
          "京东": "n"
        },
        "快应用": {
          "华为": "n",
          "联盟": "n"
        }
      }
    }
  }
}
h5/uni_modules/qf-image-cropper/readme.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
# qf-image-cropper
## å›¾ç‰‡è£å‰ªæ’ä»¶
uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### å¹³å°æ”¯æŒï¼š
1. æ”¯æŒå¾®ä¿¡å°ç¨‹åºï¼šç§»åŠ¨ç«¯ã€PC端、开发者工具
2. æ”¯æŒH5平台(2.1.0版本起)
3. æ”¯æŒAPP平台(2.1.5版本起):Android、IOS
4. å…¶ä»–平台暂未测试兼容性未知
### æ”¯æŒåŠŸèƒ½ï¼š
1. è‡ªå®šä¹‰è£å‰ªå°ºå¯¸
2. å®šç‚¹ç­‰æ¯”例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
3. è‡ªç”±æ‹–动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. å›¾ç‰‡ç¿»è½¬ï¼šåœ¨è£å‰ªå°ºå¯¸éž 1:1 çš„æƒ…况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. è£å‰ªç”Ÿæˆæ–°å›¾ç‰‡
6. æœ¬åœ°é€‰æ‹©å›¾ç‰‡
7. å¯å®šåˆ¶æ ·å¼ï¼šå¯è‡ªç”±é€‰æ‹©æ˜¯å¦æ¸²æŸ“裁剪边框、可伸缩裁剪顶角、参考线
8. è£å‰ªåœ†è§’图片:圆形、圆角矩形
### å±žæ€§è¯´æ˜Ž
| å±žæ€§å | ç±»åž‹ | é»˜è®¤å€¼ | è¯´æ˜Ž |
|:---|:---|:---|:---|
| src              | String        |         | å›¾ç‰‡èµ„源地址 |
| width            | Number        | 300     | è£å‰ªå®½åº¦ |
| height           | Number        | 300     | è£å‰ªé«˜åº¦ |
| showBorder       | Boolean       | true    | æ˜¯å¦ç»˜åˆ¶è£å‰ªåŒºåŸŸè¾¹æ¡† |
| showGrid         | Boolean       | true    | æ˜¯å¦ç»˜åˆ¶è£å‰ªåŒºåŸŸç½‘格参考线 |
| showAngle        | Boolean       | true    | æ˜¯å¦å±•示四个支持伸缩的角 |
| areaScale        | Number        | 0.3     | è£å‰ªåŒºåŸŸæœ€å°ç¼©æ”¾å€æ•° |
| maxScale         | Number        | 5       | å›¾ç‰‡æœ€å¤§ç¼©æ”¾å€æ•° |
| bounce           | Boolean       | true    | æ˜¯å¦æœ‰å›žå¼¹æ•ˆæžœï¼šæ‹–动时可以拖出边界,释放时会弹回边界 |
| rotatable        | Boolean       | true    | æ˜¯å¦æ”¯æŒç¿»è½¬ |
| choosable        | Boolean       | true    | æ˜¯å¦æ”¯æŒä»Žæœ¬åœ°é€‰æ‹©ç´ æ |
| angleSize        | Number        | 20      | å››ä¸ªè§’尺寸,单位px |
| angleBorderWidth | Number        | 2       | å››ä¸ªè§’边框宽度,单位px |
| radius           | Number        |         | è£å‰ªå›¾ç‰‡åœ†è§’半径,单位px |
| fileType         | String        | png     | ç”Ÿæˆæ–‡ä»¶çš„类型,只支持 'jpg' æˆ– 'png'。默认为 'png' |
| delay            | Number        | 1000    | å›¾ç‰‡ä»Žç»˜åˆ¶åˆ°ç”Ÿæˆæ‰€éœ€æ—¶é—´ï¼Œå•位ms<br>微信小程序平台使用 `Canvas 2D` ç»˜åˆ¶æ—¶æœ‰æ•ˆ<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` é‡‡ç”¨åŒæ­¥ç»˜åˆ¶ï¼Œéœ€è‡ªå·±æŠŠæŽ§ç»˜åˆ¶å®Œæˆæ—¶é—´ |
| navigation       | Boolean       | true    | é¡µé¢æ˜¯å¦æ˜¯åŽŸç”Ÿæ ‡é¢˜æ ï¼š<br>H5平台当 showAngle ä¸º true æ—¶ï¼Œä½¿ç”¨æ’件的页面在 `page.json` ä¸­é…ç½®äº† `"navigationStyle": "custom"` æ—¶ï¼Œå¿…须将此值设为 false ï¼Œå¦åˆ™å››ä¸ªå¯æ‹‰ä¼¸è§’的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
| @crop               | EventHandle   |         | å‰ªè£å®ŒæˆåŽè§¦å‘,event = { tempFilePath }。在H5平台下,tempFilePath ä¸º base64 |
### åŸºæœ¬ç”¨æ³•
```
<template>
    <div>
        <qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
    </div>
</template>
<script>
    import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
    export default {
        components: {
            QfImageCropper
        },
        methods: {
            handleCrop(e) {
                uni.previewImage({
                    urls: [e.tempFilePath],
                    current: 0
                });
            }
         }
    }
</script>
```
### ä½¿ç”¨è¯´æ˜Ž
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
    "enablePullDownRefresh": false,
    "disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` æ— æ³•回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API çš„ success å›žè°ƒæ‰å¯ç”¨äºŽæ’件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。