Commit 396cbd37 authored by jyx's avatar jyx

删除无用代码,优化样式

parent 4ca10570
<template>
<view class="input-search">
<view class="input-search-box" @click="searchClick">
<view class="input-search-box-icon-search"><icons color="#999999" size="36" icon="search" /></view>
<input
v-if="show || searchVal"
:focus="showSync"
:placeholder="placeholder"
:maxlength="maxlength"
class="input-search-input"
confirm-type="search"
type="text"
v-model="searchVal"
@confirm="confirm"
@blur="blur"
@focus="emitFocus"
/>
<text v-else class="input-search-placeholder">{{ placeholder }}</text>
</view>
<view class="input-search-cancel" v-if="show" @click="cancel">取消</view>
</view>
</template>
<script>
export default {
name: 'input-search',
props: {
placeholder: {
type: String,
default: ''
},
maxlength: {
type: [Number, String],
default: 100
},
value: {
type: [Number, String],
default: ''
}
},
data() {
return {
show: false,
showSync: false,
searchVal: ''
};
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.searchVal = newVal;
if (newVal) {
this.show = true;
}
}
},
focus: {
immediate: true,
handler(newVal) {
if (newVal) {
this.show = true;
this.$nextTick(() => {
this.showSync = true;
});
}
}
},
searchVal(newVal, oldVal) {
this.$emit('input', newVal);
}
},
methods: {
searchClick() {
if (this.show) {
return;
}
this.show = true;
this.$nextTick(() => {
this.showSync = true;
});
},
cancel() {
this.searchVal = '';
this.show = false;
this.showSync = false;
// #ifndef APP-PLUS
uni.hideKeyboard();
// #endif
// #ifdef APP-PLUS
plus.key.hideSoftKeybord();
// #endif
this.$emit('cancel', '');
},
confirm() {
// #ifndef APP-PLUS
uni.hideKeyboard();
// #endif
// #ifdef APP-PLUS
plus.key.hideSoftKeybord();
// #endif
this.$emit('confirm', this.searchVal);
},
blur() {
// #ifndef APP-PLUS
uni.hideKeyboard();
// #endif
// #ifdef APP-PLUS
plus.key.hideSoftKeybord();
// #endif
if (!this.searchVal) {
this.show = false;
this.$nextTick(() => {
this.showSync = false;
});
}
this.$emit('blur', this.searchVal);
},
emitFocus(e) {
this.$emit('focus', e.detail);
}
}
};
</script>
<style lang="scss">
.input-search {
display: flex;
flex-direction: row;
position: relative;
padding: 16rpx 32rpx;
&-box {
display: flex;
box-sizing: border-box;
overflow: hidden;
position: relative;
flex: 1;
justify-content: center;
flex-direction: row;
align-items: center;
height: 70rpx;
background-color: #f0f0f0;
padding: 10rpx 16rpx 10rpx 0;
border: 1rpx solid #e5e5e5;
border-radius: 70rpx;
&-icon-search {
display: flex;
flex-direction: row;
padding: 0 16rpx;
justify-content: center;
align-items: center;
color: #808080;
}
}
&-input {
flex: 1;
color: #333;
}
&-placeholder {
color: #808080;
margin-left: 10rpx;
}
&-cancel {
padding-left: 20rpx;
line-height: 70rpx;
color: #333;
cursor: pointer;
}
}
</style>
<template>
<u-popup :show="showDialog" mode="center" round="20rpx" :closeOnClickOverlay="true" @close="close">
<view class="order-wrap">
<u-icon class="close" name="close" color="#929292" size="26" @click="close"></u-icon>
<view class="item-wrap">
<view style="height: 30rpx;" />
<u-icon name="checkmark-circle-fill" color="#F8425A" size="60"></u-icon>
<u-text size="24" align="center" color="#000" text="\n恭喜您,下单成功!"></u-text>
<view class="btn" @click="clickService">联系客服</view>
<view style="color: #6F6F6F;">(购买玩偶全身及售后服务,请咨询客服)</view>
</view>
</view>
</u-popup>
</template>
<script>
import {
message
} from '@/utils/fun';
export default {
name: "order-dialog",
props: {
show: {
type: Boolean,
default: false
},
},
data() {
return {
showDialog: this.show,
dataList: this.list
};
},
watch: {
show: {
handler: function(newVal, oldVal) {
this.showDialog = newVal;
}
},
list: {
handler: function(newVal, oldVal) {
this.list = newVal
}
}
},
methods: {
close() {
this.showDialog = false
this.$emit('dismiss');
},
clickService() {
// this.close()
this.$emit('showServices');
}
},
}
</script>
<style lang="scss">
.order-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
border-radius: 30rpx;
width: 550rpx;
padding: 30rpx;
background-color: #fff;
}
.item-wrap{
width: 550rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.btn {
margin: 30rpx 0;
border: solid 2rpx #979797;
height: 70rpx;
line-height: 70rpx;
text-align: center;
width: 200rpx;
font-size: 30rpx;
border-radius: 50rpx;
}
.close {
margin-left: auto;
}
</style>
<template>
<u-popup bgColor="#00000000" :show="showDialog" mode="center" round="20rpx" :closeOnClickOverlay="true"
@close="close">
<view class="container">
<image class="bell" src="../../static/index/bells.png"></image>
<view class="container-wrap">
<view style="font-size: 46rpx;font-weight: 700;">温馨提示</view>
<view class="mt-20" style="color: #6F6F6F;">1.人脸照片仅用于建模过程,生成后会自动删除,不会保留您的人脸照片和数据。
</view>
<view style="color: #6F6F6F;margin-top: 10rpx;">
2.请确保你是用的照片获得本人授权同意禁使用未获得本人授权照片的照片。如因照片未授权对他人肖像权造成侵犯所产生的法律责任由本人承担。</view>
<view style="color: red;margin-top: 20rpx;">* 根据《中华人民共和国消费者权益保护法》第二十五条相关规定,定制化产品不支持退换货。</view>
<view class="confirm" @click="confirm">我知道了</view>
</view>
</view>
</u-popup>
</template>
<script>
import {
message
} from '@/utils/fun';
export default {
name: "pic-tip-dialog",
props: {
show: {
type: Boolean,
default: false
},
},
data() {
return {
showDialog: this.show,
dataList: this.list
};
},
watch: {
show: {
handler: function(newVal, oldVal) {
this.showDialog = newVal;
}
},
list: {
handler: function(newVal, oldVal) {
this.list = newVal
}
}
},
methods: {
close() {
this.showDialog = false
this.$emit('dismiss');
},
confirm() {
this.close()
this.$emit('confirm');
}
},
}
</script>
<style lang="scss">
.container {
width: 600rpx;
.bell {
position: relative;
width: 150rpx;
height: 150rpx;
margin-bottom: -75rpx;
left: 225rpx;
}
.container-wrap {
border-radius: 20rpx;
padding: 100rpx 50rpx 50rpx 50rpx;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.confirm {
margin-top: 30rpx;
border-radius: 50rpx;
background-color: #F8425A;
color: #fff;
width: 300rpx;
height: 70rpx;
line-height: 70rpx;
text-align: center;
}
}
</style>
<template>
<u-popup customStyle="background-color:#00000000;" :show="showDialog" mode="center" round="20rpx"
:closeOnClickOverlay="true" @close="close">
<view class="container">
<view class="service-wrap">
<view class="service-item" v-for="(item, index) in dataList" :key="index">
<u-text align="center" :text="item" mode="link" href="https://www.baidu.com"></u-text>
</view>
</view>
<u-icon name="close-circle" color="#fff" size="30" @click="close"></u-icon>
</view>
</u-popup>
</template>
<script>
import {
message
} from '@/utils/fun';
export default {
name: "service-dialog",
props: {
list: {
type: Array,
default: () => {}
},
show: {
type: Boolean,
default: false
},
},
data() {
return {
showDialog: this.show,
dataList: this.list
};
},
watch: {
show: {
handler: function(newVal, oldVal) {
this.showDialog = newVal;
}
},
list: {
handler: function(newVal, oldVal) {
this.list = newVal
}
}
},
methods: {
close() {
this.showDialog = false
this.$emit('dismiss');
},
clickService() {
}
},
}
</script>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.service-wrap {
border-radius: 30rpx;
margin-bottom: 30rpx;
width: 500rpx;
padding: 20rpx;
background-color: #fff;
.service-item {
padding: 20rpx;
}
}
</style>
<template>
<view :class="'tag ' + color">{{ text }}</view>
</template>
<script>
export default {
name: 'tag',
props: {
value: String
},
data() {
return {
text: '',
color: 'gray'
};
},
watch: {
value: {
immediate: true,
handler() {
this.create();
}
}
},
created() {
this.create();
},
methods: {
create() {
let [text, color] = this.value.split(':');
this.text = text;
this.color = color;
}
}
};
</script>
<style lang="scss">
@import '@/scss/uni.scss';
.tag {
display: inline-block;
line-height: 1;
margin-left: 20rpx;
padding: 10rpx 28rpx;
vertical-align: middle;
font-weight: normal;
font-size: 26rpx;
font-weight: bold;
color: #2F2F2F;
border-radius: 40rpx;
&.blue {
background-color: $color-primary;
}
&.green {
background-image: linear-gradient(90deg,#6FF1A8 ,#60D9EB);
}
&.yellow {
background-color: $color-warning;
}
&.red {
background-color: $color-error;
}
&.gray {
background-color: $color-cancel;
}
}
</style>
<template>
<view>
<view v-if="!hidetags1" class="tags">
<view class="tags-text">{{ text1 }}</view>
</view>
<view class="tags">
<view class="tags-text">{{ text2 }}</view>
<view class="tags-count">{{ currentCount>=100000?'10W+份':currentCount+'份' }}</view>
</view>
</view>
</template>
<script>
export default {
name: 'tags',
props: {
tags: String,
count: Number,
hideTags1: Boolean,
},
data() {
return {
text1: '',
text2: '',
currentCount: 0,
hidetags1: true,
};
},
watch: {
hideTags1: {
handler(newVal, oldVal) {
this.hidetags1 = newVal;
},
immediate: true
},
tags: {
handler(newVal, oldVal) {
let text = newVal.split(',');
this.text1 = text[0];
this.text2 = text[1];
},
immediate: true
},
count: {
handler(newVal, oldVal) {
this.currentCount = newVal;
},
immediate: true
}
}
};
</script>
<style lang="scss">
@import '@/scss/uni.scss';
.tags {
color: #103B50;
// color: $bg-color;
display: inline-block;
border-radius: 4rpx;
line-height: 1;
font-size: 16rpx;
margin-top: 14rpx;
// background-color: $main-color;
background: -webkit-linear-gradient(left, #03E6CA, #5DC6FD);
/* Safari 5.1-6.0 */
background: -o-linear-gradient(right, #03E6CA, #5DC6FD);
/* Opera 11.1-12.0 */
background: -moz-linear-gradient(right, #03E6CA, #5DC6FD);
/* Firefox 3.6-15 */
background: linear-gradient(to right, #03E6CA, #5DC6FD);
/* 标准语法 */
&-text {
display: inline-block;
padding: 7rpx 10rpx;
vertical-align: middle;
}
&-count {
display: inline-block;
background-color: #4b4b4e;
//color: $main-color;
color: #34FFEF;
border: #34FFEF solid 0.2rpx;
padding: 7rpx 10rpx;
vertical-align: middle;
}
&+.tags {
margin-left: 10rpx;
}
}
</style>
<template>
<view class="team-name flex">
<view class="team-name-avatar" style="margin-right: 14rpx;"><image class="team-name-avatar" v-if="headUrl" :src="headUrl" mode="scaleToFill"></image></view>
<view class="flex-1 ellipsis">{{ team }}</view>
</view>
</template>
<script>
export default {
name: 'team-name',
props: {
team: String,
headUrl: [String, Object]
}
};
</script>
<style lang="scss">
@import '@/scss/uni.scss';
.team-name {
color: #ADADAD;
font-size: 12px;;
align-items: center;
&-avatar {
display: inline-block;
height: 30rpx;
width: 30rpx;
border-radius: 50%;
overflow: hidden;
background-color: #d8d8d8;
}
}
</style>
<template>
<view class="time-down-pay">{{ text }}</view>
</template>
<script>
export default {
name: 'time-down-pay',
props: {
time: String
},
data() {
return {
text: '00:00:00',
timespan: 0,
timer: undefined
};
},
// watch: {
// value: {
// immediate: true,
// handler() {
// this.create();
// }
// }
// },
created() {
console.log(this.time)
this.timespan = new Date(this.time).getTime() - new Date().getTime();
this.countdown();
},
beforeDestroy() {
clearInterval(this.timer);
},
methods: {
countdown() {
if (this.timespan < 0) {
this.text = '已取消'
} else {
this.timer = setInterval(() => {
console.log('11');
let {
timespan
} = this;
this.timespan -= 1000;
if (this.timespan <= 0) {
this.text = '已取消'
} else {
let leftd = Math.floor(timespan / (1000 * 60 * 60 * 24)), //计算天数
lefth = ('00' + Math.floor((timespan / (1000 * 60 * 60)) % 24)).slice(-2), //计算小时数
leftm = ('00' + Math.floor((timespan / (1000 * 60)) % 60)).slice(-2), //计算分钟数
lefts = ('00' + Math.floor((timespan / 1000) % 60)).slice(-2); //计算秒数
this.text = (leftd > 0 ? leftd + '天' : '') + lefth + ':' + leftm + ':' +
lefts; //返回倒计时的字符串
}
}, 1000);
}
}
}
};
</script>
<style lang="scss">
@import '@/scss/uni.scss';
.time-down {
display: inline-block;
margin-left: 10rpx;
}
</style>
<template>
<view :id="elId" class="v-tabs">
<scroll-view
id="scrollContainer"
:scroll-x="scroll"
:scroll-left="scroll ? scrollLeft : 0"
:scroll-with-animation="scroll"
:style="{ position: fixed ? 'fixed' : 'relative', zIndex: 1993 }"
>
<view
class="v-tabs__container"
:style="{
display: scroll ? 'inline-flex' : 'flex',
whiteSpace: scroll ? 'nowrap' : 'normal',
background: bgColor,
height,
padding
}"
>
<view
:class="['v-tabs__container-item', { disabled: !!v.disabled }]"
v-for="(v, i) in tabs"
:key="i"
:style="{
color: current == i ? activeColor : color,
fontSize: current == i ? fontSize : fontSize,
fontWeight: bold && current == i ? 'bold' : '',
justifyContent: !scroll ? 'center' : '',
flex: scroll ? '' : 1,
padding: paddingItem
}"
@click="change(i)"
>
{{ field ? v[field] : v }}
</view>
<view
v-if="!pills"
:class="['v-tabs__container-line', { animation: lineAnimation }]"
:style="{
background: lineColor,
width: lineWidth + 'px',
height: lineHeight,
borderRadius: lineRadius,
left: lineLeft + 'px',
transform: `translateX(-${lineWidth / 2}px)`
}"
></view>
<view
v-else
:class="['v-tabs__container-pills', { animation: lineAnimation }]"
:style="{
background: pillsColor,
borderRadius: pillsBorderRadius,
left: pillsLeft + 'px',
width: currentWidth + 'px',
height
}"
></view>
</view>
</scroll-view>
<view
class="v-tabs__placeholder"
:style="{
height: fixed ? height : '0',
padding
}"
></view>
</view>
</template>
<script>
/**
* v-tabs
* @property {Number} value 选中的下标
* @property {Array} tabs tabs 列表
* @property {String} bgColor = '#fff' 背景颜色
* @property {String} color = '#333' 默认颜色
* @property {String} activeColor = '#2979ff' 选中文字颜色
* @property {String} fontSize = '28rpx' 默认文字大小
* @property {String} activeFontSize = '28rpx' 选中文字大小
* @property {Boolean} bold = [true | false] 选中文字是否加粗
* @property {Boolean} scroll = [true | false] 是否滚动
* @property {String} height = '60rpx' tab 的高度
* @property {String} lineHeight = '10rpx' 下划线的高度
* @property {String} lineColor = '#2979ff' 下划线的颜色
* @property {Number} lineScale = 0.5 下划线的宽度缩放比例
* @property {String} lineRadius = '10rpx' 下划线圆角
* @property {Boolean} pills = [true | false] 是否胶囊样式
* @property {String} pillsColor = '#2979ff' 胶囊背景色
* @property {String} pillsBorderRadius = '10rpx' 胶囊圆角大小
* @property {String} field 如果是对象,显示的键名
* @property {Boolean} fixed = [true | false] 是否固定
* @property {String} paddingItem = '0 22rpx' 选项的边距
* @property {Boolean} lineAnimation = [true | false] 下划线是否有动画
*
* @event {Function(current)} change 改变标签触发
*/
export default {
props: {
value: {
type: Number,
default: 0
},
tabs: {
type: Array,
default () {
return []
}
},
bgColor: {
type: String,
default: '#fff'
},
padding: {
type: String,
default: '0'
},
color: {
type: String,
default: '#333'
},
activeColor: {
type: String,
default: '#2979ff'
},
fontSize: {
type: String,
default: '28rpx'
},
activeFontSize: {
type: String,
default: '32rpx'
},
bold: {
type: Boolean,
default: true
},
scroll: {
type: Boolean,
default: true
},
height: {
type: String,
default: '70rpx'
},
lineColor: {
type: String,
default: '#2979ff'
},
lineHeight: {
type: String,
default: '10rpx'
},
lineScale: {
type: Number,
default: 0.5
},
lineRadius: {
type: String,
default: '10rpx'
},
pills: {
type: Boolean,
default: true
},
pillsColor: {
type: String,
default: '#2979ff'
},
pillsBorderRadius: {
type: String,
default: '10rpx'
},
field: {
type: String,
default: ''
},
fixed: {
type: Boolean,
default: false
},
paddingItem: {
type: String,
default: '0 22rpx'
},
lineAnimation: {
type: Boolean,
default: true
}
},
data () {
return {
elId: '',
lineWidth: 30,
currentWidth: 0, // 当前选项的宽度
lineLeft: 0, // 滑块距离左侧的位置
pillsLeft: 0, // 胶囊距离左侧的位置
scrollLeft: 0, // 距离左边的位置
containerWidth: 0, // 容器的宽度
current: 0 // 当前选中项
}
},
watch: {
value (newVal) {
this.current = newVal
this.$nextTick(() => {
this.getTabItemWidth()
})
},
current (newVal) {
this.$emit('input', newVal)
},
tabs (newVal) {
this.$nextTick(() => {
this.getTabItemWidth()
})
}
},
methods: {
// 产生随机字符串
randomString (len) {
len = len || 32
let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
let maxPos = $chars.length
let pwd = ''
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
}
return pwd
},
// 切换事件
change (index) {
const isDisabled = !!this.tabs[index].disabled
if (this.current !== index && !isDisabled) {
this.current = index
this.$emit('change', index)
}
},
// 获取左移动位置
getTabItemWidth () {
let query = uni
.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
// 获取容器的宽度
query
.select(`#scrollContainer`)
.boundingClientRect((data) => {
if (!this.containerWidth && data) {
this.containerWidth = data.width
}
})
.exec()
// 获取所有的 tab-item 的宽度
query
.selectAll('.v-tabs__container-item')
.boundingClientRect((data) => {
if (!data) {
return
}
let lineLeft = 0
let currentWidth = 0
if (data) {
for (let i = 0; i < data.length; i++) {
if (i < this.current) {
lineLeft += data[i].width
} else if (i == this.current) {
currentWidth = data[i].width
} else {
break
}
}
}
// 当前滑块的宽度
this.currentWidth = currentWidth
// 缩放后的滑块宽度
this.lineWidth = currentWidth * this.lineScale * 1
// 滑块作移动的位置
this.lineLeft = lineLeft + currentWidth / 2
// 胶囊距离左侧的位置
this.pillsLeft = lineLeft
// 计算滚动的距离左侧的位置
if (this.scroll) {
this.scrollLeft = this.lineLeft - this.containerWidth / 2
}
})
.exec()
}
},
mounted () {
this.elId = 'xfjpeter_' + this.randomString()
this.current = this.value
this.$nextTick(() => {
this.getTabItemWidth()
})
}
}
</script>
<style lang="scss" scoped>
.v-tabs {
width: 100%;
box-sizing: border-box;
overflow: hidden;
::-webkit-scrollbar {
display: none;
}
&__container {
min-width: 100%;
position: relative;
display: inline-flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
&-item {
justify-content: center;
width: 120rpx;
display: flex;
align-items: center;
height: 100%;
position: relative;
z-index: 10;
// padding: 0 11px;
transition: all 0.3s;
white-space: nowrap;
&.disabled {
opacity: 0.5;
color: #999;
}
}
&-line {
position: absolute;
bottom: 0;
}
&-pills {
position: absolute;
z-index: 9;
}
&-line,
&-pills {
&.animation {
transition: all 0.3s linear;
}
}
}
}
</style>
......@@ -7,16 +7,16 @@
// });
// //#endif
// },
// }
// }
export default {
data() {
return {
share: {
title: '3D打印你的专属伴侣,点击立刻拥有她/他!',
imageUrl: 'http://mints-sh.oss-cn-shanghai.aliyuncs.com/userImg/share.jpg',
path: '/pages/loading?shareId='+getApp().globalData.userId,
imageUrl: 'http://mints-sh.oss-cn-shanghai.aliyuncs.com/userImg/share.jpg',
path: '/pages/loading?shareId=' + getApp().globalData.userId,
}
}
},
......@@ -36,4 +36,4 @@ export default {
},
}
}
\ No newline at end of file
......@@ -107,7 +107,6 @@
"navigationBarBackgroundColor": "#2196f3",
"navigationBarTextStyle": "black"
}
}]
}],
// 分包预载配置
......@@ -134,14 +133,12 @@
"scrollIndicator": "none"
}
},
"condition" : { //模式配置,仅开发期间生效
"condition": { //模式配置,仅开发期间生效
"current": 0, //当前激活的模式(list 的索引项)
"list": [
{
"name": "", //模式名称
"path": "", //启动页面,必选
"query": "" //启动参数,在页面的onLoad函数里面得到
}
]
"list": [{
"name": "", //模式名称
"path": "", //启动页面,必选
"query": "" //启动参数,在页面的onLoad函数里面得到
}]
}
}
\ No newline at end of file
......@@ -81,7 +81,7 @@
data() {
return {
statusBarHeight: app.globalData.statusBarHeight,
userBean: null,
userBean: {},
versionName: app.globalData.versionName
};
},
......
......@@ -4,11 +4,11 @@
<swiper-item v-for="(list, index) in displaySwiperList" :key="index">
<view v-if="Math.abs(displayIndex-index)==0" style="height: 100%;">
<video v-if="Math.abs(displayIndex-index)==0" :id="''+list.vedioId" :controls="isqp" :isplay="true"
play-btn-position="center" :show-play-btn="isqp" :show-progress="true" :loop="!isplay"
@ended="ended" @controlstoggle="controlstoggle" :show-fullscreen-btn="false"
@click="tapVideoHover()" :enable-progress-gesture="false" :style="'width:100%; height:100%;'"
:src="list.recommendUrl" object-fit="fill" />
<video v-if="Math.abs(displayIndex-index)==0" :id="''+list.vedioId" :controls="controls"
:autoplay="true" :isplay="true" play-btn-position="center" :loop="!isplay" @ended="ended"
@controlstoggle="controlstoggle" :show-fullscreen-btn="false" :poster="list.coverImage"
:show-loading="true" @click="tapVideoHover()" :enable-progress-gesture="false"
:style="'width:100%; height:100%;'" :src="list.recommendUrl" object-fit="fill" />
</view>
<view v-if="!isqp" class="userInfo flex">
<!-- 点赞 -->
......@@ -74,6 +74,7 @@
oid: 0,
isplay: true, // 是否自动播放下一个视频
playCount: 2, // 剩余多少视频加载视频列表
issp: false,
isqp: false, // 是否全屏
urls: "https://xjc.demo.hongcd.com/api/video/videoRecommend?page=1&uid=0",
isFirstLoad: true,
......@@ -147,8 +148,7 @@
});
},
tapVideoHover() {
// 控制是否全屏
this.isqp = !this.isqp
this.isqp = !this.issp
},
ended() {
// 1.播放当前视频结束时触发,自动切换下一个视频
......@@ -226,10 +226,12 @@
this.oid = this.originIndex + 1
this.initSwiperData(this.originIndex);
}
this.issp = false
this.isqp = false
},
controlstoggle(e) {
console.log(e.detail.show);
this.issp = e.detail.show
},
detail(detail) {
navigateTo(`/pagesC/video/videoDetail?data=` + encodeURIComponent(JSON.stringify(detail)) +
......
<template>
<view class="body">
<status-title :showBack="true">会员界面</status-title>
<view class="section">
<view class="title">
选择套餐
</view>
<view class="pack-box">
<view class="pack-item" :class="[{active: index==selectedIndex}]" v-for='(item, index) in vipList'
:key='index' @click="choosePack(item, index)">
<view class="name row">
{{item.title}}
</view>
<view class="price row">
{{item.firstPayPrice}}
</view>
<view class="origin row" v-if='item.oldPrice'>
原价:{{item.oldPrice}}
</view>
<view class="cut-down" v-if='item.oldPrice-item.firstPayPrice>0'>
立省 {{item.oldPrice-item.firstPayPrice}}
<view class="body">
<status-title :showBack="true" icon-color="white" font-color="white">会员界面</status-title>
<scroll-view scroll-y style="height: 100%;">
<image style="width: 100%;" src="@/static/bg_vip_2.png" mode="widthFix" />
<view class="section">
<view class="title">
选择套餐
</view>
<view class="pack-box">
<view class="pack-item" :class="[{active: index==selectedIndex}]" v-for='(item, index) in vipList'
:key='index' @click="choosePack(item, index)">
<view class="name row">
{{item.title}}
</view>
<view class="price row">
{{item.firstPayPrice}}
</view>
<view class="origin row" v-if='item.oldPrice'>
原价:{{item.oldPrice}}
</view>
<view class="cut-down" v-if='item.oldPrice-item.firstPayPrice>0'>
立省 {{item.oldPrice-item.firstPayPrice}}
</view>
</view>
</view>
</view>
</view>
<view class="section">
<button class="apply-button"
@click="handlePay">立即开通</button>
</view>
<view class="section">
<button class="apply-button" @click="handlePay">立即开通</button>
</view>
<image style="width: 100%;" src="@/static/bg_vip_0.png" mode="widthFix" />
<image style="width: 100%;" src="@/static/bg_vip_1.png" mode="widthFix" />
</scroll-view>
</view>
</template>
......@@ -66,27 +74,27 @@
},
choosePack(item, index) {
this.selectedIndex = index;
},
handlePay(){
let vipBean = this.vipList[this.selectedIndex];
message.notify('vipBean 价格=' + vipBean.firstPayPrice);
// this.post({
// url: '/vip/getVipPayParams',
// data: {
// pid: vipBean.pid,
// payChannel: 'WEIXIN'
// },
// showLoading: false,
// success: ({
// data
// }) => {
// let wxParams = data.params
// // 发起微信支付
// }
// });
},
handlePay() {
let vipBean = this.vipList[this.selectedIndex];
message.notify('vipBean 价格=' + vipBean.firstPayPrice);
// this.post({
// url: '/vip/getVipPayParams',
// data: {
// pid: vipBean.pid,
// payChannel: 'WEIXIN'
// },
// showLoading: false,
// success: ({
// data
// }) => {
// let wxParams = data.params
// // 发起微信支付
// }
// });
}
}
}
......@@ -94,7 +102,7 @@
<style lang="scss">
.body {
background-color: whitesmoke;
background-color: black;
display: flex;
flex-direction: column;
......@@ -102,12 +110,12 @@
padding: 0 30rpx;
display: flex;
flex-direction: column;
background: white;
background: black;
.title {
font-size: 32rpx;
font-weight: 700;
color: #333;
color: white;
margin-bottom: 30rpx;
}
......@@ -178,6 +186,7 @@
}
.apply-button {
width: 86%;
border-radius: 50rpx;
background: #e8c8ae;
color: #8d5a29;
......
<template>
<view class="content">
<view style="width: 100%;height: 100%;">
<status-title style="position: absolute;" iconColor="white" :showBack="true"></status-title>
<swiper class="swiper" circular @change="swiperChange" :current="current" :vertical="true">
<swiper-item v-for="(list, index) in displaySwiperList" :key="index" :style="'width:100%; height:100%;'">
<view :style="'width:100%; height:100%;'">
<video v-if="Math.abs(displayIndex-index)==0 && list.vedioUrl" :id="list.vedioIndex"
:controls="isqp" :loop="!isplay" :enable-progress-gesture="false" :show-progress="true"
:show-play-btn="isqp" :show-loading="true" :show-fullscreen-btn="false" @ended="ended"
@controlstoggle="controlstoggle" @click="tapVides()" @timeupdate="timeupdate"
:style="'width:100%; height:100%;'" :src="list.vedioUrl" :poster="data.coverImage"
class="tsvideo" play-btn-position="center" object-fit="fill" />
<video v-if="Math.abs(displayIndex-index)==0 && list.vedioUrl" :id="list.vedioId"
:controls="controls" :loop="!isplay" :enable-progress-gesture="false" :show-loading="true"
:show-fullscreen-btn="false" @ended="ended" @controlstoggle="controlstoggle" @click="tapVides()"
@timeupdate="timeupdate" style="width:100%; height:100%;background: #f56c6c;"
:src="list.vedioUrl" :poster="data.coverImage" class="tsvideo" play-btn-position="center"
object-fit="fill" />
<view v-if="!isqp" class="userInfo">
<!-- 点赞 -->
<view class="flex" style="opacity: 0.9; margin-top: 10rpx;">
......@@ -142,6 +142,9 @@
import {
data
} from '../../uni_modules/uview-ui/libs/mixin/mixin';
import {
ref
} from "vue";
let audo = uni.createInnerAudioContext()
export default {
......@@ -171,7 +174,7 @@
oid: 0,
isplay: true, //是否自动播放下一个视频
duration: 500,
issp: '',
issp: false,
isqp: false,
mid: 0,
safeArea: 0,
......@@ -217,21 +220,22 @@
},
timeupdate(event) {
if (event.detail.currentTime > 0 && this.originList[this.originIndex].vedioIndex > this.data.unlockIndex) {
uni.createVideoContext(this.originList[this.originIndex].vedioIndex, this).seek(0);
uni.createVideoContext(this.originList[this.originIndex].vedioIndex, this).pause();
uni.createVideoContext(this.originList[this.originIndex].vedioId, this).seek(0);
uni.createVideoContext(this.originList[this.originIndex].vedioId, this).pause();
this.fenji = 2
this.$refs.select.open('bottom');
}
},
selectThisVideo(index, pays) {
selectThisVideo(index) {
this.down();
if (this.originList[this.originIndex].vedioIndex > this.data.unlockIndex) {
this.fenji = 2
this.$refs.select.open('bottom');
} else {
console.log("AAAAAAA" + index)
this.duration = 20;
this.originIndex = index
this.initSwiperData(index);
this.initSwiperData(this.originIndex);
setTimeout(() => {
this.duration = 500;
}, 500)
......@@ -256,7 +260,7 @@
this.$refs.select.open('bottom');
},
tapVides() {
this.isqp = !this.isqp
this.isqp = !this.issp
},
getData() {
this.originList = []
......@@ -296,7 +300,6 @@
} else {
this.current = 0
}
this.isqp = true
console.log('显示swiper Index:', this.displayIndex)
}
},
......@@ -328,18 +331,16 @@
this.oid = this.originList.length - 1
}
console.log('++++++++++++上一条播放数据 Index:', this.oid)
uni.createVideoContext("" + this.originList[this.oid].vedioIndex, this).stop();
uni.createVideoContext("" + this.originList[this.oid].vedioId, this).stop();
if (this.originList[originIndex].vedioIndex > this.data.unlockIndex) {
this.isqp = true
this.fenji = 2
this.$refs.select.open('bottom');
} else {
setTimeout(() => {
this.isqp = true
console.log('qqqqqq:', this.originList[originIndex].vedioIndex)
console.log('qqqqqq:', this.originList[originIndex].vedioId)
// audo.play()
uni.createVideoContext("" + this.originList[originIndex].vedioIndex, this).play();
uni.createVideoContext("" + this.originList[originIndex].vedioId, this).play();
this.commitVideo()
}, 500)
}
......@@ -369,7 +370,8 @@
this.oid = this.originIndex + 1
this.initSwiperData(this.originIndex);
}
this.issp = false
this.isqp = false
},
initEpisode() {
this.subList = []
......@@ -446,10 +448,6 @@
background: #000000;
}
.content {
height: 100%;
}
.title {
width: 100%;
display: flex;
......
## 1.8.24(2022-05-09)
本地缓存:
- 优化示例项目
## 1.8.23(2022-05-09)
本地缓存:
- 优化清除文件缓存的方法
## 1.8.22(2022-05-09)
本次更新:
- 调整计算转盘绘制的方式
## 1.8.21(2022-05-08)
本次更新:
- 调整示例项目中本地图片的引入方式
## 1.8.20(2022-04-29)
本次更新:
- 修复转盘在某个临界点可以出现多次触发的问题
## 1.8.19(2022-04-27)
本次更新:
- 奖品文字的绘制由先前的两行变成多行,根据设定的每行文字的长度分段绘制
## 1.8.18(2022-04-25)
本次更新:
- 减少小程序平台的 delay
## 1.8.17(2022-03-23)
本次更新:
- 新增配置项 `imgCircled` 奖品图片是否裁切为圆形,默认不裁切
## 1.8.16(2022-03-04)
本次更新:
- 示例项目新增绘制时长的计算,方便开发时定位绘制慢的问题
## 1.8.15(2022-03-02)
本次更新:
- 优化一处错误提示信息的展现方式
## 1.8.14(2021-11-29)
本次更新:
- 示例项目中新增开放自定义权重最大值,没有自定义则取权重数组中的最大值
- 更新文档
## 1.8.13(2021-11-03)
本次更新:
- 注释 `1.8.12` 版本中调试时的代码
## 1.8.12(2021-11-03)
本次更新:
- 修复一些老机型不支持 `flex` 导致布局错乱的问题
## 1.8.11(2021-10-29)
本次更新:
- 优化示例项目中模拟接口访问的速度
## 1.8.10(2021-10-19)
本次更新:
- 优化组件代码
- 更新示例项目
## 1.8.9(2021-09-28)
本次更新:
- 移除内置的 `奖品准备中...` 提示
## 1.8.8(2021-09-27)
本次更新:
- 修复 `1.8.6` 引起的非微信小程序平台绘制异常的问题
## 1.8.7(2021-09-23)
本次更新:
- 优化项目中使用到的图片大小
## 1.8.6(2021-09-23)
本次更新:
- 修复小程序平台在绘制 `base64` 格式的图片时无法在真机模式下正常显示的问题
## 1.8.5(2021-09-13)
本次更新:
- 修复一个已知问题
## 1.8.4(2021-09-12)
本次更新:
- 调整 `strFontColor``strFontColors`,现在可以设置每个区块的文字颜色,详见文档说明
## 1.8.3(2021-09-12)
本次更新:
- 修复因 `1.8.0` 改动引起的文字方向、无奖品图时绘制异常的问题
- 新增 `imgDrawed` 是否绘制奖品图片的配置项 ,默认为 `true`
## 1.8.2(2021-09-10)
本次更新:
**不兼容旧版本的更新**
- 移除配置项 `strKey` 字段
- 调整 `prizeList` 结构
## 1.8.1(2021-09-06)
本次更新:
- 修复 `hbx3.1.22` 在小程序平台处理 `id-name` 存在解析错误的问题
## 1.8.0(2021-09-06)
本次更新:
**该版本更新涉及破坏性的变更,请重新查看 `API - Props` 的部分**
- `px` 全面调整为 `rpx` 单位,多个`Props` 的参数相应调整,请查看文档
- 新增 `pixelRatio` 参数,该参数为设计稿的设备像素比基准值,默认为 `2` 倍素
## 1.7.18(2021-09-05)
本次更新:
- 修复一个已知问题
## 1.7.17(2021-08-23)
本次更新:
- 更新示例项目
## 1.7.16(2021-08-14)
本次更新:
- 更新示例项目
## 1.7.15(2021-08-03)
本次更新:
- 新增文字竖向展示的功能,详见文档说明
## 1.7.13(2021-08-02)
本次更新:
- 更新文档
## 1.7.12(2021-07-30)
本次更新:
- 修复示例项目的已知问题
- 现已提供Almost-Lottery抽奖转盘的uniCloud云端一体页面模板
- 现已提供Almost-Lottery抽奖转盘云端一体页面配套的Admin配置中心
## 1.7.11(2021-07-22)
本次更新:
- 修复部分安卓手机文字大小异常的问题
- 字段 `strHeightMultiple` 更换为 `strLineHeight`
## 1.7.10(2021-07-12)
本次更新:
- 修复奖品名称 `name` 为空字符串时无法成功绘制转盘的问题
- 新增 `prizeNameDrawed` 是否绘制奖品名称的配置项,现在可以仅展示奖品图片了
## 1.7.9(2021-07-09)
本次更新:
- 优化组件内部代码
- 修复奖品图片已然是 `base64` 格式时导致转盘绘制失败的问题
- 文档新增QQ群号,让沟通更便捷
## 1.7.8(2021-07-08)
本次更新:
- 调整 `Canvas` 默认宽高为 `280`
## 1.7.7(2021-07-08)
本次更新:
- 新增多个配置项,满足更多自定义需求
- 优化多行文本情况下非中文字符的字节处理
- 修复偶发的第一条数据文本不居中显示的问题
## 1.7.6(2021-07-02)
本次更新:
- 调整 `imageWidth``imageHeight` 字段为 `imgWidth``imgHeight`
- 更新示例项目
## 1.7.5(2021-07-01)
本次更新:
- 新增配置项 `imgMarginStr` 奖品图片距离奖品文字的距离
## 1.7.4(2021-06-28)
本次更新:
- 新增轮盘旋转或指针旋转配置项
- 转盘内置的外环图片以及按钮图片统一调整为 `image` 展示
- 更新相关文档说明
## 1.7.3(2021-06-16)
本次更新:
- 优化错误提示
- 优化示例项目
- 优化文档说明
## 1.7.2(2021-06-11)
本次更新:
- 新增 `canvasId` 参数配置项,多画板情况下需要配置不同的 `canvasId`
- 优化多画板情况下的缓存功能
- 优化示例项目
- 修改文档说明
## 1.7.1(2021-06-10)
本次更新:
- 优化示例项目中的注释
## 1.7.0(2021-06-04)
本次更新:
- 修复 `1.6.1` 引起的多行奖品文字行高异常的问题
- 新增配置转盘外环和抽奖按钮图片的功能,详见文档说明
- 更新示例项目,新增抽奖次数等业务有关的逻辑供参考
## 1.6.1(2021-05-28)
本次更新:
- 修复小程序平台画板模糊的问题
## 1.6.0(2021-05-28)
本次更新:
- 新增奖品区块是否开启描边的配置项,默认不开启
- 调整画板缓存为默认不开启
- 优化代码
- 优化文档说明
- 更新示例项目并修改部分注释
## 1.5.13(2021-05-22)
本次更新:
- 优化文档说明
- 更新示例项目
## 1.5.12(2021-05-22)
本次更新:
- 新增配置项 `strokeColor` 奖品区块边框颜色
- 更新文档说明
## 1.5.11(2021-05-19)
本次更新:
- 新增`strMarginOutside`参数,用于设置奖品文字距离边缘的距离
- 修复奖品文字在某些情况下不是居中显示的问题
## 1.5.10(2021-05-19)
本次更新:
- 修复示例项目中权重值相同时的取值逻辑
## 1.5.9(2021-05-14)
本次更新:
- 调整代码,优化小程序端的展示
## 1.5.8(2021-05-12)
本次更新:
- 文档增加预警提示:不再维护非 `uni_modules` 模式下的版本
## 1.5.7(2021-05-12)
本次更新:
- 修复小程序平台奖品名称不清晰的问题
## 1.5.6(2021-03-18)
本次更新:
- 适配 uni_modules 插件模式
<template>
<view class="almost-lottery">
<view class="almost-lottery__wrap" :style="{ width: lotterySize + 'rpx', height: lotterySize + 'rpx' }">
<view class="lottery-action"
:style="{ width: action.width + 'rpx', height: action.height + 'rpx', left: action.padding + 'rpx' }">
</view>
<view class="str-margin-outside" :style="{ left: strMarginOutside + 'rpx' }"></view>
<view class="img-margin-str" :style="{ left: imgMarginStr + 'rpx' }"></view>
<view class="img-size" :style="{ width: imgWidth + 'rpx', height: imgHeight + 'rpx' }"></view>
<template v-if="lotteryImg">
<image class="almost-lottery__bg" mode="widthFix" :src="lotteryBg" :style="{
width: lotteryPxSize + 'px',
height: lotteryPxSize + 'px'
}"></image>
<image class="almost-lottery__canvas-img" mode="widthFix" :src="lotteryImg" :style="{
width: canvasImgPxSize + 'px',
height: canvasImgPxSize + 'px',
left: canvasImgToLeftPx + 'px',
top: canvasImgToLeftPx + 'px',
transform: `rotate(${canvasAngle + targetAngle}deg)`,
transitionDuration: `${transitionDuration}s`
}"></image>
<image class="almost-lottery__action-bg" mode="widthFix" :src="actionBg" :style="{
width: actionPxSize + 'px',
height: actionPxSizeHeight + 'px',
left: actionBgToLeftPx + 'px',
top: actionBgToLeftPx-10 + 'px',
transform: `rotate(${actionAngle + targetActionAngle}deg)`,
transitionDuration: `${transitionDuration}s`
}" @click="handleActionStart"></image>
</template>
</view>
<!-- 为了兼容 app 端 ctx.measureText 所需的标签 -->
<text class="almost-lottery__measureText" :style="{ fontSize: higtFontSize + 'px' }">{{ measureText }}</text>
<!-- #ifdef MP-ALIPAY -->
<canvas :class="className" :id="canvasId" :width="higtCanvasSize" :height="higtCanvasSize" :style="{
width: higtCanvasSize + 'px',
height: higtCanvasSize + 'px'
}" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<canvas :class="className" :canvas-id="canvasId" :width="higtCanvasSize" :height="higtCanvasSize" :style="{
width: higtCanvasSize + 'px',
height: higtCanvasSize + 'px'
}" />
<!-- #endif -->
</view>
</template>
<script>
const systemInfo = uni.getSystemInfoSync()
import {
getStore,
setStore,
clearStore,
circleImg,
clacTextLen,
downloadFile,
pathToBase64,
base64ToPath
} from '@/uni_modules/almost-lottery/utils/almost-utils.js'
export default {
name: 'AlmostLottery',
props: {
// 设计稿的像素比基准值
pixelRatio: {
type: Number,
default: 2
},
// canvas 标识
canvasId: {
type: String,
default: 'almostLottery'
},
// 抽奖转盘的整体尺寸
lotterySize: {
type: Number,
default: 600
},
// 抽奖按钮的尺寸
// actionSize: {
// type: Number,
// default: 200
// },
action:{
type:Object,
default:{
width:200,
height:200,
top:0,
padding:90
},
},
// canvas边缘距离转盘边缘的距离
// canvasMarginOutside: {
// type: Number,
// default: 90
// },
// 奖品列表
prizeList: {
type: Array,
required: true,
validator: (value) => {
return value.length > 1
}
},
// 中奖奖品在列表中的下标
prizeIndex: {
type: Number,
required: true
},
// 奖品区块对应背景颜色
colors: {
type: Array,
default: () => [
'#FFFFFF',
'#FFBF05'
]
},
// 转盘外环背景图
lotteryBg: {
type: String,
default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__bg2x.png'
},
// 抽奖按钮背景图
actionBg: {
type: String,
default: '/uni_modules/almost-lottery/static/almost-lottery/almost-lottery__action2x.png'
},
// 是否绘制奖品名称
prizeNameDrawed: {
type: Boolean,
default: true
},
// 是否开启奖品区块描边
stroked: {
type: Boolean,
default: true
},
// 描边颜色
strokeColor: {
type: String,
default: '#FFBF05'
},
// 旋转的类型
rotateType: {
type: String,
default: 'roulette'
},
// 旋转动画时间 单位s
duration: {
type: Number,
default: 8
},
// 旋转的圈数
ringCount: {
type: Number,
default: 8
},
// 指针位置
pointerPosition: {
type: String,
default: 'edge',
validator: (value) => {
return value === 'edge' || value === 'middle'
}
},
// 文字方向
strDirection: {
type: String,
default: 'horizontal',
validator: (value) => {
return value === 'horizontal' || value === 'vertical'
}
},
// 字体颜色
strFontColors: {
type: Array,
default: () => [
'#FFBF05',
'#FFFFFF'
]
},
// 文字的大小
strFontSize: {
type: Number,
default: 24
},
// 奖品文字距离边缘的距离
strMarginOutside: {
type: Number,
default: 0
},
// 奖品图片距离奖品文字的距离
imgMarginStr: {
type: Number,
default: 20
},
// 奖品文字多行情况下的行高
strLineHeight: {
type: Number,
default: 1.2
},
// 奖品文字总长度限制
strMaxLen: {
type: Number,
default: 12
},
// 奖品文字多行情况下第一行文字长度
strLineLen: {
type: Number,
default: 6
},
// 奖品图片的宽
imgWidth: {
type: Number,
default: 70
},
// 奖品图片的高
imgHeight: {
type: Number,
default: 70,
},
// 是否绘制奖品图片
imgDrawed: {
type: Boolean,
default: true
},
// 奖品图片是否裁切为圆形
imgCircled: {
type: Boolean,
default: false
},
// 转盘绘制成功的提示
successMsg: {
type: String,
default: '奖品准备就绪,快来参与抽奖吧'
},
// 转盘绘制失败的提示
failMsg: {
type: String,
default: '奖品仍在准备中,请稍后再来...'
},
// 是否开启画板的缓存
canvasCached: {
type: Boolean,
default: false
}
},
data() {
return {
// 画板className
className: 'almost-lottery__canvas',
// 抽奖转盘的整体px尺寸
lotteryPxSize: 0,
// 画板的px尺寸
canvasImgPxSize: 0,
// 抽奖按钮的px尺寸
actionPxSize: 0,
actionPxSizeHeight:0,
// 奖品文字距离转盘边缘的距离
strMarginPxOutside: 0,
// 奖品图片相对奖品文字的距离
imgMarginPxStr: 0,
// 奖品图片的宽、高
imgPxWidth: 0,
imgPxHeight: 0,
// 画板导出的图片
lotteryImg: '',
// 旋转到奖品目标需要的角度
targetAngle: 0,
targetActionAngle: 0,
// 旋转动画时间 单位 s
transitionDuration: 0,
// 是否正在旋转
isRotate: false,
// 当前停留在那个奖品的序号
stayIndex: 0,
// 当前中奖奖品的序号
targetIndex: 0,
// 是否存在可用的缓存转盘图
isCacheImg: false,
oldLotteryImg: '',
// 解决 app 不支持 measureText 的问题
// app 已在 2.9.3 的版本中提供了对 measureText 的支持,将在后续版本逐渐稳定后移除相关兼容代码
measureText: ''
}
},
computed: {
// 高清尺寸
higtCanvasSize() {
return this.canvasImgPxSize * systemInfo.pixelRatio
},
// 高清字体
higtFontSize() {
return Math.round(this.strFontSize / this.pixelRatio) * systemInfo.pixelRatio
},
// 高清行高
higtHeightMultiple() {
return Math.round(this.strFontSize / this.pixelRatio) * this.strLineHeight * systemInfo.pixelRatio
},
canvasImgToLeftPx() {
return (this.lotteryPxSize - this.canvasImgPxSize) / 2
},
actionBgToLeftPx() {
return (this.lotteryPxSize - this.actionPxSize) / 2
},
// 根据奖品列表计算 canvas 旋转角度
canvasAngle() {
let result = 0
let prizeCount = this.prizeList.length
let prizeClip = 360 / prizeCount
let diffNum = 90 / prizeClip
if (this.pointerPosition === 'edge' || this.rotateType === 'pointer') {
result = -(prizeClip * diffNum)
} else {
result = -(prizeClip * diffNum + prizeClip / 2)
}
return result
},
actionAngle() {
return 0
},
// 外圆的半径
outsideRadius() {
return this.higtCanvasSize / 2
},
// 内圆的半径
insideRadius() {
return 20 * systemInfo.pixelRatio
},
// 文字距离边缘的距离
textRadius() {
return this.strMarginPxOutside * systemInfo.pixelRatio || (this.higtFontSize / 2)
},
// 根据画板的宽度计算奖品文字与中心点的距离
textDistance() {
const textZeroY = Math.round(this.outsideRadius - (this.insideRadius / 2))
return textZeroY - this.textRadius
}
},
watch: {
// 监听获奖序号的变动
prizeIndex(newVal, oldVal) {
if (newVal > -1) {
this.targetIndex = newVal
this.onRotateStart()
} else {
console.info('旋转结束,prizeIndex 已重置')
}
}
},
methods: {
// 开始旋转
onRotateStart() {
if (this.isRotate) return
this.isRotate = true
// 奖品总数
let prizeCount = this.prizeList.length
let baseAngle = 360 / prizeCount
let angles = 0
if (this.rotateType === 'pointer') {
if (this.targetActionAngle === 0) {
// 第一次旋转
angles = (this.targetIndex - this.stayIndex) * baseAngle + baseAngle / 2 - this.actionAngle
} else {
// 后续旋转
// 后续继续旋转 就只需要计算停留的位置与目标位置的角度
angles = (this.targetIndex - this.stayIndex) * baseAngle
}
// 更新目前序号
this.stayIndex = this.targetIndex
// 转 8 圈,圈数越多,转的越快
this.targetActionAngle += angles + 360 * this.ringCount
console.log('targetActionAngle', this.targetActionAngle)
} else {
if (this.targetAngle === 0) {
// 第一次旋转
// 因为第一个奖品是从0°开始的,即水平向右方向
// 第一次旋转角度 = 270度 - (停留的序号-目标序号) * 每个奖品区间角度 - 每个奖品区间角度的一半 - canvas自身旋转的度数
angles = (270 - (this.targetIndex - this.stayIndex) * baseAngle - baseAngle / 2) - this.canvasAngle
} else {
// 后续旋转
// 后续继续旋转 就只需要计算停留的位置与目标位置的角度
angles = -(this.targetIndex - this.stayIndex) * baseAngle
}
// 更新目前序号
this.stayIndex = this.targetIndex
// 转 8 圈,圈数越多,转的越快
this.targetAngle += angles + 360 * this.ringCount
}
// 计算转盘结束的时间,预加一些延迟确保转盘停止后触发结束事件
let endTime = this.transitionDuration * 1000 + 100
let endTimer = setTimeout(() => {
clearTimeout(endTimer)
endTimer = null
this.isRotate = false
this.$emit('draw-end')
}, endTime)
let resetPrizeTimer = setTimeout(() => {
clearTimeout(resetPrizeTimer)
resetPrizeTimer = null
// 每次抽奖结束后都要重置父级组件的 prizeIndex
this.$emit('reset-index')
}, endTime + 50)
},
// 点击 开始抽奖 按钮
handleActionStart() {
if (!this.lotteryImg) return
if (this.isRotate) return
this.$emit('draw-start')
},
// 渲染转盘
async onCreateCanvas() {
// 获取 canvas 画布
const canvasId = this.canvasId
const ctx = uni.createCanvasContext(canvasId, this)
// canvas 的宽高
let canvasW = this.higtCanvasSize
let canvasH = this.higtCanvasSize
// 根据奖品个数计算 角度
let prizeCount = this.prizeList.length
let baseAngle = Math.PI * 2 / prizeCount
// 设置字体
ctx.setFontSize(this.higtFontSize)
// 注意,开始画的位置是从0°角的位置开始画的。也就是水平向右的方向。
// 画具体内容
for (let i = 0; i < prizeCount; i++) {
let prizeItem = this.prizeList[i]
// 当前角度
let angle = i * baseAngle
// 保存当前画布的状态
ctx.save()
// x => 圆弧对应的圆心横坐标 x
// y => 圆弧对应的圆心横坐标 y
// radius => 圆弧的半径大小
// startAngle => 圆弧开始的角度,单位是弧度
// endAngle => 圆弧结束的角度,单位是弧度
// anticlockwise(可选) => 绘制方向,true 为逆时针,false 为顺时针
ctx.beginPath()
// 外圆
ctx.arc(canvasW * 0.5, canvasH * 0.5, this.outsideRadius, angle, angle + baseAngle, false)
// 内圆
ctx.arc(canvasW * 0.5, canvasH * 0.5, this.insideRadius, angle + baseAngle, angle, true)
// 每个奖品区块背景填充颜色
if (this.colors.length === 2) {
ctx.setFillStyle(this.colors[i % 2])
} else {
ctx.setFillStyle(this.colors[i])
}
// 填充颜色
ctx.fill()
// 开启描边
if (this.stroked) {
// 设置描边颜色
ctx.setStrokeStyle(`${this.strokeColor}`)
// 描边
ctx.stroke()
}
// 开始绘制奖品内容
// 重新映射画布上的 (0,0) 位置
let translateX = canvasW * 0.5 + Math.cos(angle + baseAngle / 2) * this.textDistance
let translateY = canvasH * 0.5 + Math.sin(angle + baseAngle / 2) * this.textDistance
ctx.translate(translateX, translateY)
// 绘制奖品名称
let rewardName = this.strLimit(prizeItem.prizeName)
// 设置文字颜色
if (this.strFontColors.length === 1) {
ctx.setFillStyle(this.strFontColors[0])
} else if (this.strFontColors.length === 2) {
ctx.setFillStyle(this.strFontColors[i % 2])
} else {
ctx.setFillStyle(this.strFontColors[i])
}
// rotate方法旋转当前的绘图,因为文字是和当前扇形中心线垂直的
ctx.rotate(angle + (baseAngle / 2) + (Math.PI / 2))
// 设置文本位置并处理换行
if (this.strDirection === 'horizontal') {
// 是否需要换行
if (rewardName && this.prizeNameDrawed) {
let realLen = clacTextLen(rewardName).realLen
let isLineBreak = realLen > this.strLineLen
if (isLineBreak) {
// 获得多行文本数组
let textCount = 0
let tempTxt = ''
let rewardNames = []
for (let j = 0; j < rewardName.length; j++) {
textCount += clacTextLen(rewardName[j]).byteLen
tempTxt += rewardName[j]
if (textCount === (this.strLineLen * 2)) {
rewardNames.push(tempTxt)
textCount = 0
tempTxt = ''
} else {
if ((rewardName.length - 1) === j) {
rewardNames.push(tempTxt)
textCount = 0
tempTxt = ''
}
}
}
// 循环文本数组,计算每一行的文本宽度
for (let j = 0; j < rewardNames.length; j++) {
if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
// 文本的宽度信息
let tempStrSize = ctx.measureText(rewardNames[j])
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
} else {
this.measureText = rewardNames[j]
// 等待页面重新渲染
await this.$nextTick()
let textWidth = await this.getTextWidth()
let tempStrWidth = -(textWidth / 2).toFixed(2)
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
// console.log(rewardNames[j], textWidth, j)
}
}
} else {
if (ctx.measureText && ctx.measureText(rewardName).width > 0) {
// 文本的宽度信息
let tempStrSize = ctx.measureText(rewardName)
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
ctx.fillText(rewardName, tempStrWidth, 0)
} else {
this.measureText = rewardName
// 等待页面重新渲染
await this.$nextTick()
let textWidth = await this.getTextWidth()
let tempStrWidth = -(textWidth / 2).toFixed(2)
ctx.fillText(rewardName, tempStrWidth, 0)
}
}
}
} else {
let rewardNames = rewardName.split('')
for (let j = 0; j < rewardNames.length; j++) {
if (ctx.measureText && ctx.measureText(rewardNames[j]).width > 0) {
// 文本的宽度信息
let tempStrSize = ctx.measureText(rewardNames[j])
let tempStrWidth = -(tempStrSize.width / 2).toFixed(2)
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
} else {
this.measureText = rewardNames[j]
// 等待页面重新渲染
await this.$nextTick()
let textWidth = await this.getTextWidth()
let tempStrWidth = -(textWidth / 2).toFixed(2)
ctx.fillText(rewardNames[j], tempStrWidth, j * this.higtHeightMultiple)
// console.log(rewardNames[j], textWidth, i)
}
}
}
// 绘制奖品图片,文字竖向展示时,不支持图片展示
if (this.imgDrawed && prizeItem.prizeImage && this.strDirection !== 'vertical') {
// App-Android平台 系统 webview 更新到 Chrome84+ 后 canvas 组件绘制本地图像 uni.canvasToTempFilePath 会报错
// 统一将图片处理成 base64
// https://ask.dcloud.net.cn/question/103303
let reg = /^(https|http)/g
// 处理远程图片
if (reg.test(prizeItem.prizeImage)) {
let platformTips = ''
// #ifdef APP-PLUS
platformTips = ''
// #endif
// #ifdef MP
platformTips = '需要处理好下载域名的白名单问题,'
// #endif
// #ifdef H5
platformTips = '需要处理好跨域问题,'
// #endif
console.warn(`###当前数据列表中的奖品图片为网络图片,${platformTips}开始尝试下载图片...###`)
let res = await downloadFile(prizeItem.prizeImage)
console.log('处理远程图片', res)
if (res.ok) {
let tempFilePath = res.tempFilePath
// #ifndef MP
prizeItem.prizeImage = await pathToBase64(tempFilePath)
// #endif
// #ifdef MP
prizeItem.prizeImage = tempFilePath
// #endif
} else {
this.handlePrizeImgSuc({
ok: false,
data: res.data,
msg: res.msg
})
}
} else {
// #ifndef MP
// 不是小程序环境,把本地图片处理成 base64
if (prizeItem.prizeImage.indexOf(';base64,') === -1) {
console.log('开始处理本地图片', prizeItem.prizeImage)
prizeItem.prizeImage = await pathToBase64(prizeItem.prizeImage)
console.log('处理本地图片结束', prizeItem.prizeImage)
}
// #endif
// #ifdef MP-WEIXIN
// 小程序环境,把 base64 处理成小程序的本地临时路径
if (prizeItem.prizeImage.indexOf(';base64,') !== -1) {
console.log('开始处理BASE64图片', prizeItem.prizeImage)
prizeItem.prizeImage = await base64ToPath(prizeItem.prizeImage)
console.log('处理BASE64图片完成', prizeItem.prizeImage)
}
// #endif
}
let prizeImageX = -(this.imgPxWidth * systemInfo.pixelRatio / 2)
let prizeImageY = this.imgMarginPxStr * systemInfo.pixelRatio
let prizeImageW = this.imgPxWidth * systemInfo.pixelRatio
let prizeImageH = this.imgPxHeight * systemInfo.pixelRatio
if (this.imgCircled) {
// 重置圆形背景填充色为透明色
ctx.setFillStyle('rgba(0, 0, 0, 0)')
circleImg(ctx, prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
} else {
ctx.drawImage(prizeItem.prizeImage, prizeImageX, prizeImageY, prizeImageW, prizeImageH)
}
}
ctx.restore()
}
// 保存绘图并导出图片
ctx.draw(true, () => {
let drawTimer = setTimeout(() => {
clearTimeout(drawTimer)
drawTimer = null
// #ifdef MP-ALIPAY
ctx.toTempFilePath({
destWidth: this.higtCanvasSize,
destHeight: this.higtCanvasSize,
success: (res) => {
// console.log(res.apFilePath)
this.handlePrizeImg({
ok: true,
data: res.apFilePath,
msg: '画布导出生成图片成功'
})
},
fail: (err) => {
this.handlePrizeImg({
ok: false,
data: err,
msg: '画布导出生成图片失败'
})
}
})
// #endif
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath({
canvasId: this.canvasId,
success: (res) => {
// 在 H5 平台下,tempFilePath 为 base64
// console.log(res.tempFilePath)
this.handlePrizeImg({
ok: true,
data: res.tempFilePath,
msg: '画布导出生成图片成功'
})
},
fail: (err) => {
this.handlePrizeImg({
ok: false,
data: err,
msg: '画布导出生成图片失败'
})
}
}, this)
// #endif
}, 500)
})
},
// 处理导出的图片
handlePrizeImg(res) {
if (res.ok) {
let data = res.data
if (!this.canvasCached) {
this.lotteryImg = data
this.handlePrizeImgSuc(res)
return
}
// #ifndef H5
if (this.isCacheImg) {
uni.getSavedFileList({
success: (sucRes) => {
let fileList = sucRes.fileList
// console.log('getSavedFileList Cached', fileList)
let cached = false
if (fileList.length) {
for (let i = 0; i < fileList.length; i++) {
let item = fileList[i]
if (item.filePath === data) {
cached = true
this.lotteryImg = data
console.info('经查,本地缓存中存在的转盘图可用,本次将不再绘制转盘')
this.handlePrizeImgSuc(res)
break
}
}
}
if (!cached) {
console.info('经查,本地缓存中存在的转盘图不可用,需要重新初始化转盘绘制')
this.initCanvasDraw()
}
},
fail: (err) => {
this.initCanvasDraw()
}
})
} else {
uni.saveFile({
tempFilePath: data,
success: (sucRes) => {
let filePath = sucRes.savedFilePath
// console.log('saveFile', filePath)
setStore(`${this.canvasId}LotteryImg`, filePath)
this.lotteryImg = filePath
this.handlePrizeImgSuc({
ok: true,
data: filePath,
msg: '画布导出生成图片成功'
})
},
fail: (err) => {
this.handlePrizeImg({
ok: false,
data: err,
msg: '画布导出生成图片失败'
})
}
})
}
// #endif
// #ifdef H5
setStore(`${this.canvasId}LotteryImg`, data)
this.lotteryImg = data
this.handlePrizeImgSuc(res)
// console info
let consoleText = this.isCacheImg ? '缓存' : '导出'
console.info(`当前为 H5 端,使用${consoleText}中的 base64 图`)
// #endif
} else {
console.error(res.msg, res)
// #ifdef H5
console.error('###当前为 H5 端,下载网络图片需要后端配置允许跨域###')
// #endif
// #ifdef MP
console.error('###当前为小程序端,下载网络图片需要配置域名白名单###')
// #endif
}
},
// 处理图片完成
handlePrizeImgSuc(res) {
this.$emit('finish', {
ok: res.ok,
data: res.data,
msg: res.ok ? this.successMsg : this.failMsg
})
},
// 兼容 app 端不支持 ctx.measureText
// 已知问题:初始绘制时,低端安卓机 平均耗时 2s
// hbx 2.8.12+ 已在 app 端支持
getTextWidth() {
console.warn('正在采用兼容方式获取文本的 size 信息,虽然没有任何问题,如果可以,请将此 systemInfo 及 hbx 版本号 反馈给作者', systemInfo)
let query = uni.createSelectorQuery().in(this)
let nodesRef = query.select('.almost-lottery__measureText')
return new Promise((resolve, reject) => {
nodesRef.fields({
size: true,
}, (res) => {
resolve(res.width)
}).exec()
})
},
// 处理文字溢出
strLimit(value) {
let maxLength = this.strMaxLen
if (!value || !maxLength) return value
return clacTextLen(value).realLen > maxLength ? value.slice(0, maxLength - 1) + '..' : value
},
// 检查本地缓存中是否存在转盘图
checkCacheImg() {
console.log('检查本地缓存中是否存在转盘图')
// 检查是否已有缓存的转盘图
// 检查是否与本次奖品数据相同
this.oldLotteryImg = getStore(`${this.canvasId}LotteryImg`)
let oldPrizeList = getStore(`${this.canvasId}PrizeList`)
let newPrizeList = JSON.stringify(this.prizeList)
if (this.oldLotteryImg) {
if (oldPrizeList === newPrizeList) {
console.log(`经查,本地缓存中存在转盘图 => ${this.oldLotteryImg}`)
this.isCacheImg = true
console.log('需要继续判断这张缓存图是否可用')
this.handlePrizeImg({
ok: true,
data: this.oldLotteryImg,
msg: '画布导出生成图片成功'
})
return
}
}
console.log('经查,本地缓存中不存在转盘图')
this.initCanvasDraw()
},
// 初始化绘制
initCanvasDraw() {
console.log('开始初始化转盘绘制')
this.isCacheImg = false
this.lotteryImg = ''
clearStore(`${this.canvasId}LotteryImg`)
setStore(`${this.canvasId}PrizeList`, this.prizeList)
this.onCreateCanvas()
},
// 预处理初始化
async beforeInit() {
let query = uni.createSelectorQuery().in(this)
// 处理 rpx 自适应尺寸
let lotterySize = await new Promise((resolve) => {
query.select('.almost-lottery__wrap').boundingClientRect((rects) => {
resolve(rects)
// console.log('处理 lottery rpx 的自适应', rects)
}).exec()
})
let actionSize = await new Promise((resolve) => {
query.select('.lottery-action').boundingClientRect((rects) => {
console.log(rects)
resolve(rects)
// console.log('处理 action rpx 的自适应', rects)
}).exec()
})
let strMarginSize = await new Promise((resolve) => {
query.select('.str-margin-outside').boundingClientRect((rects) => {
resolve(rects)
// console.log('处理 str-margin-outside rpx 的自适应', rects)
}).exec()
})
let imgMarginStr = await new Promise((resolve) => {
query.select('.img-margin-str').boundingClientRect((rects) => {
resolve(rects)
// console.log('处理 img-margin-str rpx 的自适应', rects)
}).exec()
})
let imgSize = await new Promise((resolve) => {
query.select('.img-size').boundingClientRect((rects) => {
resolve(rects)
// console.log('处理 img-size rpx 的自适应', rects)
}).exec()
})
this.lotteryPxSize = Math.floor(lotterySize.width)
this.canvasImgPxSize = this.lotteryPxSize - Math.floor(actionSize.left) + Math.floor(lotterySize.left)
this.actionPxSize = Math.floor(actionSize.width)
this.actionPxSizeHeight = Math.floor(actionSize.height)
this.strMarginPxOutside = Math.floor(strMarginSize.left) - Math.floor(lotterySize.left)
this.imgMarginPxStr = Math.floor(imgMarginStr.left) - Math.floor(lotterySize.left)
this.imgPxWidth = Math.floor(imgSize.width)
this.imgPxHeight = Math.floor(imgSize.height)
// console.log(this.lotteryPxSize, this.canvasImgPxSize, this.actionPxSize)
let stoTimer = setTimeout(() => {
clearTimeout(stoTimer)
stoTimer = null
// 判断画板是否设置缓存
if (this.canvasCached) {
this.checkCacheImg()
} else {
this.initCanvasDraw()
}
this.transitionDuration = this.duration
}, 50)
}
},
mounted() {
this.$nextTick(() => {
let delay = 50
// 小程序平台需要更多的延时才能获取到准确的元素 Size 信息
// // #ifdef MP
// delay = 300
// // #endif
let stoTimer = setTimeout(() => {
clearTimeout(stoTimer)
stoTimer = null
this.beforeInit()
}, delay)
})
}
}
</script>
<style lang="scss" scoped>
.almost-lottery {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
// 以下元素不可见,是 canvas 的实例
.almost-lottery__canvas {
position: absolute;
left: -9999px;
opacity: 0;
display: flex;
justify-content: center;
align-items: center;
}
// 以下元素不可见,用于获得自适应的值
.lottery-action,
.str-margin-outside,
.img-margin-str,
.img-size {
position: absolute;
left: 0;
top: 0;
z-index: -1;
// background-color: blue;
}
// 以下元素不可见,用于计算文本的宽度
.almost-lottery__measureText {
position: absolute;
left: 0;
top: 0;
white-space: nowrap;
font-size: 12px;
opacity: 0;
}
// 以下为可见内容的样式
.almost-lottery__wrap {
position: relative;
// display: flex;
// justify-content: center;
// align-items: center;
// background-color: #FFFFFF;
}
.almost-lottery__bg,
.almost-lottery__canvas-img,
.almost-lottery__action-bg {
position: absolute;
left: 0;
top: 0;
}
.almost-lottery__canvas-img {
transition: transform cubic-bezier(.34, .12, .05, .95);
}
</style>
{
"id": "almost-lottery",
"displayName": "Almost-Lottery抽奖转盘",
"version": "1.8.24",
"description": "【荣获2021插件大赛三等奖】提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中,完美支持APP、各平台小程序、H5、PC,同时提供 uniCloud 云端版本",
"keywords": [
"转盘",
"抽奖",
"大转盘抽奖"
],
"repository": "https://github.com/ialmost/almost-components_uniapp",
"engines": {
"HBuilderX": "^3.1.22"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "n"
}
}
}
}
}
\ No newline at end of file
# almost-lottery
*使用 Canvas 绘制的抽奖转盘,提供奇数、缓存等众多配置项,更有抽奖概率、抽奖次数、付费抽奖等功能内置于示例项目中*
> <br />
>
> 如果用着还行,请支持一下
> - 前往 [GitHub](https://github.com/ialmost/almost-components_uniapp) 给个 Star
> - 前往 [UniApp](https://ext.dcloud.net.cn/plugin?id=1030) 给个五星
> - 使用中遇到问题时,可以添加 **QQ群 20441313**
>
> <br />
## 基于 uniCloud 开发的云端 Almost-Lottery 抽奖转盘,欢迎尝试体验
- [Almost-Lottery抽奖转盘的云端一体页面](https://ext.dcloud.net.cn/plugin?id=5763)
- [Almost-Lottery抽奖转盘的配置中心](https://ext.dcloud.net.cn/plugin?id=5762)
## 高能预警
- 本插件仅支持 `uni_modules` 模式,强烈推荐使用该模式,**非 `uni_modules` 模式不再维护**
- 在使用本插件之前,强烈建议使用 `HBuilderX` 导入示例项目验证可用性并参照修改
## 功能概要
- [x] 可配置奖品文字 **支持横向/竖向展示**
- [x] 可配置每个奖品区块的背景颜色
- [x] 可配置每个奖品区块的奖品文字颜色
- [x] 可配置奖品区块是否开启描边以及边框的颜色,默认不开启
- [x] 可配置转盘外环和抽奖按钮图
- [x] 可配置每个奖品区块的奖品图片,**当图片是网络地址时,小程序端需要配置白名单,H5端需要允许跨域,奖品文字为竖向时不支持展示奖品图片**
- [x] 奖品列表支持奇数,**奇数时需尽量能被 `360` 除尽**
- [x] 可配置内圈与外圈的间距
- [x] 可配置轮盘旋转或指针旋转
- [x] 可配置画板是否缓存,默认不开启
- [x] 更多配置请查看API说明
## 示例项目附加功能
- [x] 中奖概率,**强烈推荐中奖概率应由后端控制**
- [x] 抽奖次数
- [x] 付费抽奖
## 注意事项
- 编译到小程序端时,请务必勾选ES6转ES5
- `@reset-index="prizeIndex = -1"` 必须默认写入到 `template` 中,不可删除
- 每个奖品区块的奖品图片尺寸不宜过大,图片越大,绘制的过程越慢,尽量将图片尺寸控制在 `100*100` 以内,且图片大小控制在 `40KB` 以内
- 关于中奖概率的配置,请下载示例项目,参照 `pages/index/index.vue` 中的代码进行配置
- 组件本身不涉及任何业务逻辑,与业务相关的代码建议都放在 `pages/index/index.vue`
## 代码演示
#### 基础用法
```
// template
// @reset-index="prizeIndex = -1" 必须默认写入到 template 中,不可删除
<almost-lottery
:prizeList="prizeList"
:prizeIndex="prizeIndex"
@reset-index="prizeIndex = -1"
@draw-start="handleDrawStart"
@draw-end="handleDrawEnd"
@finish="handleDrawFinish"
v-if="prizeList.length"
/>
// script
import AlmostLottery from '@/uni_modules/almost-lottery/components/almost-lottery/almost-lottery.vue'
export default {
components: {
AlmostLottery
},
data () {
return {
// 以下是奖品配置数据
// 奖品数据
prizeList: [],
// 中奖下标
prizeIndex: -1
}
},
methods: {
// 本次抽奖开始
handleDrawStart () {
// 这里需要处理你的中奖逻辑,并得出 prizeIndex
// 请查看示例项目中的代码
},
// 本次抽奖结束
handleDrawEnd () {
// 完成抽奖后,这里处理你拿到结果后的逻辑
// 请查看示例项目中的代码
},
// 抽奖转盘绘制完成
handleDrawFinish (res) {
// 抽奖转盘准备就绪后,这里处理你的逻辑
// 请查看示例项目中的代码
// console.log('抽奖转盘绘制完成', res)
}
}
}
```
## API
#### Props
参数 | 说明 | 类型 | 默认值
:---|:---|:---|:---
pixelRatio | 移动端设计稿的像素比基准值,**涉及到 `rpx` 的适配问题** | *`Number`* | `2`
canvasId | Canvas的标识,**多画板情况下需要配置不同的标识** | *`String`* | `'almostLottery'`
lotterySize | 抽奖转盘的整体尺寸,单位 `rpx` | *`Number`* | `600`
actionSize | 抽奖按钮的尺寸,单位 `rpx` | *`Number`* | `200`
canvasMarginOutside | Canvas边缘距离转盘边缘的距离,单位`rpx` | *`Number`* | `90`
prizeIndex | 获奖奖品在奖品列表中的序号,**每次抽奖结束后会自动重置为 `-1`** | *`Number`* | `-1`
prizeList | 奖品列表,支持奇数(尽量能被 `360` 除尽),**为奇数时需要重设 `colors` 参数** | *`Array`* | -
lotteryBg | 转盘外环图片 | `String` | `默认内置的本地图片`
actionBg | 抽奖按钮图片 | `String` | `默认内置的本地图片`
colors | 奖品区块对应的背景颜色,默认 2 个颜色相互交替,**也可以对每个区块设置不同颜色** | *`Array`* | `['#FFFFFF', '#FFBF05']`
prizeNameDrawed | 是否绘制奖品名称 | *`Boolean`* | `true`
stroked | 是否开启奖品区块描边 | *`Boolean`* | `false`
strDirection | 奖品名称展示方向,可选值 `'horizontal'` => 横向 `'vertical'` => 竖向 | *`String`* | `'horizontal'`
strokeColor | 奖品区块边框颜色 | *`String`* | `'#FFBF05'`
rotateType | 旋转的类型,可选值 `'roulette'` => 轮盘旋转 `'pointer'` => 指针旋转 | *`String`* | `'roulette'`
duration | 转盘旋转的动画时长,单位:秒 | *`Number`* | `8`
ringCount | 旋转的圈数 | *`Number`* | `8`
pointerPosition | 点击抽奖按钮指针的位置,可选值 `'edge'` => 指向边界 `'middle'` => 指向中间 | *`String`* | `'edge'`
strFontColors | 奖品文字颜色,默认 2 个颜色相互交替,**也可以对每个区块的文字设置不同颜色,或仅设置一个颜色** | *`Array`* | `['#FFBF05', '#FFFFFF']`
strFontSize | 奖品名称的字号,单位 `rpx` | *`Number`* | `24`
strLineHeight | 奖品名称多行情况下的行高 | *`Number`* | `1.2`
strMaxLen | 奖品名称长度限制,**文字竖向时不生效** | *`Number`* | `12`
strLineLen | 奖品名称在多行情况下第一行文字的长度,**文字竖向时不生效** | *`Number`* | `6`
strMarginOutside | 奖品文字相对轮盘边缘的距离,单位 `rpx` | *`Number`* | `strFontSize 的一半`
imgMarginStr | 奖品图片相对奖品文字的距离,单位 `rpx` | *`Number`* | `60`
imgWidth | 奖品图片的宽度,单位 `rpx` | *`Number`* | `50`
imgHeight | 奖品图片的高度,单位 `rpx` | *`Number`* | `50`
imgDrawed | 是否绘制奖品图片,默认绘制 | *`Boolean`* | `true`
imgCircled | 奖品图片是否裁切为圆形,默认不裁切 | *`Boolean`* | `false`
successMsg | 转盘绘制成功的提示 | *`String`* | `'奖品准备就绪,快来参与抽奖吧'`
failMsg | 转盘绘制失败的提示 | *`String`* | `'奖品仍在准备中,请稍后再来...'`
canvasCached | 是否开启缓存,避免在数据不变的情况下重复绘制,建议在生产环境中开启 | *`Boolean`* | `false`
#### Events
事件名 | 说明 | 回调参数
:---|:---|:---
@reset-index | 每次抽奖结束后重置获奖的序号为 `-1`**该事件必须默认写入到 `template` 中,不可删除** | -
@draw-start | 转盘旋转开始时触发 | -
@draw-end | 转盘旋转结束时触发 | -
@finish | Canvas转盘绘制完成时触发 | `{ ok: 绘制是否成功, data: 转盘的图片, msg: 绘制结果的提示 }`
#### prizeList 数据结构
*请按如下数据字段对你的奖品列表数据结构进行调整*
键名 | 说明 | 类型
:---|:---|:---
prizeId | 奖品对应 `ID` | *`Number`*
prizeName | 奖品名称 | *`String`*
prizeStock | 奖品库存 | *`Number`*
prizeWeight | 奖品权重 | *`Number`*
prizeImage | 奖品图片地址,网络图片仅支持`http``https`协议 | *`String`*
\ No newline at end of file
/**
* 存储 localStorage 数据
* @param {String} name - 缓存数据的标识
* @param {any} content - 缓存的数据内容
*/
export const setStore = (name, content) => {
if (!name) return
if (typeof content !== 'string') {
content = JSON.stringify(content)
}
uni.setStorageSync(name, content)
}
/**
* 获取 localStorage 数据
* @param {String} name - 缓存数据的标识
*/
export const getStore = (name) => {
if (!name) return
return uni.getStorageSync(name)
}
/**
* 清除 localStorage 数据
* @param {String} name - 缓存数据的标识
*/
export const clearStore = (name) => {
if (name) {
uni.removeStorageSync(name)
} else {
console.log('清理本地全部缓存')
uni.clearStorageSync()
}
}
/**
* 绘制圆形
* @param {String} ctx - 图片网络地址
* @param {String} img - 图片地址
* @param {String} x - x 轴偏移量
* @param {String} y - y 轴偏移量
* @param {String} w - 宽
* @param {String} h - 高
*/
export const circleImg = (ctx, img, x, y, w, h) => {
let r = Math.floor(w/2)
let cx = x + r
let cy = y + r
ctx.save()
ctx.beginPath()
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.fill()
ctx.clip()
ctx.drawImage(img, x, y, w, h)
ctx.restore()
}
/**
* 计算文本的长度
* @param {String} text - 文本内容
*/
export const clacTextLen = (text) => {
if (!text) return { byteLen: 0, realLen: 0 }
text += ''
let clacLen = 0
for (let i = 0; i < text.length; i++) {
if ((text.charCodeAt(i) < 0) || (text.charCodeAt(i) > 255)) {
clacLen += 2
} else {
clacLen += 1
}
}
// console.log(`当前文本 ${text} 的长度为 ${clacLen / 2}`)
return {
byteLen: clacLen,
realLen: clacLen / 2
}
}
/**
* 下载文件,并返回临时路径
* @return {String} 临时路径
* @param {String} fileUrl - 网络地址
*/
export const downloadFile = (fileUrl) => {
return new Promise((resolve) => {
uni.downloadFile({
url: fileUrl,
success: (res) => {
// #ifdef MP-ALIPAY
if (res.errMsg === 'downloadFile:ok') {
resolve({
ok: true,
data: res.errMsg,
tempFilePath: res.tempFilePath
})
} else {
resolve({
ok: false,
data: res.errMsg,
msg: '图片下载失败'
})
}
// #endif
// #ifndef MP-ALIPAY
if (res.statusCode === 200) {
resolve({
ok: true,
data: res.errMsg,
tempFilePath: res.tempFilePath
})
} else {
resolve({
ok: false,
data: res.errMsg,
msg: '图片下载失败'
})
}
// #endif
},
fail: (err) => {
resolve({
ok: false,
data: err.errMsg,
msg: '图片下载失败'
})
}
})
})
}
/**
* 清理应用已缓存的文件
*/
export const clearCacheFile = () => {
// #ifndef H5
uni.getSavedFileList({
success: (res) => {
let fileList = res.fileList
if (fileList.length) {
for (let i = 0; i < fileList.length; i++) {
uni.removeSavedFile({
filePath: fileList[i].filePath,
complete: () => {
console.log('清除缓存已完成')
}
})
}
}
},
fail: (err) => {
console.log('getSavedFileList Fail')
}
})
// #endif
}
// 图像转换工具,可用于图像和base64的转换
// https://ext.dcloud.net.cn/plugin?id=123
const getLocalFilePath = (path) => {
if (
path.indexOf('_www') === 0 ||
path.indexOf('_doc') === 0 ||
path.indexOf('_documents') === 0 ||
path.indexOf('_downloads') === 0
) return path
if (path.indexOf('/storage/emulated/0/') === 0) return path
if (path.indexOf('/storage/sdcard0/') === 0) return path
if (path.indexOf('/var/mobile/') === 0) return path
if (path.indexOf('file://') === 0) return path
if (path.indexOf('/') === 0) {
// ios 无法获取本地路径
let localFilePath = plus.os.name === 'iOS' ? path : plus.io.convertLocalFileSystemURL(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substring(1)
}
}
return '_www/' + path
}
export const pathToBase64 = (path) => {
return new Promise((resolve, reject) => {
if (typeof window === 'object' && 'document' in window) {
if (typeof FileReader === 'function') {
let xhr = new XMLHttpRequest()
xhr.open('GET', path, true)
xhr.responseType = 'blob'
xhr.onload = function() {
if (this.status === 200) {
let fileReader = new FileReader()
fileReader.onload = function(e) {
resolve(e.target.result)
}
fileReader.onerror = reject
fileReader.readAsDataURL(this.response)
}
}
xhr.onerror = reject
xhr.send()
return
}
let canvas = document.createElement('canvas')
let c2x = canvas.getContext('2d')
let img = new Image
img.onload = function() {
canvas.width = img.width
canvas.height = img.height
c2x.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
canvas.height = canvas.width = 0
}
img.onerror = reject
img.src = path
return
}
if (typeof plus === 'object') {
let tempPath = getLocalFilePath(path)
plus.io.resolveLocalFileSystemURL(tempPath, (entry) => {
entry.file((file) => {
let fileReader = new plus.io.FileReader()
fileReader.onload = function(data) {
resolve(data.target.result)
}
fileReader.onerror = function(error) {
console.log(error)
reject(error)
}
fileReader.readAsDataURL(file)
}, (error) => {
reject(error)
})
}, (error) => {
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
wx.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: (res) => {
resolve('data:image/png;base64,' + res.data)
},
fail: (error) => {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
export const base64ToPath = (base64) => {
return new Promise((resolve, reject) => {
if (typeof window === 'object' && 'document' in window) {
base64 = base64.split(',')
let type = base64[0].match(/:(.*?);/)[1]
let str = atob(base64[1])
let n = str.length
let array = new Uint8Array(n)
while (n--) {
array[n] = str.charCodeAt(n)
}
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], {
type: type
})))
}
let extName = base64.match(/data\:\S+\/(\S+);/)
if (extName) {
extName = extName[1]
} else {
reject(new Error('base64 error'))
}
let fileName = Date.now() + '.' + extName
if (typeof plus === 'object') {
let bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
let filePath = '_doc/uniapp_temp/' + fileName
bitmap.save(filePath, {}, () => {
bitmap.clear()
resolve(filePath)
}, (error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
return
}
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
let filePath = wx.env.USER_DATA_PATH + '/' + fileName
wx.getFileSystemManager().writeFile({
filePath: filePath,
data: base64.replace(/^data:\S+\/\S+;base64,/, ''),
encoding: 'base64',
success: () => {
resolve(filePath)
},
fail: (error) => {
reject(error)
}
})
return
}
reject(new Error('not support'))
})
}
## 0.7.0(2021-07-10)
- chore: 统一css3 变量 命名规范
- chore: 更新文档
## 0.6.9(2021-07-09)
- chore: 统一命名规范,无须主动引入组件
- fix: 修复第一次传入图片路径无自适应问题
- fix: 修复安卓方向导致无法正确得到尺寸问题
## 0.6.8(2021-06-26)
- fix: 修复 钉钉小程序 返回图片为 `null` 问题
## 0.6.7(2021-06-19)
- fix: 修复`@touchstart`书写错误
## 0.6.6(2021-06-19)
- fix: 修复支付宝引用图标的错误
## 0.6.5(2021-06-14)
- fix: 修复锁定比例失效问题
## 0.6.4(2021-04-16)
- 修复因mode引起的高度异常
## 0.6.3(2021-04-13)
- 测试到微信小程序无法进第二次`draw`,故更新使用`v-model`的方式,显示隐藏组件。
- 原先使用v-if显示隐藏组件依然可以使用,若发现无法二次`draw`请使用`v-model`
```html
<l-clipper v-model="show" />
```
```js
data: () => ({
show: false
})
```
## 0.6.2(2021-03-26)
1、修复图片初始化BUG
2、增加`fileType`属性,默认值为`png`
## 0.6.1(2021-03-10)
- 给事件添加阻止冒泡
## 0.6.0(2021-03-06)
增加`source`属性,`Object`类型,key为图片来源类型,value为选项说明。<br>
```js
// source 默认值
{
album: '从相册中选择',
camera: '拍照',
// #ifdef MP-WEIXIN
message: '从微信中选择'
// #endif
}
```
## 0.5.0(2021-02-26)
- 调整为uni_modules目录规范
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:#606060;}
.st1{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;}
</style>
<g>
<path class="st2" d="M11.6,11c0.4,0.4,0.6,0.9,0.6,1.5c0,0.6-0.2,1.1-0.6,1.4c-0.4,0.4-0.9,0.6-1.5,0.6c-0.6,0-1.1-0.2-1.5-0.6
c-0.4-0.4-0.6-0.9-0.6-1.4s0.2-1.1,0.6-1.5c0.4-0.4,0.9-0.6,1.5-0.6C10.8,10.4,11.2,10.6,11.6,11z M24.6,18.4V6.7H5.4v12l1.8-1.8
c0.3-0.3,0.6-0.4,1-0.4c0.4,0,0.7,0.1,1,0.4l1.8,1.8l5.8-7c0.3-0.3,0.6-0.5,1.1-0.5c0.4,0,0.8,0.2,1.1,0.5
C18.8,11.6,24.6,18.4,24.6,18.4z M25.6,5.7C25.9,6,26,6.3,26,6.7v16.1c0,0.4-0.1,0.7-0.4,1c-0.3,0.3-0.6,0.4-1,0.4H5.4
c-0.4,0-0.7-0.1-1-0.4c-0.3-0.3-0.4-0.6-0.4-1V6.7c0-0.4,0.1-0.7,0.4-1c0.3-0.3,0.6-0.4,1-0.4h19.3C25,5.3,25.3,5.4,25.6,5.7z"/>
<path class="st1" d="M24.3,21.5H5.7c-0.2,0-0.3-0.2-0.3-0.3V7c0-0.2,0.2-0.3,0.3-0.3h18.6c0.2,0,0.3,0.2,0.3,0.3v14.2
C24.6,21.3,24.5,21.5,24.3,21.5z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
.st1{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
C17.5,24.1,17.3,24.2,17.1,24.2z"/>
<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 "/>
</g>
</svg>
$clipper-edge-border-width ?= 6rpx
$clipper-confirm-color ?= #07c160
.flex-auto
flex auto
.bg-transparent
background-color rgba(0, 0, 0, 0.9)
transition-duration 0.35s
.lime-clipper
width 100vw
height calc( 100vh - var(--window-top))
background-color rgba(0, 0, 0, 0.9)
position fixed
top var(--window-top)
left 300vw
z-index 1
&.open
left 0
&-mask
position relative
z-index 2
pointer-events: none
&__content
pointer-events: none;
position absolute
border 1rpx solid rgba(255,255,255,.3)
box-sizing border-box
box-shadow: rgba(0, 0, 0, 0.5) 0 0 0 80vh;
background: transparent;
// transition-duration 0.35s
// transition-property left,top
&::before,&::after
content ''
position absolute
border 1rpx dashed rgba(255,255,255,.3)
&::before
width 100%
top 33.33%
height 33.33%
border-left none
border-right none
&::after
width 33.33%
left 33.33%
height 100%
border-top none
border-bottom none
&__edge
position absolute
// left 6rpx
width 34rpx
height 34rpx
border $clipper-edge-border-width solid #ffffff
pointer-events auto
&::before
content ''
position absolute
width 40rpx
height 40rpx
background-color transparent
&:nth-child(1)
left: - $clipper-edge-border-width
top: - $clipper-edge-border-width
border-bottom-width 0 !important
border-right-width 0 !important
&:before
top -50%
left -50%
&:nth-child(2)
right: - $clipper-edge-border-width
top: - $clipper-edge-border-width
border-bottom-width 0 !important
border-left-width 0 !important
&:before
top -50%
left 50%
&:nth-child(3)
left: - $clipper-edge-border-width
bottom: - $clipper-edge-border-width
border-top-width 0 !important
border-right-width 0 !important
&:before
bottom -50%
left -50%
&:nth-child(4)
right: - $clipper-edge-border-width
bottom: - $clipper-edge-border-width
border-top-width 0 !important
border-left-width 0 !important
&:before
bottom -50%
left 50%
&-image
width 100%
max-width inherit
border-style none
position absolute
top 0
left 0
z-index 1
-webkit-backface-visibility hidden
backface-visibility hidden
transform-origin center
&-canvas
position fixed
z-index 10
left -200vw
top -200vw
pointer-events none
&-tools
position absolute
left 0
bottom 10px
width 100%
z-index 99
color #fff
&__btns
font-weight bold
display flex
align-items center
justify-content space-between
width 100%
padding 20rpx 40rpx
box-sizing border-box
.cancel
width 112rpx
height 60rpx
text-align center
line-height 60rpx
.confirm
width 112rpx
height 60rpx
line-height 60rpx
background var(--lime-clipper-confirm-color, $clipper-confirm-color)
border-radius 6rpx
text-align center
image
display block
width 60rpx
height 60rpx
<template>
<view class="lime-clipper" :class="{open: value}" disable-scroll :style="'z-index: ' + zIndex + ';' + customStyle">
<view class="lime-clipper-mask" @touchstart.stop.prevent="clipTouchStart"
@touchmove.stop.prevent="clipTouchMove" @touchend.stop.prevent="clipTouchEnd">
<view class="lime-clipper__content" :style="clipStyle">
<view class="lime-clipper__edge" v-for="(item, index) in [0, 0, 0, 0]" :key="index"></view>
</view>
</view>
<image class="lime-clipper-image" @error="imageLoad" @load="imageLoad" @touchstart="imageTouchStart"
@touchmove="imageTouchMove" @touchend="imageTouchEnd" :src="image"
:mode="imageWidth == 'auto' ? 'widthFix' : 'scaleToFill'" v-if="image" :style="imageStyle" />
<canvas :canvas-id="canvasId" id="lime-clipper" disable-scroll
:style="'width: ' + canvasWidth * scaleRatio + 'px; height:' + canvasHeight * scaleRatio + 'px;'"
class="lime-clipper-canvas"></canvas>
<view class="lime-clipper-tools">
<view class="lime-clipper-tools__btns">
<view v-if="isShowCancelBtn" @tap="cancel">
<slot name="cancel" v-if="$slots.cancel" />
<view v-else class="cancel">取消</view>
</view>
<view v-if="isShowPhotoBtn" @tap="uploadImage">
<slot name="photo" v-if="$slots.photo" />
<image v-else :src="photoImg" />
</view>
<view v-if="isShowRotateBtn" @tap="rotate">
<slot name="rotate" v-if="$slots.rotate" />
<image v-else :src="rotateImg" data-type="inverse" />
</view>
<view v-if="isShowConfirmBtn" @tap="confirm">
<slot name="confirm" v-if="$slots.confirm" />
<view v-else class="confirm">确定</view>
</view>
</view>
<slot></slot>
</view>
</view>
</template>
<script>
import rotateImg from './images/rotate.svg'
import photoImg from './images/photo.svg'
import {
pathToBase64,
determineDirection,
calcImageOffset,
calcImageScale,
calcImageSize,
calcPythagoreanTheorem,
clipTouchMoveOfCalculate,
imageTouchMoveOfCalcOffset
} from './utils';
const cache = {}
export default {
name: 'lime-clipper',
props: {
value: {
type: Boolean,
default: true
},
// #ifdef MP-WEIXIN
type: {
type: String,
default: '2d'
},
// #endif
customStyle: {
type: String,
},
canvasId: {
type: String,
default: 'lime-clipper'
},
zIndex: {
type: Number,
default: 99
},
imageUrl: {
type: String | null
},
fileType: {
type: String,
default: 'png'
},
quality: {
type: Number,
default: 1
},
width: {
type: Number,
default: 400
},
height: {
type: Number,
default: 400
},
minWidth: {
type: Number,
default: 200
},
maxWidth: {
type: Number,
default: 600
},
destWidth: Number,
destHeight: Number,
minHeight: {
type: Number,
default: 200
},
maxHeight: {
type: Number,
default: 600
},
isLockWidth: {
type: Boolean,
default: false
},
isLockHeight: {
type: Boolean,
default: false
},
isLockRatio: {
type: Boolean,
default: true
},
scaleRatio: {
type: Number,
default: 1
},
minRatio: {
type: Number,
default: 0.5
},
maxRatio: {
type: Number,
default: 2
},
isDisableScale: {
type: Boolean,
default: false
},
isDisableRotate: {
type: Boolean,
default: false
},
isLimitMove: {
type: Boolean,
default: false
},
isShowPhotoBtn: {
type: Boolean,
default: true
},
isShowRotateBtn: {
type: Boolean,
default: true
},
isShowConfirmBtn: {
type: Boolean,
default: true
},
isShowCancelBtn: {
type: Boolean,
default: true
},
rotateAngle: {
type: Number,
default: 90
},
source: {
type: Object,
default: () => ({
album: '从相册中选择',
camera: '拍照',
// #ifdef MP-WEIXIN
message: '从微信中选择'
// #endif
})
}
},
data() {
return {
rotateImg,
photoImg,
canvasWidth: 0,
canvasHeight: 0,
clipX: 0,
clipY: 0,
clipWidth: 0,
clipHeight: 0,
animation: false,
imageWidth: 0,
imageHeight: 0,
imageTop: 0,
imageLeft: 0,
scale: 1,
angle: 0,
image: '',
sysinfo: {},
throttleTimer: null,
throttleFlag: true,
timeClipCenter: null,
flagClipTouch: false,
flagEndTouch: false,
clipStart: {},
animationTimer: null,
touchRelative: [{
x: 0,
y: 0
}],
hypotenuseLength: 0,
ctx: null
};
},
computed: {
clipStyle() {
const {
clipWidth,
clipHeight,
clipY,
clipX,
animation
} = this
return `
width: ${clipWidth}px;
height:${clipHeight}px;
transition-property: ${animation ? '' : 'background'};
left: ${clipX}px;
top: ${clipY}px
`
},
imageStyle() {
const {
imageWidth,
imageHeight,
imageLeft,
imageTop,
animation,
scale,
angle
} = this
return `
width: ${imageWidth ? imageWidth + 'px' : 'auto'};
height: ${imageHeight ? imageHeight + 'px' : 'auto'};
transform: translate3d(${imageLeft - imageWidth / 2}px, ${imageTop - imageHeight / 2}px, 0) scale(${scale}) rotate(${angle}deg);
transition-duration: ${animation ? 0.35 : 0}s
`
},
clipSize() {
const {
clipWidth,
clipHeight
} = this;
return {
clipWidth,
clipHeight
};
},
clipPoint() {
const {
clipY,
clipX
} = this;
return {
clipY,
clipX
};
}
},
watch: {
value(val) {
if (!val) {
this.animation = 0
this.angle = 0
} else {
if (this.imageUrl) {
const {
imageWidth,
imageHeight,
imageLeft,
imageTop,
scale,
clipX,
clipY,
clipWidth,
clipHeight,
path
} = cache?. [this.imageUrl] || {}
if (path != this.image) {
this.image = this.imageUrl;
} else {
this.setDiffData({
imageWidth,
imageHeight,
imageLeft,
imageTop,
scale,
clipX,
clipY,
clipWidth,
clipHeight
})
}
}
}
},
imageUrl(url) {
this.image = url
},
image: {
handler: async function(url) {
this.getImageInfo(url)
},
// immediate: true,
},
clipSize({
widthVal,
heightVal
}) {
let {
minWidth,
minHeight
} = this;
minWidth = minWidth / 2;
minHeight = minHeight / 2;
if (widthVal < minWidth) {
this.setDiffData({
clipWidth: minWidth
})
}
if (heightVal < minHeight) {
this.setDiffData({
clipHeight: minHeight
})
}
this.calcClipSize();
},
angle(val) {
this.animation = true;
this.moveStop();
const {
isLimitMove
} = this;
if (isLimitMove && val % 90) {
this.setDiffData({
angle: Math.round(val / 90) * 90
})
}
this.imgMarginDetectionScale();
},
animation(val) {
clearTimeout(this.animationTimer);
if (val) {
let animationTimer = setTimeout(() => {
this.setDiffData({
animation: false
})
}, 260);
this.setDiffData({
animationTimer
})
this.animationTimer = animationTimer;
}
},
isLimitMove(val) {
if (val) {
if (this.angle % 90) {
this.setDiffData({
angle: Math.round(this.angle / 90) * 90
})
}
this.imgMarginDetectionScale();
}
},
clipPoint() {
this.cutDetectionPosition();
},
width(width, oWidth) {
if (width !== oWidth) {
this.setDiffData({
clipWidth: width / 2
})
}
},
height(height, oHeight) {
if (height !== oHeight) {
this.setDiffData({
clipHeight: height / 2
})
}
}
},
mounted() {
const sysinfo = uni.getSystemInfoSync();
this.sysinfo = sysinfo;
this.setClipInfo();
this.image = this.imageUrl || this.image
this.setClipCenter();
this.calcClipSize();
this.cutDetectionPosition();
},
methods: {
setDiffData(data) {
Object.keys(data).forEach(key => {
if (this[key] !== data[key]) {
this[key] = data[key];
}
});
},
getImageInfo(url) {
if (!url) return;
if (this.value) {
uni.showLoading({
title: '请稍候...',
mask: true
});
}
uni.getImageInfo({
src: url,
success: res => {
if (['right', 'left'].includes(res.orientation)) {
this.imgComputeSize(res.height, res.width);
} else {
this.imgComputeSize(res.width, res.height);
}
this.image = res.path;
if (this.isLimitMove) {
this.imgMarginDetectionScale();
this.$emit('ready', res);
}
const {
imageWidth,
imageHeight,
imageLeft,
imageTop,
scale,
clipX,
clipY,
clipWidth,
clipHeight
} = this
cache[url] = Object.assign(res, {
imageWidth,
imageHeight,
imageLeft,
imageTop,
scale,
clipX,
clipY,
clipWidth,
clipHeight
});
},
fail: (err) => {
this.imgComputeSize();
if (this.isLimitMove) {
this.imgMarginDetectionScale();
}
}
});
},
setClipInfo() {
const {
width,
height,
sysinfo,
canvasId
} = this;
const clipWidth = width / 2;
const clipHeight = height / 2;
const clipY = (sysinfo.windowHeight - clipHeight) / 2;
const clipX = (sysinfo.windowWidth - clipWidth) / 2;
const imageLeft = sysinfo.windowWidth / 2;
const imageTop = sysinfo.windowHeight / 2;
this.ctx = uni.createCanvasContext(canvasId, this);
this.clipWidth = clipWidth;
this.clipHeight = clipHeight;
this.clipX = clipX;
this.clipY = clipY;
this.canvasHeight = clipHeight;
this.canvasWidth = clipWidth;
this.imageLeft = imageLeft;
this.imageTop = imageTop;
},
setClipCenter() {
const {
sysInfo,
clipHeight,
clipWidth,
imageTop,
imageLeft
} = this;
let sys = sysInfo || uni.getSystemInfoSync();
let clipY = (sys.windowHeight - clipHeight) * 0.5;
let clipX = (sys.windowWidth - clipWidth) * 0.5;
this.imageTop = imageTop - this.clipY + clipY;
this.imageLeft = imageLeft - this.clipX + clipX;
this.clipY = clipY;
this.clipX = clipX;
},
calcClipSize() {
const {
clipHeight,
clipWidth,
sysinfo,
clipX,
clipY
} = this;
if (clipWidth > sysinfo.windowWidth) {
this.setDiffData({
clipWidth: sysinfo.windowWidth
})
} else if (clipWidth + clipX > sysinfo.windowWidth) {
this.setDiffData({
clipX: sysinfo.windowWidth - clipX
})
}
if (clipHeight > sysinfo.windowHeight) {
this.setDiffData({
clipHeight: sysinfo.windowHeight
})
} else if (clipHeight + clipY > sysinfo.windowHeight) {
this.clipY = sysinfo.windowHeight - clipY;
this.setDiffData({
clipY: sysinfo.windowHeight - clipY
})
}
},
cutDetectionPosition() {
const {
clipX,
clipY,
sysinfo,
clipHeight,
clipWidth
} = this;
let cutDetectionPositionTop = () => {
if (clipY < 0) {
this.setDiffData({
clipY: 0
})
}
if (clipY > sysinfo.windowHeight - clipHeight) {
this.setDiffData({
clipY: sysinfo.windowHeight - clipHeight
})
}
},
cutDetectionPositionLeft = () => {
if (clipX < 0) {
this.setDiffData({
clipX: 0
})
}
if (clipX > sysinfo.windowWidth - clipWidth) {
this.setDiffData({
clipX: sysinfo.windowWidth - clipWidth
})
}
};
if (clipY === null && clipX === null) {
let newClipY = (sysinfo.windowHeight - clipHeight) * 0.5;
let newClipX = (sysinfo.windowWidth - clipWidth) * 0.5;
this.setDiffData({
clipX: newClipX,
clipY: newClipY
})
} else if (clipY !== null && clipX !== null) {
cutDetectionPositionTop();
cutDetectionPositionLeft();
} else if (clipY !== null && clipX === null) {
cutDetectionPositionTop();
this.setDiffData({
clipX: (sysinfo.windowWidth - clipWidth) / 2
})
} else if (clipY === null && clipX !== null) {
cutDetectionPositionLeft();
this.setDiffData({
clipY: (sysinfo.windowHeight - clipHeight) / 2
})
}
},
imgComputeSize(width, height) {
const {
imageWidth,
imageHeight
} = calcImageSize(width, height, this);
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
},
imgMarginDetectionScale(scale) {
if (!this.isLimitMove) return;
const currentScale = calcImageScale(this, scale);
this.imgMarginDetectionPosition(currentScale);
},
imgMarginDetectionPosition(scale) {
if (!this.isLimitMove) return;
const {
scale: currentScale,
left,
top
} = calcImageOffset(this, scale);
this.setDiffData({
imageLeft: left,
imageTop: top,
scale: currentScale
})
},
throttle() {
this.setDiffData({
throttleFlag: true
})
},
moveDuring() {
clearTimeout(this.timeClipCenter);
},
moveStop() {
clearTimeout(this.timeClipCenter);
const timeClipCenter = setTimeout(() => {
if (!this.animation) {
this.setDiffData({
animation: true
})
}
this.setClipCenter();
}, 800);
this.setDiffData({
timeClipCenter
})
},
clipTouchStart(event) {
// #ifdef H5
event.preventDefault()
// #endif
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none'
});
return;
}
const currentX = event.touches[0].clientX;
const currentY = event.touches[0].clientY;
const {
clipX,
clipY,
clipWidth,
clipHeight
} = this;
const corner = determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY);
this.moveDuring();
if (!corner) {
return
}
this.clipStart = {
width: clipWidth,
height: clipHeight,
x: currentX,
y: currentY,
clipY,
clipX,
corner
};
this.flagClipTouch = true;
this.flagEndTouch = true;
},
clipTouchMove(event) {
// #ifdef H5
event.stopPropagation()
event.preventDefault()
// #endif
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none'
});
return;
}
// 只针对单指点击做处理
if (event.touches.length !== 1) {
return;
}
const {
flagClipTouch,
throttleFlag
} = this;
if (flagClipTouch && throttleFlag) {
const {
isLockRatio,
isLockHeight,
isLockWidth
} = this;
if (isLockRatio && (isLockWidth || isLockHeight)) return;
this.setDiffData({
throttleFlag: false
})
this.throttle();
const clipData = clipTouchMoveOfCalculate(this, event);
if (clipData) {
const {
width,
height,
clipX,
clipY
} = clipData;
if (!isLockWidth && !isLockHeight) {
this.setDiffData({
clipWidth: width,
clipHeight: height,
clipX,
clipY
})
} else if (!isLockWidth) {
this.setDiffData({
clipWidth: width,
clipX
})
} else if (!isLockHeight) {
this.setDiffData({
clipHeight: height,
clipY
})
}
this.imgMarginDetectionScale();
}
}
},
clipTouchEnd() {
this.moveStop();
this.flagClipTouch = false;
},
imageTouchStart(e) {
// #ifdef H5
event.preventDefault()
// #endif
this.flagEndTouch = false;
const {
imageLeft,
imageTop
} = this;
const clientXForLeft = e.touches[0].clientX;
const clientYForLeft = e.touches[0].clientY;
let touchRelative = [];
if (e.touches.length === 1) {
touchRelative[0] = {
x: clientXForLeft - imageLeft,
y: clientYForLeft - imageTop
};
this.touchRelative = touchRelative;
} else {
const clientXForRight = e.touches[1].clientX;
const clientYForRight = e.touches[1].clientY;
let width = Math.abs(clientXForLeft - clientXForRight);
let height = Math.abs(clientYForLeft - clientYForRight);
const hypotenuseLength = calcPythagoreanTheorem(width, height);
touchRelative = [{
x: clientXForLeft - imageLeft,
y: clientYForLeft - imageTop
},
{
x: clientXForRight - imageLeft,
y: clientYForRight - imageTop
}
];
this.touchRelative = touchRelative;
this.hypotenuseLength = hypotenuseLength;
}
},
imageTouchMove(e) {
// #ifdef H5
event.preventDefault()
// #endif
const {
flagEndTouch,
throttleFlag
} = this;
if (flagEndTouch || !throttleFlag) return;
const clientXForLeft = e.touches[0].clientX;
const clientYForLeft = e.touches[0].clientY;
this.setDiffData({
throttleFlag: false
})
this.throttle();
this.moveDuring();
if (e.touches.length === 1) {
const {
left: imageLeft,
top: imageTop
} = imageTouchMoveOfCalcOffset(this, clientXForLeft, clientYForLeft);
this.setDiffData({
imageLeft,
imageTop
})
this.imgMarginDetectionPosition();
} else {
const clientXForRight = e.touches[1].clientX;
const clientYForRight = e.touches[1].clientY;
let width = Math.abs(clientXForLeft - clientXForRight),
height = Math.abs(clientYForLeft - clientYForRight),
hypotenuse = calcPythagoreanTheorem(width, height),
scale = this.scale * (hypotenuse / this.hypotenuseLength);
if (this.isDisableScale) {
scale = 1;
} else {
scale = scale <= this.minRatio ? this.minRatio : scale;
scale = scale >= this.maxRatio ? this.maxRatio : scale;
this.$emit('change', {
width: this.imageWidth * scale,
height: this.imageHeight * scale
});
}
this.imgMarginDetectionScale(scale);
this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
this.scale = scale;
}
},
imageTouchEnd() {
this.setDiffData({
flagEndTouch: true
})
this.moveStop();
},
uploadImage() {
const itemList = Object.entries(this.source)
const sizeType = ['original', 'compressed']
const success = ({
tempFilePaths: a,
tempFiles: b
}) => {
this.image = a ? a[0] : b[0].path
};
const _uploadImage = (type) => {
if (type !== 'message') {
uni.chooseImage({
count: 1,
sizeType,
sourceType: [type],
success
});
}
// #ifdef MP-WEIXIN
if (type == 'message') {
wx.chooseMessageFile({
count: 1,
type: 'image',
success
})
}
// #endif
}
if (itemList.length > 1) {
uni.showActionSheet({
itemList: itemList.map(v => v[1]),
success: ({
tapIndex: i
}) => {
_uploadImage(itemList[i][0])
}
})
} else {
_uploadImage(itemList[0][0])
}
},
imageReset() {
const sys = this.sysinfo || uni.getSystemInfoSync();
this.scale = 1;
this.angle = 0;
this.imageTop = sys.windowHeight / 2;
this.imageLeft = sys.windowWidth / 2;
},
imageLoad(e) {
this.imageReset();
uni.hideLoading();
this.$emit('ready', e.detail);
},
rotate(event) {
if (this.isDisableRotate) return;
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none'
});
return;
}
const {
rotateAngle
} = this;
const originAngle = this.angle
const type = event.currentTarget.dataset.type;
if (type === 'along') {
this.angle = originAngle + rotateAngle
} else {
this.angle = originAngle - rotateAngle
}
this.$emit('rotate', this.angle);
},
confirm() {
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none'
});
return;
}
uni.showLoading({
title: '加载中'
});
const {
canvasHeight,
canvasWidth,
clipHeight,
clipWidth,
ctx,
scale,
imageLeft,
imageTop,
clipX,
clipY,
angle,
scaleRatio: dpr,
image,
quality,
fileType,
type: imageType,
canvasId
} = this;
const draw = () => {
const imageWidth = this.imageWidth * scale * dpr;
const imageHeight = this.imageHeight * scale * dpr;
const xpos = imageLeft - clipX;
const ypos = imageTop - clipY;
ctx.translate(xpos * dpr, ypos * dpr);
ctx.rotate((angle * Math.PI) / 180);
ctx.drawImage(image, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight);
ctx.draw(false, () => {
const width = clipWidth * dpr
const height = clipHeight * dpr
let params = {
x: 0,
y: 0,
width,
height,
destWidth: this.destWidth || width,
destHeight: this.destHeight || height,
canvasId: canvasId,
fileType,
quality,
success: (res) => {
// 钉钉小程序
data.url = res.tempFilePath || res.filePath;
uni.hideLoading();
this.$emit('success', data);
this.$emit('input', false)
},
fail: (error) => {
console.error('error', error)
this.$emit('fail', error);
this.$emit('input', false)
}
};
let data = {
url: '',
width,
height
};
uni.canvasToTempFilePath(params, this)
});
};
if (canvasWidth !== clipWidth || canvasHeight !== clipHeight) {
this.canvasWidth = clipWidth;
this.canvasHeight = clipHeight;
ctx.draw();
this.$nextTick(() => {
setTimeout(() => {
draw();
}, 100);
})
} else {
draw();
}
},
cancel() {
this.$emit('cancel', false)
this.$emit('input', false)
},
}
};
</script>
<style lang="stylus" scoped>
@import './index'
</style>
/**
* 判断手指触摸位置
*/
export function determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY) {
/*
* (右下>>1 右上>>2 左上>>3 左下>>4)
*/
let corner;
/**
* 思路:(利用直角坐标系)
* 1.找出裁剪框中心点
* 2.如点击坐标在上方点与左方点区域内,则点击为左上角
* 3.如点击坐标在下方点与右方点区域内,则点击为右下角
* 4.其他角同理
*/
const mainPoint = [clipX + clipWidth / 2, clipY + clipHeight / 2]; // 中心点
const currentPoint = [currentX, currentY]; // 触摸点
if (currentPoint[0] <= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
corner = 3; // 左上
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
corner = 2; // 右上
} else if (currentPoint[0] <= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
corner = 4; // 左下
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
corner = 1; // 右下
}
return corner;
}
/**
* 图片边缘检测检测时,计算图片偏移量
*/
export function calcImageOffset(data, scale) {
let left = data.imageLeft;
let top = data.imageTop;
scale = scale || data.scale;
let imageWidth = data.imageWidth;
let imageHeight = data.imageHeight;
if ((data.angle / 90) % 2) {
imageWidth = data.imageHeight;
imageHeight = data.imageWidth;
}
const {
clipX,
clipWidth,
clipY,
clipHeight
} = data;
// 当前图片宽度/高度
const currentImageSize = (size) => (size * scale) / 2;
const currentImageWidth = currentImageSize(imageWidth);
const currentImageHeight = currentImageSize(imageHeight);
left = clipX + currentImageWidth >= left ? left : clipX + currentImageWidth;
left = clipX + clipWidth - currentImageWidth <= left ? left : clipX + clipWidth - currentImageWidth;
top = clipY + currentImageHeight >= top ? top : clipY + currentImageHeight;
top = clipY + clipHeight - currentImageHeight <= top ? top : clipY + clipHeight - currentImageHeight;
return {
left,
top,
scale
};
}
/**
* 图片边缘检测时,计算图片缩放比例
*/
export function calcImageScale(data, scale) {
scale = scale || data.scale;
let {
imageWidth,
imageHeight,
clipWidth,
clipHeight,
angle
} = data
if ((angle / 90) % 2) {
imageWidth = imageHeight;
imageHeight = imageWidth;
}
if (imageWidth * scale < clipWidth) {
scale = clipWidth / imageWidth;
}
if (imageHeight * scale < clipHeight) {
scale = Math.max(scale, clipHeight / imageHeight);
}
return scale;
}
/**
* 计算图片尺寸
*/
export function calcImageSize(width, height, data) {
let imageWidth = width,
imageHeight = height;
let {
clipWidth,
clipHeight,
sysinfo,
width: originWidth,
height: originHeight
} = data
if (imageWidth && imageHeight) {
if (imageWidth / imageHeight > (clipWidth || originWidth) / (clipWidth || originHeight)) {
imageHeight = clipHeight || originHeight;
imageWidth = (width / height) * imageHeight;
} else {
imageWidth = clipWidth || originWidth;
imageHeight = (height / width) * imageWidth;
}
} else {
let sys = sysinfo || uni.getSystemInfoSync();
imageWidth = sys.windowWidth;
imageHeight = 0;
}
return {
imageWidth,
imageHeight
};
}
/**
* 勾股定理求斜边
*/
export function calcPythagoreanTheorem(width, height) {
return Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
}
/**
* 拖动裁剪框时计算
*/
export function clipTouchMoveOfCalculate(data, event) {
const clientX = event.touches[0].clientX;
const clientY = event.touches[0].clientY;
let {
clipWidth,
clipHeight,
clipY: oldClipY,
clipX: oldClipX,
clipStart,
isLockRatio,
maxWidth,
minWidth,
maxHeight,
minHeight
} = data;
maxWidth = maxWidth / 2;
minWidth = minWidth / 2;
minHeight = minHeight / 2;
maxHeight = maxHeight / 2;
let width = clipWidth,
height = clipHeight,
clipY = oldClipY,
clipX = oldClipX,
// 获取裁剪框实际宽度/高度
// 如果大于最大值则使用最大值
// 如果小于最小值则使用最小值
sizecorrect = () => {
width = width <= maxWidth ? (width >= minWidth ? width : minWidth) : maxWidth;
height = height <= maxHeight ? (height >= minHeight ? height : minHeight) : maxHeight;
},
sizeinspect = () => {
if ((width > maxWidth || width < minWidth || height > maxHeight || height < minHeight) && isLockRatio) {
sizecorrect();
return false;
} else {
sizecorrect();
return true;
}
};
//if (clipStart.corner) {
height = clipStart.height + (clipStart.corner > 1 && clipStart.corner < 4 ? 1 : -1) * (clipStart.y - clientY);
//}
switch (clipStart.corner) {
case 1:
width = clipStart.width - clipStart.x + clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) return;
break;
case 2:
width = clipStart.width - clipStart.x + clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipY = clipStart.clipY - (height - clipStart.height);
}
break;
case 3:
width = clipStart.width + clipStart.x - clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipY = clipStart.clipY - (height - clipStart.height);
clipX = clipStart.clipX - (width - clipStart.width);
}
break;
case 4:
width = clipStart.width + clipStart.x - clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipX = clipStart.clipX - (width - clipStart.width);
}
break;
default:
break;
}
return {
width,
height,
clipX,
clipY
};
}
/**
* 单指拖动图片计算偏移
*/
export function imageTouchMoveOfCalcOffset(data, clientXForLeft, clientYForLeft) {
let left = clientXForLeft - data.touchRelative[0].x,
top = clientYForLeft - data.touchRelative[0].y;
return {
left,
top
};
}
<template>
</template>
<script>
</script>
<style>
</style>
{
"id": "lime-clipper",
"displayName": "图片剪刀",
"version": "0.7.0",
"description": "一款自我感觉良好的图片裁剪插件",
"keywords": [
"图片裁剪",
"缩放",
"旋转",
"拖动"
],
"repository": "https://gitee.com/liangei/lime-clipper",
"engines": {
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "305716444"
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment