@@ -0,0 +1,15 @@
+.DS_Store
+
+unpackage/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+# Editor directories and files
+.hbuilderx
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
@@ -0,0 +1,42 @@
+<script>
+ export default {
+ globalData: {
+ statusBarHeight: 0, // 状态导航栏高度
+ navHeight: 0, // 总体高度
+ navigationBarHeight: 0, // 导航栏高度(标题栏高度)
+ },
+ onLaunch: function() {
+ // console.log('App Launch')
+ // 状态栏高度
+ this.globalData.statusBarHeight = uni.getSystemInfoSync().statusBarHeight
+ // #ifdef MP-WEIXIN
+ // 获取微信胶囊的位置信息 width,height,top,right,left,bottom
+ const custom = wx.getMenuButtonBoundingClientRect()
+ // console.log(custom)
+ // 导航栏高度(标题栏高度) = 胶囊高度 + (顶部距离 - 状态栏高度) * 2
+ this.globalData.navigationBarHeight = custom.height + (custom.top - this.globalData.statusBarHeight) * 2
+ // console.log("导航栏高度:"+this.globalData.navigationBarHeight)
+ // 总体高度 = 状态栏高度 + 导航栏高度
+ this.globalData.navHeight = this.globalData.navigationBarHeight + this.globalData.statusBarHeight
+ // #endif
+ // console.log('this.globalData==========',this.globalData)
+ onShow: function() {
+ // console.log('App Show')
+ onHide: function() {
+ // console.log('App Hide')
+ }
+</script>
+<style lang="scss">
+ /*每个页面公共css */
+ @import "@/uni_modules/uview-ui/index.scss";
+ @import "@/static/css/common.scss";
+</style>
@@ -0,0 +1,129 @@
+<template>
+ <view class="pages">
+ <u-navbar
+ title="我的"
+ :placeholder="true"
+ :autoBack="true"
+ :safeAreaInsetTop="true"
+ >
+ </u-navbar>
+ <view class="page-wrap">
+ <view class="top">
+ <view class="code-wrap u-flex u-row-right" @click="$u.route('/center/paycode')">
+ <img class="img" src="../static/img/center-code.png" alt="">
+ <text class="text">推广码</text>
+ </view>
+ <!-- <view class="userinfo u-flex">
+ <img class="defaultavatar" src="../static/img/defaultavatar.png" alt="">
+ <view class="text">
+ <view class="name">{{vuex_user_info.username}}</view>
+ <view class="status">
+ <text>区域代理人/</text>
+ <text>白金/</text>
+ <text>贵阳市云岩区</text>
+ </view> -->
+ <img class="center-top-bg" src="../static/img/center-top-bg.png" alt="">
+ <view class="tool-wrap">
+ <u-cell-group :border="false">
+ <u-cell
+ title="修改密码"
+ value=""
+ center
+ :isLink="true"
+ :border="false"
+ url="/center/resetpass"
+ :customStyle="cellCustomStyle"
+ ></u-cell>
+ </u-cell-group>
+</template>
+ import { systemInfo } from "@/mixin.js";
+ components:{
+ mixins:[systemInfo],
+ data() {
+ return {
+ staticUrl:this.$commonConfig.staticUrl,
+ userinfo:{},
+ cellCustomStyle:{
+ 'background-color':'#fff',
+ 'border-radius':'8rpx',
+ onShow() {
+ onLoad() {
+ this.getSystemInfo();
+ methods: {
+<style>
+page{
+ background-color: #F5F5F5;
+}
+<style lang="scss" scoped>
+.top{
+ .center-top-bg{
+ position: absolute;
+ right: 30rpx;
+ bottom: 0;
+ width: 210rpx;
+ height: 169rpx;
+ z-index: 1;
+ color: #fff;
+ height: 280rpx;
+ background: linear-gradient(225deg, #5DE8BD 0%, #00BAA0 100%);
+ border-radius: 10rpx;
+ position: relative;
+ padding: 40rpx 30rpx;
+ box-sizing: border-box;
+ .code-wrap{
+ font-size: 26rpx;
+ padding-bottom: 10rpx;
+ .img{
+ width: 26rpx;
+ height: 26rpx;
+ margin-right: 10rpx;
+ .userinfo{
+ .defaultavatar{
+ width: 110rpx;
+ height: 110rpx;
+ .text{
+ margin-left: 20rpx;
+ font-size: 24rpx;
+ font-family: PingFangSC-Regular, PingFang SC;
+ font-weight: 400;
+ color: #FFFFFF;
+ line-height: 33rpx;
+ .name{
+ font-size: 40rpx;
+ font-weight: 600;
+ line-height: 56rpx;
+ margin-bottom: 10rpx;
+.tool-wrap{
+ margin-top: 20rpx;
@@ -0,0 +1,197 @@
+ title="我的会员"
+ <view class="client">
+ <img class="ico" src="../static/img/client-ico.png" alt="">
+ <view class="left-text">
+ <view class="title">会员总人数</view>
+ <view class="con">Total number of members</view>
+ <view class="number-wrap">
+ <text class="number">{{totalCount}}</text> 人
+ <view class="statistics">
+ <view class="title">会员统计</view>
+ <view class="charts-box" v-if="totalCount>0">
+ <qiun-data-charts
+ type="column"
+ :opts="opts"
+ :chartData="chartData"
+ />
+ <view v-else style="text-align: center;margin-top: 20px;">
+ 暂无会员
+<!-- https://www.ucharts.cn/v2/#/document/index -->
+ totalCount:0,
+ chartData: {},
+ opts: {
+ color: ["#FBBD4E","#FDE37F"],
+ padding: [15,15,0,5],
+ enableScroll: false,
+ legend: {
+ show:false
+ xAxis: {
+ disableGrid: true
+ yAxis: {
+ gridType:'dash',
+ data: [
+ {
+ min: 0
+ ]
+ extra: {
+ column: {
+ type: "meter",
+ width: 30,
+ activeBgColor: "#000000",
+ activeBgOpacity: 0.02,
+ // barBorderCircle: true,
+ barBorderRadius:[16,16,16,16],
+ meterFillColor:"#eeeeee",
+ meterBorder:0,
+ linearType: "custom"
+ tooltip:{
+ showBox:true
+ this.getMemberSta();
+ getMemberSta(){
+ let chartData = {
+ categories: [],
+ series: [
+ name: "目标数量",
+ data: [],
+ name: "会员数量",
+ data: []
+ };
+ this.$u.api.memberSta().then(res=>{
+ console.log('res',res.data);
+ this.totalCount = res.data.totalCount;
+ chartData.categories = res.data.levelStaList.map((item)=>{
+ return item.name
+ });
+ // 会员数量
+ chartData.series[1].data = res.data.levelStaList.map((item)=>{
+ return item.count
+ // 填充目标值
+ // const maxNumber = Math.max(...chartData.series[1].data);
+ // for (let i=0;i<res.data.levelStaList.length;i++) {
+ // chartData.series[0].data.push(maxNumber+10)
+ // }
+ this.chartData = JSON.parse(JSON.stringify(chartData));
+ }).catch(err=>{
+ console.log('memberSta',err);
+ })
+ background-color: #f5f5f5;
+.client{
+ margin-top: 40rpx;
+ margin-bottom: 58rpx;
+ height: 260rpx;
+ background: url(../static/img/client-bg.png) no-repeat;
+ background-size: 100%;
+ .ico{
+ width: 93rpx;
+ height: 93rpx;
+ left: 30rpx;
+ top: -40rpx;
+ .left-text{
+ margin-left: 30rpx;
+ padding-top: 63rpx;
+ .title{
+ color: rgba(255,255,255,0.99);
+ line-height: 37rpx;
+ .con{
+ font-size: 20rpx;
+ line-height: 28rpx;
+ text-transform: uppercase;
+ width: 185rpx;
+ opacity: 30%;
+ .number-wrap{
+ position:absolute;
+ bottom: 42rpx;
+ .number{
+ font-size: 70rpx;
+ font-family: AlibabaPuHuiTi_2_115_Black;
+.statistics{
+ font-size: 32rpx;
+ color: rgba(51,51,51,0.99);
+ line-height: 45rpx;
+ margin-bottom: 20rpx;
+ .charts-box {
+ width: 100%;
+ height: 300px;
+ background-color: #fff;
+ border-radius: 8rpx;
+ padding: 50rpx 0;
@@ -0,0 +1,180 @@
+ <view class="">
+ title="我的业绩"
+ <view class="tabs-wrap">
+ <u-tabs
+ :list="tabsList"
+ lineColor="#00A447"
+ :activeStyle="{color:'#333','font-weight': '600','font-size':'30rpx'}"
+ :inactiveStyle="{color:'#999'}"
+ @click="tabsClick"></u-tabs>
+ <mescroll-body class="" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+ <view class="page-wrap" v-if="dataList.length>0" >
+ <view class="list">
+ <view class="item u-flex u-row-between" @click="$u.route('/shopping/order')" v-for="item in dataList" :key="item.id">
+ <view class="left">
+ <view class="name">{{item.recordContent}}</view>
+ <view class="time">{{item.createTime}}</view>
+ <text class="num" v-if="item.recordType!=1">-{{item.balance}}</text>
+ <text class="num plus" v-else>+{{item.balance}}</text>
+ </mescroll-body>
+ // 引入mescroll-mixins.js
+ import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+ mixins: [MescrollMixin], // 使用mixin
+ credit:'',
+ downOption: {},
+ // 上拉加载的配置(可选, 绝大部分情况无需配置)
+ upOption: {
+ page: {
+ size: 10 // 每页数据的数量,默认10
+ noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+ empty: {
+ tip: '暂无相关数据'
+ tabsList:[{name:'昨日',recordType:2},{name:'本月',recordType:1},{name:'历史',recordType:3}],
+ params:{
+ recordType:''
+ activeIndex:0,
+ dataList: []
+ this.params.recordType = this.tabsList[this.activeIndex].recordType;
+ // console.log('1111', this.tabsList[this.activeIndex]);
+ /*下拉刷新的回调, 重置列表为第一页 (此处可删,mixins已默认)
+ downCallback(){
+ this.mescroll.resetUpScroll();
+ /*上拉加载的回调*/
+ upCallback(page) {
+ // 此处可以继续请求其他接口
+ // if(page.num == 1){
+ // // 请求其他接口...
+ // 如果希望先请求其他接口,再触发upCallback,可参考以下写法
+ // if(!this.params.id){
+ // this.mescroll.endErr()
+ // return // 此处return,先获取xx
+ let pageNum = page.num; // 页码, 默认从1开始
+ let pageSize = page.size; // 页长, 默认每页10条isAsc:0//时间排序 0:降序 1:升序 (默认星级降序排序)
+ this.params = Object.assign(this.params,{pageNum:pageNum,pageSize:pageSize});
+ this.$u.api.selectBalanceLogList(this.params).then(data => {
+ console.log('data',JSON.parse(JSON.stringify(data)));
+ // 接口返回的当前页数据列表 (数组)
+ let curPageData = data.data.rows;
+ console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+ // 接口返回的当前页数据长度 (如列表有26个数据,当前页返回8个,则curPageLen=8)
+ let curPageLen = curPageData.length;
+ // 接口返回的总页数 (如列表有26个数据,每页10条,共3页; 则totalPage=3)
+ // let totalPage = data.data.data.totalPage;
+ // 接口返回的总数据量(如列表有26个数据,每页10条,共3页; 则totalSize=26)
+ let totalSize = data.data.total;
+ // 接口返回的是否有下一页 (true/false)
+ // let hasNext = data.xxx;
+ // console.log('totalPage',totalPage,'curPageLen',curPageLen);
+ //设置列表数据
+ if(page.num == 1) this.dataList = []; //如果是第一页需手动置空列表
+ this.dataList = this.dataList.concat(curPageData); //追加新数据
+ // 请求成功,隐藏加载状态
+ //方法一(推荐): 后台接口有返回列表的总页数 totalPage
+ // this.mescroll.endByPage(curPageLen, totalPage);
+ //方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+ this.mescroll.endBySize(curPageLen, totalSize);
+ }).catch(err => {
+ this.mescroll.endErr()
+ console.log(err)
+ /*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+ reloadList() {
+ tabsClick(item){
+ this.params.recordType = item.recordType;
+ this.reloadList()
+ // console.log('item',item);
+.credit{
+ padding: 20rpx;
+ margin: 0 20rpx 20rpx;
+ font-size: 36rpx;
+ color: #00A447;
+.tabs-wrap{
+.list{
+ padding: 0 20rpx 20rpx;
+ .item{
+ padding: 20rpx 0;
+ border-bottom: 0.5px solid #ddd;
+ font-size: 30rpx;
+ color: #333333;
+ line-height: 42rpx;
+ margin-bottom: 8rpx;
+ .time{
+ color: #999999;
+ .num{
+ color: #FFB100;
+ &.plus{
@@ -0,0 +1,170 @@
+ title="我的门店"
+ @leftClick="leftClick"
+ <!-- <view class="tabs-wrap">
+ <view class="u-flex u-flex-wrap u-row-between">
+ <view class="list-item" v-for="item in dataList" :key="item.id">
+ <view class="item">
+ <view class="image-wrap">
+ <u--image mode="scaleToFill" height="300rpx" :showLoading="true" class="image" :src="item.mainImg"></u--image>
+ <view class="name ellipsis-1">{{item.goodsName}}</view>
+ tabsList:[],
+ typeId:null,
+ dataList:[]
+ // this.shopGoodsType();
+ leftClick(e){
+ let pages = getCurrentPages();
+ if(pages.length==1){uni.$u.route('/pages/index/index')};
+ //如果希望先请求其他接口,再触发upCallback,可参考以下写法
+ // if(!this.hasTypeId){
+ // this.$u.api.shopGoodsType().then(res=>{
+ // // this.tabsList = res.data;
+ // this.tabsList = res.data.map(item=>{
+ // return {name:item.dictLabel,id:item.dictValue}
+ // });
+ // this.hasTypeId = true
+ // this.mescroll.resetUpScroll() // 重新触发upCallback
+ // }).catch(()=>{
+ // })
+ // let typeId = this.tabsList[this.activeIndex].id;
+ let pageSize = page.size; // 页长, 默认每页10条
+ let params ={
+ // typeId:typeId,
+ pageNum:pageNum,
+ pageSize:pageSize
+ console.log('params',params);
+ this.$u.api.shopGoods(params).then(data => {
+ // console.log('curPageData',JSON.parse(JSON.stringify(curPageData)));
+ // let totalPage = data.data.totalPage;
+ // shopGoodsType(){
+ // console.log('shopGoodsType',res.data);
+ // console.log('tabsList',this.tabsList);
+ // }).catch(err=>{
+ // // console.log('err11111',err);
+ // },
+ this.activeIndex = item.index;
+ console.log('item',item);
+ background-color: #F9F9F9;
+.list-item{
+ overflow: hidden;
+ width: 48%;
+ margin-bottom: 30rpx;
+ padding-bottom: 20rpx;
+ .image-wrap{
+ padding: 0 20rpx;
@@ -0,0 +1,120 @@
+ title="推广码"
+ <view class="content">
+ <view class="ayQrcode" @click="refreshCode">
+ <ayQrcode
+ ref="qrcode"
+ :modal="modal_qr"
+ :url="qrContent"
+ @hideQrcode="hideQrcode"
+ :is_themeImg="true"
+ themeImg="../static/qrlogo.png"
+ :h_w_img="65"
+ :height="250" :width="250" />
+ <view class="tip">仅限消费者注册使用</view>
+ <text class="save">保存本地</text>
+ import ayQrcode from "@/components/ay-qrcode/ay-qrcode.vue"
+ ayQrcode
+ modal_qr: false,
+ qrContent: {}, // 要生成的二维码值
+ timer: null,
+ this.qrContent.qrcode = this.vuex_user_info.userid;
+ // this.qrContent.time = Date.now();
+ this.qrContent = JSON.stringify(this.qrContent);
+ let that = this;
+ // this.timer = setInterval(() => {
+ // this.refreshCode()
+ // }, 30000);
+ onUnload() {
+ // 页面离开时停止计时器
+ // clearInterval(this.timer)
+ onReady() {
+ that.showQrcode();//一加载生成二维码
+ // 展示二维码
+ showQrcode() {
+ let _this = this;
+ this.modal_qr = true;
+ // uni.showLoading()
+ setTimeout(function() {
+ // uni.hideLoading()
+ _this.$refs.qrcode.crtQrCode()
+ }, 50)
+ //传入组件的方法
+ hideQrcode() {
+ this.modal_qr = false;
+ refreshCode(){
+ this.qrContent = {};
+ this.qrContent.time = Date.now();
+ this.showQrcode()
+ background-color: #005223;
+.content{
+ text-align: center;
+ margin: 90rpx 45rpx 24rpx;
+ background: #FFFFFF;
+ padding: 100rpx 20rpx;
+ .ayQrcode{
+ width: 250px;
+ height: 250px;
+ .tip{
+ margin-bottom: 40rpx;
+ .save{
+ padding: 25rpx 98rpx;
+ background-color: #00A447;
+ border-radius: 4rpx;
@@ -0,0 +1,163 @@
+ <u--form labelPosition="left" :model="form" :rules="rules" ref="uForm" >
+ <u-form-item label="" prop="oldPassword" borderBottom ref="oldPassword" >
+ <u--input
+ v-model="form.oldPassword"
+ border="none"
+ placeholder="请输入旧密码"
+ :customStyle="inputCustomStyle"
+ ></u--input>
+ </u-form-item>
+ <u-form-item label="" prop="password" borderBottom ref="password" >
+ v-model="form.password"
+ :password="true"
+ placeholder="请输入新密码"
+ <u-form-item label="" prop="comfpassword" borderBottom ref="comfpassword" >
+ v-model="form.comfpassword"
+ placeholder="确认密码"
+ </u--form>
+ <u-button
+ @click="submit"
+ text="确定"
+ type="primary"
+ shape="circle"
+ :customStyle="{'margin-top':'60rpx',height:'98rpx','box-sizing':'border-box'}"
+ color="linear-gradient(90deg, #00D17D 0%, #00A447 100%)">
+ </u-button>
+ form:{
+ oldPassword:'',
+ password:'',
+ comfpassword:'',
+ rules: {
+ oldPassword: {
+ type: 'string',
+ required: true,
+ message: '请输入旧密码',
+ trigger: ['blur', 'change']
+ password: [
+ message: '请输入新密码',
+ min: 6,
+ max: 10,
+ message: '长度在6-10个字符之间',
+ trigger: 'blur',
+ ],
+ comfpassword: [
+ message: '请确认密码',
+ validator: (rule, value, callback) => {
+ if (value === '') {
+ callback(new Error('请再次输入密码'));
+ } else if (value !== this.form.password) {
+ callback(new Error('两次输入密码不一致!'));
+ } else {
+ callback();
+ inputCustomStyle:{
+ height:'98rpx',
+ 'border-color':'#eee',
+ 'padding-left':'30rpx',
+ 'box-sizing':'border-box',
+ this.$refs.uForm.setRules(this.rules)
+ chekPass(){
+ console.log('chekPass---',this.form);
+ return new Promise((resolve, reject)=>{
+ if(!password){
+ reject('needAuth');
+ async submit(){
+ // console.log('form',this.form);
+ this.$refs.uForm.validate().then(res => {
+ // let chekPassResult = await this.chekPass();
+ // return
+ // uni.$u.toast('校验通过')
+ const param = {
+ oldPassword:this.form.oldPassword,
+ password:this.form.password
+ this.$u.api.updatePassword(param).then(res=>{
+ // console.log('res',res.data);
+ this.$u.vuex('vuex_user_info', res.data);
+ uni.showToast({
+ title:res.msg,
+ icon:'success'
+ setTimeout(()=>{
+ uni.$u.route('/pages/login/login');
+ },2000)
+ // uni.reLaunch({url: this.backUrl});
+ console.log('login',err);
+ }).catch(errors => {
+ uni.$u.toast('请正确填写表单')
@@ -0,0 +1,89 @@
+/*
+ 接口统一管理
+*/
+const apiurl = {
+ // 用户注册登陆
+ login: {
+ url: '/agentInfo/login',
+ type: 'post'
+ // 重新登录
+ reLogin: {
+ url: '/auth/login',
+ // 修改密码
+ updatePassword: {
+ url: '/agentInfo/updatePassword',
+ type: 'put'
+ // 修改用户信息
+ updateMemberInfo: {
+ url: '/memberInfo/update',
+ //代理人端首页信息
+ indexData: {
+ url: '/agentInfo/index',
+ type: 'get'
+ //用户信息统计
+ memberSta: {
+ url: '/agentInfo/memberSta',
+ //轮播图
+ swiperList: {
+ url: '/advList/pageList',
+ //轮播图详情
+ swiperDetails: {
+ url: '/advList/selectById',
+ // 旭烁简介
+ xsGetIntro: {
+ url: '/shop/getIntro',
+ // 热销商品列表
+ topGoodList: {
+ url: '/goods/selectTopGoodList',
+ // 商品分类信息
+ goodsTypeTree: {
+ url: '/goodsType/tree',
+ // 商品分类信息列表
+ goodsTypeList: {
+ url: '/goodsType/list',
+ // 商品列表
+ memberGoodList: {
+ url: '/goods/memberGoodList',
+ // 商品详情
+ memberGoodDetails: {
+ url: '/goods/memberGetById',
+* 特殊处理接口
+const otherApiUrl = {
+ // 文件上传
+ uploadFile: '/file/upload/single/minio',
+export {
+ apiurl,
+ otherApiUrl
@@ -0,0 +1,37 @@
+/**
+ * 配置通用
+ */
+// const node_dev = process.env.H_NODE_ENV;
+// //运行到浏览器用的
+// let baseUrl='/api';
+// let upFileUrl='/api';
+// //打包用的
+// if (node_dev) {
+// baseUrl = process.env.H_BASE_URL;
+// upFileUrl = process.env.H_UP_FILE_URL
+// }
+//64
+let baseUrl='https://xusfoodapi.hw.hongweisoft.com/appapi/app';
+let upFileUrl='http://fileupload.hw.hongweisoft.com/upload/single/minio';
+let staticUrl='http://res.hw.hongweisoft.com/xushuo/';
+//正式
+// let baseUrl='https://xusapi.gzxsjt.cn/appapi/app';
+// let upFileUrl='https://xusapi.gzxsjt.cn/thirdapi/upload/single/minio';
+// let staticUrl='http://res.gzxsjt.cn/xushuo/';
+const commonConfig = {
+ wxAppid: '', // 测试wxAppid
+ baseUrl: baseUrl, // 服务器地址
+ uploadFileUrl: upFileUrl, // 上传文件路径
+ staticUrl:staticUrl,
+ paginationConfig:{
+ pageNum: 1,
+ pageSize: 10
+ },//分页参数
+ successCode:200,//接口返回状态
+ commonConfig
@@ -0,0 +1,29 @@
+import {
+ apiurl
+} from "./apiurl.js"
+// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
+const install = (Vue, vm) => {
+ let httpMap = {}
+ const http = uni.$u.http
+ // 循环请求路径对象生成对应的方式请求
+ Object.keys(apiurl).forEach((key) => {
+ if(apiurl[key]?.type=='get'){
+ httpMap[key] = (data = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, {params:data}, config);
+ }else{
+ httpMap[key] = (params = {}, config = {}) => http[apiurl[key]?.type](apiurl[key]?.url, params, config);
+ // 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
+ vm.$u.api = {
+ ...httpMap
+export default {
+ install
@@ -0,0 +1,86 @@
+import { commonConfig } from '@/common/config.js';
+import store from '../store/index.js'
+import {againToken} from '../utils/leaderToken.js'
+// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
+// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
+ Vue.prototype.$u.http.setConfig({
+ baseUrl: commonConfig.baseUrl,
+ timeout: 40000,
+ // 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
+ // 设置为true后,就需要在this.$u.http.interceptor.response进行多一次的判断,请打印查看具体值
+ // originalData: true,
+ // 设置自定义头部content-type
+ // header: {
+ // 'content-type': 'xxx'
+ // 请求拦截,配置Token等参数
+ Vue.prototype.$u.http.interceptor.request = (config) => {
+ // config.header.Token = 'xxxxxx';
+ // 方式一,存放在vuex的token,假设使用了uView封装的vuex方式,见:https://uviewui.com/components/globalVariable.html
+ // config.header.token = vm.vuex_token;
+ if(vm.vuex_user.accessToken){config.header.Authorization = `Bearer ${vm.vuex_user.accessToken}`;}
+ if(vm.vuex_user.userId){config.header['user_id'] = `${vm.vuex_user.userId}`;}
+ // 方式二,如果没有使用uView封装的vuex方法,那么需要使用$store.state获取
+ // config.header.token = vm.$store.state.token;
+ // 方式三,如果token放在了globalData,通过getApp().globalData获取
+ // config.header.token = getApp().globalData.username;
+ // 方式四,如果token放在了Storage本地存储中,拦截是每次请求都执行的,所以哪怕您重新登录修改了Storage,下一次的请求将会是最新值
+ // const token = uni.getStorageSync('token');
+ // config.header.token = token;
+ // url加时间戳
+ if(config.url.indexOf('?') > -1){
+ config.url = config.url + '&t=' + Date.now()
+ config.url = config.url + '?t=' + Date.now()
+ // 此url参数为this.$u.get(url)中的url值
+ let noTokenList = ['/wechat/h5/user','/client/auth/verifyCode'];
+ if(noTokenList.includes(config.url)) config.header.noToken = true;
+ // console.log('noTokenList.includes(config.url)',noTokenList.includes(config.url));
+ // console.log('config.url',config.url);
+ return config;
+ // 响应拦截,判断状态码是否通过
+ Vue.prototype.$u.http.interceptor.response = (res) => {
+ // 如果把originalData设置为了true,这里得到将会是服务器返回的所有的原始数据
+ // 判断可能变成了res.statueCode,或者res.data.code之类的,请打印查看结果
+ // console.log('interceptor res',res);
+ if(res.code == 200) {
+ // 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
+ return res;
+ } else if(res.msg == "令牌不能为空" || res.code == 401){
+ if(vm.vuex_user.userId) {
+ againToken(vm.vuex_user.userId)
+ const backUrl = location.href
+ const loginUrl = 'phoneLogin'
+ if (backUrl.indexOf(loginUrl) > 0) {
+ localStorage.clear()
+ // uni.showToast({
+ // title: res.msg + "即将跳转到登录页",
+ // icon: "none",
+ // duration: 2000
+ localStorage.setItem('backUrl', location.href)
+ // alert('还未登录,即将跳转登录');
+ setTimeout(() => {
+ uni.navigateTo({
+ url: "/pages/login/login"
+ }, 1000)
+ }else return res;
+import {againToken} from '../utils/againToken.js'
+import { showFullScreenLoading , tryHideFullScreenLoading } from '../utils/loading.js'
+// 此vm参数为页面的实例,可以通过它引用vuex中的变量
+module.exports = (vm) => {
+ // 初始化请求配置
+ uni.$u.http.setConfig((config) => {
+ /* config 为默认全局配置*/
+ config.baseURL = commonConfig.baseUrl; /* 根域名 */
+ return config
+ // 请求拦截
+ uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
+ // console.log('config========',config);
+ // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
+ config.data = config.data || {}
+ // 根据custom参数中配置的是否需要token,添加对应的请求头
+ if(!config?.custom?.auth) {
+ // 可以在此通过vm引用vuex中的变量,具体值在vm.$store.state中
+ // config.header.token = vm.vuex_user_info.token;
+ config.header.Authorization = `Bearer ${vm.vuex_user_info.token}`;
+ if(!config?.custom?.noload){
+ showFullScreenLoading()
+ }, config => { // 可使用async await 做异步操作
+ return Promise.reject(config)
+ let unlogin = function(){
+ if(vm.vuex_user_info.userid&&vm.vuex_wechatOpenid) {
+ againToken(vm.$u,vm.vuex_wechatOpenid,vm.vuex_user_info.userid)
+ // console.log('pages',pages);
+ let backUrl = pages[pages.length - 1].route;
+ let options =uni.$u.queryParams( pages[pages.length - 1].options);
+ let fullBackUrl = backUrl+options;
+ const loginUrl = 'login'
+ uni.removeStorage({
+ key: 'backUrl',
+ success: function (res) {
+ // console.log('success');
+ uni.setStorage({
+ data: fullBackUrl,
+ success: function () {
+ // console.log('setStorage success');
+ // 响应拦截
+ uni.$u.http.interceptors.response.use((response) => {/* 对响应成功做点什么 可使用async await 做异步操作*/
+ // console.log('response====response',response);
+ const data = response.data
+ // 自定义参数
+ const custom = response.config?.custom
+ if (data.code !== 200) {
+ // console.log('data====',data);
+ // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示
+ if (custom.toast !== false) {
+ uni.$u.toast(data.msg)
+ if(data.msg == "令牌验证失败" || data.msg == "令牌不能为空" || data.code == 401){
+ unlogin()
+ if(data.msg == "用户不存在!"||data.msg == "用户未注册"){
+ uni.clearStorage();
+ return Promise.reject(data)
+ // 如果需要catch返回,则进行reject
+ // if (custom?.catch) {
+ // return Promise.reject(data)
+ // } else {
+ // // 否则返回一个pending中的promise,请求不会进入catch中
+ // return new Promise(() => { })
+ // console.log('data--',data);
+ tryHideFullScreenLoading()
+ return data === undefined ? {} : data
+ }, (response) => {
+ // console.log('response==',response);
+ const data = response.data;
+ // console.log('data==',data);
+ // 对响应错误做点什么 (statusCode !== 200)
+ let errMap = {
+ '404':'接口不存在'
+ if (response.statusCode in errMap) {
+ uni.$u.toast(errMap[response.statusCode])
+ if(data.msg == "令牌不能为空" || data.code == 401){
+ return Promise.reject(response)
@@ -0,0 +1,411 @@
+ <view :class="modal?'show-qrcode':'hide-qrcode'">
+ <view class="box-qrcode" :style="{'margin-left': marginLeft + 'px'}" @longtap="longtapCode">
+ <!-- style="width: 550rpx;height: 550rpx;" -->
+ <canvas class="canvas-qrcode" :style="style_w_h" :canvas-id="qrcode_id">
+ <!-- #ifndef MP -->
+ <view v-if="modal&&is_themeImg" :style="style_w_h" class="box-img-qrcode">
+ <image :style="style_w_h_img" mode="scaleToFill" :src="themeImg"></image>
+ <!-- #endif -->
+ </canvas>
+ <!-- <image mode="scaleToFill" :src="imagePath"></image> -->
+ var qr_we = require("./qrcode_wx.js");
+ const qrCode = require('./weapp-qrcode.js')
+ isAndroid : false ,
+ show: true,
+ imagePath: '',
+ // qrcode_id: 'qrcode_id',
+ marginLeft: 0,
+ //一般的安卓app只需加30就能显示全
+ //苹果app的不加就能显示全,加了就要弄margin-left
+ //有些安卓app显示不全
+ add_num : 30 ,
+ add_num_key : 'rectify_code_key',
+ props: {
+ modal: {
+ type: Boolean,
+ default: false
+ url: {
+ type: String,
+ default: ''
+ height: {
+ type: Number,
+ default: 260
+ width: {
+ themeColor: {
+ default: '#333333',
+ qrcode_id: {
+ default: 'qrcode_id',
+ is_themeImg: {
+ default: false,
+ themeImg: {
+ default: 'https://cdn.pixabay.com/photo/2016/11/29/13/24/balloons-1869816__340.jpg',
+ h_w_img: {
+ default: 30
+ watch:{
+ computed: {
+ style_w_h() {
+ return this.set_style_w_h();
+ style_w_h_img() {
+ var height = parseInt(that.h_w_img);
+ var width = parseInt(that.h_w_img);
+ var style = '';
+ if (height > 0) {
+ style = `height:${height*2}rpx;`;
+ if (width > 0) {
+ style += `width:${width*2}rpx;z-index: 2;`;
+ return style;
+ created: function() {
+ try {
+ //app苹果二维码不居中
+ //#ifndef MP
+ let isAndroid = false ;
+ const res = uni.getSystemInfoSync();
+ if(res.platform == 'android'){
+ isAndroid = true ;
+ isAndroid = false ;
+ if (!isAndroid) {
+ that.marginLeft = 46;
+ that.isAndroid = isAndroid ;
+ const add_num = uni.getStorageSync(that.add_num_key);
+ if (add_num) {
+ that.add_num = add_num;
+ } catch (e) {
+ // error
+ //#ifdef MP
+ //that.marginLeft = 40;
+ set_style_w_h(){
+ var height = parseInt(that.height);
+ var width = parseInt(that.width);
+ var height = height*2 ;
+ var width = width*2 ;
+ var add = that.add_num ;
+ height += add;
+ width += add;
+ style = `height:${height}rpx;`;
+ style += `width:${width}rpx;`;
+ this.$emit("hideQrcode")
+ // 二维码生成工具
+ crtQrCode() {
+ new qrCode(that.qrcode_id, {
+ text: this.url,
+ width: that.width,
+ height: that.height,
+ colorDark: that.themeColor,//#333333
+ colorLight: "#FFFFFF",
+ correctLevel: qrCode.CorrectLevel.H,
+ that.createQrCode(this.url, that.qrcode_id, that.width, that.height,that.themeColor,that.is_themeImg,that.themeImg,that.h_w_img);
+ //that.createQrCode(this.url, that.qrcode_id, that.width, that.height);
+ createQrCode: function(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize) {
+ //调用插件中的draw方法,绘制二维码图片
+ qr_we.api.draw(url, canvasId, cavW, cavH,cavColor,haveImg,imgurl,imgsize, this, this.canvasToTempImage);
+ // setTimeout(() => { this.canvasToTempImage();},100);
+ //获取临时缓存照片路径,存入data中
+ canvasToTempImage: function() {
+ var that = this;
+ saveImage: function() {
+ uni.canvasToTempFilePath({
+ canvasId: that.qrcode_id,
+ success: function(res) {
+ var tempFilePath = res.tempFilePath;
+ console.log(tempFilePath);
+ that.imagePath = tempFilePath;
+ //保存到相册
+ // uni.saveFile({
+ // tempFilePath: tempFilePath,
+ // success: function (res2) {
+ // var savedFilePath = res2.savedFilePath;
+ uni.saveImageToPhotosAlbum({
+ filePath : tempFilePath ,
+ success: function (res3) {
+ uni.showModal({
+ title: '提示',
+ content: '保存成功',
+ confirmText: '确定',
+ showCancel: false,
+ confirmColor: '#33CCCC',
+ success(res4) {
+ fail: function(res) {
+ console.log(res);
+ }, that);
+ //微信小程序支持:长按二维码,提示是否保存相册
+ //安卓APP长按校正二维码
+ longtapCode(){
+ title: '校正二维码',
+ content: '二维码是否异常',
+ success(res) {
+ if (res.confirm) {
+ that.rectify_code();
+ //#ifdef MP-WEIXIN
+ content: '是否保存到相册',
+ that.saveImage();
+ //安卓有些手机不正常,长按可选择矫正
+ rectify_code(){
+ let add_num = that.add_num ;
+ add_num += 30 ;
+ that.crtQrCode();//重新生成才会立即覆盖
+ //第一次长按校正设置了就不用在设置
+ key: that.add_num_key,
+ data: add_num,
+ success: function() {
+ mounted() {}
+<style scoped lang="scss">
+ // .qrcode-box {
+ // position: fixed;
+ // left: 0;
+ // top: 0;
+ // right: 0;
+ // bottom: 0;
+ // height: 100vh;
+ // width: 100vw;
+ // background-color: rgba(59, 59, 59, 0.6);
+ // // opacity: 0.8;
+ // text-align: center;
+ // display: flex;
+ // align-items: center;
+ // display: none;
+ // .qrcode-item {
+ // flex: 1;
+ // position: relative;
+ // .item-box {
+ // width: 90%;
+ // margin: auto;
+ // display: inline-block;
+ // margin-top: 30%;
+ // padding-bottom: 30rpx;
+ // // animation: show 0.7s;
+ // .title {
+ // font-size: 46rpx;
+ // margin-bottom: 24rpx;
+ // .canvas {
+ // background-color: #FFFFFF;
+ .box-qrcode{
+ .box-img-qrcode{
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 2;
+ image{
+ width: 60upx;
+ height: 60upx;
+ border-radius: 50%;
+ .canvas-qrcode {
+ margin: auto;
+ display: inline-block;
+ float: left;
+ .opacity-qrcode {
+ opacity: 0;
+ display: block;
+ .show-qrcode {
+ animation: fade 0.7s;
+ // -moz-animation: fade 0.5s; /* Firefox */
+ // -webkit-animation: fade 0.5s; /* Safari 和 Chrome */
+ // -o-animation: fade 0.5s;
+ .hide-qrcode {
+ animation: hide 0.7s;
+ @keyframes fade {
+ from {
+ opacity: 0.8;
+ to {
+ opacity: 1;
+ @keyframes hide {
@@ -0,0 +1,872 @@
+!(function() {
+ // alignment pattern
+ var adelta = [
+ 0, 11, 15, 19, 23, 27, 31,
+ 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
+ 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
+ ];
+ // version block
+ var vpat = [
+ 0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
+ 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
+ 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
+ 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
+ 0x541, 0xc69
+ // final format bits with mask: level << 3 | mask
+ var fmtword = [
+ 0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, //L
+ 0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, //M
+ 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, //Q
+ 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b //H
+ // 4 per version: number of blocks 1,2; data width; ecc width
+ var eccblocks = [
+ 1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17,
+ 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28,
+ 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22,
+ 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16,
+ 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22,
+ 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28,
+ 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26,
+ 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26,
+ 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24,
+ 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28,
+ 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24,
+ 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28,
+ 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22,
+ 3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24,
+ 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24,
+ 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30,
+ 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28,
+ 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28,
+ 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26,
+ 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
+ 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30,
+ 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24,
+ 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
+ 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
+ 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
+ 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
+ 8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
+ 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
+ 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
+ 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
+ 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
+ 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
+ 17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
+ 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
+ 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
+ 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
+ 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
+ 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
+ 20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
+ 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
+ // Galois field log table
+ var glog = [
+ 0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
+ 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
+ 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
+ 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
+ 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
+ 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
+ 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
+ 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
+ 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
+ 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
+ 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
+ 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
+ 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
+ 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
+ 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
+ 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
+ // Galios field exponent table
+ var gexp = [
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
+ 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
+ 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
+ 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
+ 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
+ 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
+ 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
+ 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
+ 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
+ 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
+ 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
+ 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
+ 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
+ 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
+ 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
+ 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
+ // Working buffers:
+ // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
+ var strinbuf = [],
+ eccbuf = [],
+ qrframe = [],
+ framask = [],
+ rlens = [];
+ // Control values - width is based on version, last 4 are from table.
+ var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
+ var ecclevel = 2;
+ // set bit to indicate cell in qrframe is immutable. symmetric around diagonal
+ function setmask(x, y) {
+ var bt;
+ if (x > y) {
+ bt = x;
+ x = y;
+ y = bt;
+ // y*y = 1+3+5...
+ bt = y;
+ bt *= y;
+ bt += y;
+ bt >>= 1;
+ bt += x;
+ framask[bt] = 1;
+ // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
+ function putalign(x, y) {
+ var j;
+ qrframe[x + width * y] = 1;
+ for (j = -2; j < 2; j++) {
+ qrframe[(x + j) + width * (y - 2)] = 1;
+ qrframe[(x - 2) + width * (y + j + 1)] = 1;
+ qrframe[(x + 2) + width * (y + j)] = 1;
+ qrframe[(x + j + 1) + width * (y + 2)] = 1;
+ for (j = 0; j < 2; j++) {
+ setmask(x - 1, y + j);
+ setmask(x + 1, y - j);
+ setmask(x - j, y - 1);
+ setmask(x + j, y + 1);
+ //========================================================================
+ // Reed Solomon error correction
+ // exponentiation mod N
+ function modnn(x) {
+ while (x >= 255) {
+ x -= 255;
+ x = (x >> 8) + (x & 255);
+ return x;
+ var genpoly = [];
+ // Calculate and append ECC data to data block. Block is in strinbuf, indexes to buffers given.
+ function appendrs(data, dlen, ecbuf, eclen) {
+ var i, j, fb;
+ for (i = 0; i < eclen; i++)
+ strinbuf[ecbuf + i] = 0;
+ for (i = 0; i < dlen; i++) {
+ fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
+ if (fb != 255) /* fb term is non-zero */
+ for (j = 1; j < eclen; j++)
+ strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
+ else
+ for (j = ecbuf; j < ecbuf + eclen; j++)
+ strinbuf[j] = strinbuf[j + 1];
+ strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
+ // Frame data insert following the path rules
+ // check mask - since symmetrical use half.
+ function ismasked(x, y) {
+ bt += y * y;
+ return framask[bt];
+ // Apply the selected mask out of the 8.
+ function applymask(m) {
+ var x, y, r3x, r3y;
+ switch (m) {
+ case 0:
+ for (y = 0; y < width; y++)
+ for (x = 0; x < width; x++)
+ if (!((x + y) & 1) && !ismasked(x, y))
+ qrframe[x + y * width] ^= 1;
+ break;
+ case 1:
+ if (!(y & 1) && !ismasked(x, y))
+ case 2:
+ for (r3x = 0, x = 0; x < width; x++, r3x++) {
+ if (r3x == 3)
+ r3x = 0;
+ if (!r3x && !ismasked(x, y))
+ case 3:
+ for (r3y = 0, y = 0; y < width; y++, r3y++) {
+ if (r3y == 3)
+ r3y = 0;
+ for (r3x = r3y, x = 0; x < width; x++, r3x++) {
+ case 4:
+ for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++, r3x++) {
+ if (r3x == 3) {
+ r3y = !r3y;
+ if (!r3y && !ismasked(x, y))
+ case 5:
+ if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
+ case 6:
+ if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
+ case 7:
+ if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
+ return;
+ // Badness coefficients.
+ var N1 = 3,
+ N2 = 3,
+ N3 = 40,
+ N4 = 10;
+ // Using the table of the length of each run, calculate the amount of bad image
+ // - long runs or those that look like finders; called twice, once each for X and Y
+ function badruns(length) {
+ var i;
+ var runsbad = 0;
+ for (i = 0; i <= length; i++)
+ if (rlens[i] >= 5)
+ runsbad += N1 + rlens[i] - 5;
+ // BwBBBwB as in finder
+ for (i = 3; i < length - 1; i += 2)
+ if (rlens[i - 2] == rlens[i + 2] &&
+ rlens[i + 2] == rlens[i - 1] &&
+ rlens[i - 1] == rlens[i + 1] &&
+ rlens[i - 1] * 3 == rlens[i]
+ // white around the black pattern? Not part of spec
+ &&
+ (rlens[i - 3] == 0 // beginning
+ ||
+ i + 3 > length // end
+ rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
+ )
+ runsbad += N3;
+ return runsbad;
+ // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
+ function badcheck() {
+ var x, y, h, b, b1;
+ var thisbad = 0;
+ var bw = 0;
+ // blocks of same color.
+ for (y = 0; y < width - 1; y++)
+ for (x = 0; x < width - 1; x++)
+ if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y] &&
+ qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
+ !(qrframe[x + width * y] || qrframe[(x + 1) + width * y] ||
+ qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
+ thisbad += N2;
+ // X runs
+ for (y = 0; y < width; y++) {
+ rlens[0] = 0;
+ for (h = b = x = 0; x < width; x++) {
+ if ((b1 = qrframe[x + width * y]) == b)
+ rlens[h]++;
+ rlens[++h] = 1;
+ b = b1;
+ bw += b ? 1 : -1;
+ thisbad += badruns(h);
+ // black/white imbalance
+ if (bw < 0)
+ bw = -bw;
+ var big = bw;
+ var count = 0;
+ big += big << 2;
+ big <<= 1;
+ while (big > width * width)
+ big -= width * width, count++;
+ thisbad += count * N4;
+ // Y runs
+ for (x = 0; x < width; x++) {
+ for (h = b = y = 0; y < width; y++) {
+ return thisbad;
+ function genframe(instring) {
+ var x, y, k, t, v, i, j, m;
+ // find the smallest version that fits the string
+ t = instring.length;
+ version = 0;
+ do {
+ version++;
+ k = (ecclevel - 1) * 4 + (version - 1) * 16;
+ neccblk1 = eccblocks[k++];
+ neccblk2 = eccblocks[k++];
+ datablkw = eccblocks[k++];
+ eccblkwid = eccblocks[k];
+ k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
+ if (t <= k)
+ } while (version < 40);
+ // FIXME - insure that it fits insted of being truncated
+ width = 17 + 4 * version;
+ // allocate, clear and setup data structures
+ v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+ for (t = 0; t < v; t++)
+ eccbuf[t] = 0;
+ strinbuf = instring.slice(0);
+ for (t = 0; t < width * width; t++)
+ qrframe[t] = 0;
+ for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
+ framask[t] = 0;
+ // insert finders - black to frame, white to mask
+ for (t = 0; t < 3; t++) {
+ k = 0;
+ y = 0;
+ if (t == 1)
+ k = (width - 7);
+ if (t == 2)
+ y = (width - 7);
+ qrframe[(y + 3) + width * (k + 3)] = 1;
+ for (x = 0; x < 6; x++) {
+ qrframe[(y + x) + width * k] = 1;
+ qrframe[y + width * (k + x + 1)] = 1;
+ qrframe[(y + 6) + width * (k + x)] = 1;
+ qrframe[(y + x + 1) + width * (k + 6)] = 1;
+ for (x = 1; x < 5; x++) {
+ setmask(y + x, k + 1);
+ setmask(y + 1, k + x + 1);
+ setmask(y + 5, k + x);
+ setmask(y + x + 1, k + 5);
+ for (x = 2; x < 4; x++) {
+ qrframe[(y + x) + width * (k + 2)] = 1;
+ qrframe[(y + 2) + width * (k + x + 1)] = 1;
+ qrframe[(y + 4) + width * (k + x)] = 1;
+ qrframe[(y + x + 1) + width * (k + 4)] = 1;
+ // alignment blocks
+ if (version > 1) {
+ t = adelta[version];
+ y = width - 7;
+ for (;;) {
+ x = width - 7;
+ while (x > t - 3) {
+ putalign(x, y);
+ if (x < t)
+ x -= t;
+ if (y <= t + 9)
+ y -= t;
+ putalign(6, y);
+ putalign(y, 6);
+ // single black
+ qrframe[8 + width * (width - 8)] = 1;
+ // timing gap - mask only
+ for (y = 0; y < 7; y++) {
+ setmask(7, y);
+ setmask(width - 8, y);
+ setmask(7, y + width - 7);
+ for (x = 0; x < 8; x++) {
+ setmask(x, 7);
+ setmask(x + width - 8, 7);
+ setmask(x, width - 8);
+ // reserve mask-format area
+ for (x = 0; x < 9; x++)
+ setmask(x, 8);
+ setmask(x + width - 8, 8);
+ setmask(8, x);
+ for (y = 0; y < 7; y++)
+ setmask(8, y + width - 7);
+ // timing row/col
+ for (x = 0; x < width - 14; x++)
+ if (x & 1) {
+ setmask(8 + x, 6);
+ setmask(6, 8 + x);
+ else {
+ qrframe[(8 + x) + width * 6] = 1;
+ qrframe[6 + width * (8 + x)] = 1;
+ if (version > 6) {
+ t = vpat[version - 7];
+ k = 17;
+ for (x = 0; x < 6; x++)
+ for (y = 0; y < 3; y++, k--)
+ if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
+ qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
+ qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
+ setmask(5 - x, 2 - y + width - 11);
+ setmask(2 - y + width - 11, 5 - x);
+ // sync mask bits - only set above for white spaces, so add in black bits
+ for (x = 0; x <= y; x++)
+ if (qrframe[x + width * y])
+ setmask(x, y);
+ // convert string to bitstream
+ // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
+ v = strinbuf.length;
+ // string to array
+ for (i = 0; i < v; i++)
+ eccbuf[i] = strinbuf.charCodeAt(i);
+ strinbuf = eccbuf.slice(0);
+ // calculate max string length
+ x = datablkw * (neccblk1 + neccblk2) + neccblk2;
+ if (v >= x - 2) {
+ v = x - 2;
+ if (version > 9)
+ v--;
+ // shift and repack to insert length prefix
+ i = v;
+ if (version > 9) {
+ strinbuf[i + 2] = 0;
+ strinbuf[i + 3] = 0;
+ while (i--) {
+ t = strinbuf[i];
+ strinbuf[i + 3] |= 255 & (t << 4);
+ strinbuf[i + 2] = t >> 4;
+ strinbuf[2] |= 255 & (v << 4);
+ strinbuf[1] = v >> 4;
+ strinbuf[0] = 0x40 | (v >> 12);
+ strinbuf[i + 1] = 0;
+ strinbuf[i + 2] |= 255 & (t << 4);
+ strinbuf[i + 1] = t >> 4;
+ strinbuf[1] |= 255 & (v << 4);
+ strinbuf[0] = 0x40 | (v >> 4);
+ // fill to end with pad pattern
+ i = v + 3 - (version < 10);
+ while (i < x) {
+ strinbuf[i++] = 0xec;
+ // buffer has room if (i == x) break;
+ strinbuf[i++] = 0x11;
+ // calculate and append ECC
+ // calculate generator polynomial
+ genpoly[0] = 1;
+ for (i = 0; i < eccblkwid; i++) {
+ genpoly[i + 1] = 1;
+ for (j = i; j > 0; j--)
+ genpoly[j] = genpoly[j] ?
+ genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
+ genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
+ for (i = 0; i <= eccblkwid; i++)
+ genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
+ // append ecc to data buffer
+ k = x;
+ for (i = 0; i < neccblk1; i++) {
+ appendrs(y, datablkw, k, eccblkwid);
+ y += datablkw;
+ k += eccblkwid;
+ for (i = 0; i < neccblk2; i++) {
+ appendrs(y, datablkw + 1, k, eccblkwid);
+ y += datablkw + 1;
+ // interleave blocks
+ for (i = 0; i < datablkw; i++) {
+ for (j = 0; j < neccblk1; j++)
+ eccbuf[y++] = strinbuf[i + j * datablkw];
+ for (j = 0; j < neccblk2; j++)
+ eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
+ for (i = 0; i < eccblkwid; i++)
+ for (j = 0; j < neccblk1 + neccblk2; j++)
+ eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
+ strinbuf = eccbuf;
+ // pack bits into frame avoiding masked area.
+ x = y = width - 1;
+ k = v = 1; // up, minus
+ /* inteleaved data and ecc codes */
+ m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
+ for (i = 0; i < m; i++) {
+ for (j = 0; j < 8; j++, t <<= 1) {
+ if (0x80 & t)
+ do { // find next fill position
+ if (v)
+ x--;
+ x++;
+ if (k) {
+ if (y != 0)
+ y--;
+ x -= 2;
+ k = !k;
+ if (x == 6) {
+ y = 9;
+ if (y != width - 1)
+ y++;
+ y -= 8;
+ v = !v;
+ } while (ismasked(x, y));
+ // save pre-mask copy of frame
+ strinbuf = qrframe.slice(0);
+ t = 0; // best
+ y = 30000; // demerit
+ // for instead of while since in original arduino code
+ // if an early mask was "good enough" it wouldn't try for a better one
+ // since they get more complex and take longer.
+ for (k = 0; k < 8; k++) {
+ applymask(k); // returns black-white imbalance
+ x = badcheck();
+ if (x < y) { // current mask better than previous best?
+ y = x;
+ t = k;
+ if (t == 7)
+ break; // don't increment i to a void redoing mask
+ qrframe = strinbuf.slice(0); // reset for next pass
+ if (t != k) // redo best mask - none good enough, last wasn't t
+ applymask(t);
+ // add in final mask/ecclevel bytes
+ y = fmtword[t + ((ecclevel - 1) << 3)];
+ // low byte
+ for (k = 0; k < 8; k++, y >>= 1)
+ if (y & 1) {
+ qrframe[(width - 1 - k) + width * 8] = 1;
+ if (k < 6)
+ qrframe[8 + width * k] = 1;
+ qrframe[8 + width * (k + 1)] = 1;
+ // high byte
+ for (k = 0; k < 7; k++, y >>= 1)
+ qrframe[8 + width * (width - 7 + k)] = 1;
+ if (k)
+ qrframe[(6 - k) + width * 8] = 1;
+ qrframe[7 + width * 8] = 1;
+ return qrframe;
+ var _canvas = null;
+ var api = {
+ get ecclevel() {
+ return ecclevel;
+ set ecclevel(val) {
+ ecclevel = val;
+ get size() {
+ return _size;
+ set size(val) {
+ _size = val
+ get canvas() {
+ return _canvas;
+ set canvas(el) {
+ _canvas = el;
+ getFrame: function(string) {
+ return genframe(string);
+ //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
+ utf16to8: function(str) {
+ var out, i, len, c;
+ out = "";
+ len = str.length;
+ for (i = 0; i < len; i++) {
+ c = str.charCodeAt(i);
+ if ((c >= 0x0001) && (c <= 0x007F)) {
+ out += str.charAt(i);
+ } else if (c > 0x07FF) {
+ out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+ out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+ out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+ out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+ return out;
+ /**
+ * 新增$this参数,传入组件的this,兼容在组件中生成
+ draw: function(str, canvas, cavW, cavH, cavColor, haveImg, imageUrl, imageSize, $this, cb = function() {}, ecc) {
+ ecclevel = ecc || ecclevel;
+ canvas = canvas || _canvas;
+ if (!canvas) {
+ console.warn('No canvas provided to draw QR code in!')
+ let pre_background = "#ffffff";
+ var size = Math.min(cavW, cavH);
+ str = that.utf16to8(str); //增加中文显示
+ var frame = that.getFrame(str);
+ // 组件中生成qrcode需要绑定this
+ var ctx = uni.createCanvasContext(canvas, $this);
+ var px = Math.round(size / (width ));
+ var roundedSize = px * (width);
+ // var px = 1 ;
+ // var roundedSize = px * (width + 8) ;
+ //var roundedSize = 0 ;
+ //var offset = Math.floor((size - roundedSize) / 2);
+ var offset = 0 ;
+ size = roundedSize;
+ //ctx.clearRect(0, 0, cavW, cavW);
+ ctx.setFillStyle(pre_background)
+ ctx.fillRect(0, 0, cavW, cavW);
+ ctx.setFillStyle(cavColor);
+ for (var i = 0; i < width; i++) {
+ for (var j = 0; j < width; j++) {
+ if (frame[j * width + i]) {
+ ctx.fillRect(px * ( i) + offset, px * ( j) + offset, px, px);
+ //画图片
+ if (haveImg) {
+ var x = Number(((cavW - imageSize - 14) / 2).toFixed(2));
+ var y = Number(((cavH - imageSize -14) / 2).toFixed(2));
+ drawRoundedRect(ctx, x, y, imageSize, imageSize, imageSize / 2, 6, true, true)
+ let isNetImg = false;
+ isNetImg = imageUrl.substr(0, 4) == 'http' ? true : false;
+ if (isNetImg) {
+ //网络图片下载到本地
+ uni.getImageInfo({
+ src: imageUrl,
+ ctx.drawImage(res.path, x, y, imageSize, imageSize);
+ //--增加绘制完成回调
+ ctx.draw(false, function() {
+ cb();
+ ctx.drawImage(imageUrl, x, y, imageSize, imageSize);
+ // 画圆角矩形
+ function drawRoundedRect(ctxi, x, y, width, height, r, lineWidth, fill, stroke) {
+ ctxi.setLineWidth(lineWidth);
+ ctxi.setFillStyle(pre_background);
+ ctxi.setStrokeStyle(pre_background);
+ ctxi.beginPath(); // draw top and top right corner
+ ctxi.moveTo(x + r, y);
+ ctxi.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner
+ ctxi.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner
+ ctxi.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner
+ ctxi.arcTo(x, y, x + r, y, r);
+ ctxi.closePath();
+ if (fill) {
+ ctxi.fill();
+ if (stroke) {
+ ctxi.stroke();
+ //TODO handle the exception
+ module.exports = {
+ api
+})();
@@ -0,0 +1,78 @@
+ <view class="cart u-flex u-row-center" @click="$u.route('/shopping/cart',{type:'reLaunch'})">
+ <u-icon size="52rpx" class="icon" :name="staticUrl+'/img/cartico.png'"></u-icon>
+ <u-badge type="error" max="99" :value="cartTotal" :absolute="true" :offset="[0,5]"></u-badge>
+ <u-toast ref="uToast"></u-toast>
+ name:'cartfixed',
+ cartTotal:0,
+ mounted(){
+ this.getCartList()
+ getCartList(isAdd){
+ this.$u.api.cartList().then(res=>{
+ if(isAdd){
+ if(res.data.total==this.cartTotal){
+ title:'已在购物车',
+ // this.$refs.uToast.show({
+ // type:"success",
+ // message:'已在购物车'
+ title:'加入成功',
+ // message:'加入成功'
+ this.cartTotal = res.data.total;
+ console.log('getCartList',res);
+ console.log('getCartList',err.data);
+ addToCart(num){
+ // this.cartTotal += num;
+ console.log('addToCart',num);
+.cart{
+ position: fixed;
+ width: 100rpx;
+ height: 100rpx;
+ right: 20rpx;
+ bottom: 300rpx;
+ box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.12);
@@ -0,0 +1,152 @@
+ <view class="tabbar">
+ <u-tabbar
+ :value="tabbarValue"
+ @change="tabbarChange"
+ :fixed="true"
+ inactiveColor="#666"
+ activeColor="#00A447"
+ :customStyle="{'padding-top':'5px'}"
+ :safeAreaInsetBottom="true"
+ <u-tabbar-item text="商城" >
+ <image
+ class="u-page__item__slot-icon"
+ slot="active-icon"
+ :src="staticUrl+'/img/tabbar-mall.png'"
+ ></image>
+ slot="inactive-icon"
+ :src="staticUrl+'/img/tabbar-mall-gray.png'"
+ </u-tabbar-item>
+ <u-tabbar-item text="分类" >
+ :src="staticUrl+'/img/tabbar-classify.png'"
+ :src="staticUrl+'/img/tabbar-classify-gray.png'"
+ <u-tabbar-item text="关于旭烁" >
+ :src="staticUrl+'/img/tabbar-xushuo-v2.png'"
+ :src="staticUrl+'/img/tabbar-xushuo-v2-gray.png'"
+ <!-- <u-tabbar-item text="购物车" >
+ src="/static/img/tabbar-car.png"
+ src="/static/img/tabbar-car-gray.png"
+ </u-tabbar-item> -->
+ <u-tabbar-item text="我的" >
+ :src="staticUrl+'/img/tabbar-my.png'"
+ :src="staticUrl+'/img/tabbar-my-gray.png'"
+ </u-tabbar>
+ name: 'tabbar',
+ props:{
+ tabbarIndexProps:{
+ type:Number,
+ default:0
+ tabbarValue:0,
+ 'tabbarIndexProps': {
+ handler(newVal, oldVal) {
+ // let pages = getCurrentPages();
+ // console.log('pages============',pages);
+ this.tabbarValue = newVal
+ immediate: true
+ console.log('onLoad tabbarIndex',this.tabbarIndex);
+ console.log('tabbarIndex',this.tabbarIndex);
+ this.tabbarValue = this.tabbarIndex
+ tabbarChange(name){
+ console.log('name====',name);
+ const tabBarRoutes = {
+ 0: '/pages/index/index',
+ 1: {
+ url: '/shopping/producTypetList',
+ query: {
+ navType: 'navigateTo'
+ 2: '/xushuo/xushuo',
+ // 3: '/shopping/cart',
+ 3: '/center/center'
+ const targetRoute = tabBarRoutes[name];
+ if (typeof targetRoute === 'string') {
+ uni.reLaunch({url: targetRoute});
+ } else if (typeof targetRoute === 'object') {
+ if(targetRoute.query.navType=='navigateTo'){
+ this.$u.route(targetRoute.url, targetRoute.query);
+ return
+ let queryParams = uni.$u.queryParams(targetRoute.query);
+ uni.reLaunch({url: targetRoute.url+queryParams});
+ // this.tabbarValue = name
+.u-page__item__slot-icon{
+ width: 40rpx;
+ height: 40rpx;
+ &.big{
+ transform: scale(1.5) translateY(-0.5em);
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <script>
+ var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+ CSS.supports('top: constant(a)'))
+ document.write(
+ '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+ (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+ </script>
+ <title></title>
+ <!--preload-links-->
+ <!--app-context-->
+ </head>
+ <body>
+ <div id="app"><!--app-html--></div>
+ <script type="module" src="/main.js"></script>
+ </body>
+</html>
@@ -0,0 +1,82 @@
+import App from './App'
+// #ifndef VUE3
+import Vue from 'vue'
+Vue.config.productionTip = false
+App.mpType = 'app'
+try {
+ function isPromise(obj) {
+ return (
+ !!obj &&
+ (typeof obj === "object" || typeof obj === "function") &&
+ typeof obj.then === "function"
+ );
+ // 统一 vue2 API Promise 化返回格式与 vue3 保持一致
+ uni.addInterceptor({
+ returnValue(res) {
+ if (!isPromise(res)) {
+ return new Promise((resolve, reject) => {
+ res.then((res) => {
+ if (res[0]) {
+ reject(res[0]);
+ resolve(res[1]);
+} catch (error) { }
+import uView from '@/uni_modules/uview-ui'
+Vue.use(uView)
+import { commonConfig } from './common/config';
+Vue.prototype.$commonConfig = commonConfig;
+import './utils/filter'
+import store from '@/store';
+// 引入uView提供的对vuex的简写法文件
+let vuexStore = require('@/store/$u.mixin.js');
+Vue.mixin(vuexStore);
+const app = new Vue({
+ ...App,
+ store
+})
+//uviewui v1
+// // http拦截器,将此部分放在new Vue()和app.$mount()之间,才能App.vue中正常使用
+// import httpInterceptor from '@/common/http.interceptor.js';
+// Vue.use(httpInterceptor, app);
+// 引入请求封装,将app参数传递到配置中
+require('./common/request.js')(app)
+// http接口API抽离,免于写url或者一些固定的参数
+import httpApi from '@/common/http.api.js';
+Vue.use(httpApi, app);
+app.$mount()
+// #endif
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+ const app = createSSRApp(App)
+ app
@@ -0,0 +1,100 @@
+{
+ "name" : "xusfood_h5",
+ "appid" : "__UNI__D90E79B",
+ "description" : "",
+ "versionName" : "1.0.0",
+ "versionCode" : "100",
+ "transformPx" : false,
+ /* 5+App特有相关 */
+ "app-plus" : {
+ "usingComponents" : true,
+ "nvueStyleCompiler" : "uni-app",
+ "compilerVersion" : 3,
+ "splashscreen" : {
+ "alwaysShowBeforeRender" : true,
+ "waiting" : true,
+ "autoclose" : true,
+ "delay" : 0
+ /* 模块配置 */
+ "modules" : {},
+ /* 应用发布信息 */
+ "distribute" : {
+ /* android打包配置 */
+ "android" : {
+ "permissions" : [
+ "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+ "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+ "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+ "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+ "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+ "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+ "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+ "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+ "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+ "<uses-feature android:name=\"android.hardware.camera\"/>",
+ "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+ /* ios打包配置 */
+ "ios" : {},
+ /* SDK配置 */
+ "sdkConfigs" : {}
+ /* 快应用特有相关 */
+ "quickapp" : {},
+ /* 小程序特有相关 */
+ "mp-weixin" : {
+ "appid" : "",
+ "setting" : {
+ "urlCheck" : false,
+ "minified" : true,
+ "postcss" : true
+ "permission" : {
+ "scope.userLocation" : {
+ "desc" : "你的位置信息将用于小程序位置接口的效果展示"
+ "requiredPrivateInfos" : [ "getLocation", "chooseLocation" ],
+ "lazyCodeLoading" : "requiredComponents"
+ "mp-alipay" : {
+ "usingComponents" : true
+ "mp-baidu" : {
+ "mp-toutiao" : {
+ "uniStatistics" : {
+ "enable" : false
+ "vueVersion" : "2",
+ "h5" : {
+ "devServer" : {
+ "proxy" : {
+ "/api" : {
+ "target" : "https://xusfoodapi.hw.hongweisoft.com/appapi/app", //请求的目标域名
+ "changeOrigin" : true,
+ "secure" : false,
+ "pathRewrite" : {
+ //使用代理; 告诉他你这个连接要用代理
+ "^/api" : ""
+ "sdkConfigs" : {
+ "maps" : {}
+ "template" : "template.h5.html"
@@ -0,0 +1,18 @@
+export const systemInfo = {
+ data: () => ({
+ statusBarHeight: 0,
+ navigationBarHeight: 0,
+ navHeight: 0,
+ windowHeight: 0, // 可使用窗口高度
+ }),
+ // 获取设备信息
+ getSystemInfo() {
+ this.statusBarHeight = getApp().globalData.statusBarHeight
+ this.navigationBarHeight = getApp().globalData.navigationBarHeight
+ this.windowHeight = uni.getSystemInfoSync().windowHeight
+ this.navHeight = getApp().globalData.navHeight
@@ -0,0 +1,24 @@
+ "uni-app": {
+ "scripts": {
+ "build:build64": {
+ "title": "build:build64",
+ "env": {
+ "UNI_PLATFORM": "h5",
+ "H_NODE_ENV": "development",
+ "H_BASE_URL": "https://xusfoodapi.hw.hongweisoft.com/appapi",
+ "H_UP_FILE_URL": "https://xusfoodapi.hw.hongweisoft.com/appapi"
+ "build:buildOnline": {
+ "title": "build:online",
+ "H_NODE_ENV": "production",
@@ -0,0 +1,146 @@
+ "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "首页",
+ "navigationStyle": "custom"
+ "path": "pages/login/login",
+ "navigationBarTitleText": "登录",
+ "subPackages": [
+ "root": "center",
+ "pages": [
+ "path": "center",
+ "navigationBarTitleText": "我的",
+ "path": "resetpass",
+ "navigationBarTitleText": "修改密码",
+ "path": "dealList",
+ "navigationBarTitleText": "我的业绩",
+ "path": "mystore",
+ "navigationBarTitleText": "我的门店",
+ "path": "client",
+ "navigationBarTitleText": "我的会员",
+ "path": "paycode",
+ "navigationBarTitleText": "推广码",
+ "root": "shopping",
+ "path": "productsearch",
+ "navigationBarTitleText": "产品搜索",
+ "path": "producTypetList",
+ "navigationBarTitleText": "产品",
+ "path": "productdetails",
+ "navigationBarTitleText": "产品详情",
+ "enableShareAppMessage": true, // 允许分享
+ "path": "cart",
+ "navigationBarTitleText": "购物车",
+ "path": "submitorder",
+ "path": "paysuccess",
+ "navigationBarTitleText": "支付成功",
+ "path": "order",
+ "navigationBarTitleText": "我的订单",
+ "path": "orderdetails",
+ "navigationBarTitleText": "订单详情",
+ "preloadRule": {
+ "shopping/productList": {
+ "network": "all",
+ "packages": ["__APP__"]
+ "xushuo/xushuo": {
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "uni-app",
+ "navigationBarBackgroundColor": "#F8F8F8",
+ "backgroundColor": "#F8F8F8"
+ "uniIdRouter": {}
@@ -0,0 +1,232 @@
+ <view class="" :style="{height: navHeight+'px' }"></view>
+ <view class="user u-flex" @click="$u.route('/center/center',{})">
+ <u-icon name="account" color="#333" size="36"></u-icon>
+ <text class="name">{{vuex_user_info.username}}</text>
+ <!-- <text class="level">白金代理人</text> -->
+ <u-swiper
+ :list="bannerList"
+ indicator
+ height="130"
+ keyName="sliderImg"
+ indicatorMode="dot"
+ @click="bannerClick()"
+ ></u-swiper>
+ <view class="icon-nav u-flex u-row-around">
+ <!-- <view class="nav-item" @click="$u.route('/center/mybalance')">
+ <u--image class="image" :src="staticUrl+'/img/index-nav-4.png'" width="101rpx" height="101rpx"></u--image>
+ 需要删除
+ <view class="nav-item" @click="$u.route('/shopping/producTypetList',{})">
+ <u--image class="image" :src="staticUrl+'/img/index-nav-1.png'" width="101rpx" height="101rpx"></u--image>
+ 选品
+ <view class="nav-item" @click="$u.route('/shopping/cart',{})">
+ <u--image class="image" :src="staticUrl+'/img/index-nav-2.png'" width="101rpx" height="101rpx"></u--image>
+ 购物车
+ <view class="nav-item" @click="$u.route('/center/dealList',{})">
+ <u--image class="image" :src="staticUrl+'/img/index-nav-3.png'" width="101rpx" height="101rpx"></u--image>
+ 我的业绩
+ <view class="nav-item" @click="$u.route('/center/client',{type:'reLaunch'})">
+ 我的会员
+ <view class="nav-item" @click="$u.route('/center/mystore',{type:'reLaunch'})">
+ 我的门店
+ <view class="hot-product">
+ <view class="single-til u-flex u-row-between">
+ <text class="text">热销产品</text>
+ <!-- <view class="u-flex" @click="$u.route('/shopping/productList',{typeId:1,typeName:'热销产品',type:'redirectTo'})">
+ <text class="more-text">更多</text>
+ <u-icon name="arrow-right" color="#676767" size="24rpx"></u-icon>
+ <view class="product u-flex" @click="$u.route('/shopping/productdetails',{id:item.id})" v-for="item in hotGoods" :key="item.id">
+ <u--image :showLoading="true" :src="item.mainImg" width="180rpx" height="180rpx"></u--image>
+ <view class="name ellipsis-2">{{item.goodsName}}</view>
+ <view class="u-flex u-row-between">
+ <view class="up">
+ <view class="" v-if="vuex_member_info.priceType>1">
+ <text class="price">¥ <text class="price-num">{{item.vipPrice}}</text></text>
+ <text class="vip-icon">VIP</text>
+ <view class="" v-else>
+ <text class="price">¥ <text class="price-num">{{item.salePrice}}</text></text>
+ <view class="down">
+ <!-- <text class="discount">8.8折</text> -->
+ <text v-if="vuex_member_info.priceType>1" class="original-price gray line-through">¥ {{item.salePrice}}</text>
+ <!-- <text class="sales gray">销量 {{item.salePrice}}</text> -->
+ <!-- <u--image :showLoading="false" @click.native.stop="addCart(item.id)" :src="staticUrl+'/img/add.png'" width="48rpx" height="48rpx"></u--image> -->
+ <view class="btn">去看看</view>
+ <view class="experience">
+ <text class="text">体验店分布</text>
+ <!-- <view class="u-flex">
+ <text class="more-text" @click="$u.route('/xushuo/experience')">更多</text>
+ <view class="index-img">
+ <u--image class="image" :src="indexData.uxImg" width="100%" height="500rpx" radius="15rpx"></u--image>
+ <!-- <tabbar :tabbarIndexProps='0' /> -->
+ import tabbar from "../../components/tabbar.vue";
+ tabbar,
+ advantageSize:14,
+ bannerList: [],
+ hotGoods:[],
+ xsIntro:{},
+ indexData:{},
+ // let userInfo = uni.getStorageSync('userInfo');
+ // console.log('userInfo',userInfo);
+ this.swiperList();
+ that.getCenterImg();
+ },200);
+ // console.log('statusBarHeight',this.statusBarHeight);
+ // console.log('navigationBarHeight',this.navigationBarHeight);
+ // console.log('windowHeight',this.windowHeight);
+ // console.log('navHeight',this.navHeight);
+ this.getHotGoods();
+ this.getIndexData();
+ swiperList(){
+ this.$u.api.swiperList({postion:4}).then(res=>{
+ this.bannerList = res.data.rows;
+ // console.log('res',res.data.rows);
+ console.log('swiperList',err.data);
+ bannerClick(e){
+ // console.log('e',e);
+ // console.log('bannerClick',this.bannerList[e]);
+ let item = this.bannerList[e];
+ uni.$u.route('/xushuo/dynamicdetails', {
+ type: 'swiperDetail',
+ id: item.id
+ getCenterImg(){
+ this.$u.api.swiperList({postion:2}).then(res=>{
+ this.centerImg = res.data.rows;
+ centerImgClick(e){
+ let item = this.centerImg[e];
+ getHotGoods(){
+ this.$u.api.topGoodList({pageNum:1,pageSize:1}).then(res=>{
+ this.hotGoods = res.data.rows;
+ console.log('res',res);
+ console.log('getHotGoods',err.data);
+ getIndexData(){
+ this.$u.api.indexData().then(res=>{
+ this.indexData = res.data;
+ console.log('this.indexData',this.indexData);
+ padding-top: 0;
+.page-bg{
+ height: 100vh;
+.icon-nav{
+ margin: 50rpx auto;
+.user{
+ margin: 0 20rpx 0 24rpx;
+ .level{
+.experience{
+ margin-bottom: 50rpx;
+.product{
+ border-bottom: 0;
+ .btn{
+ padding: 14rpx 40rpx;
+ background: linear-gradient(90deg, #00D17D 0%, #00A447 100%);
+ right: -20rpx;
+ border-top-left-radius: 50rpx;
+ border-bottom-left-radius: 50rpx;
@@ -0,0 +1,226 @@
+ <view class="body" :style="{height:screenHeight}">
+ <view class="header">
+ <view class="circle circle1"></view>
+ <view class="circle circle2"></view>
+ <view class="circle circle3"></view>
+ <view class="en-text">hello</view>
+ <view class="cn-text">欢迎登录!</view>
+ <view class="login-box">
+ <u-form-item label="" prop="account" ref="account" >
+ v-model="form.account"
+ border="surround"
+ placeholder="输入账号"
+ <u-form-item label="" prop="password" ref="password" >
+ placeholder="输入密码"
+ text="登录"
+ <view class="tip">
+ 提示:初始账号和密码由管理员设置,请与管理人员联系
+ bname:'旭烁集团',
+ //屏幕高度
+ screenHeight: "",
+ backUrl:null,
+ account:'',
+ password:''
+ account: {
+ message: '请填写账号',
+ password: {
+ message: '请填写密码',
+ // 测试环境填充用户名密码
+ if(process.env.NODE_ENV=='development'){
+ this.form.account = 'dls1234';
+ this.form.password = '654321';
+ uni.getSystemInfo({
+ success: (res) => {
+ this.screenHeight = res.windowHeight + "px"
+ uni.getStorage({
+ console.log('getStorage',res);
+ complete(res) {
+ if(res.data){
+ that.backUrl = '/'+res.data;
+ that.backUrl = '/index/index';
+ console.log('backUrl',that.backUrl);
+ //onReady 为uni-app支持的生命周期之一
+ submit(){
+ console.log('form',this.form);
+ this.$u.api.login(this.form).then(res=>{
+ uni.reLaunch({url: this.backUrl});
+.body {
+.header{
+ height: 368rpx;
+ background: linear-gradient(325deg, #FCFFFD 0%, #ECFFF4 100%);
+ .circle{
+ &.circle1{
+ width: 201rpx;
+ height: 149rpx;
+ background: linear-gradient(230deg, #E5FFEC 0%, #C0FFD7 100%);
+ left:-20rpx;
+ bottom: -50rpx;
+ &.circle2{
+ width: 148rpx;
+ height: 148rpx;
+ background: linear-gradient(215deg, #E7FFEE 0%, #BFFFDB 100%);
+ opacity: 0.6;
+ right:158rpx;
+ top: 80rpx;
+ &.circle3{
+ width: 232rpx;
+ height: 237rpx;
+ background: linear-gradient(215deg, #F1FFF8 0%, #C3FFD3 100%);
+ right:-32rpx;
+ top: -65rpx;
+ left: 40rpx;
+ bottom: 80rpx;
+ .en-text{
+ font-size: 58rpx;
+ line-height: 81rpx;
+ font-weight: bold;
+ width: fit-content;
+ z-index: 5;
+ &::before{
+ content: '';
+ width: 104%;
+ height: 10rpx;
+ background-color: #FFB100;
+ left: -2%;
+ bottom: 10rpx;
+ z-index: -1;
+ .cn-text{
+ font-size: 50rpx;
+ line-height: 70rpx;
+.login-box{
+ top: -24rpx;
+ padding: 60rpx 40rpx;
+ border-top-left-radius: 24rpx;
+ border-top-right-radius: 24rpx;
+ // box-shadow: 0rpx 0rpx 16rpx 0rpx rgba(0,28,12,0.06);
+.tip{
+ bottom: 108rpx;
+ width: 100vw;
@@ -0,0 +1,471 @@
+ title="购物车"
+ :autoBack="false"
+ <view class="product-list">
+ <view class="til u-flex u-row-between" :style="{top:navHeight+'px'}">
+ <u-checkbox-group @change="allCheckboxChange">
+ <u-checkbox shape="circle" :checked="allChecked" :name="allCheckbox.name" label="商品列表"></u-checkbox>
+ </u-checkbox-group>
+ <text class="delbtn" @click="delSelect" v-if="selectGoods.length>0">删除</text>
+ <u-checkbox-group placement="column" @change="checkboxChange" >
+ <view v-for="(item,index) in dataList" :key="item.id" class="product">
+ <u-swipe-action>
+ <u-swipe-action-item :show="item.show" :index="index" @click="swipeClick(index)" :options="options">
+ <view class="swipe-action">
+ <view class="swipe-action__content u-flex">
+ <u-checkbox shape="circle" :disabled="item.quantity>item.stock" activeColor="#00A447" :key="index" :name="item.name" :checked="item.checked" @change="toggleCheck(index)" class="checkbox" />
+ <u--image @click="$u.route('/shopping/productdetails',{id:item.goodsId})" :showLoading="true" :src="item.mainImg" width="180rpx" height="180rpx"></u--image>
+ <view class="name ellipsis-2" @click="$u.route('/shopping/productdetails',{id:item.goodsId})">{{item.goodsName}}</view>
+ <!-- <text class="sales gray">销量999+</text> -->
+ <u-number-box
+ v-model="item.quantity"
+ :max="item.stock"
+ :asyncChange="true"
+ @change="changeQuantity(index, $event)" integer>
+ </u-number-box>
+ <view class="tip" v-if="item.quantity>item.stock">库存不足</view>
+ </u-swipe-action-item>
+ </u-swipe-action>
+ <view class="recommend" v-if="dataList.length>=total">
+ <view class="til u-flex">
+ <u-icon name="heart" color="#FF3C3F" size="28"></u-icon>
+ <text class="text">为你推荐</text>
+ <view class="half-product u-flex u-flex-wrap u-row-between">
+ <view class="half-product-item" v-for="data in 10" :key="data">
+ <u--image :showLoading="true" src="http://placekitten.com/340/340" width="340rpx" height="340rpx"></u--image>
+ <view class="name ellipsis-2">
+ 贵州特色酒习酒·百亿纪念酒53度500ML
+ <text class="price red">¥ <text class="price-num">{{data+1000}}</text></text>
+ <u--image :showLoading="false" :src="staticUrl+'/img/add.png'" width="48rpx" height="48rpx"></u--image>
+ <view class="cart-bottom">
+ <view class="inner u-flex u-row-between">
+ <view class="left u-flex">
+ <view class="checkbox">
+ <u-checkbox shape="circle" :checked="allChecked" :name="allCheckbox.name" label="全选"></u-checkbox>
+ <view class="total-price">
+ 合计:{{totalPrice}}
+ <view class="btn active" v-if="selectGoods.length>0&&cansubmit" @click="submitorder">去结算</view>
+ <view class="btn" v-else>去结算</view>
+ mixins: [MescrollMixin,systemInfo], // 使用mixin
+ cansubmit:true,
+ hasAddr:false,
+ buyNowId:null,//立即购买id
+ buyNowName:'',//立即购买商品
+ allCheckbox:{name: '全选'},
+ options:[
+ text: '删除',
+ style: {
+ backgroundColor: '#FF3C3F',
+ 'padding-left':'10px'
+ // 购物车列表
+ dataList: [],
+ total:10,
+ deleteArr:[],
+ // 'tabbarIndexProps': {
+ // handler(newVal, oldVal) {
+ // this.tabbarValue = newVal
+ // immediate: true
+ // 是否全选
+ allChecked() {
+ return this.dataList.every(item => item.checked)
+ // 商品合计价格
+ selectGoods() {
+ let selectGoods = [];
+ this.dataList.forEach(item => {
+ if (item.checked) {
+ selectGoods.push(item)
+ return selectGoods
+ totalPrice() {
+ return this.dataList.reduce((total, item) => {
+ let price = null;
+ if(that.vuex_member_info.priceType>1){
+ price = item.vipPrice
+ price = item.salePrice
+ total += price * item.quantity;
+ return total;
+ }, 0).toFixed(2);
+ onLoad(page) {
+ this.buyNowId = page.buyNowId;
+ this.buyNowName = page.buyNowName;
+ this.getAddrList();
+ // this.shopNewsType();
+ let params = {
+ pageNum : page.num,
+ pageSize : page.size,
+ //如果从立即购买中过来
+ if(this.buyNowName){
+ params.goodsName = this.buyNowName
+ // console.log('this.params',params);
+ this.$u.api.cartList(params).then(data => {
+ curPageData = curPageData.map( item =>{
+ item.checked = false
+ return item
+ this.total = totalSize;
+ uni.reLaunch({url: '/pages/index/index'});
+ console.log('leftClick',e);
+ // 切换全选状态
+ allCheckboxChange(n){
+ // console.log('allCheckboxChange',n[0]);
+ console.log('allCheckboxChange',n);
+ let selectAll = n[0]?true:false;
+ if(item.quantity<=item.stock){
+ item.checked = selectAll
+ checkboxChange(n){
+ // console.log('checkboxChange',n);
+ // 切换商品选中状态
+ toggleCheck(index) {
+ // console.log('toggleCheck',index);
+ this.dataList[index].checked = !this.dataList[index].checked
+ // 改变商品数量
+ changeQuantity(index, value) {
+ this.changeQuantityApi(this.dataList[index].id,value.value,index)
+ changeQuantityApi(id,num,index){
+ this.$u.api.updateQuantity({id:id,quantity:num}).then(res=>{
+ this.dataList[index].quantity = num;
+ console.log('changeQuantityApi',err.data);
+ // 结算
+ checkout() {
+ // TODO: 跳转到支付页面
+ swipeClick(index){
+ console.log('swipeClick',index);
+ // console.log('swipeClick',this.dataList[index]);
+ let item = this.dataList[index];
+ title: '温馨提示',
+ content: '确定要删除吗?',
+ success: res => {
+ // console.log('',that);
+ that.deleteArr = [];
+ that.deleteArr.push(item.id);
+ that.deleteCarts();
+ complete:()=>{}
+ deleteCarts(){
+ this.$u.api.deleteCarts(this.deleteArr).then(res=>{
+ console.log('deleteCarts',err.data);
+ submitorder(){
+ // console.log('submitorder',this.selectGoods);
+ this.cansubmit = false;
+ let param = {};
+ if(!this.hasAddr){
+ // type: 'error',
+ // title: '提示',
+ // message: "请先去'我的-地址管理'设置收货地址!",
+ // duration:3000
+ content: '请先设置地址!',
+ uni.$u.route('/center/addrlist', {
+ from: 'cart',
+ backUrl:'/shopping/cart'
+ param.goodsList = this.selectGoods.map(item=>{
+ return {goodsId:item.goodsId,quantity:item.quantity}
+ this.$u.vuex('cartGoods', param.goodsList);
+ uni.$u.route('/shopping/submitorder', {});
+ this.cansubmit = true;
+ // // console.log('param',param);取消在这个获取订单结算详情
+ // this.$u.api.getSettlement(param).then(res=>{
+ // console.log('submitorder',res.data);
+ // // return
+ // uni.$u.route('/shopping/submitorder', {
+ // selectGoods: JSON.stringify(res.data),
+ // console.log('getSettlement',err.data);
+ delSelect(){
+ let delArr = [];
+ delArr = this.selectGoods.map(item=>{
+ return item.id;
+ that.deleteArr = delArr
+ getAddrList(){
+ this.$u.api.addrList().then(res=>{
+ // this.dataList = res.data.rows;
+ if( res.data.total>0){
+ this.hasAddr = true;
+ this.hasAddr = false;
+ console.log('getAddrList',err.data);
+ page{
+ /* padding-top: 50px; */
+.product-list{
+ margin-bottom: 60rpx;
+ padding: 30rpx;
+ .product{
+ .product:last-child{
+ margin-bottom: 0;
+ padding-right: 0;
+ .til{
+ position: sticky;
+ z-index: 1001;
+ min-height: 30px;
+ .delbtn{
+ border: 1px solid #999;
+ color: #666;
+ padding: 5rpx 20rpx;
+ border-radius: 20rpx;
+ text-align: right;
+ color: #999;
+.recommend{
+ .text{margin-left: 10rpx;}
+.cart-bottom{
+ height: 98rpx;
+ .inner{
+ left: 0;
+ right: 0;
+ .total-price{
+ color: #333;
+ height: 80rpx;
+ line-height: 80rpx;
+ border-radius: 50rpx;
+ padding: 0 50rpx;
+ background-color: #eee;
+ &.active{
+ background-color: #FF3C3F;
@@ -0,0 +1,523 @@
+ title="我的订单"
+ :current="tabsCurrent"
+ <view class="order">
+ <view v-for="(item,index) in orderListWithClass" class="order-item"
+ :class="item.class"
+ @click="goOrderDetails(item.id)" :key="item.id">
+ <view class="top u-flex u-row-between">
+ <text>
+ <text v-if="item.orderType==2">[积分]</text>订单号:{{item.orderNum||''}}
+ </text>
+ <text class="status">{{item.status|filterOrderState}}</text>
+ <view class="center" :key="goodsKey">
+ <view v-if="i<2||item.showMore" v-for="(goods,i) in item.detailList" :key="goods.id" class="goods">
+ <u--image :showLoading="true" :src="goods.mainImg" width="128rpx" height="128rpx"></u--image>
+ <view class="name ellipsis-2">{{goods.goodsName}}</view>
+ <view class="specification ellipsis-1">规格:{{goods.specification}}</view>
+ <view class="right">
+ <u-icon name="arrow-right" color="#999" size="24"></u-icon>
+ <text class="price red">¥ <text class="price-num">{{goods.price}} </text></text>
+ <view class="quantity red">x {{goods.quantity}}</view>
+ <view class="comment-btn" v-if="item.status==3&&goods.isComment==0">
+ <text class="btn green" @click.stop="comment(item.id,goods.id,goods.goodsId)">
+ 评价
+ <view class="comment-btn" v-if="(item.status==3||item.status==6)&&goods.isComment==1">
+ <text class="btn" @click.stop="viewComment(goods.id,goods.goodsId,goods)">
+ 查看评价
+ <view class="bottom">
+ <view v-if="item.detailList.length>2" class="more" @click.stop="toggleMore(item,index)">
+ <text v-if="!item.showMore">查看剩余{{item.moreData.length}}件商品</text>
+ <text v-else>收起</text>
+ <view
+ class="btn"
+ :class="btn.class"
+ @click.stop="clickEven(btn.fun,item)"
+ v-for="(btn,index) in statusBtn[item.status]" :key="index">
+ {{btn.name}}
+ <view class="order-price">
+ 合计金额:
+ <text style="font-weight: 600;">
+ ¥ {{item.orderPrice}}
+ <text v-if="item.orderType==2"> + {{item.creditPrice}}积分</text>
+ goodsKey:1,
+ hasfetch:false,
+ tabsCurrent:1,
+ tabsList:[
+ {name:'全部',status:'',},
+ {name:'待付款',status:'0',badge:{isDot:false,value:null}},
+ {name:'待出库',status:'1',badge:{isDot:false,value:null}},
+ {name:'待收货',status:'2',badge:{isDot:false,value:null}},
+ {name:'待评价',status:'3',badge:{isDot:false,value:null}},
+ {name:'退款/售后',status:'7',badge:{isDot:false,value:null}}
+ status:'',
+ dataList:[],
+ statusBtn:{
+ 0:[{name:'取消订单',fun:'cancelOrder',class:''},{name:'去支付',fun:'pay',class:'red'}],
+ 1:[{name:'申请退款',fun:'refund',class:''}],
+ 2:[{name:'查看物流',fun:'logistics',class:''},{name:'确认收货',fun:'confirmReceipt',class:'green'}],
+ 3:[{name:'查看物流',fun:'logistics',class:''}],
+ //,{name:'评价',fun:'evaluate',class:'green'}
+ // 4:[{name:'查看物流',fun:'logistics',class:''},{name:'评价',fun:'evaluate',class:'green'}],
+ 5:[{name:'查看退款',fun:'viewRefund',class:'green'}],
+ 6:[{name:'查看物流',fun:'logistics',class:''}],
+ // ,{name:'查看评价',fun:'viewEvaluate',class:'green'}
+ 7:[{name:'查看退款',fun:'viewRefund',class:'green'}]
+ orderBadge:{
+ noPayNum:0,
+ deliverNum:0,
+ collectNum:0,
+ commentNum:0,
+ refundNum:0,
+ orderListWithClass() {
+ return this.dataList.map(order => {
+ // let spliceData = order.detailList.splice(0,2);
+ // let moreData = order.detailList.splice(2,order.detailList.length);
+ let moreData = order.detailList.slice(2);
+ // if(order.detailList.length>2){
+ // moreData = order.detailList;
+ ...order,
+ // spliceData:spliceData,
+ moreData:moreData,
+ showMore:false,
+ class: {
+ 0: 'status-0',
+ 1: 'status-1',
+ 2: 'status-2',
+ 3: 'status-3',
+ 4: 'status-4',
+ 5: 'status-5',
+ 6: 'status-6',
+ 7: 'status-7',
+ 8: 'status-8'
+ }[order.status] || ''
+ // console.log('page',page);
+ const status = page.status;
+ const index = this.tabsList.findIndex(item => item.status === status);
+ this.tabsCurrent = index>=0?index:0;
+ this.hasfetch&&this.reloadList()
+ },500);
+ this.statisticsOrder();
+ leftClick(){
+ uni.reLaunch({url: '/center/center'});
+ status : this.tabsList[this.tabsCurrent]?.status||'',
+ // status : this.tabsList[this.tabsCurrent].status,
+ this.$u.api.orderList(params).then(data => {
+ this.hasfetch = true;
+ // this.status = item.status;
+ this.tabsCurrent = item.index;
+ goOrderDetails(id){
+ uni.$u.route('/shopping/orderdetails', {
+ id: id
+ toggleMore(item,index){
+ // console.log('toggleMore',item);
+ this.orderListWithClass[index].showMore = !this.orderListWithClass[index].showMore
+ this.goodsKey++;
+ clickEven(fun,item){
+ // console.log('fun',fun);
+ let funObj = {
+ pay: this.pay,
+ logistics:this.logistics,
+ evaluate:this.evaluate,
+ refund:this.refund,
+ cancelOrder:this.cancelOrder,
+ viewRefund:this.viewRefund,
+ confirmReceipt:this.confirmReceipt,
+ viewEvaluate:this.viewEvaluate
+ // console.log('funObj[fun]',funObj[fun]);
+ if (fun in funObj) {
+ funObj[fun](item);
+ pay(item){
+ console.log('pay',item);
+ uni.$u.route('/shopping/pay', {
+ orderId: item.id,
+ // openid: that.vuex_wechatOpenid,
+ payAmount:item.orderPrice
+ logistics(item){
+ // console.log('logistics',item);
+ uni.$u.route('/shopping/distribution', {
+ orderId: item.id
+ evaluate(item){
+ uni.$u.route('/shopping/evaluate', {
+ comment(orderId,orderDetailId,goodsId){
+ uni.$u.route('/shopping/addcomment', {
+ orderId,
+ orderDetailId,
+ goodsId
+ viewComment(orderDetailId,goodsId,goods){
+ uni.$u.route('/shopping/viewcomment', {
+ goodsId,
+ goods:JSON.stringify(goods)
+ refund(item){
+ uni.$u.route('/shopping/refund', {
+ cancelOrder(item){
+ content: '确认取消吗!',
+ this.$u.api.cancelOrder({orderId:item.id}).then(res=>{
+ this.$refs.uToast.show({
+ message:res.msg,
+ complete() {
+ that.reloadList();
+ // uni.$u.toast(res.msg);
+ console.log('cancelOrder',err);
+ viewRefund(item){
+ uni.$u.route('/shopping/viewRefund', {
+ confirmReceipt(item){
+ content: '确认收货吗!',
+ this.$u.api.confirmReceipt({orderId:item.id}).then(res=>{
+ duration:2000,
+ console.log('confirmReceipt',err);
+ viewEvaluate(item){
+ statisticsOrder(){
+ this.$u.api.statisticsOrder().then(res=>{
+ let data = res.data || {};
+ this.orderBadge = Object.assign(this.orderBadge,data);
+ let {noPayNum,deliverNum,collectNum,commentNum,refundNum} = res.data;
+ let noPayNumindex = this.tabsList.findIndex(item => item.status == 0);
+ this.tabsList[1].badge.isDot = false;
+ this.tabsList[1].badge.value = noPayNum;
+ this.tabsList[2].badge.isDot = false;
+ this.tabsList[2].badge.value = deliverNum;
+ this.tabsList[3].badge.isDot = false;
+ this.tabsList[3].badge.value = collectNum;
+ this.tabsList[4].badge.isDot = false;
+ this.tabsList[4].badge.value = commentNum;
+ this.tabsList[5].badge.isDot = false;
+ this.tabsList[5].badge.value = refundNum;
+ // console.log('statisticsOrder',res.data);
+ // console.log('this.orderBadge',this.orderBadge);
+ console.log('memberInfo',err.data);
+.order-item{
+ padding: 30rpx 20rpx;
+ .top{
+ border-bottom: 0.5px solid #eee;
+ .status{font-size: 24rpx;}
+ .center{
+ .goods{
+ &:not(:last-child){
+ .comment-btn{
+ padding: 10rpx 50rpx;
+ border-radius: 28rpx;
+ border: 1px solid #333;
+ &.green{
+ border-color: #00A447;
+ .specification{
+ margin-top: 16rpx;
+ .right{
+ /deep/.u-icon{
+ justify-content: flex-end;
+ .order-price{
+ .bottom{
+ .left{
+ padding: 12rpx 20rpx;
+ &.red{border-color: #FF3C3F;}
+ &.green{color: #00A447;border-color: #00A447;}
+ &:not(:first-child){
+ .more{
+ &.status-0{.status{color:#FF3C3F;}}
+ &.status-1{.status{color:#FF3C3F;}}
+ &.status-2{.status{color:#0099EB;}}
+ &.status-3{.status{color:#00A447;}}
+ &.status-4{.status{color:#FF3C3F;}}
+ &.status-5{.status{color:#FF3C3F;}}
+ &.status-6{.status{color:#FF3C3F;}}
+ &.status-7{.status{color:#FFB100;}}
@@ -0,0 +1,469 @@
+ :title="title"
+ <view class="box distribution u-flex u-row-between"
+ v-if="orderDetails.status>0"
+ @click="$u.route('/shopping/distribution', {orderId: orderDetails.id})">
+ <u--image :showLoading="false" :src="staticUrl+'/img/car.png'" width="48rpx" height="48rpx"></u--image>
+ <view class="status">{{orderDetails.status|filterOrderState}}</view>
+ <view>{{orderDetails.createTime}}</view>
+ <view class="box addr u-flex u-row-between">
+ <text class="label" v-if="orderDetails.ifDefault">默认</text>{{orderDetails.address||''}}
+ <view class="center">{{orderDetails.receiveAdress}}</view>
+ <view class="bottom">{{orderDetails.receiveName}} {{orderDetails.receivePhone}}</view>
+ <!-- <u-icon name="arrow-right" color="#676767" size="20" @click="selectAddr"></u-icon> -->
+ <!-- <view class="payway page-wrap u-flex u-row-between">
+ <view class="left">支付方式</view>
+ <view class="u-flex">
+ 微信支付
+ <u-icon name="arrow-right" color="#676767" size="20"></u-icon>
+ <view class="box order-product">
+ <view class="product u-flex" v-for="(item,index) in orderDetails.detailList" :key="item.id">
+ <view class="info">
+ {{item.specification}}
+ {{item.unit}}
+ <view class="down u-flex u-row-between">
+ <text class="price">¥ <text class="price-num">{{item.price}}</text></text>
+ <u-number-box v-model="item.quantity" :disabled="true" @change="changeQuantity(index, $event)" integer></u-number-box>
+ <!-- <view class="page-wrap order-reduced">
+ <view class="reduced-item u-flex u-row-between u-border-bottom">
+ <text>优惠券</text>
+ <text class="label">已选一张</text>
+ <view class="right u-flex">
+ <text class="price red">-¥ <text class="price-num">1000</text></text>
+ <view class="reduced-item integral u-flex u-row-between">
+ <text>积分</text>
+ <text class="num">3676</text>
+ <text class="tip">满1000可用</text>
+ <text class="price red" v-if="useIntegral">-¥ <text class="price-num">1000</text></text>
+ <u-checkbox-group @change="integralCheckboxChange">
+ <u-checkbox shape="circle" activeColor="#02AB35" name="integral" ></u-checkbox>
+ <view class="box total">
+ <view class="total-item u-flex u-row-between u-border-bottom">
+ 商品金额
+ <view class="right red">
+ ¥ {{orderDetails.originalOrderPrice}}
+ <view class="total-item u-flex u-row-between">
+ 运费
+ <!-- <text class="gray">总重:{{totalWeight}}</text> -->
+ <text v-if="orderDetails.originalFreightPrice">+ ¥ {{orderDetails.originalFreightPrice}}</text>
+ <text v-else>免运费</text>
+ <view class="box order-info page-wrap" v-if="orderDetails.status>0">
+ <view class="order-info-item" v-for="(item,index) in orderInfo[orderDetails.status]" :key="index">
+ <text class="til">{{item.name}}</text>
+ <text class="con">
+ <text v-if="item.key=='payType'">{{orderDetails[item.key]|filterPayType}}</text>
+ <text v-else>{{orderDetails[item.key]}}</text>
+ <view class="total-price" v-if="orderDetails.status==0">
+ 待支付:<text class="red">¥ {{orderDetails.orderPrice}}</text>
+ <!-- <view class="btn" @click="submitOrder">去结算</view> -->
+ <view class="btn-wrap u-flex">
+ @click.stop="clickEven(btn.fun,orderDetails)"
+ v-for="(btn,index) in statusBtn[orderDetails.status]" :key="index">
+ id:'',
+ title:'订单详情',
+ orderDetails:{},
+ fromPage:'',
+ useIntegral:false,
+ theAddr:{},
+ isDefaultAddr:false,
+ 5:[{name:'查看退款',fun:'viewRefund',class:''}],
+ // 6:[{name:'查看退款',fun:'viewRefund',class:'green'}],
+ 7:[{name:'查看退款',fun:'viewRefund',class:''}]
+ orderInfo:{
+ 0:[],
+ 1:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ 2:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ 3:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ 4:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'}],
+ 5:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ 6:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ 7:[{name:'订单编号',key:'orderNum'},{name:'下单时间',key:'createTime'},{name:'支付方式',key:'payType'},{name:'支付时间',key:'payTime'}],
+ this.id = page.id;
+ this.getOrderDetails(this.id);
+ computed:{
+ payType(){
+ return function(value){
+ let payTypeList = ['', '微信', '余额', '积分']
+ return '-' + payTypeList[value]
+ console.log('orderdetails leftClick');
+ getOrderDetails(id){
+ this.$u.api.orderDetails({id:id}).then(res=>{
+ this.orderDetails = res.data
+ console.log('orderDetails',JSON.parse(JSON.stringify(res.data)));
+ console.log('getOrderDetails',err);
+ confirmReceipt:this.confirmReceipt
+ that.getOrderDetails(that.id);
+.box{
+.addr{
+ margin-bottom: 16rpx;
+ .label{
+ background-color: #FFEBEB;
+ border: 1px solid #FF5F62;
+ color: #FF5F62;
+ border-radius: 30rpx;
+ line-height: 50rpx;
+.order-reduced{
+ .reduced-item{
+ padding-bottom: 30rpx;
+ border-radius: 2px;
+ border: 1px solid #FF3C3F;
+ color: #FF3C3F;
+ padding: 0 10rpx;
+ .integral{
+ color: #02AB35;
+ margin-right: 16rpx;
+ .price{
+.total{
+ .total-item{
+ .gray{
+ margin-left: 15rpx;
+ height: 64rpx;
+ line-height: 64rpx;
+ padding: 0 40rpx;
+ background-color: transparent;
+ margin-left: 10rpx;
+ &.red{
+ border-color: #FF3C3F;
+ height: auto;
+ .info{
+ color: #666666;
+.order-info{
+ .order-info-item{
+ margin-right: 90rpx;
+.distribution{
+ margin-left: 16rpx;
+ .status{
+ line-height: 38rpx;
@@ -0,0 +1,62 @@
+ <view class="icon-wrap u-flex u-row-center">
+ <u-icon name="checkmark" color="#fff" size="80"></u-icon>
+ <view class="title">{{title}}</view>
+ <view class="full-btn" @click="btnClick">完成</view>
+ title:'付款成功'
+ this.fromPage = page.fromPage;
+ if(this.fromPage=='nocash'){
+ this.title = '兑换成功'
+ btnClick(){
+.pages{
+.icon-wrap{
+ width: 180rpx;
+ height: 180rpx;
+ margin: 120rpx auto 20rpx;
+.title{
+ line-height: 40rpx;
+ margin-bottom: 120rpx;
+.full-btn{
+ margin: 20rpx 10%;
@@ -0,0 +1,344 @@
+ title="产品"
+ <view class="search-wrap">
+ <u-search
+ placeholder="请输入搜索的商品"
+ :clearabled="true"
+ :showAction="false"
+ height="80rpx"
+ @search="search"
+ @custom="search"
+ @clear="reloadList"
+ bgColor="#fff"
+ borderColor="#00A447"
+ v-model="params.goodsName">
+ </u-search>
+ <view class="out-wrap u-flex u-col-top">
+ <view class="types">
+ <view class="type"
+ :class="{active:index==typesIndex}"
+ @click="typesClick(index)"
+ v-for="(item,index) in goodsTypeTree" :key="item.parentId+index">
+ {{item.name}}
+ <view class="second-types">
+ :class="{active:index==secondTypesIndex}"
+ @click="secondTypesClick(index)"
+ v-for="(item,index) in subTypeList" :key="item.id">
+ <u-icon
+ @click="showMoreSecondTypes"
+ v-if="subTypeListMore.length>0"
+ size="24px"
+ :name="showMoreSecondTypesIcon"></u-icon>
+ <view class="more-second-types u-flex" v-if="moreSecondTypes">
+ :class="{active:index==moreSecondTypesIndex}"
+ @click="moreSecondTypesClick(index)"
+ v-for="(item,index) in subTypeListMore" :key="item.id">
+ <mescroll-body class="mescroll-body" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+ <view class="page-wrap" @click="$u.route('/shopping/productdetails',{id:item.id})" v-for="item in dataList" :key="item.id">
+ <view class="hot">
+ <view class="product small-product u-flex">
+ <u--image :showLoading="true" :src="item.mainImg" width="160rpx" height="160rpx"></u--image>
+ <text class="sales gray">销量 {{item.saleCount}}</text>
+ <u--image :showLoading="false" @click.native.stop="addCart(item.id)" :src="staticUrl+'/img/add.png'" width="48rpx" height="48rpx"></u--image>
+ <!-- <tabbar :tabbarIndexProps="Number(1)" /> -->
+ <!-- <cartfixed ref="cartfixed" @getCartList="getCartList" /> -->
+ import tabbar from "../components/tabbar.vue";
+ import cartfixed from "../components/cartfixed.vue"
+ components: {
+ cartfixed
+ typesIndex:0,
+ secondTypesIndex:0,
+ moreSecondTypesIndex:null,
+ moreSecondTypes:false,
+ showMoreSecondTypesIcon:'list',
+ //菜单
+ goodsTypeTree:[],
+ //子菜单
+ subTypeList:[],
+ subTypeListMore:[],
+ // 列表数据
+ goodsName:'',
+ // isExplode:0,//是否爆款 0不是,1是
+ typesIndex:{
+ handler(val){
+ console.log('typesIndex',val);
+ this.params.goodsName = '' ;
+ let data = uni.$u.deepClone(this.goodsTypeTree[val]);
+ this.subTypeList = data.child.splice(0,3);
+ this.subTypeListMore= data.child;
+ this.secondTypesIndex = 0;
+ this.moreSecondTypesIndex = null;
+ immediate:false
+ this.getgoodsTypeTree();
+ // this.$refs.cartfixed.getCartList();
+ if(this.moreSecondTypesIndex||this.moreSecondTypesIndex==0){
+ this.params.typeId = this.subTypeListMore[this.moreSecondTypesIndex]?.id
+ this.params.typeId = this.subTypeList[this.secondTypesIndex]?.id
+ if(!this.params.typeId){
+ // this.getgoodsTypeTree();
+ return // 此处return,先获取xx
+ console.log('this.params.typeId',this.params.typeId);
+ this.$u.api.memberGoodList(this.params).then(data => {
+ search(e){
+ this.reloadList();
+ getgoodsTypeTree(){
+ this.$u.api.goodsTypeTree().then(res=>{
+ this.goodsTypeTree = res.data.filter(item=> item.child.length>0);
+ console.log('this.goodsTypeTree',this.goodsTypeTree);
+ let data = uni.$u.deepClone(this.goodsTypeTree[this.typesIndex]);
+ // console.log('this.goodsTypeTree',this.goodsTypeTree);
+ // console.log('data',data);
+ console.log('getgoodsTypeTree',err);
+ typesClick(index){
+ this.typesIndex = index;
+ secondTypesClick(index){
+ this.secondTypesIndex = index;
+ moreSecondTypesClick(index){
+ this.moreSecondTypesIndex = index;
+ this.secondTypesIndex = null;
+ showMoreSecondTypes(){
+ this.moreSecondTypes = !this.moreSecondTypes;
+ if(this.showMoreSecondTypesIcon=='list'){
+ this.showMoreSecondTypesIcon = 'close'
+ this.showMoreSecondTypesIcon = 'list'
+ addCart(id){
+ // console.log('addCart',id);
+ this.$u.api.addCart({goodsId:id}).then(res=>{
+ this.$refs.cartfixed.getCartList('isAdd');
+ console.log('addCart',err);
+ /* padding-top: 0; */
+.out-wrap{
+ flex: 1;
+.types{
+ width: 200rpx;
+ .type{
+ padding: 25rpx 0;
+ &:before{
+ width: 4rpx;
+ height: 1.6em;
+ top: 50%;
+ transform: translateY(-0.8em);
+.second-types{
+ font-size: 22rpx;
+ padding: 7rpx 22rpx;
+ border-radius: 22rpx;
+ background-color: #E5F6EC;
+ .type + .type{
+ .more-second-types{
+ bottom: -120rpx;
+ padding: 24px 20rpx;
+ z-index: 99;
+ box-shadow:
+ 0px 4.5px 3.6px rgba(0, 0, 0, 0.024),
+ 0px 12.5px 10px rgba(0, 0, 0, 0.035),
+ 0px 30.1px 24.1px rgba(0, 0, 0, 0.046),
+ 0px 100px 80px rgba(0, 0, 0, 0.07)
+ ;
+.mescroll-body{
@@ -0,0 +1,395 @@
+ title="产品详情"
+ v-if="details.slideImgList.length>0"
+ :list="details.slideImgList"
+ height="700rpx"
+ @change="e => currentNum = e.current"
+ :autoplay="false"
+ indicatorStyle="right: 20px"
+ slot="indicator"
+ class="indicator-num"
+ <text class="indicator-num__text">{{ currentNum + 1 }}/{{ details.slideImgList.length }}</text>
+ </u-swiper>
+ <view class="product-info view-wrap">
+ <view class="red">
+ <text class="price">¥ <text class="price-num">{{details.vipPrice}}</text></text>
+ <text class="exchangeCredit" v-if="details.isCredit==1">+{{details.exchangeCredit}}积分</text>
+ <text v-if="details.isCredit==1&&details.exchangeType==0" class="price">¥ <text class="price-num">0</text></text>
+ <text v-else class="price">¥ <text class="price-num">{{details.salePrice}}</text></text>
+ <view class="u-flex u-row-between gray">
+ <text v-if="vuex_member_info.priceType>1" class="line-through">¥ <text class="">{{details.salePrice}}</text></text>
+ <text class="">库存 {{details.stock}}</text>
+ <view class="name">{{details.goodsName}}</view>
+ <view class="specification info-line u-flex view-wrap" v-if="details.specification">
+ <view class="info-til">规格</view>
+ <view class="info-con u-flex">{{details.specification}}</view>
+ <view class="addr view-wrap">
+ <view class="addr-line u-flex">
+ <view class="info-til">快递</view>
+ <view class="info-con u-flex">
+ 包邮
+ <!-- <view class="addr-line u-flex">
+ <text class="addr-til">送至</text>
+ <view class="addr-con u-flex u-flex-1">
+ <view class="u-flex u-row-between u-flex-1">
+ <text>贵阳市 南明区</text>
+ <view class="comment view-wrap u-flex u-row-between">
+ <text class="til">评价</text>
+ <text class="num">({{details.commentNum}})</text>
+ <view class="right u-flex" @click="$u.route('/shopping/comment',{id:details.id})">
+ <text>查看全部</text>
+ <view class="detail view-wrap">
+ <view class="til">详情</view>
+ <view class="con">
+ <!-- <view class="" v-html="details.detail"></view> -->
+ <u-parse :content="details.detail"></u-parse>
+ <!-- {{details.detail}} -->
+ <view class="details-tool-wrap">
+ <view class="details-tool u-flex u-row-between">
+ <view class="icon-wrap">
+ <button class="share-btn" data-name="shareBtn" open-type="share">
+ <u-icon name="share" color="#676767" size="30"></u-icon>
+ 分享
+ </button>
+ <view class="icon-wrap" v-if="details.isCredit!=1" @click="$u.route('/shopping/cart')">
+ <u-icon name="shopping-cart" color="#676767" size="30"></u-icon>
+ <u-badge class="badge" numberType="overflow" type="error " max="99" :value="cartTotal" :absolute="true" :offset="[0,0]"></u-badge>
+ <view class="right" v-if="details.stock>0">
+ <view class="u-flex" v-if="details.isCredit==1">
+ <view class="btn add-btn" @click="addCreditOrder(details.id)">立即兑换</view>
+ <view class=" u-flex" v-else>
+ <view class="btn add-btn" @click="addCart(details.id)">加入购物车</view>
+ <view class="btn buy-btn" @click="buyNow(details.id)">立即购买</view>
+ <view class="right" v-else>
+ <view class="btn gray">暂无库存</view>
+ currentNum:0,
+ swiperList: [],
+ details:{
+ slideImgList:[]
+ // console.log('this.id',this.id);
+ this.getDetails(this.id);
+ onShow(){
+ this.getCartList();
+ if(pages.length==1){
+ uni.$u.route('/pages/index/index')
+ uni.navigateBack()
+ type:"success",
+ message:'已在购物车'
+ getDetails(id){
+ this.$u.api.memberGoodDetails({id:id}).then(res=>{
+ this.details = res.data;
+ if(!res.data.slideImgList&&res.data.mainImg){
+ this.details.slideImgList = [];
+ this.details.slideImgList.push(res.data.mainImg)
+ console.log('getDetails',err.data);
+ addCart(id,buyNow){
+ message:res.msg
+ if(buyNow){
+ uni.$u.route('/shopping/cart', {
+ buyNowId: id,
+ buyNowName:this.details.goodsName
+ this.getCartList('isAdd');
+ buyNow(id){
+ // this.addCart(id,'buyNow')//跳购物车
+ let url = encodeURIComponent(`/shopping/productdetails?id=${that.id}`) ;
+ from: 'productdetails',
+ backUrl:url
+ this.$u.vuex('buyNowGoods',[{goodsId:this.id,quantity:1}]);
+ uni.$u.route('/shopping/submitorder', {fromPage: 'productdetails'});
+ addCreditOrder(id){
+ let creditGoods = [];
+ creditGoods.push({id:that.details.id,quantity:1})
+ // console.log('creditGoods',creditGoods);
+ that.$u.vuex('cartGoods',creditGoods);
+ uni.$u.route('/shopping/submitorder', {fromPage:'creditOrder'});
+ onShareAppMessage: function( options ){
+ // 设置菜单中的转发按钮触发转发事件时的转发内容
+ var shareObj = {
+ title: this.details.goodsName, // 默认是小程序的名称(可以写slogan等)
+ path: '/shopping/productdetails', // 默认是当前页面,必须是以‘/'开头的完整路径
+ imageUrl: '', //自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径,支持PNG及JPG,不传入 imageUrl 则使用默认截图。显示图片长宽比是 5:4
+ success: function(res){
+ // 转发成功之后的回调
+ if(res.errMsg == 'shareAppMessage:ok'){
+ fail: function(){
+ // 转发失败之后的回调
+ if(res.errMsg == 'shareAppMessage:fail cancel'){
+ // 用户取消转发
+ }else if(res.errMsg == 'shareAppMessage:fail'){
+ // 转发失败,其中 detail message 为详细失败信息
+ complete:function(){
+ // 转发结束之后的回调(转发成不成功都会执行)
+ // 来自页面内的按钮的转发
+ if( options.from == 'button' ){
+ var eData = options.target.dataset;
+ console.log('options.target.dataset',options.target.dataset);
+ console.log('id' ,this.details.id); // shareBtn
+ // 此处可以修改 shareObj 中的内容
+ shareObj.path = '/shopping/productdetails?id='+this.details.id;
+ // 返回shareObj
+ return shareObj;
+ console.log('getAddrList',res);
+.indicator-num {
+ padding: 2px 0;
+ background-color: rgba(0, 0, 0, 0.35);
+ border-radius: 100px;
+ width: 35px;
+ @include flex;
+ &__text {
+ font-size: 12px;
+.product-info{
+ .price-num{
+ font-size: 60rpx;
+ .exchangeCredit{
+ margin-top: 30rpx;
+.info-line{
+.info-til{
+ margin-right: 40rpx;
+.info-con{
+ .addr-line:not(:last-child){
+.comment{
+ margin-right: 20rpx;
+.detail{
+ // background-color: #F5F5F5;
+ img{max-width: 100%;}
+.details-tool-wrap{
+ .details-tool{
+ .icon-wrap{
+ padding: 19rpx 40rpx;
+ border-radius: 40rpx;
+ .add-btn{
+ .buy-btn{
+ background-color: #ddd;
+ .share-btn{
+ padding: 0;
+ margin: 0;
+ border: 0;
+ line-height: 1;
+ outline: none;
+ &::after{
+ border: none;
@@ -0,0 +1,233 @@
+ <view class="search-history page-wrap" v-if="searchHistory.length>0">
+ <view class="search-title u-flex u-row-between">
+ <text>最近搜索</text>
+ <u-icon @click="clearStorage" name="trash" color="#666" size="35"></u-icon>
+ <view class="history-wrap">
+ <text class="history" v-for="(item,index) in searchHistory" :key="index">
+ <view class="page-wrap" >
+ <view class="" v-for="item in dataList" :key="item.id" @click="$u.route('/shopping/productdetails',{id:item.id})">
+ <view class="product u-flex">
+ <cartfixed ref="cartfixed" @getCartList="getCartList" />
+ noMoreSize: 3, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+ title:'产品搜索',
+ searchHistory:[],
+ this.params.goodsName = page.goodsName;
+ key:'searchgoodshistory',
+ success: function(storagedata) {
+ that.searchHistory = storagedata.data;
+ console.log('storagedata',storagedata.data);
+ this.$refs.cartfixed.getCartList();
+ console.log('this.params',this.params);
+ // 存储历史数据
+ let index = this.searchHistory.findIndex(item => item.name === that.params.goodsName);
+ console.log('index',index);
+ if (index !== -1) {
+ // 已经存在,不用重复添加了
+ this.searchHistory.push({ name: that.params.goodsName });
+ key: 'searchgoodshistory',
+ data: that.searchHistory,
+ // console.log('数据存储成功')
+ fail: function() {
+ // console.log('数据存储失败')
+ // this.reloadList();
+ console.log('search',e)
+ console.log('addCart',err.data);
+ clearStorage(){
+ uni.clearStorage({
+ success:function(){
+ that.searchHistory = []
+.search-history{
+ .search-title{
+ .history{
+ height: 48rpx;
+ line-height: 48rpx;
+ background: #F5F5F5;
+ border-radius: 24rpx;
+ padding: 0 24rpx;
+ min-width: 100rpx;
@@ -0,0 +1,445 @@
+ title="提交订单"
+ <view class="addr page-wrap u-flex u-row-between">
+ <text class="label" v-if="orderInfo.receive.ifDefault">默认</text>{{receive.address}}
+ <view class="center">{{receive.receiveAdress}}</view>
+ <view class="bottom">{{receive.receiveName}} {{receive.receivePhone}}</view>
+ <u-icon name="arrow-right" color="#676767" size="20" @click="selectAddr"></u-icon>
+ <view class="order-product page-wrap">
+ <view class="product u-flex" v-for="(item,index) in orderInfo.goodsList" :key="item.id">
+ <text class="original-price gray line-through">¥ {{item.salePrice}}</text>
+ v-if="fromPage!='creditOrder'"
+ <view class="page-wrap order-reduced">
+ <!-- <view class="reduced-item u-flex u-row-between u-border-bottom">
+ <view class="reduced-item integral u-flex u-row-between" v-if="orderTotalCredit">
+ <!-- <text class="num">3676</text> -->
+ <!-- <text class="tip">满1000可用</text> -->
+ <text class="price red" >- <text class="price-num">{{orderTotalCredit}}</text></text>
+ <!-- <u-checkbox-group @change="integralCheckboxChange">
+ </u-checkbox-group> -->
+ <view class="page-wrap total">
+ ¥ {{totalAmount}}
+ <text class="gray">总重:{{totalWeight}}</text>
+ <text v-if="distribution.distributionPrice">+ ¥ {{distribution.distributionPrice}}</text>
+ 待支付:<text class="red">¥ {{payAmount + distribution.distributionPrice}}</text>
+ <view class="btn gray" v-if="orderTotalCredit&&userTotalCredit<orderTotalCredit">积分不够</view>
+ <view class="btn" v-if="cansubmit" @click="submitOrder">去结算</view>
+ <view class="btn gray" v-else>去结算</view>
+ receiveId:'',
+ receive:{},
+ orderInfo:{goodsList:[]},
+ distribution:{},//物流信息
+ totalAmount:null,//订单总金额
+ payAmount:null,//订单支付金额
+ userTotalBalance:null,//用户总余额
+ userTotalCredit:null,//用户总积分
+ orderTotalCredit:null,//订单需要的总积分
+ console.log('page',page);
+ // console.log('this.creditGoods',this.creditGoods);
+ console.log('fromPage',this.fromPage);
+ // if(this.cartGoods>0&&this.fromPage!='cart'){
+ // console.log('fromPagecart',this.fromPage);
+ // this.orderInfo.goodsList = this.cartGoods;
+ // if(this.creditGoods.length>0&&this.fromPage=='creditOrder'){
+ // console.log('fromPagecreditOrder',this.fromPage);
+ // this.orderInfo.goodsList = this.creditGoods;
+ this.orderInfo.goodsList = this.cartGoods;
+ console.log('this.buyNowGoods',this.buyNowGoods);
+ if(this.buyNowGoods.length>0&&this.fromPage=='productdetails'){
+ console.log('fromPagecart',this.fromPage);
+ this.orderInfo.goodsList = this.buyNowGoods;
+ this.settlement();
+ this.$u.vuex('buyNowGoods',[]);
+ if(this.fromPage=='addrlist'){
+ this.settlement()
+ // console.log('cartGoods===',this.cartGoods);
+ totalWeight() {
+ return this.orderInfo.goodsList.reduce((total, item) => {
+ let weight = null;
+ weight = item.weight
+ total += weight * item.quantity;
+ }, 0);
+ integralCheckboxChange(e){
+ this.useIntegral = e[0] && e[0] === 'integral' ? true : false;
+ selectAddr(){
+ from: 'settlement'
+ // getAddrList(){
+ // this.$u.api.addrList().then(res=>{
+ // this.theAddr = res.data.rows;
+ // let defaultAddress;
+ // let firstAddress = res.data.rows[0];
+ // res.data.rows.forEach(address => {
+ // if (address.ifDefault) { // 如果有默认地址标识
+ // defaultAddress = address;
+ // this.isDefaultAddr = true;
+ // if (!defaultAddress) { // 如果没有默认地址标识
+ // defaultAddress = firstAddress;
+ // this.theAddr = defaultAddress;
+ // console.log('getAddrList',err.data);
+ // console.log('goodsList',this.orderInfo.goodsList);
+ // console.log('index',index);
+ // console.log('value',value);
+ this.orderInfo.goodsList[index].quantity = value.value;
+ settlement(){
+ let orderType = 1;
+ // console.log('settlement',this.selectGoods);
+ let param = {
+ receiveId:this.receive.id
+ if(this.fromPage == 'creditOrder'){
+ orderType = 2
+ param.orderType = orderType;
+ param.goodsList = this.orderInfo.goodsList.map(item=>{
+ return {goodsId:item.id||item.goodsId,quantity:item.quantity}
+ console.log('param',param);
+ this.$u.api.getSettlement(param).then(res=>{
+ this.setData(res.data)
+ console.log('getSettlement',err);
+ setData(originData){
+ console.log('originData',originData);
+ let dataType = typeof originData;
+ let jsonData;
+ if(dataType=='string'){
+ jsonData = JSON.parse(originData);
+ jsonData = originData
+ let {
+ receive,
+ distribution,
+ totalAmount,
+ payAmount,
+ userTotalBalance,
+ userTotalCredit,
+ orderTotalCredit } = jsonData || {};
+ console.log('orderTotalCredit',orderTotalCredit);
+ this.orderInfo = jsonData;
+ this.receive = receive;
+ this.distribution = distribution;//物流信息
+ this.totalAmount = totalAmount;//订单总金额
+ this.payAmount = payAmount;// 订单支付金额
+ this.userTotalBalance = userTotalBalance;// 用户总余额
+ this.userTotalCredit = userTotalCredit;// 用户总积分
+ this.orderTotalCredit = orderTotalCredit;//订单需要的总积分
+ submitOrder(){
+ receiveId:this.receive.id,
+ distribution:this.distribution,
+ return {goodsId:item.id,quantity:item.quantity}
+ //获取经纬度
+ uni.getLocation({
+ type: 'gcj02',
+ param.latitude = res.latitude;
+ param.longitude = res.longitude;
+ that.$u.api.submitOrder(param).then(res=>{
+ that.cansubmit = true;
+ if(res.data.payAmount>0){
+ orderId: res.data.orderId,
+ fromPage:that.fromPage,
+ payAmount:res.data.payAmount
+ uni.$u.route('/shopping/paysuccess', {
+ fromPage:'nocash'
+ console.log('submitOrder',err);
+.page-wrap{
+ &.gray{
@@ -0,0 +1,90 @@
+.page-wrap{padding: 20rpx;}
+.search-wrap{
+ margin-bottom: 24rpx;
+.search-wrap::after {
+ content: "";
+ height: 14rpx;
+ bottom: -20rpx;
+ background: linear-gradient(to bottom, #F5F5F5, #fff);
+.vip-icon{
+ background: linear-gradient(180deg, #FFD49C 0%, #C27D22 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+.gray{
+.red{
+.line-through{
+ text-decoration: line-through;
+.view-wrap{
+ border-radius: 44rpx;
+ padding: 22rpx 0;
+ border-bottom: 10rpx solid #F5F5F5;
+.single-til{
+ .more-text{
+.news-content{
+ line-height: 44rpx;
+ p{
+ text-indent: 2em;
+ /deep/ .u-image{
+ max-width: 100%;
+ margin: 30rpx auto 15rpx;
@@ -0,0 +1,71 @@
+.u-flex {
+ flex-direction: row;
+.u-flex-wrap {
+ flex-wrap: wrap;
+.u-flex-nowrap {
+ flex-wrap: nowrap;
+.u-col-center {
+.u-col-top {
+ align-items: flex-start;
+.u-col-bottom {
+ align-items: flex-end;
+.u-row-center {
+.u-row-left {
+ justify-content: flex-start;
+.u-row-right {
+.u-row-between {
+ justify-content: space-between;
+.u-row-around {
+ justify-content: space-around;
+.u-text-left {
+ text-align: left;
+.u-text-center {
+.u-text-right {
+.u-flex-col {
+ /* #ifndef APP-NVUE */
+ /* #endif */
+// 定义flex等分
+@for $i from 0 through 12 {
+ .u-flex-#{$i} {
+ flex: $i;
+// $u.mixin.js
+import { mapState } from 'vuex'
+import store from "@/store"
+// 尝试将用户在根目录中的store/index.js的vuex的state变量,全部加载到全局变量中
+let $uStoreKey = [];
+try{
+ $uStoreKey = store.state ? Object.keys(store.state) : [];
+}catch(e){
+module.exports = {
+ created() {
+ // 将vuex方法挂在到$u中
+ // 使用方法为:如果要修改vuex的state中的user.name变量为"史诗" => this.$u.vuex('user.name', '史诗')
+ // 如果要修改vuex的state的version变量为1.0.1 => this.$u.vuex('version', '1.0.1')
+ this.$u.vuex = (name, value) => {
+ this.$store.commit('$uStore', {
+ name,value
+ // 将vuex的state中的所有变量,解构到全局混入的mixin中
+ ...mapState($uStoreKey)
@@ -0,0 +1,87 @@
+import Vuex from 'vuex'
+Vue.use(Vuex)
+let lifeData = {};
+ // 尝试获取本地是否存在lifeData变量,第一次启动APP时是不存在的
+ lifeData = uni.getStorageSync('lifeData');
+// 需要永久存储,且下次APP启动需要取出的,在state中的变量名
+let saveStateKeys = ['vuex_member_info', 'vuex_user_info','vuex_wechatOpenid'];
+// 保存变量到本地存储中
+const saveLifeData = function(key, value){
+ // 判断变量名是否在需要存储的数组中
+ if(saveStateKeys.indexOf(key) != -1) {
+ // 获取本地存储的lifeData对象,将变量添加到对象中
+ let tmp = uni.getStorageSync('lifeData');
+ // 第一次打开APP,不存在lifeData变量,故放一个{}空对象
+ tmp = tmp ? tmp : {};
+ tmp[key] = value;
+ // 执行这一步后,所有需要存储的变量,都挂载在本地的lifeData对象中
+ uni.setStorageSync('lifeData', tmp);
+const store = new Vuex.Store({
+ state: {
+ // 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
+ // 加上vuex_前缀,是防止变量名冲突,也让人一目了然
+ vuex_member_info: lifeData.vuex_member_info ? lifeData.vuex_member_info : {},
+ vuex_user_info: lifeData.vuex_user_info ? lifeData.vuex_user_info : {},
+ vuex_wechatOpenid:lifeData.vuex_wechatOpenid ? lifeData.vuex_wechatOpenid : '',
+ cartGoods:[],//购物车商品
+ creditGoods:[],//积分商品
+ buyNowGoods:[],//立即购买商品
+ // vuex_version: '1.0.1',
+ mutations: {
+ $uStore(state, payload) {
+ // 判断是否多层级调用,state中为对象存在的情况,诸如user.info.score = 1
+ let nameArr = payload.name.split('.');
+ let saveKey = '';
+ let len = nameArr.length;
+ if(len >= 2) {
+ let obj = state[nameArr[0]];
+ for(let i = 1; i < len - 1; i ++) {
+ obj = obj[nameArr[i]];
+ obj[nameArr[len - 1]] = payload.value;
+ saveKey = nameArr[0];
+ // 单层级变量,在state就是一个普通变量的情况
+ state[payload.name] = payload.value;
+ saveKey = payload.name;
+ // 保存变量到本地,见顶部函数定义
+ saveLifeData(saveKey, state[saveKey])
+ actions: {
+ // 获取用户信息
+ getUserInfo({ commit },backFu) {
+ if(this.state.user_info && this.state.user_info.userId) {
+ Vue.prototype.$u.api.checkToken().then((data) => {
+ if(data && data.code ===200) {
+ backFu(this.state.user_info)
+ Vue.prototype.$u.api.userInfo().then((data) => {
+ console.log(data)
+ Vue.prototype.$u.vuex('user_info', data.data)
+ backFu(data.data)
+export default store
+<html lang="zh-CN">
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
+ <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+ <title>
+ <%= htmlWebpackPlugin.options.title %>
+ </title>
+ document.addEventListener('DOMContentLoaded', function() {
+ document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
+ <link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
+ <noscript>
+ <strong>本站点必须要开启JavaScript才能运行</strong>
+ </noscript>
+ <div id="app"></div>
@@ -0,0 +1,305 @@
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+/* 颜色变量 */
+@import '@/uni_modules/uview-ui/theme.scss';
+/* 行为相关颜色 */
+$uni-color-primary: #00A447;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+$uni-text-color-red:#FF3C3F;
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+$uni-background-color:#F5F5F5;
+/* 边框颜色 */
+$uni-border-color:#eee;
+/* 尺寸变量 */
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16;
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;
+ // &:has(.u-navbar){
+ // padding-top: 104px;
+// @import url("@/static/css/flex.scss");
+@mixin multi-ellipsis($line: 1) {
+ @if $line <= 0 {
+ $line: 1;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: $line;
+ -webkit-box-orient: vertical;
+.ellipsis-1 {
+ @include multi-ellipsis(1);
+.ellipsis-2 {
+ @include multi-ellipsis(2);
+ padding-right: 20rpx;
+ border-bottom: 1px solid $uni-border-color;
+ margin-left: 50rpx;
+ height: 76rpx;
+ color: $uni-text-color-red;
+ margin-left: 2px;
+ .original-price{
+ .sales{margin-left: 30rpx;}
+ .discount{
+ padding: 4rpx 14rpx;
+ margin-right: 50rpx;
+ .up{
+ .down{
+.product.small-product{
+ .name{font-size: 26rpx}
+ .price-num{font-size: 30rpx}
+ .discount{margin-right: 5px}
+ margin-bottom: 45rpx;
+ .u-image{
+ margin-bottom: 15rpx;
+ .nav-item{
+.half-product{
+ &-item{
+ .price-num{font-size: 40rpx;margin-left: 10rpx}
+ &-item:nth-child(even){
+.news{
+ .news-item{
+ line-height: 1.5;
+ margin-bottom: 17rpx;
+ top: 0;
@@ -0,0 +1,6 @@
+## 1.3.7(2021-04-13)
+1. 新增`mescroll-swiper-sticky.vue`的示例, 轮播吸顶菜单导航
+2. 新增`mescroll-empty.vue`的示例, 单独使用空布局组件
+3. 简化tabs在具体项目中的使用,并简化对应的示例
+4. mescroll-uni 支持动态禁止滚动的属性 disableScroll (注: mescroll-body不支持)
+-by 小瑾同学
@@ -0,0 +1,19 @@
+.mescroll-body {
+ position: relative; /* 下拉刷新区域相对自身定位 */
+ height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+ overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+ box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+ overflow: unset !important
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+ .mescroll-safearea {
+ padding-bottom: constant(safe-area-inset-bottom);
+ padding-bottom: env(safe-area-inset-bottom);
@@ -0,0 +1,400 @@
+ class="mescroll-body mescroll-render-touch"
+ :class="{'mescorll-sticky': sticky}"
+ :style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
+ @touchstart="wxsBiz.touchstartEvent"
+ @touchmove="wxsBiz.touchmoveEvent"
+ @touchend="wxsBiz.touchendEvent"
+ @touchcancel="wxsBiz.touchendEvent"
+ :change:prop="wxsBiz.propObserver"
+ :prop="wxsProp"
+ <!-- 状态栏 -->
+ <view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+ <view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+ <!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+ <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+ <view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+ <view class="downwarp-content">
+ <view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
+ <view class="downwarp-tip">{{downText}}</view>
+ <!-- 列表内容 -->
+ <slot></slot>
+ <!-- 空布局 -->
+ <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+ <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+ <!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+ <view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+ <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+ <view v-show="upLoadType===1">
+ <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
+ <view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
+ <!-- 无数据 -->
+ <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
+ <!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+ <!-- #ifdef H5 -->
+ <view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+ <!-- 适配iPhoneX -->
+ <view v-if="safearea" class="mescroll-safearea"></view>
+ <!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+ <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+ <!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+ <!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+ <view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+ import renderBiz from "../mescroll-uni/wxs/renderjs.js";
+ mixins: [renderBiz]
+ // 引入mescroll-uni.js,处理核心逻辑
+ import MeScroll from "../mescroll-uni/mescroll-uni.js";
+ // 引入全局配置
+ import GlobalOption from "../mescroll-uni/mescroll-uni-option.js";
+ // 引入国际化工具类
+ import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+ // 引入回到顶部组件
+ import MescrollTop from "../mescroll-uni/components/mescroll-top.vue";
+ // 引入兼容wxs(含renderjs)写法的mixins
+ import WxsMixin from "../mescroll-uni/wxs/mixins.js";
+ * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
+ * @property {Object} down 下拉刷新的参数配置
+ * @property {Object} up 上拉加载的参数配置
+ * @property {Object} i18n 国际化的参数配置
+ * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+ * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+ * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+ * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+ * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
+ * @event {Function} init 初始化完成的回调
+ * @event {Function} down 下拉刷新的回调
+ * @event {Function} up 上拉加载的回调
+ * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+ * @event {Function} topclick 点击回到顶部的按钮回调
+ * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+ * @example <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
+ name: 'mescroll-body',
+ mixins: [WxsMixin],
+ MescrollTop
+ down: Object,
+ up: Object,
+ i18n: Object,
+ top: [String, Number],
+ topbar: [Boolean, String],
+ bottom: [String, Number],
+ safearea: Boolean,
+ height: [String, Number],
+ bottombar:{
+ default: true
+ sticky: Boolean
+ mescroll: {optDown:{},optUp:{}}, // mescroll实例
+ downHight: 0, //下拉刷新: 容器高度
+ downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+ downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+ upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+ isShowEmpty: false, // 是否显示空布局
+ isShowToTop: false, // 是否显示回到顶部按钮
+ windowHeight: 0, // 可使用窗口的高度
+ windowBottom: 0, // 可使用窗口的底部位置
+ statusBarHeight: 0 // 状态栏高度
+ // mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ minHeight(){
+ return this.toPx(this.height || '100%') + 'px'
+ // 下拉布局往下偏移的距离 (px)
+ numTop() {
+ return this.toPx(this.top)
+ padTop() {
+ return this.numTop + 'px';
+ // 上拉布局往上偏移 (px)
+ numBottom() {
+ return this.toPx(this.bottom);
+ padBottom() {
+ return this.numBottom + 'px';
+ // 是否为重置下拉的状态
+ isDownReset() {
+ return this.downLoadType === 3 || this.downLoadType === 4;
+ // 过渡
+ transition() {
+ return this.isDownReset ? 'transform 300ms' : '';
+ translateY() {
+ return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+ // 是否在加载中
+ isDownLoading(){
+ return this.downLoadType === 3
+ // 旋转的角度
+ downRotate(){
+ return 'rotate(' + 360 * this.downRate + 'deg)'
+ // 文本提示
+ downText(){
+ if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+ switch (this.downLoadType){
+ case 1: return this.mescroll.optDown.textInOffset;
+ case 2: return this.mescroll.optDown.textOutOffset;
+ case 3: return this.mescroll.optDown.textLoading;
+ case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+ default: return this.mescroll.optDown.textInOffset;
+ //number,rpx,upx,px,% --> px的数值
+ toPx(num) {
+ if (typeof num === 'string') {
+ if (num.indexOf('px') !== -1) {
+ if (num.indexOf('rpx') !== -1) {
+ // "10rpx"
+ num = num.replace('rpx', '');
+ } else if (num.indexOf('upx') !== -1) {
+ // "10upx"
+ num = num.replace('upx', '');
+ // "10px"
+ return Number(num.replace('px', ''));
+ } else if (num.indexOf('%') !== -1) {
+ // 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+ let rate = Number(num.replace('%', '')) / 100;
+ return this.windowHeight * rate;
+ return num ? uni.upx2px(Number(num)) : 0;
+ // 点击空布局的按钮回调
+ emptyClick() {
+ this.$emit('emptyclick', this.mescroll);
+ // 点击回到顶部的按钮回调
+ toTopClick() {
+ this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+ this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+ // 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+ let vm = this;
+ let diyOption = {
+ // 下拉刷新的配置
+ down: {
+ inOffset() {
+ vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+ outOffset() {
+ vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+ onMoving(mescroll, rate, downHight) {
+ // 下拉过程中的回调,滑动过程一直在执行;
+ vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+ vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+ showLoading(mescroll, downHight) {
+ vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+ beforeEndDownScroll(mescroll){
+ vm.downLoadType = 4;
+ return mescroll.optDown.beforeEndDelay // 延时结束的时长
+ endDownScroll() {
+ vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+ vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+ if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+ vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+ if(vm.downLoadType === 4) vm.downLoadType = 0
+ },300)
+ // 派发下拉刷新的回调
+ callback: function(mescroll) {
+ vm.$emit('down', mescroll);
+ // 上拉加载的配置
+ up: {
+ // 显示加载中的回调
+ showLoading() {
+ vm.upLoadType = 1;
+ // 显示无更多数据的回调
+ showNoMore() {
+ vm.upLoadType = 2;
+ // 隐藏上拉加载的回调
+ hideUpScroll(mescroll) {
+ vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+ // 空布局
+ onShow(isShow) {
+ // 显示隐藏的回调
+ vm.isShowEmpty = isShow;
+ // 回到顶部
+ toTop: {
+ vm.isShowToTop = isShow;
+ // 派发上拉加载的回调
+ vm.$emit('up', mescroll);
+ let i18nType = mescrollI18n.getType() // 当前语言类型
+ let i18nOption = {type: i18nType} // 国际化配置
+ MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+ MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+ MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+ MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+ let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+ MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+ // 初始化MeScroll对象
+ vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+ // 挂载语言包
+ vm.mescroll.i18n = i18nOption;
+ // init回调mescroll对象
+ vm.$emit('init', vm.mescroll);
+ // 设置高度
+ const sys = uni.getSystemInfoSync();
+ if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+ if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+ if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+ // 使down的bottomOffset生效
+ vm.mescroll.setBodyHeight(sys.windowHeight);
+ // 因为使用的是page的scroll,这里需自定义scrollTo
+ vm.mescroll.resetScrollTo((y, t) => {
+ if(typeof y === 'string'){
+ // 滚动到指定view (y为css选择器)
+ setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+ let selector;
+ if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+ selector = '#'+y // 不带#和. 则默认为id选择器
+ selector = y
+ // #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+ if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+ selector = y.split('>>>')[1].trim()
+ uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+ if (rect) {
+ let top = rect.top
+ top += vm.mescroll.getScrollTop()
+ uni.pageScrollTo({
+ scrollTop: top,
+ duration: t
+ } else{
+ console.error(selector + ' does not exist');
+ }).exec()
+ },30)
+ // 滚动到指定位置 (y必须为数字)
+ scrollTop: y,
+ // 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+ if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+ vm.mescroll.optUp.toTop.safearea = vm.safearea;
+ // 全局配置监听
+ uni.$on("setMescrollGlobalOption", options=>{
+ if(!options) return;
+ let i18nType = options.i18n ? options.i18n.type : null
+ if(i18nType && vm.mescroll.i18n.type != i18nType){
+ vm.mescroll.i18n.type = i18nType
+ mescrollI18n.setType(i18nType)
+ MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+ if(options.down){
+ let down = MeScroll.extend({}, options.down)
+ vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+ if(options.up){
+ let up = MeScroll.extend({}, options.up)
+ vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+ destroyed() {
+ // 注销全局配置监听
+ uni.$off("setMescrollGlobalOption")
+ @import "../mescroll-body/mescroll-body.css";
+ @import "../mescroll-uni/components/mescroll-down.css";
+ @import "../mescroll-uni/components/mescroll-up.css";
@@ -0,0 +1,47 @@
+/*下拉刷新--标语*/
+.mescroll-downwarp .downwarp-slogan{
+ width: 420rpx;
+ height: 168rpx;
+/*下拉刷新--向下进度动画*/
+.mescroll-downwarp .downwarp-progress{
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png);
+ transition: all 300ms;
+/*下拉刷新--进度条*/
+.mescroll-downwarp .downwarp-loading{
+ width: 32rpx;
+ height: 32rpx;
+ border: 2rpx solid #FF8095;
+ border-bottom-color: transparent;
+/*下拉刷新--吉祥物*/
+.mescroll-downwarp .downwarp-mascot{
+ right: 16rpx;
+ animation: animMascot .6s steps(1,end) infinite;
+@keyframes animMascot {
+ 0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+ 25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)}
+ 50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)}
+ 75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)}
+ 100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
@@ -0,0 +1,39 @@
+<!-- 下拉刷新区域 -->
+ <view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+ <image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+ <view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+ <view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+ <view class="downwarp-mascot"></view>
+ option: Object , // down的配置项
+ type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+ // 支付宝小程序需写成计算属性,prop定义default仍报错
+ mOption(){
+ return this.option || {}
+ return this.type === 3
+ return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+};
+@import "../../../mescroll-uni/components/mescroll-down.css";
+@import "./mescroll-down.css";
@@ -0,0 +1,360 @@
+ <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
+ <!-- 底部是否偏移TabBar的高度(仅H5端生效) -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+ import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+ import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+ import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+ import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+ import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+ import GlobalOption from './mescroll-uni-option.js';
+ mescroll: null, // mescroll实例
+ down: Object, // 下拉刷新的参数配置
+ up: Object, // 上拉加载的参数配置
+ i18n: Object, // 国际化的参数配置
+ top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+ bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+ height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+ bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+ sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+ return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+ @import "../../mescroll-body/mescroll-body.css";
+ @import "../../mescroll-uni/components/mescroll-down.css";
+ @import "../../mescroll-uni/components/mescroll-up.css";
+ @import "./components/mescroll-down.css";
@@ -0,0 +1,49 @@
+// mescroll-uni和mescroll-body 的全局配置
+const GlobalOption = {
+ // 其他down的配置参数也可以写,这里只展示了常用的配置:
+ offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
+ native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ // 其他up的配置参数也可以写,这里只展示了常用的配置:
+ offset: 150, // 距底部多远时,触发upCallback
+ // 回到顶部按钮,需配置src才显示
+ src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+ offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+ right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+ use: true, // 是否显示空布局
+ icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+ // 国际化配置
+ i18n: {
+ // 中文
+ zh: {
+ textLoading: '加载中 ...', // 加载中的提示文本
+ textNoMore: '-- END --', // 没有更多数据的提示文本
+ tip: '~ 暂无相关数据 ~' // 空提示
+ // 英文
+ en: {
+ textLoading: 'loading ...',
+ textNoMore: '-- END --',
+ tip: '~ absolutely empty ~'
+export default GlobalOption
@@ -0,0 +1,437 @@
+ <view class="mescroll-uni-warp">
+ <scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+ <view class="mescroll-uni-content mescroll-render-touch"
+ :prop="wxsProp">
+ <view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+ </scroll-view>
+ <!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+ viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+ upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+ scrollTop: 0, // 滚动条的位置
+ scrollAnim: false, // 是否开启滚动动画
+ windowTop: 0, // 可使用窗口的顶部位置
+ fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+ height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ disableScroll: Boolean // 是否禁止滚动
+ // 是否使用fixed定位 (当height有值,则不使用)
+ isFixed(){
+ return !this.height && this.fixed
+ // mescroll的高度
+ scrollHeight(){
+ if (this.isFixed) {
+ return "auto"
+ } else if(this.height){
+ return this.toPx(this.height) + 'px'
+ return "100%"
+ fixedTop() {
+ return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+ return !this.isFixed ? this.numTop + 'px' : 0
+ return this.toPx(this.bottom)
+ fixedBottom() {
+ return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+ return !this.isFixed ? this.numBottom + 'px' : 0
+ isDownReset(){
+ return this.downLoadType===3 || this.downLoadType===4
+ return this.isDownReset ? 'transform 300ms' : ''
+ return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+ // 列表是否可滑动
+ scrollable(){
+ if(this.disableScroll) return false
+ return this.downLoadType===0 || this.isDownReset
+ toPx(num){
+ if(typeof num === "string"){
+ if(num.indexOf('rpx') !== -1) { // "10rpx"
+ } else if(num.indexOf('upx') !== -1) { // "10upx"
+ } else { // "10px"
+ return Number(num.replace('px', ''))
+ }else if (num.indexOf('%') !== -1){
+ let rate = Number(num.replace("%","")) / 100
+ return this.windowHeight * rate
+ return num ? uni.upx2px(Number(num)) : 0
+ //注册列表滚动事件,用于下拉刷新和上拉加载
+ scroll(e) {
+ this.mescroll.scroll(e.detail, () => {
+ this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+ this.$emit('emptyclick', this.mescroll)
+ // 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+ setClientHeight() {
+ if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+ this.isExec = true; // 避免多次获取
+ this.$nextTick(() => { // 确保dom已渲染
+ this.getClientInfo(data=>{
+ this.isExec = false;
+ if (data) {
+ this.mescroll.setClientHeight(data.height);
+ } else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+ this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+ this.setClientHeight()
+ }, this.clientNum * 100)
+ // 获取滚动区域的信息
+ getClientInfo(success){
+ let query = uni.createSelectorQuery();
+ // #ifndef MP-ALIPAY || MP-DINGTALK
+ query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+ let view = query.select('#' + this.viewId);
+ view.boundingClientRect(data => {
+ success(data)
+ }).exec();
+ vm.downResetTimer && clearTimeout(vm.downResetTimer)
+ vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+ if(vm.downLoadType===4) vm.downLoadType = 0
+ vm.$emit('down', mescroll)
+ onShow(isShow) { // 显示隐藏的回调
+ // 更新容器的高度 (多mescroll的情况)
+ vm.setClientHeight()
+ let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+ vm.mescroll = new MeScroll(myOption);
+ vm.mescroll.viewId = vm.viewId; // 附带id
+ if(sys.windowTop) vm.windowTop = sys.windowTop;
+ if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+ if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+ if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+ // 因为使用的是scrollview,这里需自定义scrollTo
+ vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+ // 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+ vm.getClientInfo(function(rect){
+ let mescrollTop = rect.top // mescroll到顶部的距离
+ let curY = vm.mescroll.getScrollTop()
+ let top = rect.top - mescrollTop
+ top += curY
+ if(!vm.isFixed) top -= vm.numTop
+ vm.scrollTop = curY;
+ vm.$nextTick(function() {
+ vm.scrollTop = top
+ if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+ vm.scrollTop = y
+ vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+ vm.scrollTop = step
+ }, t)
+ mounted() {
+ // 设置容器的高度
+ @import "../../mescroll-uni/mescroll-uni.css";
@@ -0,0 +1,44 @@
+/*下拉刷新--上下箭头*/
+.mescroll-downwarp .downwarp-arrow {
+ width: 20px;
+ height: 20px;
+ margin: 10px;
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png);
+ vertical-align: middle;
+/*下拉刷新--旋转进度条*/
+ width: 36px;
+ height: 36px;
+ animation: progressRotate 0.6s steps(6, start) infinite;
+@keyframes progressRotate {
+ 0% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+ 16% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+ 32% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+ 48% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+ 64% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+ 80% {
+ background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+ 100% {
@@ -0,0 +1,53 @@
+ <view v-if="isDownLoading" class="downwarp-progress"></view>
+ <view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
+ <view class="downwarp-tip">{{ downText }}</view>
+ option: Object, // down的配置项
+ mOption() {
+ return this.option || {};
+ isDownLoading() {
+ return this.type === 3;
+ downRotate() {
+ return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+ downText() {
+ switch (this.type) {
+ return this.mOption.textInOffset;
+ return this.mOption.textOutOffset;
+ return this.mOption.textLoading;
+ default:
+@import '../../../mescroll-uni/components/mescroll-down.css';
+@import './mescroll-down.css';
@@ -0,0 +1,32 @@
+/*上拉加载--旋转进度条*/
+.mescroll-upwarp .upwarp-progress {
@@ -0,0 +1,40 @@
+<!-- 上拉加载区域 -->
+ <view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+ <view v-show="isUpLoading">
+ <view class="upwarp-progress mescroll-rotate"></view>
+ <view class="upwarp-tip">{{ mOption.textLoading }}</view>
+ <view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+ option: Object, // up的配置项
+ type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+ // 加载中
+ isUpLoading() {
+ return this.type === 1;
+ // 没有更多了
+ isUpNoMore() {
+ return this.type === 2;
+@import '../../../mescroll-uni/components/mescroll-up.css';
+@import './mescroll-up.css';
@@ -0,0 +1,380 @@
+ <!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+ <!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+ <view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
+ return this.downLoadType === 3;
+ return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+ if(!this.mescroll) return "";
+ switch (this.downLoadType) {
+ return this.mescroll.optDown.textInOffset;
+ return this.mescroll.optDown.textOutOffset;
+ return this.mescroll.optDown.textLoading;
+ return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+ @import "./components/mescroll-up.css";
@@ -0,0 +1,64 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+ offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+ offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+ textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+ textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+ textSuccess: '加载成功', // 加载成功的文本
+ textErr: '加载失败', // 加载失败的文本
+ tip: '~ 空空如也 ~' // 空提示
+ textInOffset: 'drop down refresh',
+ textOutOffset: 'release updates',
+ textSuccess: 'loaded successfully',
+ textErr: 'loading failed'
@@ -0,0 +1,462 @@
+ let myOption = JSON.parse(JSON.stringify({
+ 'down': vm.down,
+ 'up': vm.up
+ })) // 深拷贝,避免对props的影响
@@ -0,0 +1,116 @@
+<!--空布局:
+遵循easycom规范, 可作为独立的组件, 不使用mescroll的页面也能使用:
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+-->
+ <view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
+ <view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
+ <view v-if="tip" class="empty-tip">{{ tip }}</view>
+ <view v-if="btnText" class="empty-btn" @click="emptyClick">{{ btnText }}</view>
+// 引入全局配置
+import GlobalOption from '../mescroll-uni/mescroll-uni-option.js';
+// 引入国际化工具类
+import mescrollI18n from '../mescroll-uni/mescroll-i18n.js';
+ // empty的配置项: 默认为GlobalOption.up.empty
+ option: {
+ type: Object,
+ default() {
+ return {};
+ // 使用computed获取配置,用于支持option的动态配置
+ // 图标
+ icon() {
+ if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
+ return this.option.icon
+ let i18nType = mescrollI18n.getType() // 国际化配置
+ if (this.option.i18n) {
+ return this.option.i18n[i18nType].icon
+ return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
+ tip() {
+ if (this.option.tip != null) { // 支持传空串不显示文本提示
+ return this.option.tip
+ return this.option.i18n[i18nType].tip
+ return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
+ // 按钮文本
+ btnText() {
+ return this.option.i18n[i18nType].btnText
+ return this.option.btnText
+ // 点击按钮
+ this.$emit('emptyclick');
+/* 无任何数据的空布局 */
+.mescroll-empty {
+ padding: 100rpx 50rpx;
+.mescroll-empty.empty-fixed {
+ position: absolute; /*transform会使fixed失效,最终会降级为absolute */
+ top: 100rpx;
+.mescroll-empty .empty-icon {
+ width: 280rpx;
+.mescroll-empty .empty-tip {
+ color: gray;
+.mescroll-empty .empty-btn {
+ min-width: 200rpx;
+ padding: 18rpx;
+ font-size: 28rpx;
+ border: 1rpx solid #e04b28;
+ border-radius: 60rpx;
+ color: #e04b28;
+.mescroll-empty .empty-btn:active {
+ opacity: 0.75;
@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+ top: -100%;
+ height: 100%;
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+ min-height: 60rpx;
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+ /* color: gray; 已在style设置color,此处删去*/
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+ border: 2rpx solid gray;
+ border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+ animation: mescrollDownRotate 0.6s linear infinite;
+@keyframes mescrollDownRotate {
+ transform: rotate(0deg);
+ transform: rotate(360deg);
+ <view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+ <view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+ type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+ rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+ return 'rotate(' + 360 * this.rate + 'deg)'
+ switch (this.type){
+ case 1: return this.mOption.textInOffset;
+ case 2: return this.mOption.textOutOffset;
+ case 3: return this.mOption.textLoading;
+ case 4: return this.mOption.textLoading;
+ default: return this.mOption.textInOffset;
@@ -0,0 +1,83 @@
+<!-- 回到顶部的按钮 -->
+ v-if="mOption.src"
+ class="mescroll-totop"
+ :class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
+ :style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
+ :src="mOption.src"
+ mode="widthFix"
+ @click="toTopClick"
+ // up.toTop的配置项
+ option: Object,
+ // 是否显示
+ value: false
+ // 优先显示左边
+ left(){
+ return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
+ // 右边距离 (优先显示左边)
+ right() {
+ return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
+ addUnit(num){
+ if(!num) return 0;
+ if(typeof num === 'number') return num + 'rpx';
+ return num
+ this.$emit('input', false); // 使v-model生效
+ this.$emit('click'); // 派发点击事件
+/* 回到顶部的按钮 */
+.mescroll-totop {
+ z-index: 9990;
+ position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+ bottom: 120rpx;
+ width: 72rpx;
+ transition: opacity 0.5s; /* 过渡 */
+ margin-bottom: var(--window-bottom); /* css变量 */
+ .mescroll-totop-safearea {
+ margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+ margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+ pointer-events: none;
+/* 上拉加载区域 */
+.mescroll-upwarp {
+ min-height: 110rpx;
+ padding: 30rpx 0;
+ clear: both;
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+.mescroll-upwarp .upwarp-tip {
+/*旋转进度条 */
+.mescroll-upwarp .mescroll-rotate {
+ animation: mescrollUpRotate 0.6s linear infinite;
+@keyframes mescrollUpRotate {
+ <view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+ <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
+ type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
+// 国际化工具类
+const mescrollI18n = {
+ // 默认语言
+ def: "zh",
+ // 获取当前语言类型
+ getType(){
+ return uni.getStorageSync("mescroll-i18n") || this.def
+ // 设置当前语言类型
+ setType(type){
+ uni.setStorageSync("mescroll-i18n", type)
+export default mescrollI18n
@@ -0,0 +1,57 @@
+const MescrollMixin = {
+ mescroll: null //mescroll实例对象
+ // 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ onPullDownRefresh(){
+ this.mescroll && this.mescroll.onPullDownRefresh();
+ // 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+ onPageScroll(e) {
+ this.mescroll && this.mescroll.onPageScroll(e);
+ // 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+ onReachBottom() {
+ this.mescroll && this.mescroll.onReachBottom();
+ // mescroll组件初始化的回调,可获取到mescroll对象
+ mescrollInit(mescroll) {
+ this.mescroll = mescroll;
+ this.mescrollInitByRef(); // 兼容字节跳动小程序
+ // 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+ mescrollInitByRef() {
+ if(!this.mescroll || !this.mescroll.resetUpScroll){
+ let mescrollRef = this.$refs.mescrollRef;
+ if(mescrollRef) this.mescroll = mescrollRef.mescroll
+ // 下拉刷新的回调 (mixin默认resetUpScroll)
+ downCallback() {
+ if(this.mescroll.optUp.use){
+ this.mescroll.resetUpScroll()
+ this.mescroll.endSuccess();
+ }, 500)
+ // 上拉加载的回调
+ upCallback() {
+ // mixin默认延时500自动结束加载
+ this.mescroll.endErr();
+ this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
+export default MescrollMixin;
@@ -0,0 +1,36 @@
+.mescroll-uni-warp{
+.mescroll-uni-content{
+.mescroll-uni {
+ min-height: 200rpx;
+ overflow-y: auto;
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed{
+ width: auto; /* 使right生效 */
+ height: auto; /* 使bottom生效 */
@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.7
+ * 2021-04-12 wenju
+ * https://www.mescroll.com
+export default function MeScroll(options, isScrollBody) {
+ let me = this;
+ me.version = '1.3.7'; // mescroll版本号
+ me.options = options || {}; // 配置
+ me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+ me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+ me.isUpScrolling = false; // 是否在执行上拉加载的回调
+ let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+ // 初始化下拉刷新
+ me.initDownScroll();
+ // 初始化上拉加载,则初始化
+ me.initUpScroll();
+ // 自动加载
+ setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+ // 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+ if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+ if (me.optDown.autoShowLoading) {
+ me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
+ me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
+ // 自动触发上拉加载
+ if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+ setTimeout(function(){
+ me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
+ },100)
+ }, 30); // 需让me.optDown.inited和me.optUp.inited先执行
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function(optDown) {
+ MeScroll.extend(optDown, {
+ use: true, // 是否启用下拉刷新; 默认true
+ auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+ native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+ autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+ isLock: false, // 是否锁定下拉刷新,默认false;
+ startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+ inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+ outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+ bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+ minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+ beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+ bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+ textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+ inited: null, // 下拉刷新初始化完毕的回调
+ inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+ outOffset: null, // 下拉的距离大于offset那一刻的回调
+ onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+ beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+ showLoading: null, // 显示下拉刷新进度的回调
+ afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+ beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+ endDownScroll: null, // 结束下拉刷新的回调
+ afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+ // 下拉刷新的回调;默认重置上拉加载列表为第一页
+ mescroll.resetUpScroll();
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+ MeScroll.extend(optUp, {
+ use: true, // 是否启用上拉加载; 默认true
+ auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+ isLock: false, // 是否锁定上拉加载,默认false;
+ isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+ callback: null, // 上拉加载的回调;function(page,mescroll){ }
+ num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+ size: 10, // 每页数据的数量
+ time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+ noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+ bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+ inited: null, // 初始化完毕的回调
+ showLoading: null, // 显示加载中的回调
+ showNoMore: null, // 显示无更多数据的回调
+ hideUpScroll: null, // 隐藏上拉加载的回调
+ errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+ src: null, // 图片路径,默认null (绝对路径或网络图)
+ offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+ duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+ btnClick: null, // 点击按钮的回调
+ onShow: null, // 是否显示的回调
+ zIndex: 9990, // fixed定位z-index值
+ left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+ width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+ icon: null, // 图标路径
+ tip: '~ 暂无相关数据 ~', // 提示
+ btnText: '', // 按钮
+ fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+ top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+ zIndex: 99 // fixed定位z-index值
+ onScroll: false // 是否监听滚动事件
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+ if (!userOption) return defaultOption;
+ for (let key in defaultOption) {
+ if (userOption[key] == null) {
+ let def = defaultOption[key];
+ if (def != null && typeof def === 'object') {
+ userOption[key] = MeScroll.extend({}, def); // 深度匹配
+ userOption[key] = def;
+ } else if (typeof userOption[key] === 'object') {
+ MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+ return userOption;
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+ if(!color) return false;
+ let c = color.toLowerCase();
+ return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+ // 配置参数
+ me.optDown = me.options.down || {};
+ if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+ me.extendDownScroll(me.optDown);
+ // 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+ if(me.isScrollBody && me.optDown.native){
+ me.optDown.use = false
+ me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+ me.downHight = 0; // 下拉区域的高度
+ // 在页面中加入下拉布局
+ if (me.optDown.use && me.optDown.inited) {
+ // 初始化完毕的回调
+ me.optDown.inited(me);
+ }, 0)
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function(e) {
+ if (!this.optDown.use) return;
+ this.startPoint = this.getPoint(e); // 记录起点
+ this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
+ this.startAngle = 0; // 初始角度
+ this.lastPoint = this.startPoint; // 重置上次move的点
+ this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+ this.inTouchend = false; // 标记不是touchend
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function(e) {
+ let scrollTop = me.getScrollTop(); // 当前滚动条的距离
+ let curPoint = me.getPoint(e); // 当前点
+ let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 向下拉 && 在顶部
+ // mescroll-body,直接判定在顶部即可
+ // scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+ // scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+ if (moveY > 0 && (
+ (me.isScrollBody && scrollTop <= 0)
+ (!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+ )) {
+ // 可下拉的条件
+ if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+ me.optUp.isBoth))) {
+ // 下拉的初始角度是否在配置的范围内
+ if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+ if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
+ // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+ if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+ me.inTouchend = true; // 标记执行touchend
+ me.touchendEvent(); // 提前触发touchend
+ me.preventDefault(e); // 阻止默认事件
+ let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+ // 下拉距离 < 指定距离
+ if (me.downHight < me.optDown.offset) {
+ if (me.movetype !== 1) {
+ me.movetype = 1; // 加入标记,保证只执行一次
+ me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
+ me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+ me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+ me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+ // 指定距离 <= 下拉距离
+ if (me.movetype !== 2) {
+ me.movetype = 2; // 加入标记,保证只执行一次
+ me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+ if (diff > 0) { // 向下拉
+ me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+ } else { // 向上收
+ me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+ me.downHight = Math.round(me.downHight) // 取整
+ let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+ me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+ me.lastPoint = curPoint; // 记录本次移动的点
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function(e) {
+ // 如果下拉区域高度已改变,则需重置回来
+ if (this.isMoveDown) {
+ if (this.downHight >= this.optDown.offset) {
+ // 符合触发刷新的条件
+ this.triggerDownScroll();
+ // 不符合的话 则重置
+ this.downHight = 0;
+ this.endDownScrollCall(this);
+ this.movetype = 0;
+ this.isMoveDown = false;
+ } else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+ let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 上滑
+ if (isScrollUp) {
+ // 需检查滑动的角度
+ let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
+ if (angle > 80) {
+ // 检查并触发上拉
+ this.triggerUpScroll(true);
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function(e) {
+ if (!e) {
+ x: 0,
+ y: 0
+ if (e.touches && e.touches[0]) {
+ x: e.touches[0].pageX,
+ y: e.touches[0].pageY
+ } else if (e.changedTouches && e.changedTouches[0]) {
+ x: e.changedTouches[0].pageX,
+ y: e.changedTouches[0].pageY
+ x: e.clientX,
+ y: e.clientY
+/* 计算两点之间的角度: 区间 [0,90]*/
+MeScroll.prototype.getAngle = function(p1, p2) {
+ let x = Math.abs(p1.x - p2.x);
+ let y = Math.abs(p1.y - p2.y);
+ let z = Math.sqrt(x * x + y * y);
+ let angle = 0;
+ if (z !== 0) {
+ angle = Math.asin(y / z) / Math.PI * 180;
+ return angle
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function() {
+ if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+ //return true则处于完全自定义状态
+ this.showDownScroll(); // 下拉刷新中...
+ !this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function() {
+ this.isDownScrolling = true; // 标记下拉中
+ if (this.optDown.native) {
+ uni.startPullDownRefresh(); // 系统自带的下拉刷新
+ this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
+ this.downHight = this.optDown.offset; // 更新下拉区域高度
+ this.showDownLoadingCall(this.downHight); // 下拉刷新中...
+MeScroll.prototype.showDownLoadingCall = function(downHight) {
+ this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
+ this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function() {
+ this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function() {
+ if (this.optDown.native) { // 结束原生下拉刷新
+ this.isDownScrolling = false;
+ uni.stopPullDownRefresh();
+ // 结束下拉刷新的方法
+ let endScroll = function() {
+ me.downHight = 0;
+ me.isDownScrolling = false;
+ me.endDownScrollCall(me);
+ if(!me.isScrollBody){
+ me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+ me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+ // 结束下拉刷新时的回调
+ let delay = 0;
+ if (me.optDown.beforeEndDownScroll) {
+ delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
+ if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
+ if (typeof delay === 'number' && delay > 0) {
+ setTimeout(endScroll, delay);
+ endScroll();
+MeScroll.prototype.endDownScrollCall = function() {
+ this.optDown.endDownScroll && this.optDown.endDownScroll(this);
+ this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function(isLock) {
+ if (isLock == null) isLock = true;
+ this.optDown.isLock = isLock;
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function(isLock) {
+ this.optUp.isLock = isLock;
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function() {
+ me.optUp = me.options.up || {use: false}
+ if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+ me.extendUpScroll(me.optUp);
+ if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
+ me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
+ me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
+ if (me.optUp.inited) {
+ me.optUp.inited(me);
+/*滚动到底部的事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onReachBottom = function() {
+ if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+ if (!this.optUp.isLock && this.optUp.hasNext) {
+ this.triggerUpScroll();
+/*列表滚动事件 (仅mescroll-body生效)*/
+MeScroll.prototype.onPageScroll = function(e) {
+ if (!this.isScrollBody) return;
+ // 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+ this.setScrollTop(e.scrollTop);
+ // 顶部按钮的显示隐藏
+ if (e.scrollTop >= this.optUp.toTop.offset) {
+ this.showTopBtn();
+ this.hideTopBtn();
+/*列表滚动事件*/
+MeScroll.prototype.scroll = function(e, onScroll) {
+ // 更新滚动条的位置
+ // 更新滚动内容高度
+ this.setScrollHeight(e.scrollHeight);
+ // 向上滑还是向下滑动
+ if (this.preScrollY == null) this.preScrollY = 0;
+ this.isScrollUp = e.scrollTop - this.preScrollY > 0;
+ this.preScrollY = e.scrollTop;
+ // 上滑 && 检查并触发上拉
+ this.isScrollUp && this.triggerUpScroll(true);
+ // 滑动监听
+ this.optUp.onScroll && onScroll && onScroll()
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function(isCheck) {
+ if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+ // 是否校验在底部; 默认不校验
+ if (isCheck === true) {
+ let canUp = false;
+ // 还有下一页 && 没有锁定 && 不在下拉中
+ if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+ if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+ canUp = true; // 标记可上拉
+ if (canUp === false) return;
+ this.showUpScroll(); // 上拉加载中...
+ this.optUp.page.num++; // 预先加一页,如果失败则减回
+ this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+ this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+ this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.optUp.callback(this); // 执行回调,联网加载数据
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function() {
+ this.isUpScrolling = true; // 标记上拉加载中
+ this.optUp.showLoading && this.optUp.showLoading(this); // 回调
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function() {
+ this.optUp.hasNext = false; // 标记无更多数据
+ this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function() {
+ this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function(isShowNoMore) {
+ if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+ if (isShowNoMore) {
+ this.showNoMore(); // isShowNoMore=true,显示无更多数据
+ this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
+ this.isUpScrolling = false; // 标记结束上拉加载
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+MeScroll.prototype.resetUpScroll = function(isShowLoading) {
+ if (this.optUp && this.optUp.use) {
+ let page = this.optUp.page;
+ this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
+ this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
+ page.num = this.startNum; // 重置为第一页
+ page.time = null; // 重置时间为空
+ if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+ if (isShowLoading == null) {
+ this.removeEmpty(); // 移除空布局
+ this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
+ this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
+ this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
+ this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
+ this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function(num) {
+ this.optUp.page.num = num - 1;
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function(size) {
+ this.optUp.page.size = size;
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
+ let hasNext;
+ if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
+ this.endSuccess(dataSize, hasNext, systime);
+ * totalSize: 列表所有数据总数量(必传)
+MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
+ if (this.optUp.use && totalSize != null) {
+ let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
+ hasNext = loadSize < totalSize; // 是否还有下一页
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
+ // 结束下拉刷新
+ if (me.isDownScrolling) {
+ me.isDownEndSuccess = true
+ me.endDownScroll();
+ // 结束上拉加载
+ if (me.optUp.use) {
+ let isShowNoMore; // 是否已无更多数据
+ if (dataSize != null) {
+ let pageNum = me.optUp.page.num; // 当前页码
+ let pageSize = me.optUp.page.size; // 每页长度
+ // 如果是第一页
+ if (pageNum === 1) {
+ if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
+ if (dataSize < pageSize || hasNext === false) {
+ // 返回的数据不满一页时,则说明已无更多数据
+ me.optUp.hasNext = false;
+ if (dataSize === 0 && pageNum === 1) {
+ // 如果第一页无任何数据且配置了空布局
+ isShowNoMore = false;
+ me.showEmpty();
+ // 总列表数少于配置的数量,则不显示无更多数据
+ let allDataSize = (pageNum - 1) * pageSize + dataSize;
+ if (allDataSize < me.optUp.noMoreSize) {
+ isShowNoMore = true;
+ me.removeEmpty(); // 移除空布局
+ // 还有下一页
+ me.optUp.hasNext = true;
+ // 隐藏上拉
+ me.endUpScroll(isShowNoMore);
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function(errDistance) {
+ // 结束下拉,回调失败重置回原来的页码和时间
+ if (this.isDownScrolling) {
+ this.isDownEndSuccess = false
+ if (page && this.prePageNum) {
+ page.num = this.prePageNum;
+ page.time = this.prePageTime;
+ this.endDownScroll();
+ // 结束上拉,回调失败重置回原来的页码
+ if (this.isUpScrolling) {
+ this.optUp.page.num--;
+ this.endUpScroll(false);
+ // 如果是mescroll-body,则需往回滚一定距离
+ if(this.isScrollBody && errDistance !== 0){ // 不处理0
+ if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
+ this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function() {
+ this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function() {
+ this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function() {
+ if (!this.topBtnShow) {
+ this.topBtnShow = true;
+ this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function() {
+ if (this.topBtnShow) {
+ this.topBtnShow = false;
+ this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function() {
+ return this.scrollTop || 0
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function(y) {
+ this.scrollTop = y;
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function(y, t) {
+ this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function(myScrollTo) {
+ this.myScrollTo = myScrollTo
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function() {
+ return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+ * */
+MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
+ let diff = end - star; // 差值
+ if (t === 0 || diff === 0) {
+ callback && callback(end);
+ t = t || 300; // 时长 300ms
+ rate = rate || 30; // 周期 30ms
+ let count = t / rate; // 次数
+ let step = diff / count; // 步长
+ let i = 0; // 计数
+ let timer = setInterval(function() {
+ if (i < count - 1) {
+ star += step;
+ callback && callback(star, timer);
+ i++;
+ callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
+ clearInterval(timer);
+ }, rate);
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function(isReal) {
+ let h = this.clientHeight || 0
+ if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+ h = this.getBodyHeight()
+ return h
+MeScroll.prototype.setClientHeight = function(h) {
+ this.clientHeight = h;
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function() {
+ return this.scrollHeight || 0;
+MeScroll.prototype.setScrollHeight = function(h) {
+ this.scrollHeight = h;
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function() {
+ return this.bodyHeight || 0;
+MeScroll.prototype.setBodyHeight = function(h) {
+ this.bodyHeight = h;
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function(e) {
+ // 小程序不支持e.preventDefault, 已在wxs中禁止
+ // app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+ // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+ if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
@@ -0,0 +1,477 @@
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+ import renderBiz from './wxs/renderjs.js';
+ mixins:[renderBiz]
+ import MeScroll from './mescroll-uni.js';
+ import mescrollI18n from './mescroll-i18n.js';
+ import MescrollTop from './components/mescroll-top.vue';
+ import WxsMixin from './wxs/mixins.js';
+ * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+ * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+ * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+ * @example <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+ name: 'mescroll-uni',
+ fixed: {
+ disableScroll: Boolean
+ vm.mescroll.i18n = i18nOption; // 挂载语言包
+ @import "./mescroll-uni.css";
+ @import './components/mescroll-up.css';
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+const MescrollCompMixin = {
+ // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+ this.handlePageScroll(e)
+ this.handleReachBottom()
+ // 当down的native: true时, 还需传递此方法进到子组件
+ this.handlePullDownRefresh()
+ mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+ onPageScroll: e=>{
+ onReachBottom: ()=>{
+ onPullDownRefresh: ()=>{
+ methods:{
+ handlePageScroll(e){
+ let item = this.$refs["mescrollItem"];
+ if(item && item.mescroll) item.mescroll.onPageScroll(e);
+ handleReachBottom(){
+ if(item && item.mescroll) item.mescroll.onReachBottom();
+ handlePullDownRefresh(){
+ if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+export default MescrollCompMixin;
@@ -0,0 +1,66 @@
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+const MescrollMoreItemMixin = {
+ // 支付宝小程序不支持props的mixin,需写在具体的页面中
+ i: Number, // 每个tab页的专属下标
+ index: { // 当前tab的下标
+ default(){
+ return 0
+ downOption:{
+ auto:false // 不自动加载
+ upOption:{
+ isInit: false // 当前tab是否已初始化
+ // 监听下标的变化
+ index(val){
+ if (this.i === val && !this.isInit) this.mescrollTrigger()
+ // 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
+ let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
+ // mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+ this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+ // 自动加载当前tab的数据
+ if(this.i === this.index){
+ this.mescrollTrigger()
+ // 主动触发加载
+ mescrollTrigger(){
+ this.isInit = true; // 标记为true
+ if (this.mescroll) {
+ if (this.mescroll.optDown.use) {
+ this.mescroll.triggerDownScroll();
+ this.mescroll.triggerUpScroll();
+export default MescrollMoreItemMixin;
@@ -0,0 +1,74 @@
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+const MescrollMoreMixin = {
+ tabIndex: 0, // 当前tab下标
+ // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+ let mescroll = this.getMescroll(this.tabIndex);
+ mescroll && mescroll.onPageScroll(e);
+ mescroll && mescroll.onReachBottom();
+ mescroll && mescroll.onPullDownRefresh();
+ // 根据下标获取对应子组件的mescroll
+ getMescroll(i){
+ if(!this.mescrollItems) this.mescrollItems = [];
+ if(!this.mescrollItems[i]) {
+ // v-for中的refs
+ let vForItem = this.$refs["mescrollItem"];
+ if(vForItem){
+ this.mescrollItems[i] = vForItem[i]
+ // 普通的refs,不可重复
+ this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+ let item = this.mescrollItems[i]
+ return item ? item.mescroll : null
+ // 切换tab,恢复滚动条位置
+ tabChange(i){
+ let mescroll = this.getMescroll(i);
+ if(mescroll){
+ // 延时(比$nextTick靠谱一些),确保元素已渲染
+ mescroll.scrollTo(mescroll.getScrollTop(),0)
+export default MescrollMoreMixin;
@@ -0,0 +1,109 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+ // 传入wxs视图层的数据 (响应式)
+ wxsProp: {
+ optDown:{}, // 下拉刷新的配置
+ scrollTop:0, // 滚动条的距离
+ bodyHeight:0, // body的高度
+ isDownScrolling:false, // 是否正在下拉刷新中
+ isUpScrolling:false, // 是否正在上拉加载中
+ isScrollBody:true, // 是否为mescroll-body滚动
+ isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+ t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+ // 标记调用wxs视图层的方法
+ callProp: {
+ callType: '', // 方法名
+ // 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+ // #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+ wxsBiz: {
+ //注册列表touchstart事件,用于下拉刷新
+ touchstartEvent: e=> {
+ this.mescroll.touchstartEvent(e);
+ //注册列表touchmove事件,用于下拉刷新
+ touchmoveEvent: e=> {
+ this.mescroll.touchmoveEvent(e);
+ //注册列表touchend事件,用于下拉刷新
+ touchendEvent: e=> {
+ this.mescroll.touchendEvent(e);
+ propObserver(){}, // 抹平wxs的写法
+ callObserver(){} // 抹平wxs的写法
+ // 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+ // #ifndef APP-PLUS || H5
+ renderBiz: {
+ propObserver(){} // 抹平renderjs的写法
+ // wxs视图层调用逻辑层的回调
+ wxsCall(msg){
+ if(msg.type === 'setWxsProp'){
+ // 更新wxsProp数据 (值改变才触发更新)
+ this.wxsProp = {
+ optDown: this.mescroll.optDown,
+ scrollTop: this.mescroll.getScrollTop(),
+ bodyHeight: this.mescroll.getBodyHeight(),
+ isDownScrolling: this.mescroll.isDownScrolling,
+ isUpScrolling: this.mescroll.isUpScrolling,
+ isUpBoth: this.mescroll.optUp.isBoth,
+ isScrollBody:this.mescroll.isScrollBody,
+ t: Date.now()
+ }else if(msg.type === 'setLoadType'){
+ // 设置inOffset,outOffset的状态
+ this.downLoadType = msg.downLoadType
+ // 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+ this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+ // 重置是否加载成功的状态
+ this.$set(this.mescroll, 'isDownEndSuccess', null)
+ }else if(msg.type === 'triggerDownScroll'){
+ // 主动触发下拉刷新
+ }else if(msg.type === 'endDownScroll'){
+ this.mescroll.endDownScroll();
+ }else if(msg.type === 'triggerUpScroll'){
+ // 主动触发上拉加载
+ this.mescroll.triggerUpScroll(true);
+ // #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+ // 配置主动触发wxs显示加载进度的回调
+ this.mescroll.optDown.afterLoading = ()=>{
+ this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ // 配置主动触发wxs隐藏加载进度的回调
+ this.mescroll.optDown.afterEndDownScroll = ()=>{
+ this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
+ if(this.downLoadType === 4 || this.downLoadType === 0){
+ this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+ }, delay)
+ // 初始化wxs的数据
+ this.wxsCall({type: 'setWxsProp'})
+export default WxsMixin;
@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+// 与wxs的me实例一致
+var me = {}
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+ window.$mescrollRenderInit = true
+ window.addEventListener('touchstart', function(e){
+ if (me.disabled()) return;
+ me.startPoint = me.getPoint(e); // 记录起点
+ }, {passive: true})
+ window.addEventListener('touchmove', function(e){
+ if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+ var curPoint = me.getPoint(e); // 当前点
+ var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ // 向下拉
+ if (moveY > 0) {
+ if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+ // 只有touch在mescroll的view上面,才禁止bounce
+ var el = e.target;
+ var isMescrollTouch = false;
+ while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+ var cls = el.classList;
+ if (cls && cls.contains('mescroll-render-touch')) {
+ isMescrollTouch = true
+ el = el.parentNode; // 继续检查其父元素
+ // 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+ if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+ }, {passive: false})
+me.getScrollTop = function() {
+ return me.scrollTop || 0
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+ return !me.optDown || !me.optDown.use || me.optDown.native
+me.getPoint = function(e) {
+ return {x: 0,y: 0}
+ return {x: e.touches[0].pageX,y: e.touches[0].pageY}
+ return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
+ return {x: e.clientX,y: e.clientY}
+ * 监听逻辑层数据的变化 (实时更新数据)
+function propObserver(wxsProp) {
+ me.optDown = wxsProp.optDown
+ me.scrollTop = wxsProp.scrollTop
+ me.isDownScrolling = wxsProp.isDownScrolling
+ me.isUpScrolling = wxsProp.isUpScrolling
+ me.isUpBoth = wxsProp.isUpBoth
+/* 导出模块 */
+const renderBiz = {
+ propObserver: propObserver,
+export default renderBiz;
@@ -0,0 +1,268 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+// ------ 自定义下拉刷新动画 start ------
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+ ins.requestAnimationFrame(function () {
+ ins.selectComponent('.mescroll-wxs-content').setStyle({
+ 'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
+ 'transform': 'translateY(' + downHight + 'px)',
+ 'transition': ''
+ // 环形进度条
+ var progress = ins.selectComponent('.mescroll-wxs-progress')
+ progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+ me.downHight = me.optDown.offset
+ 'will-change': 'auto',
+ 'transform': 'translateY(' + me.downHight + 'px)',
+ 'transition': 'transform 300ms'
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+ 'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+ 'will-change': '',
+ 'transform': '',
+// ------ 自定义下拉刷新动画 end ------
+ me.bodyHeight = wxsProp.bodyHeight
+ me.isScrollBody = wxsProp.isScrollBody
+ me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+function callObserver(callProp, oldValue, ins) {
+ if(callProp.callType){
+ // 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+ if(callProp.callType === 'showLoading'){
+ me.showLoading(ins)
+ }else if(callProp.callType === 'endDownScroll'){
+ me.endDownScroll(ins)
+ }else if(callProp.callType === 'clearTransform'){
+ me.clearTransform(ins)
+ * touch事件
+function touchstartEvent(e, ins) {
+ me.downHight = 0; // 下拉的距离
+ me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+ me.startAngle = 0; // 初始角度
+ me.lastPoint = me.startPoint; // 重置上次move的点
+ me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+ me.inTouchend = false; // 标记不是touchend
+ me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+function touchmoveEvent(e, ins) {
+ var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+ if (me.disabled()) return isPrevent;
+ var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+ me.isUpBoth))) {
+ // 下拉的角度是否在配置的范围内
+ if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+ touchendEvent(e, ins); // 提前触发touchend
+ return isPrevent;
+ isPrevent = false // 小程序是return false
+ var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+ // me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+ me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+ // me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+ me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+ var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+ // me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+ me.onMoving(ins, rate, me.downHight)
+ return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+function touchendEvent(e, ins) {
+ if (me.isMoveDown) {
+ if (me.downHight >= me.optDown.offset) {
+ me.downHight = me.optDown.offset; // 更新下拉区域高度
+ // me.triggerDownScroll();
+ me.callMethod(ins, {type: 'triggerDownScroll'})
+ // me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+ me.callMethod(ins, {type: 'endDownScroll'})
+ me.movetype = 0;
+ me.isMoveDown = false;
+ } else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+ var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+ var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+ // me.triggerUpScroll(true);
+ me.callMethod(ins, {type: 'triggerUpScroll'})
+me.getAngle = function (p1, p2) {
+ var x = Math.abs(p1.x - p2.x);
+ var y = Math.abs(p1.y - p2.y);
+ var z = Math.sqrt(x * x + y * y);
+ var angle = 0;
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+ return me.bodyHeight || 0;
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+ if(ins) ins.callMethod('wxsCall', param)
+ callObserver: callObserver,
+ touchstartEvent: touchstartEvent,
+ touchmoveEvent: touchmoveEvent,
+ touchendEvent: touchendEvent
@@ -0,0 +1,80 @@
+ "id": "mescroll-uni",
+ "displayName": "【wxs+renderjs实现】高性能的下拉刷新上拉加载组件",
+ "version": "1.3.7",
+ "description": "支持uni-app的下拉刷新和上拉加载的组件,支持原生页面和局部区域滚动,支持国际化",
+ "keywords": [
+ "mescroll",
+ "下拉刷新",
+ "上拉加载",
+ "翻页",
+ "分页"
+],
+ "repository": "https://github.com/mescroll/mescroll",
+ "engines": {
+ "HBuilderX": "^3.1.0"
+ "dcloudext": {
+ "category": [
+ "前端组件",
+ "通用组件"
+ "sale": {
+ "regular": {
+ "price": "0.00"
+ "sourcecode": {
+ "contact": {
+ "qq": ""
+ "declaration": {
+ "ads": "无",
+ "data": "无",
+ "permissions": "无"
+ "npmurl": "https://www.npmjs.com/package/mescroll-uni"
+ "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"
+ "快应用": {
+ "华为": "y",
+ "联盟": "y"
@@ -0,0 +1,45 @@
+## mescroll --【wxs+renderjs实现】高性能的下拉刷新上拉加载组件
+1. mescroll的uni版本 是专门用在uni-app的下拉刷新和上拉加载的组件
+2. mescroll的uni版本 继承了mescroll.js的实用功能: 自动处理分页, 自动控制无数据, 空布局提示, 回到顶部按钮 ..
+3. mescroll的uni版本 丰富的案例, 自由灵活的api, 超详细的注释, 可让您快速自定义真正属于自己的下拉上拉组件
+<br/>
+## 最新文档(1.3.7版本): <a href="https://www.mescroll.com/uni.html">https://www.mescroll.com/uni.html</a>
+2021-04-13 by 小瑾同学 (文档可能会有缓存,建议打开时刷新一下)
+## 1.3.5版本已调整为[uni_modules](https://uniapp.dcloud.io/uni_modules)
+uni_modules版本的mescroll-body 和 mescroll-empty 支持 [easycom规范](https://uniapp.dcloud.io/collocation/pages?id=easycom)
+所以 main.js 无需再为mescroll-body注册全局组件
+所以个别页面要单独使用 mescroll-empty , 也无需手动注册
+#### 1.3.5以前的用户升级为uni_modules版本:
+```
+1. 删除原来的 @/components/mescroll-uni 组件
+2. 删除 main.js 注册的 mescroll 组件
+3. 从插件市场导入最新mescroll组件 (1.3.5+uni_modules版本)
+4. 全局搜索 '@/components/mescroll-uni/' 替换为 '@/uni_modules/mescroll-uni/components/mescroll-uni/'
+5. mescroll-empty遵循easycom规范, 若某些页面单独使用 'mescroll-empty.vue', 可删除手动导入的代码
+## 近期已更新优化的内容:
+1. 微信小程序, app, h5使用高性能wxs和renderjs, 下拉刷新更流畅丝滑, 尤其能明显解决Android小程序下拉卡顿的问题
+2. 新增`入门极简`示例, 国际化`mescroll-i18n.vue`示例, 轮播吸顶菜单`mescroll-swiper-sticky.vue`示例
+3. 新增 "局部区域滚动" 的案例: mescroll-body-part.vue 和 mescroll-uni-part.vue
+4. 新增 me-video 视频组件, 解决APP端视频下拉悬浮错位的问题, 参考 mescroll-options.vue 示例
+5. 新增 me-tabs 组件,tabs支持水平滑动; 优化mescroll-more和mescroll-swiper的案例, 顶部tab支持水平滑动
+6. 吸顶悬浮提供了原生sticky和监听滚动条实现的示例: sticky.vue 和 sticky-scroll.vue (推荐使用sticky样式实现)
+7. mescroll.scrollTo(y)的y支持css选择器, 包括跨自定义组件的后代选择器, 支持滚动到子组件的view (参考 mescroll-options.vue)
+8. topbar 顶部是否预留状态栏的高度, 默认false; 还可支持设置状态栏背景: 如 '#ffff00', 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
+9. down.bgColor 和 up.bgColor 加载区域的背景,不仅支持色值, 而且还是支持背景图和渐变: 如 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'
+10. topbar,bgColor支持一行代码定义background: [https://www.runoob.com/cssref/css3-pr-background.html](https://www.runoob.com/cssref/css3-pr-background.html)
+<a href="https://ext.dcloud.net.cn/plugin?id=343&update_log">查看更多 ... </a>
+#### mescroll不支持nvue,也暂无支持的计划哈,so sorry~
@@ -0,0 +1,320 @@
+## 2.5.0-20230101(2023-01-01)
+- 秋云图表组件 修改条件编译顺序,确保uniapp的cli方式的项目依赖不完整时可以正常显示
+- 秋云图表组件 恢复props属性directory的使用,以修复vue3项目中,开启echarts后,echarts目录识别错误的bug
+- uCharts.js 修复区域图、混合图只有一个数据时图表显示不正确的bug
+- uCharts.js 修复折线图、区域图中时间轴类别图表tooltip指示点显示不正确的bug
+- uCharts.js 修复x轴使用labelCount时,并且boundaryGap = 'justify' 并且关闭Y轴显示的时候,最后一个坐标值不显示的bug
+- uCharts.js 修复折线图只有一组数据时 ios16 渲染颜色不正确的bug
+- uCharts.js 修复玫瑰图半径显示不正确的bug
+- uCharts.js 柱状图、山峰图增加正负图功能,y轴网格如果需要显示0轴则由 min max 及 splitNumber 确定,后续版本优化自动显示0轴
+- uCharts.js 柱状图column增加 opts.extra.column.labelPosition,数据标签位置,有效值为 outside外部, insideTop内顶部, center内中间, bottom内底部
+- uCharts.js 雷达图radar增加 opts.extra.radar.labelShow,否显示各项标识文案是,默认true
+- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.boxPadding,提示窗边框填充距离,默认3px
+- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.fontSize,提示窗字体大小配置,默认13px
+- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.lineHeight,提示窗文字行高,默认20px
+- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShow,是否显示左侧图例,默认true
+- uCharts.js 提示窗tooltip增加 opts.extra.tooltip.legendShape,图例形状,图例标识样式,有效值为 auto自动跟随图例, diamond◆, circle●, triangle▲, square■, rect▬, line-
+- uCharts.js 标记线markLine增加 opts.extra.markLine.labelFontSize,字体大小配置,默认13px
+- uCharts.js 标记线markLine增加 opts.extra.markLine.labelPadding,标签边框内填充距离,默认6px
+- uCharts.js 折线图line增加 opts.extra.line.linearType,渐变色类型,可选值 none关闭渐变色,custom 自定义渐变色。使用自定义渐变色时请赋值serie.linearColor作为颜色值
+- uCharts.js 折线图line增加 serie.linearColor,渐变色数组,格式为2维数组[起始位置,颜色值],例如[[0,'#0EE2F8'],[0.3,'#2BDCA8'],[0.6,'#1890FF'],[1,'#9A60B4']]
+- uCharts.js 折线图line增加 opts.extra.line.onShadow,是否开启折线阴影,开启后请赋值serie.setShadow阴影设置
+- uCharts.js 折线图line增加 serie.setShadow,阴影配置,格式为4位数组:[offsetX,offsetY,blur,color]
+- uCharts.js 折线图line增加 opts.extra.line.animation,动画效果方向,可选值为vertical 垂直动画效果,horizontal 水平动画效果
+- uCharts.js X轴xAxis增加 opts.xAxis.lineHeight,X轴字体行高,默认20px
+- uCharts.js X轴xAxis增加 opts.xAxis.marginTop,X轴文字距离轴线的距离,默认0px
+- uCharts.js X轴xAxis增加 opts.xAxis.title,当前X轴标题
+- uCharts.js X轴xAxis增加 opts.xAxis.titleFontSize,标题字体大小,默认13px
+- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetY,标题纵向偏移距离,负数为向上偏移,正数向下偏移
+- uCharts.js X轴xAxis增加 opts.xAxis.titleOffsetX,标题横向偏移距离,负数为向左偏移,正数向右偏移
+- uCharts.js X轴xAxis增加 opts.xAxis.titleFontColor,标题字体颜色,默认#666666
+## 报错TypeError: Cannot read properties of undefined (reading 'length')
+- 如果是uni-modules版本组件,请先登录HBuilderX账号;
+- 在HBuilderX中的manifest.json,点击重新获取uniapp的appid,或者删除appid重新粘贴,重新运行;
+- 如果是cli项目请使用码云上的非uniCloud版本组件;
+- 或者添加uniCloud的依赖;
+- 或者使用原生uCharts;
+## 2.4.5-20221130(2022-11-30)
+- uCharts.js 优化tooltip当文字很多变为左侧显示时,如果画布仍显显示不下,提示框错位置变为以左侧0位置起画
+- uCharts.js 折线图修复特殊情况下只有单点数据,并改变线宽后点变为圆形的bug
+- uCharts.js 修复Y轴disabled启用后无效并报错的bug
+- uCharts.js 修复仪表盘起始结束角度特殊情况下显示不正确的bug
+- uCharts.js 雷达图新增参数 opts.extra.radar.radius , 自定义雷达图半径
+- uCharts.js 折线图、区域图增加tooltip指示点,opts.extra.line.activeType/opts.extra.area.activeType,可选值"none"不启用激活指示点,"hollow"空心点模式,"solid"实心点模式
+## 2.4.4-20221102(2022-11-02)
+- 秋云图表组件 修复使用echarts时reload、reshow无法调用重新渲染的bug,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/40)
+- 秋云图表组件 修复使用echarts时,初始化时宽高不正确的bug,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/42)
+- 秋云图表组件 修复uniapp的h5使用history模式时,无法加载echarts的bug
+- 秋云图表组件 小程序端@complete、@scrollLeft、@scrollRight、@getTouchStart、@getTouchMove、@getTouchEnd事件增加opts参数传出,方便一些特殊需求的交互获取数据。
+- uCharts.js 修复calTooltipYAxisData方法内formatter格式化方法未与y轴方法同步的问题,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/43)
+- uCharts.js 地图新增参数opts.series[i].fillOpacity,以透明度方式来设置颜色过度效果,[详见码云PR](https://gitee.com/uCharts/uCharts/pulls/38)
+- uCharts.js 地图新增参数opts.extra.map.active,是否启用点击激活变色
+- uCharts.js 地图新增参数opts.extra.map.activeTextColor,是否启用点击激活变色
+- uCharts.js 地图新增渲染完成事件renderComplete
+- uCharts.js 漏斗图修复当部分数据相同时tooltip提示窗点击错误的bug
+- uCharts.js 漏斗图新增参数series.data[i].centerText 居中标签文案
+- uCharts.js 漏斗图新增参数series.data[i].centerTextSize 居中标签文案字体大小,默认opts.fontSize
+- uCharts.js 漏斗图新增参数series.data[i].centerTextColor 居中标签文案字体颜色,默认#FFFFFF
+- uCharts.js 漏斗图新增参数opts.extra.funnel.minSize 最小值的最小宽度,默认0
+- uCharts.js 进度条新增参数opts.extra.arcbar.direction,动画方向,可选值为cw顺时针、ccw逆时针
+- uCharts.js 混合图新增参数opts.extra.mix.line.width,折线的宽度,默认2
+- uCharts.js 修复tooltip开启horizentalLine水平横线标注时,图表显示错位的bug
+- uCharts.js 修复开启滚动条后X轴文字超出绘图区域后的隐藏逻辑
+- uCharts.js 柱状图、条状图修复堆叠模式不能通过{value,color}赋值单个柱子颜色的问题
+- uCharts.js 气泡图修复不识别series.textSize和series.textColor的bug
+1. 如果是uni-modules版本组件,请先登录HBuilderX账号;
+2. 在HBuilderX中的manifest.json,点击重新获取uniapp的appid,或者删除appid重新粘贴,重新运行;
+3. 如果是cli项目请使用码云上的非uniCloud版本组件;
+4. 或者添加uniCloud的依赖;
+5. 或者使用原生uCharts;
+## 2.4.3-20220505(2022-05-05)
+- 秋云图表组件 修复开启canvas2d后将series赋值为空数组显示加载图标时,再次赋值后画布闪动的bug
+- 秋云图表组件 修复升级hbx最新版后ECharts的highlight方法报错的bug
+- uCharts.js 雷达图新增参数opts.extra.radar.gridEval,数据点位网格抽希,默认1
+- uCharts.js 雷达图新增参数opts.extra.radar.axisLabel, 是否显示刻度点值,默认false
+- uCharts.js 雷达图新增参数opts.extra.radar.axisLabelTofix,刻度点值小数位数,默认0
+- uCharts.js 雷达图新增参数opts.extra.radar.labelPointShow,是否显示末端刻度圆点,默认false
+- uCharts.js 雷达图新增参数opts.extra.radar.labelPointRadius,刻度圆点的半径,默认3
+- uCharts.js 雷达图新增参数opts.extra.radar.labelPointColor,刻度圆点的颜色,默认#cccccc
+- uCharts.js 雷达图新增参数opts.extra.radar.linearType,渐变色类型,可选值"none"关闭渐变,"custom"开启渐变
+- uCharts.js 雷达图新增参数opts.extra.radar.customColor,自定义渐变颜色,数组类型对应series的数组长度以匹配不同series颜色的不同配色方案,例如["#FA7D8D", "#EB88E2"]
+- uCharts.js 雷达图优化支持series.textColor、series.textSize属性
+- uCharts.js 柱状图中温度计式图标,优化支持全圆角类型,修复边框有缝隙的bug,详见官网【演示】中的温度计图表
+- uCharts.js 柱状图新增参数opts.extra.column.activeWidth,当前点击柱状图的背景宽度,默认一个单元格单位
+- uCharts.js 混合图增加opts.extra.mix.area.gradient 区域图是否开启渐变色
+- uCharts.js 混合图增加opts.extra.mix.area.opacity 区域图透明度,默认0.2
+- uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelText,自定义标签文字,避免formatter格式化的繁琐,详见官网【演示】中的饼图
+- uCharts.js 饼图、圆环图、玫瑰图、漏斗图,增加opts.series[0].data[i].labelShow,自定义是否显示某一个指示标签,避免因饼图类别太多导致标签重复或者居多导致图形变形的问题,详见官网【演示】中的饼图
+- uCharts.js 增加opts.series[i].legendText/opts.series[0].data[i].legendText(与series.name同级)自定义图例显示文字的方法
+- uCharts.js 优化X轴、Y轴formatter格式化方法增加形参,统一为fromatter:function(value,index,opts){}
+- uCharts.js 修复横屏模式下无法使用双指缩放方法的bug
+- uCharts.js 修复当只有一条数据或者多条数据值相等的时候Y轴自动计算的最大值错误的bug
+- 【官网模板】增加外部自定义图例与图表交互的例子,[点击跳转](https://www.ucharts.cn/v2/#/layout/info?id=2)
+## 注意:非unimodules 版本如因更新 hbx 至 3.4.7 导致报错如下,请到码云更新非 unimodules 版本组件,[点击跳转](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6)
+> Error in callback for immediate watcher "uchartsOpts": "SyntaxError: Unexpected token u in JSON at position 0"
+## 2.4.2-20220421(2022-04-21)
+- 秋云图表组件 修复HBX升级3.4.6.20220420版本后echarts报错的问题
+## 2.4.2-20220420(2022-04-20)
+## 重要!此版本uCharts新增了很多功能,修复了诸多已知问题
+- 秋云图表组件 新增onzoom开启双指缩放功能(仅uCharts),前提需要直角坐标系类图表类型,并且ontouch为true、opts.enableScroll为true,详见实例项目K线图
+- 秋云图表组件 新增optsWatch是否监听opts变化,关闭optsWatch后,动态修改opts不会触发图表重绘
+- 秋云图表组件 修复开启canvas2d功能后,动态更新数据后画布闪动的bug
+- 秋云图表组件 去除directory属性,改为自动获取echarts.min.js路径(升级不受影响)
+- 秋云图表组件 增加getImage()方法及@getImage事件,通过ref调用getImage()方法获,触发@getImage事件获取当前画布的base64图片文件流。
+- 秋云图表组件 支付宝、字节跳动、飞书、快手小程序支持开启canvas2d同层渲染设置。
+- 秋云图表组件 新增加【非uniCloud】版本组件,避免有些不需要uniCloud的使用组件发布至小程序需要提交隐私声明问题,请到码云[【非uniCloud版本】](https://gitee.com/uCharts/uCharts/tree/master/uni-app/uCharts-%E7%BB%84%E4%BB%B6),或npm[【非uniCloud版本】](https://www.npmjs.com/package/@qiun/uni-ucharts)下载使用。
+- uCharts.js 新增dobuleZoom双指缩放功能
+- uCharts.js 新增山峰图type="mount",数据格式为饼图类格式,不需要传入categories,具体详见新版官网在线演示
+- uCharts.js 修复折线图当数据中存在null时tooltip报错的bug
+- uCharts.js 修复饼图类当画布比较小时自动计算的半径是负数报错的bug
+- uCharts.js 统一各图表类型的series.formatter格式化方法的形参为(val, index, series, opts),方便格式化时有更多参数可用
+- uCharts.js 标记线功能增加labelText自定义显示文字,增加labelAlign标签显示位置(左侧或右侧),增加标签显示位置微调labelOffsetX、labelOffsetY
+- uCharts.js 修复条状图当数值很小时开启圆角后样式错误的bug
+- uCharts.js 修复X轴开启disabled后,X轴仍占用空间的bug
+- uCharts.js 修复X轴开启滚动条并且开启rotateLabel后,X轴文字与滚动条重叠的bug
+- uCharts.js 增加X轴rotateAngle文字旋转自定义角度,取值范围(-90至90)
+- uCharts.js 修复地图文字标签层级显示不正确的bug
+- uCharts.js 修复饼图、圆环图、玫瑰图当数据全部为0的时候不显示数据标签的bug
+- uCharts.js 修复当opts.padding上边距为0时,Y轴顶部刻度标签位置不正确的bug
+## 另外我们还开发了各大原生小程序组件,已发布至码云和npm
+[https://gitee.com/uCharts/uCharts](https://gitee.com/uCharts/uCharts)
+[https://www.npmjs.com/~qiun](https://www.npmjs.com/~qiun)
+## 对于原生uCharts文档我们已上线新版官方网站,详情点击下面链接进入官网
+[https://www.uCharts.cn/v2/](https://www.ucharts.cn/v2/)
+## 2.3.7-20220122(2022-01-22)
+## 重要!使用vue3编译,请使用cli模式并升级至最新依赖,HbuilderX编译需要使用3.3.8以上版本
+- uCharts.js 修复uni-app平台组件模式使用vue3编译到小程序报错的bug。
+## 2.3.7-20220118(2022-01-18)
+## 注意,使用vue3的前提是需要3.3.8.20220114-alpha版本的HBuilder!
+## 2.3.67-20220118(2022-01-18)
+- 秋云图表组件 组件初步支持vue3,全端编译会有些问题,具体详见下面修改:
+1. 小程序端运行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new uni_modules_qiunDataCharts_js_sdk_uCharts_uCharts.uCharts,将.uCharts去掉。
+2. 小程序端发行时,在uni_modules文件夹的qiun-data-charts.js中搜索 new e.uCharts,将.uCharts去掉,变为 new e。
+3. 如果觉得上述步骤比较麻烦,如果您的项目只编译到小程序端,可以修改u-charts.js最后一行导出方式,将 export default uCharts;变更为 export default { uCharts: uCharts }; 这样变更后,H5和App端的renderjs会有问题,请开发者自行选择。(此问题非组件问题,请等待DC官方修复Vue3的小程序端)
+## 2.3.6-20220111(2022-01-11)
+- 秋云图表组件 修改组件 props 属性中的 background 默认值为 rgba(0,0,0,0)
+## 2.3.6-20211201(2021-12-01)
+- uCharts.js 修复bar条状图开启圆角模式时,值很小时圆角渲染错误的bug
+## 2.3.5-20211014(2021-10-15)
+- uCharts.js 增加vue3的编译支持(仅原生uCharts,qiun-data-charts组件后续会支持,请关注更新)
+## 2.3.4-20211012(2021-10-12)
+- 秋云图表组件 修复 mac os x 系统 mouseover 事件丢失的 bug
+## 2.3.3-20210706(2021-07-06)
+- uCharts.js 增加雷达图开启数据点值(opts.dataLabel)的显示
+## 2.3.2-20210627(2021-06-27)
+- 秋云图表组件 修复tooltipCustom个别情况下传值不正确报错TypeError: Cannot read property 'name' of undefined的bug
+## 2.3.1-20210616(2021-06-16)
+- uCharts.js 修复圆角柱状图使用4角圆角时,当数值过大时不正确的bug
+## 2.3.0-20210612(2021-06-12)
+- uCharts.js 【重要】uCharts增加nvue兼容,可在nvue项目中使用gcanvas组件渲染uCharts,[详见码云uCharts-demo-nvue](https://gitee.com/uCharts/uCharts)
+- 秋云图表组件 增加tapLegend属性,是否开启图例点击交互事件
+- 秋云图表组件 getIndex事件中增加返回uCharts实例中的opts参数,以便在页面中调用参数
+- 示例项目 pages/other/other.vue增加app端自定义tooltip的方法,详见showOptsTooltip方法
+## 2.2.1-20210603(2021-06-03)
+- uCharts.js 修复饼图、圆环图、玫瑰图,当起始角度不为0时,tooltip位置不准确的bug
+- uCharts.js 增加温度计式柱状图开启顶部半圆形的配置
+## 2.2.0-20210529(2021-05-29)
+- uCharts.js 增加条状图type="bar"
+- 示例项目 pages/ucharts/ucharts.vue增加条状图的demo
+## 2.1.7-20210524(2021-05-24)
+- uCharts.js 修复大数据量模式下曲线图不平滑的bug
+## 2.1.6-20210523(2021-05-23)
+- 秋云图表组件 修复小程序端开启滚动条更新数据后滚动条位置不符合预期的bug
+## 2.1.5-2021051702(2021-05-17)
+- uCharts.js 修复自定义Y轴min和max值为0时不能正确显示的bug
+## 2.1.5-20210517(2021-05-17)
+- uCharts.js 修复Y轴自定义min和max时,未按指定的最大值最小值显示坐标轴刻度的bug
+## 2.1.4-20210516(2021-05-16)
+- 秋云图表组件 优化onWindowResize防抖方法
+- 秋云图表组件 修复APP端uCharts更新数据时,清空series显示loading图标后再显示图表,图表抖动的bug
+- uCharts.js 修复开启canvas2d后,x轴、y轴、series自定义字体大小未按比例缩放的bug
+- 示例项目 修复format-e.vue拼写错误导致app端使用uCharts渲染图表
+## 2.1.3-20210513(2021-05-13)
+- 秋云图表组件 修改uCharts变更chartData数据为updateData方法,支持带滚动条的数据动态打点
+- 秋云图表组件 增加onWindowResize防抖方法 fix by ど誓言,如尘般染指流年づ
+- 秋云图表组件 H5或者APP变更chartData数据显示loading图表时,原数据闪现的bug
+- 秋云图表组件 props增加errorReload禁用错误点击重新加载的方法
+- uCharts.js 增加tooltip显示category(x轴对应点位)标题的功能,opts.extra.tooltip.showCategory,默认为false
+- uCharts.js 修复mix混合图只有柱状图时,tooltip的分割线显示位置不正确的bug
+- uCharts.js 修复开启滚动条,图表在拖动中动态打点,滚动条位置不正确的bug
+- uCharts.js 修复饼图类数据格式为echarts数据格式,series为空数组报错的bug
+- 示例项目 修改uCharts.js更新到v2.1.2版本后,@getIndex方法获取索引值变更为e.currentIndex.index
+- 示例项目 pages/updata/updata.vue增加滚动条拖动更新(数据动态打点)的demo
+- 示例项目 pages/other/other.vue增加errorReload禁用错误点击重新加载的demo
+## 2.1.2-20210509(2021-05-09)
+秋云图表组件 修复APP端初始化时就传入chartData或lacaldata不显示图表的bug
+## 2.1.1-20210509(2021-05-09)
+- 秋云图表组件 变更ECharts的eopts配置在renderjs内执行,支持在config-echarts.js配置文件内写function配置。
+- 秋云图表组件 修复APP端报错Prop being mutated: "onmouse"错误的bug。
+- 秋云图表组件 修复APP端报错Error: Not Found:Page[6][-1,27] at view.umd.min.js:1的bug。
+## 2.1.0-20210507(2021-05-07)
+- 秋云图表组件 修复初始化时就有数据或者数据更新的时候loading加载动画闪动的bug
+- uCharts.js 修复x轴format方法categories为字符串类型时返回NaN的bug
+- uCharts.js 修复series.textColor、legend.fontColor未执行全局默认颜色的bug
+## 2.1.0-20210506(2021-05-06)
+- 秋云图表组件 修复极个别情况下报错item.properties undefined的bug
+- 秋云图表组件 修复极个别情况下关闭加载动画reshow不起作用,无法显示图表的bug
+- 示例项目 pages/ucharts/ucharts.vue 增加时间轴折线图(type="tline")、时间轴区域图(type="tarea")、散点图(type="scatter")、气泡图demo(type="bubble")、倒三角形漏斗图(opts.extra.funnel.type="triangle")、金字塔形漏斗图(opts.extra.funnel.type="pyramid")
+- 示例项目 pages/format-u/format-u.vue 增加X轴format格式化示例
+- uCharts.js 升级至v2.1.0版本
+- uCharts.js 修复 玫瑰图面积模式点击tooltip位置不正确的bug
+- uCharts.js 修复 玫瑰图点击图例,只剩一个类别显示空白的bug
+- uCharts.js 修复 饼图类图点击图例,其他图表tooltip位置某些情况下不准的bug
+- uCharts.js 修复 x轴为矢量轴(时间轴)情况下,点击tooltip位置不正确的bug
+- uCharts.js 修复 词云图获取点击索引偶尔不准的bug
+- uCharts.js 增加 直角坐标系图表X轴format格式化方法(原生uCharts.js用法请使用formatter)
+- uCharts.js 增加 漏斗图扩展配置,倒三角形(opts.extra.funnel.type="triangle"),金字塔形(opts.extra.funnel.type="pyramid")
+- uCharts.js 增加 散点图(opts.type="scatter")、气泡图(opts.type="bubble")
+- 后期计划 完善散点图、气泡图,增加markPoints标记点,增加横向条状图。
+## 2.0.0-20210502(2021-05-02)
+- uCharts.js 修复词云图获取点击索引不正确的bug
+## 2.0.0-20210501(2021-05-01)
+- 秋云图表组件 修复QQ小程序、百度小程序在关闭动画效果情况下,v-for循环使用图表,显示不正确的bug
+## 2.0.0-20210426(2021-04-26)
+- 秋云图表组件 修复QQ小程序不支持canvas2d的bug
+- 秋云图表组件 修复钉钉小程序某些情况点击坐标计算错误的bug
+- uCharts.js 增加 extra.column.categoryGap 参数,柱状图类每个category点位(X轴点)柱子组之间的间距
+- uCharts.js 增加 yAxis.data[i].titleOffsetY 参数,标题纵向偏移距离,负数为向上偏移,正数向下偏移
+- uCharts.js 增加 yAxis.data[i].titleOffsetX 参数,标题横向偏移距离,负数为向左偏移,正数向右偏移
+- uCharts.js 增加 extra.gauge.labelOffset 参数,仪表盘标签文字径向便宜距离,默认13px
+## 2.0.0-20210422-2(2021-04-22)
+秋云图表组件 修复 formatterAssign 未判断 args[key] == null 的情况导致栈溢出的 bug
+## 2.0.0-20210422(2021-04-22)
+- 秋云图表组件 修复H5、APP、支付宝小程序、微信小程序canvas2d模式下横屏模式的bug
+## 2.0.0-20210421(2021-04-21)
+- uCharts.js 修复多行图例的情况下,图例在上方或者下方时,图例float为左侧或者右侧时,第二行及以后的图例对齐方式不正确的bug
+## 2.0.0-20210420(2021-04-20)
+- 秋云图表组件 修复微信小程序开启canvas2d模式后,windows版微信小程序不支持canvas2d模式的bug
+- 秋云图表组件 修改非uni_modules版本为v2.0版本qiun-data-charts组件
+## 2.0.0-20210419(2021-04-19)
+## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧绿色【使用HBuilderX导入插件】即可使用,示例项目请点击右侧蓝色按钮【使用HBuilderX导入示例项目】。
+## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,如仍不好用,请重启电脑;
+## 如果是cli项目,请尝试清理node_modules,重新install,还不行就删除项目,再重新install。
+## 此问题已于DCloud官方确认,HBuilderX下个版本会修复。
+## 其他图表不显示问题详见[常见问题选项卡](https://demo.ucharts.cn)
+## <font color=#FF0000> 新手请先完整阅读帮助文档及常见问题3遍,右侧蓝色按钮示例项目请看2遍! </font>
+## [DEMO演示及在线生成工具(v2.0文档)https://demo.ucharts.cn](https://demo.ucharts.cn)
+## [图表组件在项目中的应用参见 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
+- uCharts.js 修复混合图中柱状图单独设置颜色不生效的bug
+- uCharts.js 修复多Y轴单独设置fontSize时,开启canvas2d后,未对应放大字体的bug
+## 2.0.0-20210418(2021-04-18)
+- 秋云图表组件 增加directory配置,修复H5端history模式下如果发布到二级目录无法正确加载echarts.min.js的bug
+## 2.0.0-20210416(2021-04-16)
+- 秋云图表组件 修复APP端某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员
+- 示例项目 修复APP端v-for循环某些情况下报错`Not Found Page`的bug,fix by 高级bug开发技术员
+- uCharts.js 修复非直角坐标系tooltip提示窗右侧超出未变换方向显示的bug
+## 2.0.0-20210415(2021-04-15)
+- 秋云图表组件 修复H5端发布到二级目录下echarts无法加载的bug
+- 秋云图表组件 修复某些情况下echarts.off('finished')移除监听事件报错的bug
+## 2.0.0-20210414(2021-04-14)
+- 秋云图表组件 修复H5端在cli项目下ECharts引用地址错误的bug
+- 示例项目 增加ECharts的formatter用法的示例(详见示例项目format-e.vue)
+- uCharts.js 增加圆环图中心背景色的配置extra.ring.centerColor
+- uCharts.js 修复微信小程序安卓端柱状图开启透明色后显示不正确的bug
+## 2.0.0-20210413(2021-04-13)
+- 秋云图表组件 修复百度小程序多个图表真机未能正确获取根元素dom尺寸的bug
+- 秋云图表组件 修复百度小程序横屏模式方向不正确的bug
+- 秋云图表组件 修改ontouch时,@getTouchStart@getTouchMove@getTouchEnd的触发条件
+- uCharts.js 修复饼图类数据格式series属性不生效的bug
+- uCharts.js 增加时序区域图 详见示例项目中ucharts.vue
+## 2.0.0-20210412-2(2021-04-12)
+## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX。如仍不好用,请重启电脑,此问题已于DCloud官方确认,HBuilderX下个版本会修复。
+## [图表组件在uniCloudAdmin中的应用 UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
+- 秋云图表组件 修复uCharts在APP端横屏模式下不能正确渲染的bug
+- 示例项目 增加ECharts柱状图渐变色、圆角柱状图、横向柱状图(条状图)的示例
+## 2.0.0-20210412(2021-04-12)
+- 秋云图表组件 修复created中判断echarts导致APP端无法识别,改回mounted中判断echarts初始化
+- uCharts.js 修复2d模式下series.textOffset未乘像素比的bug
+## 2.0.0-20210411(2021-04-11)
+## 初次使用如果提示未注册<qiun-data-charts>组件,请重启HBuilderX,并清空小程序开发者工具缓存。
+- uCharts.js 折线图区域图增加connectNulls断点续连的功能,详见示例项目中ucharts.vue
+- 秋云图表组件 变更初始化方法为created,变更type2d默认值为true,优化2d模式下组件初始化后dom获取不到的bug
+- 秋云图表组件 修复左右布局时,右侧图表点击坐标错误的bug,修复tooltip柱状图自定义颜色显示object的bug
+## 2.0.0-20210410(2021-04-10)
+- 修复左右布局时,右侧图表点击坐标错误的bug,修复柱状图自定义颜色tooltip显示object的bug
+- 增加标记线及柱状图自定义颜色的demo
+## 2.0.0-20210409(2021-04-08)
+## v1.0版本已停更,建议转uni_modules版本组件方式调用,点击右侧【使用HBuilderX导入插件】即可体验,DEMO演示及在线生成工具(v2.0文档)[https://demo.ucharts.cn](https://demo.ucharts.cn)
+## 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
+- uCharts.js 修复钉钉小程序百度小程序measureText不准确的bug,修复2d模式下饼图类activeRadius为按比例放大的bug
+- 修复组件在支付宝小程序端点击位置不准确的bug
+## 2.0.0-20210408(2021-04-07)
+- 修复组件在支付宝小程序端不能显示的bug(目前支付宝小程不能点击交互,后续修复)
+- uCharts.js 修复高分屏下柱状图类,圆弧进度条 自定义宽度不能按比例放大的bug
+## 2.0.0-20210407(2021-04-06)
+## 增加 通过tofix和unit快速格式化y轴的demo add by `howcode`
+## 增加 图表组件在uniCloudAdmin中的应用 [UReport数据报表](https://ext.dcloud.net.cn/plugin?id=4651)
+## 2.0.0-20210406(2021-04-05)
+# 秋云图表组件+uCharts v2.0版本同步上线,使用方法详见https://demo.ucharts.cn帮助页
+## 2.0.0(2021-04-05)
@@ -0,0 +1,1618 @@
+<!--
+ * qiun-data-charts 秋云高性能跨全端图表组件
+ * Copyright (c) 2021 QIUN® 秋云 https://www.ucharts.cn All rights reserved.
+ * Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+ * 复制使用请保留本段注释,感谢支持开源!
+ * 为方便更多开发者使用,如有更好的建议请提交码云 Pull Requests !
+ * uCharts®官方网站
+ * https://www.uCharts.cn
+ * 开源地址:
+ * https://gitee.com/uCharts/uCharts
+ * uni-app插件市场地址:
+ * http://ext.dcloud.net.cn/plugin?id=271
+ -->
+ <view class="chartsview" :id="'ChartBoxId'+cid">
+ <view v-if="mixinDatacomLoading">
+ <!-- 自定义加载状态,请改这里 -->
+ <qiun-loading :loadingType="loadingType" />
+ <view v-if="mixinDatacomErrorMessage && errorShow" @tap="reloading">
+ <!-- 自定义错误提示,请改这里 -->
+ <qiun-error :errorMessage="errorMessage" />
+ <!-- APP和H5采用renderjs渲染图表 -->
+ <!-- #ifdef APP-VUE || H5 -->
+ <block v-if="echarts">
+ :style="{ background: background }"
+ style="width: 100%;height: 100%;"
+ :data-directory="directory"
+ :id="'EC'+cid"
+ :prop="echartsOpts"
+ :change:prop="rdcharts.ecinit"
+ :resize="echartsResize"
+ :change:resize="rdcharts.ecresize"
+ v-show="showchart"
+ </block>
+ <block v-else>
+ v-on:tap="rdcharts.tap"
+ v-on:mousemove="rdcharts.mouseMove"
+ v-on:mousedown="rdcharts.mouseDown"
+ v-on:mouseup="rdcharts.mouseUp"
+ v-on:touchstart="rdcharts.touchStart"
+ v-on:touchmove="rdcharts.touchMove"
+ v-on:touchend="rdcharts.touchEnd"
+ :id="'UC'+cid"
+ :prop="uchartsOpts"
+ :change:prop="rdcharts.ucinit"
+ <canvas
+ :id="cid"
+ :canvasId="cid"
+ :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
+ :disable-scroll="disableScroll"
+ @error="_error"
+ <!-- 支付宝小程序 -->
+ <!-- #ifdef MP-ALIPAY -->
+ <block v-if="ontouch">
+ :width="cWidth * pixel"
+ :height="cHeight * pixel"
+ :disable-scroll="disScroll"
+ @tap="_tap"
+ @touchstart="_touchStart"
+ @touchmove="_touchMove"
+ @touchend="_touchEnd"
+ <block v-if="!ontouch">
+ <!-- 其他小程序通过vue渲染图表 -->
+ <!-- #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO || MP-KUAISHOU || MP-LARK || MP-JD || MP-360 -->
+ <block v-if="type2d">
+ <view v-if="ontouch" @tap="_tap">
+ type="2d"
+ <view v-if="!ontouch" @tap="_tap">
+ <block v-if="!type2d">
+ v-if="showchart"
+ <view v-if="!ontouch" >
+import uCharts from '../../js_sdk/u-charts/u-charts.js';
+import cfu from '../../js_sdk/u-charts/config-ucharts.js';
+// #ifdef APP-VUE || H5
+import cfe from '../../js_sdk/u-charts/config-echarts.js';
+function deepCloneAssign(origin = {}, ...args) {
+ for (let i in args) {
+ for (let key in args[i]) {
+ if (args[i].hasOwnProperty(key)) {
+ origin[key] = args[i][key] && typeof args[i][key] === 'object' ? deepCloneAssign(Array.isArray(args[i][key]) ? [] : {}, origin[key], args[i][key]) : args[i][key];
+ return origin;
+function formatterAssign(args,formatter) {
+ for (let key in args) {
+ if(args.hasOwnProperty(key) && args[key] !== null && typeof args[key] === 'object'){
+ formatterAssign(args[key],formatter)
+ }else if(key === 'format' && typeof args[key] === 'string'){
+ args['formatter'] = formatter[args[key]] ? formatter[args[key]] : undefined;
+ return args;
+// 时间转换函数,为了匹配uniClinetDB读取出的时间与categories不同
+function getFormatDate(date) {
+ var seperator = "-";
+ var year = date.getFullYear();
+ var month = date.getMonth() + 1;
+ var strDate = date.getDate();
+ if (month >= 1 && month <= 9) {
+ month = "0" + month;
+ if (strDate >= 0 && strDate <= 9) {
+ strDate = "0" + strDate;
+ var currentdate = year + seperator + month + seperator + strDate;
+ return currentdate;
+var lastMoveTime = null;
+ * 防抖
+ * @param { Function } fn 要执行的方法
+ * @param { Number } wait 防抖多少毫秒
+ * 在 vue 中使用(注意:不能使用箭头函数,否则this指向不对,并且不能再次封装如:
+ * move(){ // 错误调用方式
+ * debounce(function () {
+ * console.log(this.title);
+ * }, 1000)});
+ * 应该直接使用:// 正确调用方式
+ * move: debounce(function () {
+ * }, 1000)
+function debounce(fn, wait) {
+ let timer = false;
+ return function() {
+ clearTimeout(timer);
+ timer && clearTimeout(timer);
+ timer = setTimeout(() => {
+ timer = false;
+ fn.apply(this, arguments); // 把参数传进去
+ }, wait);
+ name: 'qiun-data-charts',
+ mixins: [uniCloud.mixinDatacom],
+ type: {
+ default: null
+ canvasId: {
+ default: 'uchartsid'
+ canvas2d: {
+ background: {
+ default: 'rgba(0,0,0,0)'
+ animation: {
+ chartData: {
+ series: []
+ eopts: {
+ loadingType: {
+ default: 2
+ errorShow: {
+ errorReload: {
+ errorMessage: {
+ inScrollView: {
+ reshow: {
+ reload: {
+ disableScroll: {
+ optsWatch: {
+ onzoom: {
+ ontap: {
+ ontouch: {
+ onmouse: {
+ onmovetip: {
+ echartsH5: {
+ echartsApp: {
+ tooltipShow: {
+ tooltipFormat: {
+ default: undefined
+ tooltipCustom: {
+ startDate: {
+ endDate: {
+ textEnum: {
+ type: Array,
+ default () {
+ return []
+ groupEnum: {
+ pageScrollTop: {
+ default: 0
+ directory: {
+ default: '/'
+ tapLegend: {
+ menus: {
+ cid: 'uchartsid',
+ inWx: false,
+ inAli: false,
+ inTt: false,
+ inBd: false,
+ inH5: false,
+ inApp: false,
+ inWin: false,
+ type2d: true,
+ disScroll: false,
+ openmouse: false,
+ pixel: 1,
+ cWidth: 375,
+ cHeight: 250,
+ showchart: false,
+ echarts: false,
+ echartsResize:{
+ state:false
+ uchartsOpts: {},
+ echartsOpts: {},
+ drawData:{},
+ lastDrawTime:null,
+ created(){
+ this.cid = this.canvasId
+ if (this.canvasId == 'uchartsid' || this.canvasId == '') {
+ let t = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ let len = t.length
+ let id = ''
+ for (let i = 0; i < 32; i++) {
+ id += t.charAt(Math.floor(Math.random() * len))
+ this.cid = id
+ const systemInfo = uni.getSystemInfoSync()
+ if(systemInfo.platform === 'windows' || systemInfo.platform === 'mac'){
+ this.inWin = true;
+ this.inWx = true;
+ if (this.canvas2d === false || systemInfo.platform === 'windows' || systemInfo.platform === 'mac') {
+ this.type2d = false;
+ this.type2d = true;
+ this.pixel = systemInfo.pixelRatio;
+ //非微信小程序端强制关闭canvas2d模式
+ // #ifndef MP-WEIXIN
+ // #ifdef MP-TOUTIAO || MP-LARK || MP-ALIPAY
+ this.type2d = this.canvas2d;
+ // #ifdef MP-ALIPAY
+ this.inAli = true;
+ // #ifdef MP-BAIDU
+ this.inBd = true;
+ // #ifdef MP-TOUTIAO
+ this.inTt = true;
+ this.disScroll = this.disableScroll;
+ // #ifdef APP-VUE
+ this.inApp = true;
+ if (this.echartsApp === true) {
+ this.echarts = true;
+ this.openmouse = false;
+ // #ifdef APP-NVUE
+ this.mixinDatacomLoading = false
+ this.mixinDatacomErrorMessage = "暂不支持NVUE"
+ // #ifdef H5
+ this.inH5 = true;
+ if(this.inWin === true){
+ this.openmouse = this.onmouse;
+ if (this.echartsH5 === true) {
+ this.$nextTick(()=>{
+ this.beforeInit();
+ // #ifndef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || APP-VUE
+ const time = this.inH5 ? 500 : 200;
+ const _this = this;
+ uni.onWindowResize(
+ debounce(function(res) {
+ if (_this.mixinDatacomLoading == true) {
+ let errmsg = _this.mixinDatacomErrorMessage;
+ if (errmsg !== null && errmsg !== 'null' && errmsg !== '') {
+ if (_this.echarts) {
+ _this.echartsResize.state = !_this.echartsResize.state;
+ _this.resizeHandler();
+ }, time)
+ destroyed(){
+ if(this.echarts === true){
+ delete cfe.option[this.cid]
+ delete cfe.instance[this.cid]
+ delete cfu.option[this.cid]
+ delete cfu.instance[this.cid]
+ // #ifndef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
+ uni.offWindowResize(()=>{})
+ watch: {
+ chartDataProps: {
+ handler(val, oldval) {
+ if (typeof val === 'object') {
+ if (JSON.stringify(val) !== JSON.stringify(oldval)) {
+ this._clearChart();
+ if (val.series && val.series.length > 0) {
+ this.mixinDatacomLoading = true;
+ this.showchart = false;
+ this.mixinDatacomErrorMessage = null;
+ this.mixinDatacomLoading = false;
+ this.mixinDatacomErrorMessage = '参数错误:chartData数据类型错误';
+ immediate: false,
+ deep: true
+ localdata:{
+ if (val.length > 0) {
+ optsProps: {
+ if (JSON.stringify(val) !== JSON.stringify(oldval) && this.echarts === false && this.optsWatch == true) {
+ this.checkData(this.drawData);
+ this.mixinDatacomErrorMessage = '参数错误:opts数据类型错误';
+ eoptsProps: {
+ if (JSON.stringify(val) !== JSON.stringify(oldval) && this.echarts === true) {
+ this.mixinDatacomErrorMessage = '参数错误:eopts数据类型错误';
+ reshow(val, oldval) {
+ if (val === true && this.mixinDatacomLoading === false) {
+ this.echartsResize.state = !this.echartsResize.state;
+ }, 200);
+ reload(val, oldval) {
+ if (val === true) {
+ this.reloading();
+ mixinDatacomErrorMessage(val, oldval) {
+ if (val) {
+ this.emitMsg({name: 'error', params: {type:"error", errorShow: this.errorShow, msg: val, id: this.cid}});
+ if(this.errorShow){
+ console.log('[秋云图表组件]' + val);
+ errorMessage(val, oldval) {
+ if (val && this.errorShow && val !== null && val !== 'null' && val !== '') {
+ this.mixinDatacomErrorMessage = val;
+ optsProps() {
+ return JSON.parse(JSON.stringify(this.opts));
+ eoptsProps() {
+ return JSON.parse(JSON.stringify(this.eopts));
+ chartDataProps() {
+ return JSON.parse(JSON.stringify(this.chartData));
+ beforeInit(){
+ if (typeof this.chartData === 'object' && this.chartData != null && this.chartData.series !== undefined && this.chartData.series.length > 0) {
+ //拷贝一下chartData,为了opts变更后统一数据来源
+ this.drawData = deepCloneAssign({}, this.chartData);
+ this.showchart = true;
+ this.checkData(this.chartData);
+ }else if(this.localdata.length>0){
+ this.localdataInit(this.localdata);
+ }else if(this.collection !== ''){
+ this.getCloudData();
+ localdataInit(resdata){
+ //替换enum类型为正确的描述
+ if(this.groupEnum.length>0){
+ for (let i = 0; i < resdata.length; i++) {
+ for (let j = 0; j < this.groupEnum.length; j++) {
+ if(resdata[i].group === this.groupEnum[j].value){
+ resdata[i].group = this.groupEnum[j].text
+ if(this.textEnum.length>0){
+ for (let j = 0; j < this.textEnum.length; j++) {
+ if(resdata[i].text === this.textEnum[j].value){
+ resdata[i].text = this.textEnum[j].text
+ let needCategories = false;
+ let tmpData = {categories:[], series:[]}
+ let tmpcategories = []
+ let tmpseries = [];
+ //拼接categories
+ needCategories = cfe.categories.includes(this.type)
+ needCategories = cfu.categories.includes(this.type)
+ if(needCategories === true){
+ //如果props中的chartData带有categories,则优先使用chartData的categories
+ if(this.chartData && this.chartData.categories && this.chartData.categories.length>0){
+ tmpcategories = this.chartData.categories
+ //如果是日期类型的数据,不管是本地数据还是云数据,都按起止日期自动拼接categories
+ if(this.startDate && this.endDate){
+ let idate = new Date(this.startDate)
+ let edate = new Date(this.endDate)
+ while (idate <= edate) {
+ tmpcategories.push(getFormatDate(idate))
+ idate = idate.setDate(idate.getDate() + 1)
+ idate = new Date(idate)
+ //否则从结果中去重并拼接categories
+ let tempckey = {};
+ resdata.map(function(item, index) {
+ if (item.text != undefined && !tempckey[item.text]) {
+ tmpcategories.push(item.text)
+ tempckey[item.text] = true
+ tmpData.categories = tmpcategories
+ //拼接series
+ let tempskey = {};
+ if (item.group != undefined && !tempskey[item.group]) {
+ tmpseries.push({ name: item.group, data: [] });
+ tempskey[item.group] = true;
+ //如果没有获取到分组名称(可能是带categories的数据,也可能是不带的饼图类)
+ if (tmpseries.length == 0) {
+ tmpseries = [{ name: '默认分组', data: [] }];
+ //如果是需要categories的图表类型
+ for (let j = 0; j < tmpcategories.length; j++) {
+ let seriesdata = 0;
+ if (resdata[i].text == tmpcategories[j]) {
+ seriesdata = resdata[i].value;
+ tmpseries[0].data.push(seriesdata);
+ //如果是饼图类的图表类型
+ tmpseries[0].data.push({"name": resdata[i].text,"value": resdata[i].value});
+ //如果有分组名
+ for (let k = 0; k < tmpseries.length; k++) {
+ //如果有categories
+ if (tmpcategories.length > 0) {
+ if (tmpseries[k].name == resdata[i].group && resdata[i].text == tmpcategories[j]) {
+ tmpseries[k].data.push(seriesdata);
+ //如果传了group而没有传text,即没有categories(正常情况下这种数据是不符合数据要求规范的)
+ if (tmpseries[k].name == resdata[i].group) {
+ tmpseries[k].data.push(resdata[i].value);
+ tmpData.series = tmpseries
+ this.drawData = deepCloneAssign({}, tmpData);
+ this.checkData(tmpData)
+ reloading() {
+ if(this.errorReload === false){
+ if (this.collection !== '') {
+ this.onMixinDatacomPropsChange(true);
+ checkData(anyData) {
+ let cid = this.cid
+ //复位opts或eopts
+ cfe.option[cid] = deepCloneAssign({}, this.eopts);
+ cfe.option[cid].id = cid;
+ cfe.option[cid].type = this.type;
+ if (this.type && cfu.type.includes(this.type)) {
+ cfu.option[cid] = deepCloneAssign({}, cfu[this.type], this.opts);
+ cfu.option[cid].canvasId = cid;
+ this.mixinDatacomErrorMessage = '参数错误:props参数中type类型不正确';
+ //挂载categories和series
+ let newData = deepCloneAssign({}, anyData);
+ if (newData.series !== undefined && newData.series.length > 0) {
+ if (this.echarts === true) {
+ cfe.option[cid].chartData = newData;
+ this.init()
+ cfu.option[cid].categories = newData.categories;
+ cfu.option[cid].series = newData.series;
+ resizeHandler() {
+ //渲染防抖
+ let currTime = Date.now();
+ let lastDrawTime = this.lastDrawTime?this.lastDrawTime:currTime-3000;
+ let duration = currTime - lastDrawTime;
+ if (duration < 1000) return;
+ let chartdom = uni
+ .createSelectorQuery()
+ // #ifndef MP-ALIPAY
+ .in(this)
+ .select('#ChartBoxId'+this.cid)
+ .boundingClientRect(data => {
+ if (data.width > 0 && data.height > 0) {
+ if (data.width !== this.cWidth || data.height !== this.cHeight) {
+ this.checkData(this.drawData)
+ .exec();
+ getCloudData() {
+ if (this.mixinDatacomLoading == true) {
+ this.mixinDatacomGet()
+ .then(res => {
+ this.mixinDatacomResData = res.result.data;
+ this.localdataInit(this.mixinDatacomResData);
+ .catch(err => {
+ this.mixinDatacomErrorMessage = '请求错误:' + err;
+ onMixinDatacomPropsChange(needReset, changed) {
+ if (needReset == true && this.collection !== '') {
+ _clearChart() {
+ if (this.echarts !== true && cfu.option[cid] && cfu.option[cid].context) {
+ const ctx = cfu.option[cid].context;
+ if(typeof ctx === "object" && !!!cfu.option[cid].update){
+ ctx.clearRect(0, 0, this.cWidth*this.pixel, this.cHeight*this.pixel);
+ ctx.draw();
+ init() {
+ .select('#ChartBoxId'+cid)
+ this.lastDrawTime = Date.now();
+ this.cWidth = data.width;
+ this.cHeight = data.height;
+ if(this.echarts !== true){
+ cfu.option[cid].background = this.background == 'rgba(0,0,0,0)' ? '#FFFFFF' : this.background;
+ cfu.option[cid].canvas2d = this.type2d;
+ cfu.option[cid].pixelRatio = this.pixel;
+ cfu.option[cid].animation = this.animation;
+ cfu.option[cid].width = data.width * this.pixel;
+ cfu.option[cid].height = data.height * this.pixel;
+ cfu.option[cid].onzoom = this.onzoom;
+ cfu.option[cid].ontap = this.ontap;
+ cfu.option[cid].ontouch = this.ontouch;
+ cfu.option[cid].onmouse = this.openmouse;
+ cfu.option[cid].onmovetip = this.onmovetip;
+ cfu.option[cid].tooltipShow = this.tooltipShow;
+ cfu.option[cid].tooltipFormat = this.tooltipFormat;
+ cfu.option[cid].tooltipCustom = this.tooltipCustom;
+ cfu.option[cid].inScrollView = this.inScrollView;
+ cfu.option[cid].lastDrawTime = this.lastDrawTime;
+ cfu.option[cid].tapLegend = this.tapLegend;
+ //如果是H5或者App端,采用renderjs渲染图表
+ if (this.inH5 || this.inApp) {
+ if (this.echarts == true) {
+ cfe.option[cid].ontap = this.ontap;
+ cfe.option[cid].onmouse = this.openmouse;
+ cfe.option[cid].tooltipShow = this.tooltipShow;
+ cfe.option[cid].tooltipFormat = this.tooltipFormat;
+ cfe.option[cid].tooltipCustom = this.tooltipCustom;
+ cfe.option[cid].lastDrawTime = this.lastDrawTime;
+ this.echartsOpts = deepCloneAssign({}, cfe.option[cid]);
+ cfu.option[cid].rotateLock = cfu.option[cid].rotate;
+ this.uchartsOpts = deepCloneAssign({}, cfu.option[cid]);
+ //如果是小程序端,采用uCharts渲染
+ cfu.option[cid] = formatterAssign(cfu.option[cid],cfu.formatter)
+ if (this.type2d === true) {
+ const query = uni.createSelectorQuery().in(this)
+ query
+ .select('#' + cid)
+ .fields({ node: true, size: true })
+ .exec(res => {
+ const canvas = res[0].node;
+ const ctx = canvas.getContext('2d');
+ cfu.option[cid].context = ctx;
+ if(cfu.instance[cid] && cfu.option[cid] && cfu.option[cid].update === true){
+ this._updataUChart(cid)
+ canvas.width = data.width * this.pixel;
+ canvas.height = data.height * this.pixel;
+ canvas._width = data.width * this.pixel;
+ canvas._height = data.height * this.pixel;
+ cfu.option[cid].context.restore();
+ cfu.option[cid].context.save();
+ this._newChart(cid)
+ this.mixinDatacomErrorMessage = '参数错误:开启2d模式后,未获取到dom节点,canvas-id:' + cid;
+ if(this.inAli){
+ cfu.option[cid].context = uni.createCanvasContext(cid, this);
+ if (this.reshow == true) {
+ this.mixinDatacomErrorMessage = '布局错误:未获取到父元素宽高尺寸!canvas-id:' + cid;
+ saveImage(){
+ canvasId: this.cid,
+ success: res=>{
+ //#ifdef H5
+ var a = document.createElement("a");
+ a.href = res.tempFilePath;
+ a.download = this.cid;
+ a.target = '_blank'
+ a.click();
+ //#endif
+ //#ifndef H5
+ filePath: res.tempFilePath,
+ title: '保存成功',
+ duration: 2000
+ },this);
+ getImage(){
+ if(this.type2d == false){
+ this.emitMsg({name: 'getImage', params: {type:"getImage", base64: res.tempFilePath}});
+ .select('#' + this.cid)
+ this.emitMsg({name: 'getImage', params: {type:"getImage", base64: canvas.toDataURL('image/png')}});
+ // #ifndef APP-VUE || H5
+ _newChart(cid) {
+ cfu.instance[cid] = new uCharts(cfu.option[cid]);
+ cfu.instance[cid].addEventListener('renderComplete', () => {
+ this.emitMsg({name: 'complete', params: {type:"complete", complete: true, id: cid, opts: cfu.instance[cid].opts}});
+ cfu.instance[cid].delEventListener('renderComplete')
+ cfu.instance[cid].addEventListener('scrollLeft', () => {
+ this.emitMsg({name: 'scrollLeft', params: {type:"scrollLeft", scrollLeft: true, id: cid, opts: cfu.instance[cid].opts}});
+ cfu.instance[cid].addEventListener('scrollRight', () => {
+ this.emitMsg({name: 'scrollRight', params: {type:"scrollRight", scrollRight: true, id: cid, opts: cfu.instance[cid].opts}});
+ _updataUChart(cid) {
+ cfu.instance[cid].updateData(cfu.option[cid])
+ _tooltipDefault(item, category, index, opts) {
+ if (category) {
+ let data = item.data
+ if(typeof item.data === "object"){
+ data = item.data.value
+ return category + ' ' + item.name + ':' + data;
+ if (item.properties && item.properties.name) {
+ return item.properties.name;
+ return item.name + ':' + item.data;
+ _showTooltip(e) {
+ let tc = cfu.option[cid].tooltipCustom
+ if (tc && tc !== undefined && tc !== null) {
+ let offset = undefined;
+ if (tc.x >= 0 && tc.y >= 0) {
+ offset = { x: tc.x, y: tc.y + 10 };
+ cfu.instance[cid].showToolTip(e, {
+ index: tc.index,
+ offset: offset,
+ textList: tc.textList,
+ formatter: (item, category, index, opts) => {
+ if (typeof cfu.option[cid].tooltipFormat === 'string' && cfu.formatter[cfu.option[cid].tooltipFormat]) {
+ return cfu.formatter[cfu.option[cid].tooltipFormat](item, category, index, opts);
+ return this._tooltipDefault(item, category, index, opts);
+ _tap(e,move) {
+ let currentIndex = null;
+ let legendIndex = null;
+ if (this.inScrollView === true || this.inAli) {
+ .select('#'+this.cid)
+ e.changedTouches=[];
+ if (this.inAli) {
+ e.changedTouches.unshift({ x: e.detail.clientX - data.left, y: e.detail.clientY - data.top});
+ e.changedTouches.unshift({ x: e.detail.x - data.left, y: e.detail.y - data.top - this.pageScrollTop});
+ if(move){
+ if (this.tooltipShow === true) {
+ this._showTooltip(e);
+ currentIndex = cfu.instance[cid].getCurrentDataIndex(e);
+ legendIndex = cfu.instance[cid].getLegendDataIndex(e);
+ if(this.tapLegend === true){
+ cfu.instance[cid].touchLegend(e);
+ this.emitMsg({name: 'getIndex', params: { type:"getIndex", event:{ x: e.detail.x - data.left, y: e.detail.y - data.top }, currentIndex: currentIndex, legendIndex: legendIndex, id: cid, opts: cfu.instance[cid].opts}});
+ e.changedTouches.unshift({ x: e.detail.x - e.currentTarget.offsetLeft, y: e.detail.y - e.currentTarget.offsetTop });
+ this.emitMsg({name: 'getIndex', params: {type:"getIndex", event:{ x: e.detail.x, y: e.detail.y - e.currentTarget.offsetTop }, currentIndex: currentIndex, legendIndex: legendIndex, id: cid, opts: cfu.instance[cid].opts}});
+ _touchStart(e) {
+ lastMoveTime=Date.now();
+ if(cfu.option[cid].enableScroll === true && e.touches.length == 1){
+ cfu.instance[cid].scrollStart(e);
+ this.emitMsg({name:'getTouchStart', params:{type:"touchStart", event:e.changedTouches[0], id:cid, opts: cfu.instance[cid].opts}});
+ _touchMove(e) {
+ let currMoveTime = Date.now();
+ let duration = currMoveTime - lastMoveTime;
+ let touchMoveLimit = cfu.option[cid].touchMoveLimit || 24;
+ if (duration < Math.floor(1000 / touchMoveLimit)) return;//每秒60帧
+ lastMoveTime = currMoveTime;
+ if(cfu.option[cid].enableScroll === true && e.changedTouches.length == 1){
+ cfu.instance[cid].scroll(e);
+ if(this.ontap === true && cfu.option[cid].enableScroll === false && this.onmovetip === true){
+ this._tap(e,true)
+ if(this.ontouch === true && cfu.option[cid].enableScroll === true && this.onzoom === true && e.changedTouches.length == 2){
+ cfu.instance[cid].dobuleZoom(e);
+ this.emitMsg({name: 'getTouchMove', params: {type:"touchMove", event:e.changedTouches[0], id: cid, opts: cfu.instance[cid].opts}});
+ _touchEnd(e) {
+ if(cfu.option[cid].enableScroll === true && e.touches.length == 0){
+ cfu.instance[cid].scrollEnd(e);
+ this.emitMsg({name:'getTouchEnd', params:{type:"touchEnd", event:e.changedTouches[0], id:cid, opts: cfu.instance[cid].opts}});
+ _error(e) {
+ this.mixinDatacomErrorMessage = e.detail.errMsg;
+ emitMsg(msg) {
+ this.$emit(msg.name, msg.params);
+ getRenderType() {
+ //防止如果开启echarts且父元素为v-if的情况renderjs监听不到prop变化的问题
+ if(this.echarts===true && this.mixinDatacomLoading===false){
+ this.beforeInit()
+ toJSON(){
+ return this
+<!-- #ifdef APP-VUE || H5 -->
+<script module="rdcharts" lang="renderjs">
+import uChartsRD from '../../js_sdk/u-charts/u-charts.js';
+var that = {};
+var rootdom = null;
+function rddeepCloneAssign(origin = {}, ...args) {
+ origin[key] = args[i][key] && typeof args[i][key] === 'object' ? rddeepCloneAssign(Array.isArray(args[i][key]) ? [] : {}, origin[key], args[i][key]) : args[i][key];
+function rdformatterAssign(args,formatter) {
+ rdformatterAssign(args[key],formatter)
+ rid:null
+ rootdom = {top:0,left:0}
+ let dm = document.querySelectorAll('uni-main')[0]
+ if(dm === undefined){
+ dm = document.querySelectorAll('uni-page-wrapper')[0]
+ rootdom = {top:dm.offsetTop,left:dm.offsetLeft}
+ if(this.rid === null){
+ this.$ownerInstance && this.$ownerInstance.callMethod('getRenderType')
+ },200)
+ delete cfu.option[this.rid]
+ delete cfu.instance[this.rid]
+ delete cfe.option[this.rid]
+ delete cfe.instance[this.rid]
+ //==============以下是ECharts的方法====================
+ ecinit(newVal, oldVal, owner, instance){
+ let cid = JSON.stringify(newVal.id)
+ this.rid = cid
+ that[cid] = this.$ownerInstance || instance
+ let eopts = JSON.parse(JSON.stringify(newVal))
+ let type = eopts.type;
+ //载入并覆盖默认配置
+ if (type && cfe.type.includes(type)) {
+ cfe.option[cid] = rddeepCloneAssign({}, cfe[type], eopts);
+ cfe.option[cid] = rddeepCloneAssign({}, eopts);
+ let newData = eopts.chartData;
+ if(newData){
+ if(cfe.option[cid].xAxis && cfe.option[cid].xAxis.type && cfe.option[cid].xAxis.type === 'category'){
+ cfe.option[cid].xAxis.data = newData.categories
+ if(cfe.option[cid].yAxis && cfe.option[cid].yAxis.type && cfe.option[cid].yAxis.type === 'category'){
+ cfe.option[cid].yAxis.data = newData.categories
+ cfe.option[cid].series = []
+ for (var i = 0; i < newData.series.length; i++) {
+ cfe.option[cid].seriesTemplate = cfe.option[cid].seriesTemplate ? cfe.option[cid].seriesTemplate : {}
+ let Template = rddeepCloneAssign({},cfe.option[cid].seriesTemplate,newData.series[i])
+ cfe.option[cid].series.push(Template)
+ if (typeof window.echarts === 'object') {
+ this.newEChart()
+ const script = document.createElement('script')
+ script.src = './uni_modules/qiun-data-charts/static/app-plus/echarts.min.js'
+ const rooturl = window.location.origin
+ const directory = instance.getDataset().directory
+ script.src = rooturl + directory + 'uni_modules/qiun-data-charts/static/h5/echarts.min.js'
+ script.onload = this.newEChart
+ document.head.appendChild(script)
+ ecresize(newVal, oldVal, owner, instance){
+ if(cfe.instance[this.rid]){
+ cfe.instance[this.rid].resize()
+ newEChart(){
+ let cid = this.rid
+ if(cfe.instance[cid] === undefined){
+ cfe.instance[cid] = echarts.init(that[cid].$el.children[0])
+ //ontap开启后才触发click事件
+ if(cfe.option[cid].ontap === true){
+ cfe.instance[cid].on('click', resdata => {
+ let event = JSON.parse(JSON.stringify({
+ x:resdata.event.offsetX,y:resdata.event.offsetY
+ }))
+ that[cid].callMethod('emitMsg',{name:"getIndex", params:{type:"getIndex", event:event, currentIndex:resdata.dataIndex, value:resdata.data, seriesName: resdata.seriesName,id:cid}})
+ // 增加ECharts的highlight消息,实现按下移动返回索引功能。add by onefish 创建于 2021-12-11 09:50
+ cfe.instance[cid].on('highlight', resdata => {
+ that[cid].callMethod('emitMsg',{name:"getHighlight", params:{type:"highlight", res:resdata, id:cid}})
+ this.updataEChart(cid,cfe.option[cid])
+ updataEChart(cid,option){
+ //替换option内format属性为formatter的预定义方法
+ option = rdformatterAssign(option,cfe.formatter)
+ if(option.tooltip){
+ option.tooltip.show = option.tooltipShow?true:false;
+ option.tooltip.position = this.tooltipPosition()
+ //tooltipFormat方法,替换组件的tooltipFormat为config-echarts.js内对应的方法
+ if (typeof option.tooltipFormat === 'string' && cfe.formatter[option.tooltipFormat]) {
+ option.tooltip.formatter = option.tooltip.formatter ? option.tooltip.formatter : cfe.formatter[option.tooltipFormat]
+ // 颜色渐变添加的方法
+ if (option.series) {
+ for (let i in option.series) {
+ let linearGradient = option.series[i].linearGradient
+ if (linearGradient) {
+ option.series[i].color = new echarts.graphic.LinearGradient(linearGradient[0],linearGradient[1],linearGradient[2],linearGradient[3],linearGradient[4])
+ cfe.instance[cid].setOption(option, option.notMerge)
+ cfe.instance[cid].on('finished', function(){
+ that[cid].callMethod('emitMsg',{name:"complete",params:{type:"complete",complete:true,id:cid}})
+ if(cfe.instance[cid]){
+ cfe.instance[cid].off('finished')
+ //修复init初始化实例获取宽高不正确问题
+ if(
+ typeof that[cid].$el.children[0].clientWidth != 'undefined' &&
+ (
+ Math.abs( that[cid].$el.children[0].clientWidth - cfe.instance[cid].getWidth() )>3 ||
+ Math.abs( that[cid].$el.children[0].clientHeight - cfe.instance[cid].getHeight() )>3
+ ){this.ecresize();}
+ tooltipPosition(){
+ return (point, params, dom, rect, size) => {
+ let x = point[0]
+ let y = point[1]
+ let viewWidth = size.viewSize[0]
+ let viewHeight = size.viewSize[1]
+ let boxWidth = size.contentSize[0]
+ let boxHeight = size.contentSize[1]
+ let posX = x + 30
+ let posY = y + 30
+ if (posX + boxWidth > viewWidth) {
+ posX = x - boxWidth - 30
+ if (posY + boxHeight > viewHeight) {
+ posY = y - boxHeight - 30
+ return [posX, posY]
+ //==============以下是uCharts的方法====================
+ ucinit(newVal, oldVal, owner, instance){
+ if(JSON.stringify(newVal) == JSON.stringify(oldVal)){
+ if(!newVal.canvasId){
+ let cid = JSON.parse(JSON.stringify(newVal.canvasId))
+ cfu.option[cid] = JSON.parse(JSON.stringify(newVal))
+ cfu.option[cid] = rdformatterAssign(cfu.option[cid],cfu.formatter)
+ let canvasdom = document.getElementById(cid)
+ if(canvasdom && canvasdom.children[0]){
+ cfu.option[cid].context = canvasdom.children[0].getContext("2d")
+ this.updataUChart()
+ this.newUChart()
+ newUChart() {
+ cfu.instance[cid] = new uChartsRD(cfu.option[cid])
+ that[cid].callMethod('emitMsg',{name:"complete",params:{type:"complete",complete:true,id:cid, opts: cfu.instance[cid].opts}})
+ that[cid].callMethod('emitMsg',{name:"scrollLeft",params:{type:"scrollLeft",scrollLeft:true,id:cid, opts: cfu.instance[cid].opts}})
+ that[cid].callMethod('emitMsg',{name:"scrollRight",params:{type:"scrollRight",scrollRight:true,id:cid, opts: cfu.instance[cid].opts}})
+ updataUChart() {
+ tooltipDefault(item, category, index, opts) {
+ return item.properties.name ;
+ showTooltip(e,cid) {
+ return this.tooltipDefault(item, category, index, opts);
+ tap(e) {
+ let ontap = cfu.option[cid].ontap
+ let tooltipShow = cfu.option[cid].tooltipShow
+ let tapLegend = cfu.option[cid].tapLegend
+ if(ontap == false) return;
+ let currentIndex=null
+ let legendIndex=null
+ let rchartdom = document.getElementById('UC'+cid).getBoundingClientRect()
+ let tmpe = {}
+ if(e.detail.x){//tap或者click的事件
+ tmpe = { x: e.detail.x - rchartdom.left, y:e.detail.y - rchartdom.top + rootdom.top}
+ }else{//mouse的事件
+ tmpe = { x: e.clientX - rchartdom.left, y:e.clientY - rchartdom.top + rootdom.top}
+ e.changedTouches = [];
+ e.changedTouches.unshift(tmpe)
+ currentIndex=cfu.instance[cid].getCurrentDataIndex(e)
+ legendIndex=cfu.instance[cid].getLegendDataIndex(e)
+ if(tapLegend === true){
+ if(tooltipShow==true){
+ this.showTooltip(e,cid)
+ that[cid].callMethod('emitMsg',{name:"getIndex",params:{type:"getIndex",event:tmpe,currentIndex:currentIndex,legendIndex:legendIndex,id:cid, opts: cfu.instance[cid].opts}})
+ touchStart(e) {
+ let ontouch = cfu.option[cid].ontouch
+ if(ontouch == false) return;
+ that[cid].callMethod('emitMsg',{name:"getTouchStart",params:{type:"touchStart",event:e.changedTouches[0],id:cid, opts: cfu.instance[cid].opts}})
+ touchMove(e) {
+ if(cfu.option[cid].ontap === true && cfu.option[cid].enableScroll === false && cfu.option[cid].onmovetip === true){
+ let tmpe = { x: e.changedTouches[0].clientX - rchartdom.left, y:e.changedTouches[0].clientY - rchartdom.top + rootdom.top}
+ if(cfu.option[cid].tooltipShow === true){
+ if(ontouch === true && cfu.option[cid].enableScroll === true && cfu.option[cid].onzoom === true && e.changedTouches.length == 2){
+ that[cid].callMethod('emitMsg',{name:"getTouchMove",params:{type:"touchMove",event:e.changedTouches[0],id:cid, opts: cfu.instance[cid].opts}})
+ touchEnd(e) {
+ that[cid].callMethod('emitMsg',{name:"getTouchEnd",params:{type:"touchEnd",event:e.changedTouches[0],id:cid, opts: cfu.instance[cid].opts}})
+ mouseDown(e) {
+ let onmouse = cfu.option[cid].onmouse
+ if(onmouse == false) return;
+ cfu.instance[cid].scrollStart(e)
+ cfu.option[cid].mousedown=true;
+ that[cid].callMethod('emitMsg',{name:"getTouchStart",params:{type:"mouseDown",event:tmpe,id:cid, opts: cfu.instance[cid].opts}})
+ mouseMove(e) {
+ if(cfu.option[cid].mousedown){
+ cfu.instance[cid].scroll(e)
+ that[cid].callMethod('emitMsg',{name:"getTouchMove",params:{type:"mouseMove",event:tmpe,id:cid, opts: cfu.instance[cid].opts}})
+ }else if(cfu.instance[cid]){
+ mouseUp(e) {
+ cfu.instance[cid].scrollEnd(e)
+ cfu.option[cid].mousedown=false;
+ that[cid].callMethod('emitMsg',{name:"getTouchEnd",params:{type:"mouseUp",event:tmpe,id:cid, opts: cfu.instance[cid].opts}})
+<style scoped>
+.chartsview {
@@ -0,0 +1,162 @@
+ <view class="container loading1">
+ <view class="shape shape1"></view>
+ <view class="shape shape2"></view>
+ <view class="shape shape3"></view>
+ <view class="shape shape4"></view>
+ name: 'loading1',
+<style scoped="true">
+.container {
+ width: 30px;
+ height: 30px;
+.container.loading1 {
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+.container .shape {
+ width: 10px;
+ height: 10px;
+ border-radius: 1px;
+.container .shape.shape1 {
+ background-color: #1890FF;
+.container .shape.shape2 {
+ background-color: #91CB74;
+.container .shape.shape3 {
+ background-color: #FAC858;
+.container .shape.shape4 {
+ background-color: #EE6666;
+.loading1 .shape1 {
+ -webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
+ animation: animation1shape1 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation1shape1 {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ -webkit-transform: translate(16px, 16px);
+ transform: translate(16px, 16px);
+@keyframes animation1shape1 {
+.loading1 .shape2 {
+ -webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
+ animation: animation1shape2 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation1shape2 {
+ -webkit-transform: translate(-16px, 16px);
+ transform: translate(-16px, 16px);
+@keyframes animation1shape2 {
+.loading1 .shape3 {
+ -webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
+ animation: animation1shape3 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation1shape3 {
+ -webkit-transform: translate(16px, -16px);
+ transform: translate(16px, -16px);
+@keyframes animation1shape3 {
+.loading1 .shape4 {
+ -webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
+ animation: animation1shape4 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation1shape4 {
+ -webkit-transform: translate(-16px, -16px);
+ transform: translate(-16px, -16px);
+@keyframes animation1shape4 {
+ <view class="container loading2">
+ name: 'loading2',
+.container.loading2 {
+ -webkit-transform: rotate(10deg);
+ transform: rotate(10deg);
+.container.loading2 .shape {
+ border-radius: 5px;
+.container.loading2{
+ -webkit-animation: rotation 1s infinite;
+ animation: rotation 1s infinite;
+.loading2 .shape1 {
+ -webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
+ animation: animation2shape1 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation2shape1 {
+ -webkit-transform: translate(20px, 20px);
+ transform: translate(20px, 20px);
+@keyframes animation2shape1 {
+.loading2 .shape2 {
+ -webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
+ animation: animation2shape2 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation2shape2 {
+ -webkit-transform: translate(-20px, 20px);
+ transform: translate(-20px, 20px);
+@keyframes animation2shape2 {
+.loading2 .shape3 {
+ -webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
+ animation: animation2shape3 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation2shape3 {
+ -webkit-transform: translate(20px, -20px);
+ transform: translate(20px, -20px);
+@keyframes animation2shape3 {
+.loading2 .shape4 {
+ -webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
+ animation: animation2shape4 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation2shape4 {
+ -webkit-transform: translate(-20px, -20px);
+ transform: translate(-20px, -20px);
+@keyframes animation2shape4 {
@@ -0,0 +1,173 @@
+ <view class="container loading3">
+ name: 'loading3',
+ .container.loading3 {
+.container.loading3 .shape1 {
+ border-top-left-radius: 10px;
+.container.loading3 .shape2 {
+ border-top-right-radius: 10px;
+.container.loading3 .shape3 {
+ border-bottom-left-radius: 10px;
+.container.loading3 .shape4 {
+ border-bottom-right-radius: 10px;
+.loading3 .shape1 {
+ -webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
+ animation: animation3shape1 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation3shape1 {
+ -webkit-transform: translate(5px, 5px);
+ transform: translate(5px, 5px);
+@keyframes animation3shape1 {
+.loading3 .shape2 {
+ -webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
+ animation: animation3shape2 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation3shape2 {
+ -webkit-transform: translate(-5px, 5px);
+ transform: translate(-5px, 5px);
+@keyframes animation3shape2 {
+.loading3 .shape3 {
+ -webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
+ animation: animation3shape3 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation3shape3 {
+ -webkit-transform: translate(5px, -5px);
+ transform: translate(5px, -5px);
+@keyframes animation3shape3 {
+.loading3 .shape4 {
+ -webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
+ animation: animation3shape4 0.5s ease 0s infinite alternate;
+@-webkit-keyframes animation3shape4 {
+ -webkit-transform: translate(-5px, -5px);
+ transform: translate(-5px, -5px);
+@keyframes animation3shape4 {
@@ -0,0 +1,222 @@
+ <view class="container loading5">
+ name: 'loading5',
+.container.loading5 .shape {
+ width: 15px;
+ height: 15px;
+.loading5 .shape1 {
+ animation: animation5shape1 2s ease 0s infinite reverse;
+@-webkit-keyframes animation5shape1 {
+ 25% {
+ -webkit-transform: translate(0, 15px);
+ transform: translate(0, 15px);
+ 50% {
+ -webkit-transform: translate(15px, 15px);
+ transform: translate(15px, 15px);
+ 75% {
+ -webkit-transform: translate(15px, 0);
+ transform: translate(15px, 0);
+@keyframes animation5shape1 {
+.loading5 .shape2 {
+ animation: animation5shape2 2s ease 0s infinite reverse;
+@-webkit-keyframes animation5shape2 {
+ -webkit-transform: translate(-15px, 0);
+ transform: translate(-15px, 0);
+ -webkit-transform: translate(-15px, 15px);
+ transform: translate(-15px, 15px);
+@keyframes animation5shape2 {
+.loading5 .shape3 {
+ animation: animation5shape3 2s ease 0s infinite reverse;
+@-webkit-keyframes animation5shape3 {
+ -webkit-transform: translate(15px, -15px);
+ transform: translate(15px, -15px);
+ -webkit-transform: translate(0, -15px);
+ transform: translate(0, -15px);
+@keyframes animation5shape3 {
+.loading5 .shape4 {
+ animation: animation5shape4 2s ease 0s infinite reverse;
+@-webkit-keyframes animation5shape4 {
+ -webkit-transform: translate(-15px, -15px);
+ transform: translate(-15px, -15px);
+@keyframes animation5shape4 {
@@ -0,0 +1,229 @@
+ <view class="container loading6">
+ name: 'loading6',
+.container.loading6 {
+.container.loading6 .shape {
+ width: 12px;
+ height: 12px;
+.loading6 .shape1 {
+ -webkit-animation: animation6shape1 2s linear 0s infinite normal;
+ animation: animation6shape1 2s linear 0s infinite normal;
+@-webkit-keyframes animation6shape1 {
+ -webkit-transform: translate(0, 18px);
+ transform: translate(0, 18px);
+ -webkit-transform: translate(18px, 18px);
+ transform: translate(18px, 18px);
+ -webkit-transform: translate(18px, 0);
+ transform: translate(18px, 0);
+@keyframes animation6shape1 {
+.loading6 .shape2 {
+ -webkit-animation: animation6shape2 2s linear 0s infinite normal;
+ animation: animation6shape2 2s linear 0s infinite normal;
+@-webkit-keyframes animation6shape2 {
+ -webkit-transform: translate(-18px, 0);
+ transform: translate(-18px, 0);
+ -webkit-transform: translate(-18px, 18px);
+ transform: translate(-18px, 18px);
+@keyframes animation6shape2 {
+.loading6 .shape3 {
+ -webkit-animation: animation6shape3 2s linear 0s infinite normal;
+ animation: animation6shape3 2s linear 0s infinite normal;
+@-webkit-keyframes animation6shape3 {
+ -webkit-transform: translate(18px, -18px);
+ transform: translate(18px, -18px);
+ -webkit-transform: translate(0, -18px);
+ transform: translate(0, -18px);
+@keyframes animation6shape3 {
+.loading6 .shape4 {
+ -webkit-animation: animation6shape4 2s linear 0s infinite normal;
+ animation: animation6shape4 2s linear 0s infinite normal;
+@-webkit-keyframes animation6shape4 {
+ -webkit-transform: translate(-18px, -18px);
+ transform: translate(-18px, -18px);
+@keyframes animation6shape4 {
+ <view>
+ <Loading1 v-if="loadingType==1"/>
+ <Loading2 v-if="loadingType==2"/>
+ <Loading3 v-if="loadingType==3"/>
+ <Loading4 v-if="loadingType==4"/>
+ <Loading5 v-if="loadingType==5"/>
+ import Loading1 from "./loading1.vue";
+ import Loading2 from "./loading2.vue";
+ import Loading3 from "./loading3.vue";
+ import Loading4 from "./loading4.vue";
+ import Loading5 from "./loading5.vue";
+ components:{Loading1,Loading2,Loading3,Loading4,Loading5},
+ name: 'qiun-loading',
@@ -0,0 +1,422 @@
+ * uCharts®
+ * 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
+ * Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
+// 通用配置项
+// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
+const color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
+const cfe = {
+ //demotype为自定义图表类型
+ "type": ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge","candle","demotype"],
+ //增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype"
+ "categories": ["line", "column", "area", "radar", "gauge", "candle","demotype"],
+ //instance为实例变量承载属性,option为eopts承载属性,不要删除
+ "instance": {},
+ "option": {},
+ //下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
+ "formatter":{
+ "tooltipDemo1":function(res){
+ let result = ''
+ for (let i in res) {
+ if (i == 0) {
+ result += res[i].axisValueLabel + '年销售额'
+ let value = '--'
+ if (res[i].data !== null) {
+ value = res[i].data
+ result += '\n' + res[i].seriesName + ':' + value + ' 万元'
+ // #ifdef APP-PLUS
+ result += '<br/>' + res[i].marker + res[i].seriesName + ':' + value + ' 万元'
+ return result;
+ legendFormat:function(name){
+ return "自定义图例+"+name;
+ yAxisFormatDemo:function (value, index) {
+ return value + '元';
+ seriesFormatDemo:function(res){
+ return res.name + '年' + res.value + '元';
+ //这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在eopts参数,会将demotype与eopts中option合并后渲染图表。
+ "demotype":{
+ "color": color,
+ //在这里填写echarts的option即可
+ //下面是自定义配置,请添加项目所需的通用配置
+ "column": {
+ "title": {
+ "text": ''
+ "tooltip": {
+ "trigger": 'axis'
+ "grid": {
+ "top": 30,
+ "bottom": 50,
+ "right": 15,
+ "left": 40
+ "legend": {
+ "bottom": 'left',
+ "toolbox": {
+ "show": false,
+ "xAxis": {
+ "type": 'category',
+ "axisLabel": {
+ "color": '#666666'
+ "axisLine": {
+ "lineStyle": {
+ "color": '#CCCCCC'
+ "boundaryGap": true,
+ "data": []
+ "yAxis": {
+ "type": 'value',
+ "axisTick": {
+ "seriesTemplate": {
+ "name": '',
+ "type": 'bar',
+ "data": [],
+ "barwidth": 20,
+ "label": {
+ "show": true,
+ "color": "#666666",
+ "position": 'top',
+ "line": {
+ "type": 'line',
+ "area": {
+ "areaStyle": {},
+ "pie": {
+ "trigger": 'item'
+ "top": 40,
+ "bottom": 30,
+ "left": 15
+ "type": 'pie',
+ "radius": '50%',
+ "ring": {
+ "radius": ['40%', '70%'],
+ "avoidLabelOverlap": false,
+ "labelLine": {
+ "show": true
+ "rose": {
+ "top": 'bottom'
+ "radius": "55%",
+ "center": ['50%', '50%'],
+ "roseType": 'area',
+ "funnel": {
+ "trigger": 'item',
+ "formatter": "{b} : {c}%"
+ "type": 'funnel',
+ "left": '10%',
+ "top": 60,
+ "bottom": 60,
+ "width": '80%',
+ "min": 0,
+ "max": 100,
+ "minSize": '0%',
+ "maxSize": '100%',
+ "sort": 'descending',
+ "gap": 2,
+ "position": 'inside'
+ "length": 10,
+ "width": 1,
+ "type": 'solid'
+ "itemStyle": {
+ "bordercolor": '#fff',
+ "borderwidth": 1
+ "emphasis": {
+ "fontSize": 20
+ "gauge": {
+ "formatter": '{a} <br/>{b} : {c}%'
+ "name": '业务指标',
+ "type": 'gauge',
+ "detail": {"formatter": '{value}%'},
+ "data": [{"value": 50, "name": '完成率'}]
+ "candle": {
+ "yAxis": {},
+ "dataZoom": [{
+ "type": 'inside',
+ "xAxisIndex": [0, 1],
+ "start": 10,
+ "end": 100
+ "type": 'slider',
+ "bottom": 10,
+ "type": 'k',
+export default cfe;
@@ -0,0 +1,606 @@
+//事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改
+const formatDateTime = (timeStamp, returnType)=>{
+ var date = new Date();
+ date.setTime(timeStamp * 1000);
+ var y = date.getFullYear();
+ var m = date.getMonth() + 1;
+ m = m < 10 ? ('0' + m) : m;
+ var d = date.getDate();
+ d = d < 10 ? ('0' + d) : d;
+ var h = date.getHours();
+ h = h < 10 ? ('0' + h) : h;
+ var minute = date.getMinutes();
+ var second = date.getSeconds();
+ minute = minute < 10 ? ('0' + minute) : minute;
+ second = second < 10 ? ('0' + second) : second;
+ if(returnType == 'full'){return y + '-' + m + '-' + d + ' '+ h +':' + minute + ':' + second;}
+ if(returnType == 'y-m-d'){return y + '-' + m + '-' + d;}
+ if(returnType == 'h:m'){return h +':' + minute;}
+ if(returnType == 'h:m:s'){return h +':' + minute +':' + second;}
+ return [y, m, d, h, minute, second];
+const cfu = {
+ //demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可
+ "type":["pie","ring","rose","word","funnel","map","arcbar","line","column","mount","bar","area","radar","gauge","candle","mix","tline","tarea","scatter","bubble","demotype"],
+ "range":["饼状图","圆环图","玫瑰图","词云图","漏斗图","地图","圆弧进度条","折线图","柱状图","山峰图","条状图","区域图","雷达图","仪表盘","K线图","混合图","时间轴折线","时间轴区域","散点图","气泡图","自定义类型"],
+ //增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype"
+ //自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories
+ "categories":["line","column","mount","bar","area","radar","gauge","candle","mix","demotype"],
+ //instance为实例变量承载属性,不要删除
+ "instance":{},
+ //option为opts及eopts承载属性,不要删除
+ "option":{},
+ "yAxisDemo1":function(val, index, opts){return val+'元'},
+ "yAxisDemo2":function(val, index, opts){return val.toFixed(2)},
+ "xAxisDemo1":function(val, index, opts){return val+'年';},
+ "xAxisDemo2":function(val, index, opts){return formatDateTime(val,'h:m')},
+ "seriesDemo1":function(val, index, series, opts){return val+'元'},
+ "tooltipDemo1":function(item, category, index, opts){
+ if(index==0){
+ return '随便用'+item.data+'年'
+ return '其他我没改'+item.data+'天'
+ "pieDemo":function(val, index, series, opts){
+ if(index !== undefined){
+ return series[index].name+':'+series[index].data+'元'
+ //这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在opts参数,会将demotype与opts中option合并后渲染图表。
+ //我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
+ "type": "line",
+ "padding": [15,10,0,15],
+ "disableGrid": true,
+ "gridType": "dash",
+ "dashLength": 2,
+ "extra": {
+ "type": "curve",
+ "width": 2
+ "pie":{
+ "type": "pie",
+ "padding": [5,5,5,5],
+ "activeOpacity": 0.5,
+ "activeRadius": 10,
+ "offsetAngle": 0,
+ "labelWidth": 15,
+ "border": true,
+ "borderWidth": 3,
+ "borderColor": "#FFFFFF"
+ "ring":{
+ "type": "ring",
+ "rotate": false,
+ "dataLabel": true,
+ "position": "right",
+ "lineHeight": 25,
+ "name": "收益率",
+ "fontSize": 15,
+ "color": "#666666"
+ "subtitle": {
+ "name": "70%",
+ "fontSize": 25,
+ "color": "#7cb5ec"
+ "ringWidth":30,
+ "rose":{
+ "type": "rose",
+ "position": "left",
+ "type": "area",
+ "minRadius": 50,
+ "border": false,
+ "borderWidth": 2,
+ "word":{
+ "type": "word",
+ "word": {
+ "type": "normal",
+ "autoColors": false
+ "funnel":{
+ "type": "funnel",
+ "padding": [15,15,0,15],
+ "activeOpacity": 0.3,
+ "activeWidth": 10,
+ "borderColor": "#FFFFFF",
+ "fillOpacity": 1,
+ "labelAlign": "right"
+ "map":{
+ "type": "map",
+ "padding": [0,0,0,0],
+ "map": {
+ "borderWidth": 1,
+ "borderColor": "#666666",
+ "fillOpacity": 0.6,
+ "activeBorderColor": "#F04864",
+ "activeFillColor": "#FACC14",
+ "activeFillOpacity": 1
+ "arcbar":{
+ "type": "arcbar",
+ "name": "百分比",
+ "color": "#00FF00"
+ "name": "默认标题",
+ "arcbar": {
+ "type": "default",
+ "width": 12,
+ "backgroundColor": "#E9E9E9",
+ "startAngle": 0.75,
+ "endAngle": 0.25,
+ "gap": 2
+ "line":{
+ "type": "straight",
+ "width": 2,
+ "activeType": "hollow"
+ "tline":{
+ "disableGrid": false,
+ "boundaryGap":"justify",
+ "data":[
+ "min":0,
+ "max":80
+ "tarea":{
+ "opacity": 0.2,
+ "addLine": true,
+ "gradient": true,
+ "column":{
+ "type": "column",
+ "padding": [15,15,0,5],
+ "data":[{"min":0}]
+ "type": "group",
+ "width": 30,
+ "activeBgColor": "#000000",
+ "activeBgOpacity": 0.08
+ "mount":{
+ "type": "mount",
+ "mount": {
+ "widthRatio": 1.5,
+ "bar":{
+ "type": "bar",
+ "padding": [15,30,0,5],
+ "disableGrid":false,
+ "axisLine":false
+ "bar": {
+ "meterBorde": 1,
+ "meterFillColor": "#FFFFFF",
+ "area":{
+ "gradient": false,
+ "radar":{
+ "type": "radar",
+ "dataLabel": false,
+ "radar": {
+ "gridType": "radar",
+ "gridColor": "#CCCCCC",
+ "gridCount": 3,
+ "max": 200,
+ "labelShow": true
+ "gauge":{
+ "type": "gauge",
+ "name": "66Km/H",
+ "color": "#2fc25b",
+ "offsetY": 50
+ "name": "实时速度",
+ "color": "#1890ff",
+ "offsetY": -50
+ "labelColor": "#666666",
+ "startNumber": 0,
+ "endNumber": 100,
+ "labelFormat": "",
+ "splitLine": {
+ "fixRadius": 0,
+ "splitNumber": 10,
+ "color": "#FFFFFF",
+ "childNumber": 5,
+ "childWidth": 12
+ "pointer": {
+ "width": 24,
+ "color": "auto"
+ "candle":{
+ "type": "candle",
+ "enableScroll": true,
+ "enableMarkLine": true,
+ "labelCount": 4,
+ "itemCount": 40,
+ "gridType": "solid",
+ "dashLength": 4,
+ "scrollShow": true,
+ "scrollAlign": "left",
+ "scrollColor": "#A6A6A6",
+ "scrollBackgroundColor": "#EFEBEF"
+ "color": {
+ "upLine": "#f04864",
+ "upFill": "#f04864",
+ "downLine": "#2fc25b",
+ "downFill": "#2fc25b"
+ "average": {
+ "name": ["MA5","MA10","MA30"],
+ "day": [5,10,20],
+ "color": ["#1890ff","#2fc25b","#facc14"]
+ "markLine": {
+ "type": "dash",
+ "dashLength": 5,
+ "data": [
+ "value": 2150,
+ "lineColor": "#f04864",
+ "showLabel": true
+ "value": 2350,
+ "mix":{
+ "type": "mix",
+ "disabled": false,
+ "splitNumber": 5,
+ "padding": 10,
+ "showTitle": true,
+ "mix": {
+ "width": 20
+ "scatter":{
+ "type": "scatter",
+ "color":color,
+ "padding":[15,15,0,15],
+ "dataLabel":false,
+ "gridType":"dash",
+ "splitNumber":5,
+ "min":0
+ "scatter": {
+ "bubble":{
+ "type": "bubble",
+ "max":250
+ "data":[{
+ "max":150
+ }]
+ "bubble": {
+ "border":2,
+ "opacity": 0.5,
+export default cfu;
@@ -0,0 +1,5 @@
+# uCharts JSSDK说明
+1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。
+2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。
+3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
+4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。
@@ -0,0 +1,7706 @@
+ * uCharts (R)
+ * 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360/快手)、Vue、Taro等支持canvas的框架平台
+ * Copyright (C) 2018-2022 QIUN (R) 秋云 https://www.ucharts.cn All rights reserved.
+ * uCharts (R) 官方网站
+'use strict';
+var config = {
+ version: 'v2.5.0-20230101',
+ yAxisWidth: 15,
+ xAxisHeight: 22,
+ padding: [10, 10, 10, 10],
+ rotate: false,
+ fontSize: 13,
+ fontColor: '#666666',
+ dataPointShape: ['circle', 'circle', 'circle', 'circle'],
+ color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'],
+ linearColor: ['#0EE2F8', '#2BDCA8', '#FA7D8D', '#EB88E2', '#2AE3A0', '#0EE2F8', '#EB88E2', '#6773E3', '#F78A85'],
+ pieChartLinePadding: 15,
+ pieChartTextPadding: 5,
+ titleFontSize: 20,
+ subtitleFontSize: 15,
+ radarLabelTextMargin: 13,
+var assign = function(target, ...varArgs) {
+ if (target == null) {
+ throw new TypeError('[uCharts] Cannot convert undefined or null to object');
+ if (!varArgs || varArgs.length <= 0) {
+ return target;
+ // 深度合并对象
+ function deepAssign(obj1, obj2) {
+ for (let key in obj2) {
+ obj1[key] = obj1[key] && obj1[key].toString() === "[object Object]" ?
+ deepAssign(obj1[key], obj2[key]) : obj1[key] = obj2[key];
+ return obj1;
+ varArgs.forEach(val => {
+ target = deepAssign(target, val);
+var util = {
+ toFixed: function toFixed(num, limit) {
+ limit = limit || 2;
+ if (this.isFloat(num)) {
+ num = num.toFixed(limit);
+ return num;
+ isFloat: function isFloat(num) {
+ return num % 1 !== 0;
+ approximatelyEqual: function approximatelyEqual(num1, num2) {
+ return Math.abs(num1 - num2) < 1e-10;
+ isSameSign: function isSameSign(num1, num2) {
+ return Math.abs(num1) === num1 && Math.abs(num2) === num2 || Math.abs(num1) !== num1 && Math.abs(num2) !== num2;
+ isSameXCoordinateArea: function isSameXCoordinateArea(p1, p2) {
+ return this.isSameSign(p1.x, p2.x);
+ isCollision: function isCollision(obj1, obj2) {
+ obj1.end = {};
+ obj1.end.x = obj1.start.x + obj1.width;
+ obj1.end.y = obj1.start.y - obj1.height;
+ obj2.end = {};
+ obj2.end.x = obj2.start.x + obj2.width;
+ obj2.end.y = obj2.start.y - obj2.height;
+ var flag = obj2.start.x > obj1.end.x || obj2.end.x < obj1.start.x || obj2.end.y > obj1.start.y || obj2.start.y < obj1.end.y;
+ return !flag;
+//兼容H5点击事件
+function getH5Offset(e) {
+ e.mp = {
+ changedTouches: []
+ e.mp.changedTouches.push({
+ x: e.offsetX,
+ y: e.offsetY
+ return e;
+// hex 转 rgba
+function hexToRgb(hexValue, opc) {
+ var rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ var hex = hexValue.replace(rgx, function(m, r, g, b) {
+ return r + r + g + g + b + b;
+ var rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ var r = parseInt(rgb[1], 16);
+ var g = parseInt(rgb[2], 16);
+ var b = parseInt(rgb[3], 16);
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + opc + ')';
+function findRange(num, type, limit) {
+ if (isNaN(num)) {
+ throw new Error('[uCharts] series数据需为Number格式');
+ limit = limit || 10;
+ type = type ? type : 'upper';
+ var multiple = 1;
+ while (limit < 1) {
+ limit *= 10;
+ multiple *= 10;
+ if (type === 'upper') {
+ num = Math.ceil(num * multiple);
+ num = Math.floor(num * multiple);
+ while (num % limit !== 0) {
+ if (num == num + 1) { //修复数据值过大num++无效的bug by 向日葵 @xrk_jy
+ num++;
+ num--;
+ return num / multiple;
+function calCandleMA(dayArr, nameArr, colorArr, kdata) {
+ let seriesTemp = [];
+ for (let k = 0; k < dayArr.length; k++) {
+ let seriesItem = {
+ name: nameArr[k],
+ color: colorArr[k]
+ for (let i = 0, len = kdata.length; i < len; i++) {
+ if (i < dayArr[k]) {
+ seriesItem.data.push(null);
+ continue;
+ let sum = 0;
+ for (let j = 0; j < dayArr[k]; j++) {
+ sum += kdata[i - j][1];
+ seriesItem.data.push(+(sum / dayArr[k]).toFixed(3));
+ seriesTemp.push(seriesItem);
+ return seriesTemp;
+function calValidDistance(self, distance, chartData, config, opts) {
+ var dataChartAreaWidth = opts.width - opts.area[1] - opts.area[3];
+ var dataChartWidth = chartData.eachSpacing * (opts.chartData.xAxisData.xAxisPoints.length - 1);
+ if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1){
+ if(opts.extra.mount.widthRatio>2) opts.extra.mount.widthRatio = 2
+ dataChartWidth += (opts.extra.mount.widthRatio - 1)*chartData.eachSpacing;
+ var validDistance = distance;
+ if (distance >= 0) {
+ validDistance = 0;
+ self.uevent.trigger('scrollLeft');
+ self.scrollOption.position = 'left'
+ opts.xAxis.scrollPosition = 'left';
+ } else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) {
+ validDistance = dataChartAreaWidth - dataChartWidth;
+ self.uevent.trigger('scrollRight');
+ self.scrollOption.position = 'right'
+ opts.xAxis.scrollPosition = 'right';
+ self.scrollOption.position = distance
+ opts.xAxis.scrollPosition = distance;
+ return validDistance;
+function isInAngleRange(angle, startAngle, endAngle) {
+ function adjust(angle) {
+ while (angle < 0) {
+ angle += 2 * Math.PI;
+ while (angle > 2 * Math.PI) {
+ angle -= 2 * Math.PI;
+ return angle;
+ angle = adjust(angle);
+ startAngle = adjust(startAngle);
+ endAngle = adjust(endAngle);
+ if (startAngle > endAngle) {
+ endAngle += 2 * Math.PI;
+ if (angle < startAngle) {
+ return angle >= startAngle && angle <= endAngle;
+function createCurveControlPoints(points, i) {
+ function isNotMiddlePoint(points, i) {
+ if (points[i - 1] && points[i + 1]) {
+ return points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) || points[i].y <= Math.min(points[i - 1].y,
+ points[i + 1].y);
+ return false;
+ function isNotMiddlePointX(points, i) {
+ return points[i].x >= Math.max(points[i - 1].x, points[i + 1].x) || points[i].x <= Math.min(points[i - 1].x,
+ points[i + 1].x);
+ var a = 0.2;
+ var b = 0.2;
+ var pAx = null;
+ var pAy = null;
+ var pBx = null;
+ var pBy = null;
+ if (i < 1) {
+ pAx = points[0].x + (points[1].x - points[0].x) * a;
+ pAy = points[0].y + (points[1].y - points[0].y) * a;
+ pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a;
+ pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a;
+ if (i > points.length - 3) {
+ var last = points.length - 1;
+ pBx = points[last].x - (points[last].x - points[last - 1].x) * b;
+ pBy = points[last].y - (points[last].y - points[last - 1].y) * b;
+ pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b;
+ pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b;
+ if (isNotMiddlePoint(points, i + 1)) {
+ pBy = points[i + 1].y;
+ if (isNotMiddlePoint(points, i)) {
+ pAy = points[i].y;
+ if (isNotMiddlePointX(points, i + 1)) {
+ pBx = points[i + 1].x;
+ if (isNotMiddlePointX(points, i)) {
+ pAx = points[i].x;
+ if (pAy >= Math.max(points[i].y, points[i + 1].y) || pAy <= Math.min(points[i].y, points[i + 1].y)) {
+ if (pBy >= Math.max(points[i].y, points[i + 1].y) || pBy <= Math.min(points[i].y, points[i + 1].y)) {
+ if (pAx >= Math.max(points[i].x, points[i + 1].x) || pAx <= Math.min(points[i].x, points[i + 1].x)) {
+ if (pBx >= Math.max(points[i].x, points[i + 1].x) || pBx <= Math.min(points[i].x, points[i + 1].x)) {
+ ctrA: {
+ x: pAx,
+ y: pAy
+ ctrB: {
+ x: pBx,
+ y: pBy
+function convertCoordinateOrigin(x, y, center) {
+ x: center.x + x,
+ y: center.y - y
+function avoidCollision(obj, target) {
+ if (target) {
+ // is collision test
+ while (util.isCollision(obj, target)) {
+ if (obj.start.x > 0) {
+ obj.start.y--;
+ } else if (obj.start.x < 0) {
+ obj.start.y++;
+ if (obj.start.y > 0) {
+ return obj;
+function fixPieSeries(series, opts, config){
+ let pieSeriesArr = [];
+ if(series.length>0 && series[0].data.constructor.toString().indexOf('Array') > -1){
+ opts._pieSeries_ = series;
+ let oldseries = series[0].data;
+ for (var i = 0; i < oldseries.length; i++) {
+ oldseries[i].formatter = series[0].formatter;
+ oldseries[i].data = oldseries[i].value;
+ pieSeriesArr.push(oldseries[i]);
+ opts.series = pieSeriesArr;
+ pieSeriesArr = series;
+ return pieSeriesArr;
+function fillSeries(series, opts, config) {
+ var index = 0;
+ for (var i = 0; i < series.length; i++) {
+ let item = series[i];
+ if (!item.color) {
+ item.color = config.color[index];
+ index = (index + 1) % config.color.length;
+ if (!item.linearIndex) {
+ item.linearIndex = i;
+ if (!item.index) {
+ item.index = 0;
+ if (!item.type) {
+ item.type = opts.type;
+ if (typeof item.show == "undefined") {
+ item.show = true;
+ if (!item.pointShape) {
+ item.pointShape = "circle";
+ if (!item.legendShape) {
+ switch (item.type) {
+ case 'line':
+ item.legendShape = "line";
+ case 'column':
+ case 'bar':
+ item.legendShape = "rect";
+ case 'area':
+ case 'mount':
+ item.legendShape = "triangle";
+ item.legendShape = "circle";
+ return series;
+function fillCustomColor(linearType, customColor, series, config) {
+ var newcolor = customColor || [];
+ if (linearType == 'custom' && newcolor.length == 0 ) {
+ newcolor = config.linearColor;
+ if (linearType == 'custom' && newcolor.length < series.length) {
+ let chazhi = series.length - newcolor.length;
+ for (var i = 0; i < chazhi; i++) {
+ newcolor.push(config.linearColor[(i + 1) % config.linearColor.length]);
+ return newcolor;
+function getDataRange(minData, maxData) {
+ var limit = 0;
+ var range = maxData - minData;
+ if (range >= 10000) {
+ limit = 1000;
+ } else if (range >= 1000) {
+ limit = 100;
+ } else if (range >= 100) {
+ limit = 10;
+ } else if (range >= 10) {
+ limit = 5;
+ } else if (range >= 1) {
+ limit = 1;
+ } else if (range >= 0.1) {
+ limit = 0.1;
+ } else if (range >= 0.01) {
+ limit = 0.01;
+ } else if (range >= 0.001) {
+ limit = 0.001;
+ } else if (range >= 0.0001) {
+ limit = 0.0001;
+ } else if (range >= 0.00001) {
+ limit = 0.00001;
+ limit = 0.000001;
+ minRange: findRange(minData, 'lower', limit),
+ maxRange: findRange(maxData, 'upper', limit)
+function measureText(text, fontSize, context) {
+ var width = 0;
+ text = String(text);
+ // #ifdef MP-ALIPAY || MP-BAIDU || APP-NVUE
+ context = false;
+ if (context !== false && context !== undefined && context.setFontSize && context.measureText) {
+ context.setFontSize(fontSize);
+ return context.measureText(text).width;
+ var text = text.split('');
+ for (let i = 0; i < text.length; i++) {
+ let item = text[i];
+ if (/[a-zA-Z]/.test(item)) {
+ width += 7;
+ } else if (/[0-9]/.test(item)) {
+ width += 5.5;
+ } else if (/\./.test(item)) {
+ width += 2.7;
+ } else if (/-/.test(item)) {
+ width += 3.25;
+ } else if (/:/.test(item)) {
+ width += 2.5;
+ } else if (/[\u4e00-\u9fa5]/.test(item)) {
+ width += 10;
+ } else if (/\(|\)/.test(item)) {
+ width += 3.73;
+ } else if (/\s/.test(item)) {
+ } else if (/%/.test(item)) {
+ width += 8;
+ return width * fontSize / 10;
+function dataCombine(series) {
+ return series.reduce(function(a, b) {
+ return (a.data ? a.data : a).concat(b.data);
+ }, []);
+function dataCombineStack(series, len) {
+ var sum = new Array(len);
+ for (var j = 0; j < sum.length; j++) {
+ sum[j] = 0;
+ sum[j] += series[i].data[j];
+ return (a.data ? a.data : a).concat(b.data).concat(sum);
+function getTouches(touches, opts, e) {
+ let x, y;
+ if (touches.clientX) {
+ if (opts.rotate) {
+ y = opts.height - touches.clientX * opts.pix;
+ x = (touches.pageY - e.currentTarget.offsetTop - (opts.height / opts.pix / 2) * (opts.pix - 1)) * opts.pix;
+ x = touches.clientX * opts.pix;
+ y = (touches.pageY - e.currentTarget.offsetTop - (opts.height / opts.pix / 2) * (opts.pix - 1)) * opts.pix;
+ y = opts.height - touches.x * opts.pix;
+ x = touches.y * opts.pix;
+ x = touches.x * opts.pix;
+ y = touches.y * opts.pix;
+ x: x,
+ y: y
+function getSeriesDataItem(series, index, group) {
+ var data = [];
+ var newSeries = [];
+ var indexIsArr = index.constructor.toString().indexOf('Array') > -1;
+ if(indexIsArr){
+ let tempSeries = filterSeries(series);
+ for (var i = 0; i < group.length; i++) {
+ newSeries.push(tempSeries[group[i]]);
+ newSeries = series;
+ for (let i = 0; i < newSeries.length; i++) {
+ let item = newSeries[i];
+ let tmpindex = -1;
+ tmpindex = index[i];
+ tmpindex = index;
+ if (item.data[tmpindex] !== null && typeof item.data[tmpindex] !== 'undefined' && item.show) {
+ let seriesItem = {};
+ seriesItem.color = item.color;
+ seriesItem.type = item.type;
+ seriesItem.style = item.style;
+ seriesItem.pointShape = item.pointShape;
+ seriesItem.disableLegend = item.disableLegend;
+ seriesItem.legendShape = item.legendShape;
+ seriesItem.name = item.name;
+ seriesItem.show = item.show;
+ seriesItem.data = item.formatter ? item.formatter(item.data[tmpindex]) : item.data[tmpindex];
+ data.push(seriesItem);
+ return data;
+function getMaxTextListLength(list, fontSize, context) {
+ var lengthList = list.map(function(item) {
+ return measureText(item, fontSize, context);
+ return Math.max.apply(null, lengthList);
+function getRadarCoordinateSeries(length) {
+ var eachAngle = 2 * Math.PI / length;
+ var CoordinateSeries = [];
+ for (var i = 0; i < length; i++) {
+ CoordinateSeries.push(eachAngle * i);
+ return CoordinateSeries.map(function(item) {
+ return -1 * item + Math.PI / 2;
+function getToolTipData(seriesData, opts, index, group, categories) {
+ var option = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
+ var calPoints = opts.chartData.calPoints?opts.chartData.calPoints:[];
+ let points = {};
+ if(group.length > 0){
+ let filterPoints = [];
+ for (let i = 0; i < group.length; i++) {
+ filterPoints.push(calPoints[group[i]])
+ points = filterPoints[0][index[0]];
+ for (let i = 0; i < calPoints.length; i++) {
+ if(calPoints[i][index]){
+ points = calPoints[i][index];
+ var textList = seriesData.map(function(item) {
+ let titleText = null;
+ if (opts.categories && opts.categories.length>0) {
+ titleText = categories[index];
+ text: option.formatter ? option.formatter(item, titleText, index, opts) : item.name + ': ' + item.data,
+ color: item.color,
+ legendShape: opts.extra.tooltip.legendShape == 'auto'? item.legendShape : opts.extra.tooltip.legendShape
+ var offset = {
+ x: Math.round(points.x),
+ y: Math.round(points.y)
+ textList: textList,
+ offset: offset
+function getMixToolTipData(seriesData, opts, index, categories) {
+ var option = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
+ var points = opts.chartData.xAxisPoints[index] + opts.chartData.eachSpacing / 2;
+ text: option.formatter ? option.formatter(item, categories[index], index, opts) : item.name + ': ' + item.data,
+ disableLegend: item.disableLegend ? true : false,
+ textList = textList.filter(function(item) {
+ if (item.disableLegend !== true) {
+ return item;
+ x: Math.round(points),
+function getCandleToolTipData(series, seriesData, opts, index, categories, extra) {
+ var option = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : {};
+ var calPoints = opts.chartData.calPoints;
+ let upColor = extra.color.upFill;
+ let downColor = extra.color.downFill;
+ //颜色顺序为开盘,收盘,最低,最高
+ let color = [upColor, upColor, downColor, upColor];
+ var textList = [];
+ seriesData.map(function(item) {
+ if (index == 0) {
+ if (item.data[1] - item.data[0] < 0) {
+ color[1] = downColor;
+ color[1] = upColor;
+ if (item.data[0] < series[index - 1][1]) {
+ color[0] = downColor;
+ if (item.data[1] < item.data[0]) {
+ if (item.data[2] > series[index - 1][1]) {
+ color[2] = upColor;
+ if (item.data[3] < series[index - 1][1]) {
+ color[3] = downColor;
+ let text1 = {
+ text: '开盘:' + item.data[0],
+ color: color[0],
+ let text2 = {
+ text: '收盘:' + item.data[1],
+ color: color[1],
+ let text3 = {
+ text: '最低:' + item.data[2],
+ color: color[2],
+ let text4 = {
+ text: '最高:' + item.data[3],
+ color: color[3],
+ textList.push(text1, text2, text3, text4);
+ var validCalPoints = [];
+ let points = calPoints[i];
+ if (typeof points[index] !== 'undefined' && points[index] !== null) {
+ validCalPoints.push(points[index]);
+ offset.x = Math.round(validCalPoints[0][0].x);
+function filterSeries(series) {
+ let tempSeries = [];
+ for (let i = 0; i < series.length; i++) {
+ if (series[i].show == true) {
+ tempSeries.push(series[i])
+ return tempSeries;
+function findCurrentIndex(currentPoints, calPoints, opts, config) {
+ var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
+ var current={ index:-1, group:[] };
+ var spacing = opts.chartData.eachSpacing / 2;
+ let xAxisPoints = [];
+ if (calPoints && calPoints.length > 0) {
+ if (!opts.categories) {
+ spacing = 0;
+ for (let i = 1; i < opts.chartData.xAxisPoints.length; i++) {
+ xAxisPoints.push(opts.chartData.xAxisPoints[i] - spacing);
+ if ((opts.type == 'line' || opts.type == 'area') && opts.xAxis.boundaryGap == 'justify') {
+ xAxisPoints = opts.chartData.xAxisPoints;
+ if (isInExactChartArea(currentPoints, opts, config)) {
+ let timePoints = Array(calPoints.length);
+ timePoints[i] = Array(calPoints[i].length)
+ for (let j = 0; j < calPoints[i].length; j++) {
+ timePoints[i][j] = (Math.abs(calPoints[i][j].x - currentPoints.x));
+ let pointValue = Array(timePoints.length);
+ let pointIndex = Array(timePoints.length);
+ for (let i = 0; i < timePoints.length; i++) {
+ pointValue[i] = Math.min.apply(null, timePoints[i]);
+ pointIndex[i] = timePoints[i].indexOf(pointValue[i]);
+ let minValue = Math.min.apply(null, pointValue);
+ current.index = [];
+ for (let i = 0; i < pointValue.length; i++) {
+ if(pointValue[i] == minValue){
+ current.group.push(i);
+ current.index.push(pointIndex[i]);
+ xAxisPoints.forEach(function(item, index) {
+ if (currentPoints.x + offset + spacing > item) {
+ current.index = index;
+ return current;
+function findBarChartCurrentIndex(currentPoints, calPoints, opts, config) {
+ let yAxisPoints = opts.chartData.yAxisPoints;
+ yAxisPoints.forEach(function(item, index) {
+ if (currentPoints.y + offset + spacing > item) {
+function findLegendIndex(currentPoints, legendData, opts) {
+ let currentIndex = -1;
+ let gap = 0;
+ if (isInExactLegendArea(currentPoints, legendData.area)) {
+ let points = legendData.points;
+ let index = -1;
+ for (let i = 0, len = points.length; i < len; i++) {
+ let item = points[i];
+ for (let j = 0; j < item.length; j++) {
+ index += 1;
+ let area = item[j]['area'];
+ if (area && currentPoints.x > area[0] - gap && currentPoints.x < area[2] + gap && currentPoints.y > area[1] - gap && currentPoints.y < area[3] + gap) {
+ currentIndex = index;
+ return currentIndex;
+function isInExactLegendArea(currentPoints, area) {
+ return currentPoints.x > area.start.x && currentPoints.x < area.end.x && currentPoints.y > area.start.y && currentPoints.y < area.end.y;
+function isInExactChartArea(currentPoints, opts, config) {
+ return currentPoints.x <= opts.width - opts.area[1] + 10 && currentPoints.x >= opts.area[3] - 10 && currentPoints.y >= opts.area[0] && currentPoints.y <= opts.height - opts.area[2];
+function findRadarChartCurrentIndex(currentPoints, radarData, count) {
+ var eachAngleArea = 2 * Math.PI / count;
+ var currentIndex = -1;
+ if (isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)) {
+ var fixAngle = function fixAngle(angle) {
+ if (angle < 0) {
+ if (angle > 2 * Math.PI) {
+ var angle = Math.atan2(radarData.center.y - currentPoints.y, currentPoints.x - radarData.center.x);
+ angle = -1 * angle;
+ var angleList = radarData.angleList.map(function(item) {
+ item = fixAngle(-1 * item);
+ angleList.forEach(function(item, index) {
+ var rangeStart = fixAngle(item - eachAngleArea / 2);
+ var rangeEnd = fixAngle(item + eachAngleArea / 2);
+ if (rangeEnd < rangeStart) {
+ rangeEnd += 2 * Math.PI;
+ if (angle >= rangeStart && angle <= rangeEnd || angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd) {
+function findFunnelChartCurrentIndex(currentPoints, funnelData) {
+ for (var i = 0, len = funnelData.series.length; i < len; i++) {
+ var item = funnelData.series[i];
+ if (currentPoints.x > item.funnelArea[0] && currentPoints.x < item.funnelArea[2] && currentPoints.y > item.funnelArea[1] && currentPoints.y < item.funnelArea[3]) {
+ currentIndex = i;
+function findWordChartCurrentIndex(currentPoints, wordData) {
+ for (var i = 0, len = wordData.length; i < len; i++) {
+ var item = wordData[i];
+ if (currentPoints.x > item.area[0] && currentPoints.x < item.area[2] && currentPoints.y > item.area[1] && currentPoints.y < item.area[3]) {
+function findMapChartCurrentIndex(currentPoints, opts) {
+ var cData = opts.chartData.mapData;
+ var data = opts.series;
+ var tmp = pointToCoordinate(currentPoints.y, currentPoints.x, cData.bounds, cData.scale, cData.xoffset, cData.yoffset);
+ var poi = [tmp.x, tmp.y];
+ for (var i = 0, len = data.length; i < len; i++) {
+ var item = data[i].geometry.coordinates;
+ if (isPoiWithinPoly(poi, item, opts.chartData.mapData.mercator)) {
+function findRoseChartCurrentIndex(currentPoints, pieData, opts) {
+ var series = getRoseDataPoints(opts._series_, opts.extra.rose.type, pieData.radius, pieData.radius);
+ if (pieData && pieData.center && isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) {
+ var angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x);
+ angle = -angle;
+ if(opts.extra.rose && opts.extra.rose.offsetAngle){
+ angle = angle - opts.extra.rose.offsetAngle * Math.PI / 180;
+ for (var i = 0, len = series.length; i < len; i++) {
+ if (isInAngleRange(angle, series[i]._start_, series[i]._start_ + series[i]._rose_proportion_ * 2 * Math.PI)) {
+function findPieChartCurrentIndex(currentPoints, pieData, opts) {
+ var series = getPieDataPoints(pieData.series);
+ if(opts.extra.pie && opts.extra.pie.offsetAngle){
+ angle = angle - opts.extra.pie.offsetAngle * Math.PI / 180;
+ if(opts.extra.ring && opts.extra.ring.offsetAngle){
+ angle = angle - opts.extra.ring.offsetAngle * Math.PI / 180;
+ if (isInAngleRange(angle, series[i]._start_, series[i]._start_ + series[i]._proportion_ * 2 * Math.PI)) {
+function isInExactPieChartArea(currentPoints, center, radius) {
+ return Math.pow(currentPoints.x - center.x, 2) + Math.pow(currentPoints.y - center.y, 2) <= Math.pow(radius, 2);
+function splitPoints(points,eachSeries) {
+ var newPoints = [];
+ var items = [];
+ points.forEach(function(item, index) {
+ if(eachSeries.connectNulls){
+ if (item !== null) {
+ items.push(item);
+ if (items.length) {
+ newPoints.push(items);
+ items = [];
+ return newPoints;
+function calLegendData(series, opts, config, chartData, context) {
+ let legendData = {
+ area: {
+ start: {
+ end: {
+ width: 0,
+ height: 0,
+ wholeWidth: 0,
+ wholeHeight: 0
+ points: [],
+ widthArr: [],
+ heightArr: []
+ if (opts.legend.show === false) {
+ chartData.legendData = legendData;
+ return legendData;
+ let padding = opts.legend.padding * opts.pix;
+ let margin = opts.legend.margin * opts.pix;
+ let fontSize = opts.legend.fontSize ? opts.legend.fontSize * opts.pix : config.fontSize;
+ let shapeWidth = 15 * opts.pix;
+ let shapeRight = 5 * opts.pix;
+ let lineHeight = Math.max(opts.legend.lineHeight * opts.pix, fontSize);
+ if (opts.legend.position == 'top' || opts.legend.position == 'bottom') {
+ let legendList = [];
+ let widthCount = 0;
+ let widthCountArr = [];
+ let currentRow = [];
+ const legendText = item.legendText ? item.legendText : item.name;
+ let itemWidth = shapeWidth + shapeRight + measureText(legendText || 'undefined', fontSize, context) + opts.legend.itemGap * opts.pix;
+ if (widthCount + itemWidth > opts.width - opts.area[1] - opts.area[3]) {
+ legendList.push(currentRow);
+ widthCountArr.push(widthCount - opts.legend.itemGap * opts.pix);
+ widthCount = itemWidth;
+ currentRow = [item];
+ widthCount += itemWidth;
+ currentRow.push(item);
+ if (currentRow.length) {
+ legendData.widthArr = widthCountArr;
+ let legendWidth = Math.max.apply(null, widthCountArr);
+ switch (opts.legend.float) {
+ case 'left':
+ legendData.area.start.x = opts.area[3];
+ legendData.area.end.x = opts.area[3] + legendWidth + 2 * padding;
+ case 'right':
+ legendData.area.start.x = opts.width - opts.area[1] - legendWidth - 2 * padding;
+ legendData.area.end.x = opts.width - opts.area[1];
+ legendData.area.start.x = (opts.width - legendWidth) / 2 - padding;
+ legendData.area.end.x = (opts.width + legendWidth) / 2 + padding;
+ legendData.area.width = legendWidth + 2 * padding;
+ legendData.area.wholeWidth = legendWidth + 2 * padding;
+ legendData.area.height = legendList.length * lineHeight + 2 * padding;
+ legendData.area.wholeHeight = legendList.length * lineHeight + 2 * padding + 2 * margin;
+ legendData.points = legendList;
+ let len = series.length;
+ let maxHeight = opts.height - opts.area[0] - opts.area[2] - 2 * margin - 2 * padding;
+ let maxLength = Math.min(Math.floor(maxHeight / lineHeight), len);
+ legendData.area.height = maxLength * lineHeight + padding * 2;
+ legendData.area.wholeHeight = maxLength * lineHeight + padding * 2;
+ case 'top':
+ legendData.area.start.y = opts.area[0] + margin;
+ legendData.area.end.y = opts.area[0] + margin + legendData.area.height;
+ case 'bottom':
+ legendData.area.start.y = opts.height - opts.area[2] - margin - legendData.area.height;
+ legendData.area.end.y = opts.height - opts.area[2] - margin;
+ legendData.area.start.y = (opts.height - legendData.area.height) / 2;
+ legendData.area.end.y = (opts.height + legendData.area.height) / 2;
+ let lineNum = len % maxLength === 0 ? len / maxLength : Math.floor((len / maxLength) + 1);
+ for (let i = 0; i < lineNum; i++) {
+ let temp = series.slice(i * maxLength, i * maxLength + maxLength);
+ currentRow.push(temp);
+ legendData.points = currentRow;
+ for (let i = 0; i < currentRow.length; i++) {
+ let item = currentRow[i];
+ let maxWidth = 0;
+ let itemWidth = shapeWidth + shapeRight + measureText(item[j].name || 'undefined', fontSize, context) + opts.legend.itemGap * opts.pix;
+ if (itemWidth > maxWidth) {
+ maxWidth = itemWidth;
+ legendData.widthArr.push(maxWidth);
+ legendData.heightArr.push(item.length * lineHeight + padding * 2);
+ let legendWidth = 0
+ for (let i = 0; i < legendData.widthArr.length; i++) {
+ legendWidth += legendData.widthArr[i];
+ legendData.area.width = legendWidth - opts.legend.itemGap * opts.pix + 2 * padding;
+ legendData.area.wholeWidth = legendData.area.width + padding;
+ switch (opts.legend.position) {
+ legendData.area.start.y = opts.height - opts.area[2] - legendData.area.height - margin;
+ legendData.area.end.x = opts.area[3] + legendData.area.width;
+ legendData.area.start.x = opts.width - opts.area[1] - legendData.area.width;
+function calCategoriesData(categories, opts, config, eachSpacing, context) {
+ var result = {
+ angle: 0,
+ xAxisHeight: opts.xAxis.lineHeight * opts.pix + opts.xAxis.marginTop * opts.pix
+ var fontSize = opts.xAxis.fontSize * opts.pix;
+ var categoriesTextLenth = categories.map(function(item,index) {
+ var xitem = opts.xAxis.formatter ? opts.xAxis.formatter(item,index,opts) : item;
+ return measureText(String(xitem), fontSize, context);
+ var maxTextLength = Math.max.apply(this, categoriesTextLenth);
+ if (opts.xAxis.rotateLabel == true) {
+ result.angle = opts.xAxis.rotateAngle * Math.PI / 180;
+ let tempHeight = opts.xAxis.marginTop * opts.pix * 2 + Math.abs(maxTextLength * Math.sin(result.angle))
+ tempHeight = tempHeight < fontSize + opts.xAxis.marginTop * opts.pix * 2 ? tempHeight + opts.xAxis.marginTop * opts.pix * 2 : tempHeight;
+ result.xAxisHeight = tempHeight;
+ if (opts.enableScroll && opts.xAxis.scrollShow) {
+ result.xAxisHeight += 6 * opts.pix;
+ if (opts.xAxis.disabled){
+ result.xAxisHeight = 0;
+function getXAxisTextList(series, opts, config, stack) {
+ var index = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : -1;
+ var data;
+ if (stack == 'stack') {
+ data = dataCombineStack(series, opts.categories.length);
+ data = dataCombine(series);
+ var sorted = [];
+ // remove null from data
+ data = data.filter(function(item) {
+ //return item !== null;
+ if (typeof item === 'object' && item !== null) {
+ if (item.constructor.toString().indexOf('Array') > -1) {
+ return item !== null;
+ return item.value !== null;
+ data.map(function(item) {
+ if (typeof item === 'object') {
+ if (opts.type == 'candle') {
+ item.map(function(subitem) {
+ sorted.push(subitem);
+ sorted.push(item[0]);
+ sorted.push(item.value);
+ sorted.push(item);
+ var minData = 0;
+ var maxData = 0;
+ if (sorted.length > 0) {
+ minData = Math.min.apply(this, sorted);
+ maxData = Math.max.apply(this, sorted);
+ //为了兼容v1.9.0之前的项目
+ if (index > -1) {
+ if (typeof opts.xAxis.data[index].min === 'number') {
+ minData = Math.min(opts.xAxis.data[index].min, minData);
+ if (typeof opts.xAxis.data[index].max === 'number') {
+ maxData = Math.max(opts.xAxis.data[index].max, maxData);
+ if (typeof opts.xAxis.min === 'number') {
+ minData = Math.min(opts.xAxis.min, minData);
+ if (typeof opts.xAxis.max === 'number') {
+ maxData = Math.max(opts.xAxis.max, maxData);
+ if (minData === maxData) {
+ var rangeSpan = maxData || 10;
+ maxData += rangeSpan;
+ //var dataRange = getDataRange(minData, maxData);
+ var minRange = minData;
+ var maxRange = maxData;
+ var range = [];
+ var eachRange = (maxRange - minRange) / opts.xAxis.splitNumber;
+ for (var i = 0; i <= opts.xAxis.splitNumber; i++) {
+ range.push(minRange + eachRange * i);
+ return range;
+function calXAxisData(series, opts, config, context) {
+ //堆叠图重算Y轴
+ var columnstyle = assign({}, {
+ type: ""
+ }, opts.extra.bar);
+ result.ranges = getXAxisTextList(series, opts, config, columnstyle.type);
+ result.rangesFormat = result.ranges.map(function(item) {
+ //item = opts.xAxis.formatter ? opts.xAxis.formatter(item) : util.toFixed(item, 2);
+ item = util.toFixed(item, 2);
+ var xAxisScaleValues = result.ranges.map(function(item) {
+ // 如果刻度值是浮点数,则保留两位小数
+ // 若有自定义格式则调用自定义的格式化函数
+ //item = opts.xAxis.formatter ? opts.xAxis.formatter(Number(item)) : item;
+ result = Object.assign(result, getXAxisPoints(xAxisScaleValues, opts, config));
+ // 计算X轴刻度的属性譬如每个刻度的间隔,刻度的起始点\结束点以及总长
+ var eachSpacing = result.eachSpacing;
+ var textLength = xAxisScaleValues.map(function(item) {
+ return measureText(item, opts.xAxis.fontSize * opts.pix, context);
+ if (opts.xAxis.disabled === true) {
+function getRadarDataPoints(angleList, center, radius, series, opts) {
+ var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1;
+ var radarOption = opts.extra.radar || {};
+ radarOption.max = radarOption.max || 0;
+ var maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series)));
+ let each = series[i];
+ let listItem = {};
+ listItem.color = each.color;
+ listItem.legendShape = each.legendShape;
+ listItem.pointShape = each.pointShape;
+ listItem.data = [];
+ each.data.forEach(function(item, index) {
+ let tmp = {};
+ tmp.angle = angleList[index];
+ tmp.proportion = item / maxData;
+ tmp.value = item;
+ tmp.position = convertCoordinateOrigin(radius * tmp.proportion * process * Math.cos(tmp.angle), radius * tmp.proportion * process * Math.sin(tmp.angle), center);
+ listItem.data.push(tmp);
+ data.push(listItem);
+function getPieDataPoints(series, radius) {
+ var process = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1;
+ var _start_ = 0;
+ item.data = item.data === null ? 0 : item.data;
+ count += item.data;
+ if (count === 0) {
+ item._proportion_ = 1 / series.length * process;
+ item._proportion_ = item.data / count * process;
+ item._radius_ = radius;
+ item._start_ = _start_;
+ _start_ += 2 * item._proportion_ * Math.PI;
+function getFunnelDataPoints(series, radius, option, eachSpacing) {
+ var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
+ if(option.type == 'funnel'){
+ series[i].radius = series[i].data / series[0].data * radius * process;
+ series[i].radius = (eachSpacing * (series.length - i)) / (eachSpacing * series.length) * radius * process;
+ series[i]._proportion_ = series[i].data / series[0].data;
+ // if(option.type !== 'pyramid'){
+ // series.reverse();
+function getRoseDataPoints(series, type, minRadius, radius) {
+ var dataArr = [];
+ dataArr.push(item.data);
+ var minData = Math.min.apply(null, dataArr);
+ var maxData = Math.max.apply(null, dataArr);
+ var radiusLength = radius - minRadius;
+ item._rose_proportion_ = 1 / series.length * process;
+ if(type == 'area'){
+ item._rose_proportion_ = item.data / count * process;
+ item._radius_ = minRadius + radiusLength * ((item.data - minData) / (maxData - minData)) || radius;
+ _start_ += 2 * item._rose_proportion_ * Math.PI;
+function getArcbarDataPoints(series, arcbarOption) {
+ if (process == 1) {
+ process = 0.999999;
+ let totalAngle;
+ if (arcbarOption.type == 'circle') {
+ totalAngle = 2;
+ if(arcbarOption.direction == 'ccw'){
+ if (arcbarOption.startAngle < arcbarOption.endAngle) {
+ totalAngle = 2 + arcbarOption.startAngle - arcbarOption.endAngle;
+ totalAngle = arcbarOption.startAngle - arcbarOption.endAngle;
+ if (arcbarOption.endAngle < arcbarOption.startAngle) {
+ totalAngle = 2 + arcbarOption.endAngle - arcbarOption.startAngle;
+ item._proportion_ = totalAngle * item.data * process + arcbarOption.startAngle;
+ item._proportion_ = arcbarOption.startAngle - totalAngle * item.data * process ;
+ if (item._proportion_ >= 2) {
+ item._proportion_ = item._proportion_ % 2;
+function getGaugeArcbarDataPoints(series, arcbarOption) {
+function getGaugeAxisPoints(categories, startAngle, endAngle) {
+ if (endAngle < startAngle) {
+ totalAngle = 2 + endAngle - startAngle;
+ totalAngle = startAngle - endAngle;
+ let tempStartAngle = startAngle;
+ for (let i = 0; i < categories.length; i++) {
+ categories[i].value = categories[i].value === null ? 0 : categories[i].value;
+ categories[i]._startAngle_ = tempStartAngle;
+ categories[i]._endAngle_ = totalAngle * categories[i].value + startAngle;
+ if (categories[i]._endAngle_ >= 2) {
+ categories[i]._endAngle_ = categories[i]._endAngle_ % 2;
+ tempStartAngle = categories[i]._endAngle_;
+ return categories;
+function getGaugeDataPoints(series, categories, gaugeOption) {
+ let process = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;
+ if (gaugeOption.pointer.color == 'auto') {
+ if (item.data <= categories[i].value) {
+ item.color = categories[i].color;
+ item.color = gaugeOption.pointer.color;
+ if (gaugeOption.endAngle < gaugeOption.startAngle) {
+ totalAngle = 2 + gaugeOption.endAngle - gaugeOption.startAngle;
+ totalAngle = gaugeOption.startAngle - gaugeOption.endAngle;
+ item._endAngle_ = totalAngle * item.data + gaugeOption.startAngle;
+ item._oldAngle_ = gaugeOption.oldAngle;
+ if (gaugeOption.oldAngle < gaugeOption.endAngle) {
+ item._oldAngle_ += 2;
+ if (item.data >= gaugeOption.oldData) {
+ item._proportion_ = (item._endAngle_ - item._oldAngle_) * process + gaugeOption.oldAngle;
+ item._proportion_ = item._oldAngle_ - (item._oldAngle_ - item._endAngle_) * process;
+function getPieTextMaxLength(series, config, context, opts) {
+ series = getPieDataPoints(series);
+ let maxLength = 0;
+ let text = item.formatter ? item.formatter(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%';
+ maxLength = Math.max(maxLength, measureText(text, item.textSize * opts.pix || config.fontSize, context));
+ return maxLength;
+function fixColumeData(points, eachSpacing, columnLen, index, config, opts) {
+ return points.map(function(item) {
+ if (item === null) {
+ return null;
+ var seriesGap = 0;
+ var categoryGap = 0;
+ if (opts.type == 'mix') {
+ seriesGap = opts.extra.mix.column.seriesGap * opts.pix || 0;
+ categoryGap = opts.extra.mix.column.categoryGap * opts.pix || 0;
+ seriesGap = opts.extra.column.seriesGap * opts.pix || 0;
+ categoryGap = opts.extra.column.categoryGap * opts.pix || 0;
+ seriesGap = Math.min(seriesGap, eachSpacing / columnLen)
+ categoryGap = Math.min(categoryGap, eachSpacing / columnLen)
+ item.width = Math.ceil((eachSpacing - 2 * categoryGap - seriesGap * (columnLen - 1)) / columnLen);
+ if (opts.extra.mix && opts.extra.mix.column.width && +opts.extra.mix.column.width > 0) {
+ item.width = Math.min(item.width, +opts.extra.mix.column.width * opts.pix);
+ if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) {
+ item.width = Math.min(item.width, +opts.extra.column.width * opts.pix);
+ if (item.width <= 0) {
+ item.width = 1;
+ item.x += (index + 0.5 - columnLen / 2) * (item.width + seriesGap);
+function fixBarData(points, eachSpacing, columnLen, index, config, opts) {
+ seriesGap = opts.extra.bar.seriesGap * opts.pix || 0;
+ categoryGap = opts.extra.bar.categoryGap * opts.pix || 0;
+ if (opts.extra.bar && opts.extra.bar.width && +opts.extra.bar.width > 0) {
+ item.width = Math.min(item.width, +opts.extra.bar.width * opts.pix);
+ item.y += (index + 0.5 - columnLen / 2) * (item.width + seriesGap);
+function fixColumeMeterData(points, eachSpacing, columnLen, index, config, opts, border) {
+ var categoryGap = opts.extra.column.categoryGap * opts.pix || 0;
+ item.width = eachSpacing - 2 * categoryGap;
+ if (index > 0) {
+ item.width -= border;
+function fixColumeStackData(points, eachSpacing, columnLen, index, config, opts, series) {
+ return points.map(function(item, indexn) {
+ item.width = Math.ceil(eachSpacing - 2 * categoryGap);
+function fixBarStackData(points, eachSpacing, columnLen, index, config, opts, series) {
+ var categoryGap = opts.extra.bar.categoryGap * opts.pix || 0;
+function getXAxisPoints(categories, opts, config) {
+ var spacingValid = opts.width - opts.area[1] - opts.area[3];
+ var dataCount = opts.enableScroll ? Math.min(opts.xAxis.itemCount, categories.length) : categories.length;
+ if ((opts.type == 'line' || opts.type == 'area' || opts.type == 'scatter' || opts.type == 'bubble' || opts.type == 'bar') && dataCount > 1 && opts.xAxis.boundaryGap == 'justify') {
+ dataCount -= 1;
+ var widthRatio = 0;
+ widthRatio = opts.extra.mount.widthRatio - 1;
+ dataCount += widthRatio;
+ var eachSpacing = spacingValid / dataCount;
+ var xAxisPoints = [];
+ var startX = opts.area[3];
+ var endX = opts.width - opts.area[1];
+ categories.forEach(function(item, index) {
+ xAxisPoints.push(startX + widthRatio / 2 * eachSpacing + index * eachSpacing);
+ if (opts.xAxis.boundaryGap !== 'justify') {
+ if (opts.enableScroll === true) {
+ xAxisPoints.push(startX + widthRatio * eachSpacing + categories.length * eachSpacing);
+ xAxisPoints.push(endX);
+ xAxisPoints: xAxisPoints,
+ startX: startX,
+ endX: endX,
+ eachSpacing: eachSpacing
+function getCandleDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) {
+ var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1;
+ var points = [];
+ var validHeight = opts.height - opts.area[0] - opts.area[2];
+ data.forEach(function(item, index) {
+ points.push(null);
+ var cPoints = [];
+ item.forEach(function(items, indexs) {
+ var point = {};
+ point.x = xAxisPoints[index] + Math.round(eachSpacing / 2);
+ var value = items.value || items;
+ var height = validHeight * (value - minRange) / (maxRange - minRange);
+ height *= process;
+ point.y = opts.height - Math.round(height) - opts.area[2];
+ cPoints.push(point);
+ points.push(cPoints);
+ return points;
+function getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) {
+ var boundaryGap = 'center';
+ if (opts.type == 'line' || opts.type == 'area' || opts.type == 'scatter' || opts.type == 'bubble' ) {
+ boundaryGap = opts.xAxis.boundaryGap;
+ var validWidth = opts.width - opts.area[1] - opts.area[3];
+ point.color = item.color;
+ point.x = xAxisPoints[index];
+ var value = item;
+ let xranges, xminRange, xmaxRange;
+ xranges = [].concat(opts.chartData.xAxisData.ranges);
+ xminRange = xranges.shift();
+ xmaxRange = xranges.pop();
+ value = item[1];
+ point.x = opts.area[3] + validWidth * (item[0] - xminRange) / (xmaxRange - xminRange);
+ if(opts.type == 'bubble'){
+ point.r = item[2];
+ point.t = item[3];
+ value = item.value;
+ if (boundaryGap == 'center') {
+ point.x += eachSpacing / 2;
+ point.y = opts.height - height - opts.area[2];
+ points.push(point);
+function getLineDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, lineOption, process){
+ var process = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : 1;
+ var boundaryGap = opts.xAxis.boundaryGap;
+ if(lineOption.animation == 'vertical'){
+ point.x = xAxisPoints[0] + eachSpacing * index * process;
+function getColumnDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, zeroPoints, process){
+ var height = validHeight * (value * process - minRange) / (maxRange - minRange);
+function getMountDataPoints(series, minRange, maxRange, xAxisPoints, eachSpacing, opts, mountOption, zeroPoints) {
+ var mountWidth = eachSpacing * mountOption.widthRatio;
+ series.forEach(function(item, index) {
+ var value = item.data;
+ point.value = value;
+ point.width = mountWidth;
+function getBarDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config) {
+ point.y = yAxisPoints[index];
+ var height = validWidth * (value - minRange) / (maxRange - minRange);
+ point.height = height;
+ point.x = height + opts.area[3];
+function getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, stackSeries) {
+ var process = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : 1;
+ if (seriesIndex > 0) {
+ var value = 0;
+ for (let i = 0; i <= seriesIndex; i++) {
+ value += stackSeries[i].data[index];
+ var value0 = value - item;
+ var height0 = validHeight * (value0 - minRange) / (maxRange - minRange);
+ var height0 = 0;
+ var heightc = height0;
+ heightc *= process;
+ point.y0 = opts.height - Math.round(heightc) - opts.area[2];
+function getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, stackSeries) {
+ var validHeight = opts.width - opts.area[1] - opts.area[3];
+ point.height = height - heightc;
+ point.x = opts.area[3] + height;
+ point.x0 = opts.area[3] + heightc;
+function getYAxisTextList(series, opts, config, stack, yData) {
+ var index = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : -1;
+ sorted.push(item[1]);
+ var minData = yData.min || 0;
+ var maxData = yData.max || 0;
+ if(maxData == 0){
+ maxData = 10;
+ minData = 0;
+ var dataRange = getDataRange(minData, maxData);
+ var minRange = (yData.min === undefined || yData.min === null) ? dataRange.minRange : yData.min;
+ var maxRange = (yData.max === undefined || yData.max === null) ? dataRange.maxRange : yData.max;
+ var eachRange = (maxRange - minRange) / opts.yAxis.splitNumber;
+ for (var i = 0; i <= opts.yAxis.splitNumber; i++) {
+ return range.reverse();
+function calYAxisData(series, opts, config, context) {
+ }, opts.extra.column);
+ //如果是多Y轴,重新计算
+ var YLength = opts.yAxis.data.length;
+ var newSeries = new Array(YLength);
+ if (YLength > 0) {
+ for (let i = 0; i < YLength; i++) {
+ newSeries[i] = [];
+ for (let j = 0; j < series.length; j++) {
+ if (series[j].index == i) {
+ newSeries[i].push(series[j]);
+ var rangesArr = new Array(YLength);
+ var rangesFormatArr = new Array(YLength);
+ var yAxisWidthArr = new Array(YLength);
+ let yData = opts.yAxis.data[i];
+ //如果总开关不显示,强制每个Y轴为不显示
+ if (opts.yAxis.disabled == true) {
+ yData.disabled = true;
+ if(yData.type === 'categories'){
+ if(!yData.formatter){
+ yData.formatter = (val,index,opts) => {return val + (yData.unit || '')};
+ yData.categories = yData.categories || opts.categories;
+ rangesArr[i] = yData.categories;
+ yData.formatter = (val,index,opts) => {return util.toFixed(val, yData.tofix || 0) + (yData.unit || '')};
+ rangesArr[i] = getYAxisTextList(newSeries[i], opts, config, columnstyle.type, yData, i);
+ let yAxisFontSizes = yData.fontSize * opts.pix || config.fontSize;
+ yAxisWidthArr[i] = {
+ position: yData.position ? yData.position : 'left',
+ width: 0
+ rangesFormatArr[i] = rangesArr[i].map(function(items,index) {
+ items = yData.formatter(items,index,opts);
+ yAxisWidthArr[i].width = Math.max(yAxisWidthArr[i].width, measureText(items, yAxisFontSizes, context) + 5);
+ return items;
+ let calibration = yData.calibration ? 4 * opts.pix : 0;
+ yAxisWidthArr[i].width += calibration + 3 * opts.pix;
+ if (yData.disabled === true) {
+ yAxisWidthArr[i].width = 0;
+ var rangesArr = new Array(1);
+ var rangesFormatArr = new Array(1);
+ var yAxisWidthArr = new Array(1);
+ if(opts.type === 'bar'){
+ rangesArr[0] = opts.categories;
+ if(!opts.yAxis.formatter){
+ opts.yAxis.formatter = (val,index,opts) => {return val + (opts.yAxis.unit || '')}
+ opts.yAxis.formatter = (val,index,opts) => {return val.toFixed(opts.yAxis.tofix ) + (opts.yAxis.unit || '')}
+ rangesArr[0] = getYAxisTextList(series, opts, config, columnstyle.type, {});
+ yAxisWidthArr[0] = {
+ position: 'left',
+ var yAxisFontSize = opts.yAxis.fontSize * opts.pix || config.fontSize;
+ rangesFormatArr[0] = rangesArr[0].map(function(item,index) {
+ item = opts.yAxis.formatter(item,index,opts);
+ yAxisWidthArr[0].width = Math.max(yAxisWidthArr[0].width, measureText(item, yAxisFontSize, context) + 5);
+ yAxisWidthArr[0].width += 3 * opts.pix;
+ if (opts.yAxis.disabled === true) {
+ opts.yAxis.data[0] = {
+ disabled: true
+ disabled: false,
+ max: opts.yAxis.max,
+ min: opts.yAxis.min,
+ formatter: opts.yAxis.formatter
+ opts.yAxis.data[0].categories = opts.categories;
+ opts.yAxis.data[0].type = 'categories';
+ rangesFormat: rangesFormatArr,
+ ranges: rangesArr,
+ yAxisWidth: yAxisWidthArr
+function calTooltipYAxisData(point, series, opts, config, eachSpacing) {
+ let ranges = [].concat(opts.chartData.yAxisData.ranges);
+ let spacingValid = opts.height - opts.area[0] - opts.area[2];
+ let minAxis = opts.area[0];
+ let items = [];
+ for (let i = 0; i < ranges.length; i++) {
+ let maxVal = Math.max.apply(this, ranges[i]);
+ let minVal = Math.min.apply(this, ranges[i]);
+ let item = maxVal - (maxVal - minVal) * (point - minAxis) / spacingValid;
+ item = opts.yAxis.data && opts.yAxis.data[i].formatter ? opts.yAxis.data[i].formatter(item, i, opts) : item.toFixed(0);
+ items.push(String(item))
+function calMarkLineData(points, opts) {
+ let minRange, maxRange;
+ for (let i = 0; i < points.length; i++) {
+ points[i].yAxisIndex = points[i].yAxisIndex ? points[i].yAxisIndex : 0;
+ let range = [].concat(opts.chartData.yAxisData.ranges[points[i].yAxisIndex]);
+ minRange = range.pop();
+ maxRange = range.shift();
+ let height = spacingValid * (points[i].value - minRange) / (maxRange - minRange);
+ points[i].y = opts.height - Math.round(height) - opts.area[2];
+function contextRotate(context, opts) {
+ if (opts.rotateLock !== true) {
+ context.translate(opts.height, 0);
+ context.rotate(90 * Math.PI / 180);
+ } else if (opts._rotate_ !== true) {
+ opts._rotate_ = true;
+function drawPointShape(points, color, shape, context, opts) {
+ context.beginPath();
+ if (opts.dataPointShapeType == 'hollow') {
+ context.setStrokeStyle(color);
+ context.setFillStyle(opts.background);
+ context.setLineWidth(2 * opts.pix);
+ context.setStrokeStyle("#ffffff");
+ context.setFillStyle(color);
+ context.setLineWidth(1 * opts.pix);
+ if (shape === 'diamond') {
+ context.moveTo(item.x, item.y - 4.5);
+ context.lineTo(item.x - 4.5, item.y);
+ context.lineTo(item.x, item.y + 4.5);
+ context.lineTo(item.x + 4.5, item.y);
+ context.lineTo(item.x, item.y - 4.5);
+ } else if (shape === 'circle') {
+ context.moveTo(item.x + 2.5 * opts.pix, item.y);
+ context.arc(item.x, item.y, 3 * opts.pix, 0, 2 * Math.PI, false);
+ } else if (shape === 'square') {
+ context.moveTo(item.x - 3.5, item.y - 3.5);
+ context.rect(item.x - 3.5, item.y - 3.5, 7, 7);
+ } else if (shape === 'triangle') {
+ context.lineTo(item.x - 4.5, item.y + 4.5);
+ context.lineTo(item.x + 4.5, item.y + 4.5);
+ } else if (shape === 'none') {
+ context.closePath();
+ context.fill();
+ context.stroke();
+function drawActivePoint(points, color, shape, context, opts, option, seriesIndex) {
+ if(!opts.tooltip){
+ if(opts.tooltip.group.length>0 && opts.tooltip.group.includes(seriesIndex) == false){
+ var pointIndex = typeof opts.tooltip.index === 'number' ? opts.tooltip.index : opts.tooltip.index[opts.tooltip.group.indexOf(seriesIndex)];
+ if (option.activeType == 'hollow') {
+ if (item !== null && pointIndex == index ) {
+ if (item !== null && pointIndex == index) {
+function drawRingTitle(opts, config, context, center) {
+ var titlefontSize = opts.title.fontSize || config.titleFontSize;
+ var subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize;
+ var title = opts.title.name || '';
+ var subtitle = opts.subtitle.name || '';
+ var titleFontColor = opts.title.color || opts.fontColor;
+ var subtitleFontColor = opts.subtitle.color || opts.fontColor;
+ var titleHeight = title ? titlefontSize : 0;
+ var subtitleHeight = subtitle ? subtitlefontSize : 0;
+ var margin = 5;
+ if (subtitle) {
+ var textWidth = measureText(subtitle, subtitlefontSize * opts.pix, context);
+ var startX = center.x - textWidth / 2 + (opts.subtitle.offsetX|| 0) * opts.pix ;
+ var startY = center.y + subtitlefontSize * opts.pix / 2 + (opts.subtitle.offsetY || 0) * opts.pix;
+ if (title) {
+ startY += (titleHeight * opts.pix + margin) / 2;
+ context.setFontSize(subtitlefontSize * opts.pix);
+ context.setFillStyle(subtitleFontColor);
+ context.fillText(subtitle, startX, startY);
+ var _textWidth = measureText(title, titlefontSize * opts.pix, context);
+ var _startX = center.x - _textWidth / 2 + (opts.title.offsetX || 0);
+ var _startY = center.y + titlefontSize * opts.pix / 2 + (opts.title.offsetY || 0) * opts.pix;
+ _startY -= (subtitleHeight * opts.pix + margin) / 2;
+ context.setFontSize(titlefontSize * opts.pix);
+ context.setFillStyle(titleFontColor);
+ context.fillText(title, _startX, _startY);
+function drawPointText(points, series, config, context, opts) {
+ // 绘制数据文案
+ var data = series.data;
+ var textOffset = series.textOffset ? series.textOffset : 0;
+ var fontSize = series.textSize ? series.textSize * opts.pix : config.fontSize;
+ context.setFillStyle(series.textColor || opts.fontColor);
+ var value = data[index]
+ if (typeof data[index] === 'object' && data[index] !== null) {
+ if (data[index].constructor.toString().indexOf('Array')>-1) {
+ value = data[index][1];
+ value = data[index].value
+ var formatVal = series.formatter ? series.formatter(value,index,series,opts) : value;
+ context.setTextAlign('center');
+ context.fillText(String(formatVal), item.x, item.y - 4 + textOffset * opts.pix);
+ context.setTextAlign('left');
+function drawColumePointText(points, series, config, context, opts) {
+ var Position = opts.extra.column.labelPosition;
+ var startY = item.y - 4 * opts.pix + textOffset * opts.pix;
+ if(item.y > series.zeroPoints){
+ startY = item.y + textOffset * opts.pix + fontSize;
+ if(Position == 'insideTop'){
+ startY = item.y + fontSize + textOffset * opts.pix;
+ startY = item.y - textOffset * opts.pix - 4 * opts.pix;
+ if(Position == 'center'){
+ startY = item.y + textOffset * opts.pix + (opts.height - opts.area[2] - item.y + fontSize)/2;
+ if(series.zeroPoints < opts.height - opts.area[2]){
+ startY = item.y + textOffset * opts.pix + (series.zeroPoints - item.y + fontSize)/2;
+ startY = item.y - textOffset * opts.pix - (item.y - series.zeroPoints - fontSize)/2;
+ if(opts.extra.column.type == 'stack'){
+ startY = item.y + textOffset * opts.pix + (item.y0 - item.y + fontSize)/2;
+ if(Position == 'bottom'){
+ startY = opts.height - opts.area[2] + textOffset * opts.pix - 4 * opts.pix;
+ startY = series.zeroPoints + textOffset * opts.pix - 4 * opts.pix;
+ startY = series.zeroPoints - textOffset * opts.pix + fontSize + 2 * opts.pix;
+ startY = item.y0 + textOffset * opts.pix - 4 * opts.pix;
+ context.fillText(String(formatVal), item.x, startY);
+function drawMountPointText(points, series, config, context, opts, zeroPoints) {
+ var Position = opts.extra.mount.labelPosition;
+ var fontSize = series[index].textSize ? series[index].textSize * opts.pix : config.fontSize;
+ context.setFillStyle(series[index].textColor || opts.fontColor);
+ var value = item.value
+ var formatVal = series[index].formatter ? series[index].formatter(value,index,series,opts) : value;
+ if(item.y > zeroPoints){
+function drawBarPointText(points, series, config, context, opts) {
+ value = data[index].value ;
+ context.fillText(String(formatVal), item.x + 4 * opts.pix , item.y + fontSize / 2 - 3 );
+function drawGaugeLabel(gaugeOption, radius, centerPosition, opts, config, context) {
+ radius -= gaugeOption.width / 2 + gaugeOption.labelOffset * opts.pix;
+ radius = radius < 10 ? 10 : radius;
+ let splitAngle = totalAngle / gaugeOption.splitLine.splitNumber;
+ let totalNumber = gaugeOption.endNumber - gaugeOption.startNumber;
+ let splitNumber = totalNumber / gaugeOption.splitLine.splitNumber;
+ let nowAngle = gaugeOption.startAngle;
+ let nowNumber = gaugeOption.startNumber;
+ for (let i = 0; i < gaugeOption.splitLine.splitNumber + 1; i++) {
+ var pos = {
+ x: radius * Math.cos(nowAngle * Math.PI),
+ y: radius * Math.sin(nowAngle * Math.PI)
+ var labelText = gaugeOption.formatter ? gaugeOption.formatter(nowNumber,i,opts) : nowNumber;
+ pos.x += centerPosition.x - measureText(labelText, config.fontSize, context) / 2;
+ pos.y += centerPosition.y;
+ var startX = pos.x;
+ var startY = pos.y;
+ context.setFontSize(config.fontSize);
+ context.setFillStyle(gaugeOption.labelColor || opts.fontColor);
+ context.fillText(labelText, startX, startY + config.fontSize / 2);
+ nowAngle += splitAngle;
+ if (nowAngle >= 2) {
+ nowAngle = nowAngle % 2;
+ nowNumber += splitNumber;
+function drawRadarLabel(angleList, radius, centerPosition, opts, config, context) {
+ angleList.forEach(function(angle, index) {
+ if(radarOption.labelPointShow === true && opts.categories[index] !== ''){
+ var posPoint = {
+ x: radius * Math.cos(angle),
+ y: radius * Math.sin(angle)
+ var posPointAxis = convertCoordinateOrigin(posPoint.x, posPoint.y, centerPosition);
+ context.setFillStyle(radarOption.labelPointColor);
+ context.arc(posPointAxis.x, posPointAxis.y, radarOption.labelPointRadius * opts.pix, 0, 2 * Math.PI, false);
+ if(radarOption.labelShow === true){
+ x: (radius + config.radarLabelTextMargin * opts.pix) * Math.cos(angle),
+ y: (radius + config.radarLabelTextMargin * opts.pix) * Math.sin(angle)
+ var posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition);
+ var startX = posRelativeCanvas.x;
+ var startY = posRelativeCanvas.y;
+ if (util.approximatelyEqual(pos.x, 0)) {
+ startX -= measureText(opts.categories[index] || '', config.fontSize, context) / 2;
+ } else if (pos.x < 0) {
+ startX -= measureText(opts.categories[index] || '', config.fontSize, context);
+ context.setFillStyle(radarOption.labelColor || opts.fontColor);
+ context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2);
+function drawPieText(series, opts, config, context, radius, center) {
+ var lineRadius = config.pieChartLinePadding;
+ var textObjectCollection = [];
+ var lastTextObject = null;
+ var seriesConvert = series.map(function(item,index) {
+ var text = item.formatter ? item.formatter(item,index,series,opts) : util.toFixed(item._proportion_.toFixed(4) * 100) + '%';
+ text = item.labelText ? item.labelText : text;
+ var arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2);
+ if (item._rose_proportion_) {
+ arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._rose_proportion_ / 2);
+ var color = item.color;
+ var radius = item._radius_;
+ arc: arc,
+ text: text,
+ color: color,
+ radius: radius,
+ textColor: item.textColor,
+ textSize: item.textSize,
+ labelShow: item.labelShow
+ for (let i = 0; i < seriesConvert.length; i++) {
+ let item = seriesConvert[i];
+ // line end
+ let orginX1 = Math.cos(item.arc) * (item.radius + lineRadius);
+ let orginY1 = Math.sin(item.arc) * (item.radius + lineRadius);
+ // line start
+ let orginX2 = Math.cos(item.arc) * item.radius;
+ let orginY2 = Math.sin(item.arc) * item.radius;
+ // text start
+ let orginX3 = orginX1 >= 0 ? orginX1 + config.pieChartTextPadding : orginX1 - config.pieChartTextPadding;
+ let orginY3 = orginY1;
+ let textWidth = measureText(item.text, item.textSize * opts.pix || config.fontSize, context);
+ let startY = orginY3;
+ if (lastTextObject && util.isSameXCoordinateArea(lastTextObject.start, {
+ x: orginX3
+ })) {
+ if (orginX3 > 0) {
+ startY = Math.min(orginY3, lastTextObject.start.y);
+ } else if (orginX1 < 0) {
+ startY = Math.max(orginY3, lastTextObject.start.y);
+ if (orginY3 > 0) {
+ if (orginX3 < 0) {
+ orginX3 -= textWidth;
+ let textObject = {
+ lineStart: {
+ x: orginX2,
+ y: orginY2
+ lineEnd: {
+ x: orginX1,
+ y: orginY1
+ x: orginX3,
+ y: startY
+ width: textWidth,
+ height: config.fontSize,
+ text: item.text,
+ textSize: item.textSize
+ lastTextObject = avoidCollision(textObject, lastTextObject);
+ textObjectCollection.push(lastTextObject);
+ for (let i = 0; i < textObjectCollection.length; i++) {
+ if(seriesConvert[i].labelShow === false){
+ let item = textObjectCollection[i];
+ let lineStartPoistion = convertCoordinateOrigin(item.lineStart.x, item.lineStart.y, center);
+ let lineEndPoistion = convertCoordinateOrigin(item.lineEnd.x, item.lineEnd.y, center);
+ let textPosition = convertCoordinateOrigin(item.start.x, item.start.y, center);
+ context.setFontSize(item.textSize * opts.pix || config.fontSize);
+ context.setStrokeStyle(item.color);
+ context.setFillStyle(item.color);
+ context.moveTo(lineStartPoistion.x, lineStartPoistion.y);
+ let curveStartX = item.start.x < 0 ? textPosition.x + item.width : textPosition.x;
+ let textStartX = item.start.x < 0 ? textPosition.x - 5 : textPosition.x + 5;
+ context.quadraticCurveTo(lineEndPoistion.x, lineEndPoistion.y, curveStartX, textPosition.y);
+ context.moveTo(textPosition.x + item.width, textPosition.y);
+ context.arc(curveStartX, textPosition.y, 2 * opts.pix, 0, 2 * Math.PI);
+ context.setFillStyle(item.textColor || opts.fontColor);
+ context.fillText(item.text, textStartX, textPosition.y + 3);
+function drawToolTipSplitLine(offsetX, opts, config, context) {
+ var toolTipOption = opts.extra.tooltip || {};
+ toolTipOption.gridType = toolTipOption.gridType == undefined ? 'solid' : toolTipOption.gridType;
+ toolTipOption.dashLength = toolTipOption.dashLength == undefined ? 4 : toolTipOption.dashLength;
+ var startY = opts.area[0];
+ var endY = opts.height - opts.area[2];
+ if (toolTipOption.gridType == 'dash') {
+ context.setLineDash([toolTipOption.dashLength, toolTipOption.dashLength]);
+ context.setStrokeStyle(toolTipOption.gridColor || '#cccccc');
+ context.moveTo(offsetX, startY);
+ context.lineTo(offsetX, endY);
+ context.setLineDash([]);
+ if (toolTipOption.xAxisLabel) {
+ let labelText = opts.categories[opts.tooltip.index];
+ let textWidth = measureText(labelText, config.fontSize, context);
+ let textX = offsetX - 0.5 * textWidth;
+ let textY = endY + 2 * opts.pix;
+ context.setFillStyle(hexToRgb(toolTipOption.labelBgColor || config.toolTipBackground, toolTipOption.labelBgOpacity || config.toolTipOpacity));
+ context.setStrokeStyle(toolTipOption.labelBgColor || config.toolTipBackground);
+ context.rect(textX - toolTipOption.boxPadding * opts.pix, textY, textWidth + 2 * toolTipOption.boxPadding * opts.pix, config.fontSize + 2 * toolTipOption.boxPadding * opts.pix);
+ context.setFillStyle(toolTipOption.labelFontColor || opts.fontColor);
+ context.fillText(String(labelText), textX, textY + toolTipOption.boxPadding * opts.pix + config.fontSize);
+function drawMarkLine(opts, config, context) {
+ let markLineOption = assign({}, {
+ type: 'solid',
+ dashLength: 4,
+ }, opts.extra.markLine);
+ let startX = opts.area[3];
+ let endX = opts.width - opts.area[1];
+ let points = calMarkLineData(markLineOption.data, opts);
+ let item = assign({}, {
+ lineColor: '#DE4A42',
+ showLabel: false,
+ labelFontSize: 13,
+ labelPadding: 6,
+ labelFontColor: '#666666',
+ labelBgColor: '#DFE8FF',
+ labelBgOpacity: 0.8,
+ labelAlign: 'left',
+ labelOffsetX: 0,
+ labelOffsetY: 0,
+ }, points[i]);
+ if (markLineOption.type == 'dash') {
+ context.setLineDash([markLineOption.dashLength, markLineOption.dashLength]);
+ context.setStrokeStyle(item.lineColor);
+ context.moveTo(startX, item.y);
+ context.lineTo(endX, item.y);
+ if (item.showLabel) {
+ let fontSize = item.labelFontSize * opts.pix;
+ let labelText = item.labelText ? item.labelText : item.value;
+ let textWidth = measureText(labelText, fontSize, context);
+ let bgWidth = textWidth + item.labelPadding * opts.pix * 2;
+ let bgStartX = item.labelAlign == 'left' ? opts.area[3] - bgWidth : opts.width - opts.area[1];
+ bgStartX += item.labelOffsetX;
+ let bgStartY = item.y - 0.5 * fontSize - item.labelPadding * opts.pix;
+ bgStartY += item.labelOffsetY;
+ let textX = bgStartX + item.labelPadding * opts.pix;
+ let textY = item.y;
+ context.setFillStyle(hexToRgb(item.labelBgColor, item.labelBgOpacity));
+ context.setStrokeStyle(item.labelBgColor);
+ context.rect(bgStartX, bgStartY, bgWidth, fontSize + 2 * item.labelPadding * opts.pix);
+ context.setFillStyle(item.labelFontColor);
+ context.fillText(String(labelText), textX, bgStartY + fontSize + item.labelPadding * opts.pix/2);
+function drawToolTipHorizentalLine(opts, config, context, eachSpacing, xAxisPoints) {
+ var toolTipOption = assign({}, {
+ gridType: 'solid',
+ dashLength: 4
+ }, opts.extra.tooltip);
+ context.moveTo(startX, opts.tooltip.offset.y);
+ context.lineTo(endX, opts.tooltip.offset.y);
+ if (toolTipOption.yAxisLabel) {
+ let boxPadding = toolTipOption.boxPadding * opts.pix;
+ let labelText = calTooltipYAxisData(opts.tooltip.offset.y, opts.series, opts, config, eachSpacing);
+ let widthArr = opts.chartData.yAxisData.yAxisWidth;
+ let tStartLeft = opts.area[3];
+ let tStartRight = opts.width - opts.area[1];
+ for (let i = 0; i < labelText.length; i++) {
+ context.setFontSize(toolTipOption.fontSize * opts.pix);
+ let textWidth = measureText(labelText[i], toolTipOption.fontSize * opts.pix, context);
+ let bgStartX, bgEndX, bgWidth;
+ if (widthArr[i].position == 'left') {
+ bgStartX = tStartLeft - (textWidth + boxPadding * 2) - 2 * opts.pix;
+ bgEndX = Math.max(bgStartX, bgStartX + textWidth + boxPadding * 2);
+ bgStartX = tStartRight + 2 * opts.pix;
+ bgEndX = Math.max(bgStartX + widthArr[i].width, bgStartX + textWidth + boxPadding * 2);
+ bgWidth = bgEndX - bgStartX;
+ let textX = bgStartX + (bgWidth - textWidth) / 2;
+ let textY = opts.tooltip.offset.y;
+ context.rect(bgStartX, textY - 0.5 * config.fontSize - boxPadding, bgWidth, config.fontSize + 2 * boxPadding);
+ context.fillText(labelText[i], textX, textY + 0.5 * config.fontSize);
+ tStartLeft -= (widthArr[i].width + opts.yAxis.padding * opts.pix);
+ tStartRight += widthArr[i].width + opts.yAxis.padding * opts.pix;
+function drawToolTipSplitArea(offsetX, opts, config, context, eachSpacing) {
+ activeBgColor: '#000000',
+ activeBgOpacity: 0.08,
+ activeWidth: eachSpacing
+ toolTipOption.activeWidth = toolTipOption.activeWidth > eachSpacing ? eachSpacing : toolTipOption.activeWidth;
+ context.setFillStyle(hexToRgb(toolTipOption.activeBgColor, toolTipOption.activeBgOpacity));
+ context.rect(offsetX - toolTipOption.activeWidth / 2, startY, toolTipOption.activeWidth, endY - startY);
+ context.setFillStyle("#FFFFFF");
+function drawBarToolTipSplitArea(offsetX, opts, config, context, eachSpacing) {
+ activeBgOpacity: 0.08
+ context.rect( startX ,offsetX - eachSpacing / 2 , endX - startX,eachSpacing);
+function drawToolTip(textList, offset, opts, config, context, eachSpacing, xAxisPoints) {
+ showBox: true,
+ showArrow: true,
+ showCategory: false,
+ bgColor: '#000000',
+ bgOpacity: 0.7,
+ borderColor: '#000000',
+ borderWidth: 0,
+ borderRadius: 0,
+ borderOpacity: 0.7,
+ boxPadding: 3,
+ fontColor: '#FFFFFF',
+ lineHeight: 20,
+ legendShow: true,
+ legendShape: 'auto',
+ splitLine: true,
+ if(toolTipOption.showCategory==true && opts.categories){
+ textList.unshift({text:opts.categories[opts.tooltip.index],color:null})
+ var fontSize = toolTipOption.fontSize * opts.pix;
+ var lineHeight = toolTipOption.lineHeight * opts.pix;
+ var boxPadding = toolTipOption.boxPadding * opts.pix;
+ var legendWidth = fontSize;
+ var legendMarginRight = 5 * opts.pix;
+ if(toolTipOption.legendShow == false){
+ legendWidth = 0;
+ legendMarginRight = 0;
+ var arrowWidth = toolTipOption.showArrow ? 8 * opts.pix : 0;
+ var isOverRightBorder = false;
+ if (opts.type == 'line' || opts.type == 'mount' || opts.type == 'area' || opts.type == 'candle' || opts.type == 'mix') {
+ if (toolTipOption.splitLine == true) {
+ drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context);
+ offset = assign({
+ }, offset);
+ offset.y -= 8 * opts.pix;
+ var textWidth = textList.map(function(item) {
+ return measureText(item.text, fontSize, context);
+ var toolTipWidth = legendWidth + legendMarginRight + 4 * boxPadding + Math.max.apply(null, textWidth);
+ var toolTipHeight = 2 * boxPadding + textList.length * lineHeight;
+ if (toolTipOption.showBox == false) {
+ // if beyond the right border
+ if (offset.x - Math.abs(opts._scrollDistance_ || 0) + arrowWidth + toolTipWidth > opts.width) {
+ isOverRightBorder = true;
+ if (toolTipHeight + offset.y > opts.height) {
+ offset.y = opts.height - toolTipHeight;
+ // draw background rect
+ context.setFillStyle(hexToRgb(toolTipOption.bgColor, toolTipOption.bgOpacity));
+ context.setLineWidth(toolTipOption.borderWidth * opts.pix);
+ context.setStrokeStyle(hexToRgb(toolTipOption.borderColor, toolTipOption.borderOpacity));
+ var radius = toolTipOption.borderRadius;
+ if (isOverRightBorder) {
+ // 增加左侧仍然超出的判断
+ if(toolTipWidth + arrowWidth > opts.width){
+ offset.x = opts.width + Math.abs(opts._scrollDistance_ || 0) + arrowWidth + (toolTipWidth - opts.width)
+ if(toolTipWidth > offset.x){
+ if (toolTipOption.showArrow) {
+ context.moveTo(offset.x, offset.y + 10 * opts.pix);
+ context.lineTo(offset.x - arrowWidth, offset.y + 10 * opts.pix + 5 * opts.pix);
+ context.arc(offset.x - arrowWidth - radius, offset.y + toolTipHeight - radius, radius, 0, Math.PI / 2, false);
+ context.arc(offset.x - arrowWidth - Math.round(toolTipWidth) + radius, offset.y + toolTipHeight - radius, radius,
+ Math.PI / 2, Math.PI, false);
+ context.arc(offset.x - arrowWidth - Math.round(toolTipWidth) + radius, offset.y + radius, radius, -Math.PI, -Math.PI / 2, false);
+ context.arc(offset.x - arrowWidth - radius, offset.y + radius, radius, -Math.PI / 2, 0, false);
+ context.lineTo(offset.x - arrowWidth, offset.y + 10 * opts.pix - 5 * opts.pix);
+ context.lineTo(offset.x, offset.y + 10 * opts.pix);
+ context.lineTo(offset.x + arrowWidth, offset.y + 10 * opts.pix - 5 * opts.pix);
+ context.arc(offset.x + arrowWidth + radius, offset.y + radius, radius, -Math.PI, -Math.PI / 2, false);
+ context.arc(offset.x + arrowWidth + Math.round(toolTipWidth) - radius, offset.y + radius, radius, -Math.PI / 2, 0,
+ false);
+ context.arc(offset.x + arrowWidth + Math.round(toolTipWidth) - radius, offset.y + toolTipHeight - radius, radius, 0,
+ Math.PI / 2, false);
+ context.arc(offset.x + arrowWidth + radius, offset.y + toolTipHeight - radius, radius, Math.PI / 2, Math.PI, false);
+ context.lineTo(offset.x + arrowWidth, offset.y + 10 * opts.pix + 5 * opts.pix);
+ if (toolTipOption.borderWidth > 0) {
+ // draw legend
+ if(toolTipOption.legendShow){
+ textList.forEach(function(item, index) {
+ if (item.color !== null) {
+ var startX = offset.x + arrowWidth + 2 * boxPadding;
+ var startY = offset.y + (lineHeight - fontSize) / 2 + lineHeight * index + boxPadding + 1;
+ startX = offset.x - toolTipWidth - arrowWidth + 2 * boxPadding;
+ switch (item.legendShape) {
+ context.moveTo(startX, startY + 0.5 * legendWidth - 2 * opts.pix);
+ context.fillRect(startX, startY + 0.5 * legendWidth - 2 * opts.pix, legendWidth, 4 * opts.pix);
+ case 'triangle':
+ context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * legendWidth - 5 * opts.pix);
+ context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * legendWidth + 5 * opts.pix);
+ context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * legendWidth + 5 * opts.pix);
+ context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * legendWidth - 5 * opts.pix);
+ case 'diamond':
+ context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * legendWidth);
+ context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * legendWidth + 5 * opts.pix);
+ context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * legendWidth);
+ case 'circle':
+ context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * legendWidth);
+ context.arc(startX + 7.5 * opts.pix, startY + 0.5 * legendWidth, 5 * opts.pix, 0, 2 * Math.PI);
+ case 'rect':
+ context.moveTo(startX, startY + 0.5 * legendWidth - 5 * opts.pix);
+ context.fillRect(startX, startY + 0.5 * legendWidth - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix);
+ case 'square':
+ context.moveTo(startX + 2 * opts.pix, startY + 0.5 * legendWidth - 5 * opts.pix);
+ context.fillRect(startX + 2 * opts.pix, startY + 0.5 * legendWidth - 5 * opts.pix, 10 * opts.pix, 10 * opts.pix);
+ // draw text list
+ var startX = offset.x + arrowWidth + 2 * boxPadding + legendWidth + legendMarginRight;
+ startX = offset.x - toolTipWidth - arrowWidth + 2 * boxPadding + legendWidth + legendMarginRight;
+ var startY = offset.y + lineHeight * index + (lineHeight - fontSize)/2 - 1 + boxPadding + fontSize;
+ context.setTextBaseline('normal');
+ context.setFillStyle(toolTipOption.fontColor);
+ context.fillText(item.text, startX, startY);
+function drawColumnDataPoints(series, opts, config, context) {
+ let process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
+ let xAxisData = opts.chartData.xAxisData,
+ xAxisPoints = xAxisData.xAxisPoints,
+ eachSpacing = xAxisData.eachSpacing;
+ let columnOption = assign({}, {
+ type: 'group',
+ width: eachSpacing / 2,
+ meterBorder: 4,
+ meterFillColor: '#FFFFFF',
+ barBorderCircle: false,
+ barBorderRadius: [],
+ seriesGap: 2,
+ linearType: 'none',
+ linearOpacity: 1,
+ customColor: [],
+ colorStop: 0,
+ labelPosition: 'outside'
+ let calPoints = [];
+ context.save();
+ let leftNum = -2;
+ let rightNum = xAxisPoints.length + 2;
+ if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
+ context.translate(opts._scrollDistance_, 0);
+ leftNum = Math.floor(-opts._scrollDistance_ / eachSpacing) - 2;
+ rightNum = leftNum + opts.xAxis.itemCount + 4;
+ if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) {
+ drawToolTipSplitArea(opts.tooltip.offset.x, opts, config, context, eachSpacing);
+ columnOption.customColor = fillCustomColor(columnOption.linearType, columnOption.customColor, series, config);
+ series.forEach(function(eachSeries, seriesIndex) {
+ let ranges, minRange, maxRange;
+ ranges = [].concat(opts.chartData.yAxisData.ranges[eachSeries.index]);
+ minRange = ranges.pop();
+ maxRange = ranges.shift();
+ // 计算0轴坐标
+ let zeroHeight = spacingValid * (0 - minRange) / (maxRange - minRange);
+ let zeroPoints = opts.height - Math.round(zeroHeight) - opts.area[2];
+ eachSeries.zeroPoints = zeroPoints;
+ var data = eachSeries.data;
+ switch (columnOption.type) {
+ case 'group':
+ var points = getColumnDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, zeroPoints, process);
+ var tooltipPoints = getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, series, process);
+ calPoints.push(tooltipPoints);
+ points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts);
+ //fix issues/I27B1N yyoinge & Joeshu
+ if (item !== null && i > leftNum && i < rightNum) {
+ var startX = item.x - item.width / 2;
+ var height = opts.height - item.y - opts.area[2];
+ var fillColor = item.color || eachSeries.color
+ var strokeColor = item.color || eachSeries.color
+ if (columnOption.linearType !== 'none') {
+ var grd = context.createLinearGradient(startX, item.y, startX, zeroPoints);
+ //透明渐变
+ if (columnOption.linearType == 'opacity') {
+ grd.addColorStop(0, hexToRgb(fillColor, columnOption.linearOpacity));
+ grd.addColorStop(1, hexToRgb(fillColor, 1));
+ grd.addColorStop(0, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity));
+ grd.addColorStop(columnOption.colorStop, hexToRgb(columnOption.customColor[eachSeries.linearIndex],columnOption.linearOpacity));
+ fillColor = grd
+ // 圆角边框
+ if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle === true) {
+ const left = startX;
+ const top = item.y > zeroPoints ? zeroPoints : item.y;
+ const width = item.width;
+ const height = Math.abs(zeroPoints - item.y);
+ if (columnOption.barBorderCircle) {
+ columnOption.barBorderRadius = [width / 2, width / 2, 0, 0];
+ columnOption.barBorderRadius = [0, 0,width / 2, width / 2];
+ let [r0, r1, r2, r3] = columnOption.barBorderRadius;
+ let minRadius = Math.min(width/2,height/2);
+ r0 = r0 > minRadius ? minRadius : r0;
+ r1 = r1 > minRadius ? minRadius : r1;
+ r2 = r2 > minRadius ? minRadius : r2;
+ r3 = r3 > minRadius ? minRadius : r3;
+ r0 = r0 < 0 ? 0 : r0;
+ r1 = r1 < 0 ? 0 : r1;
+ r2 = r2 < 0 ? 0 : r2;
+ r3 = r3 < 0 ? 0 : r3;
+ context.arc(left + r0, top + r0, r0, -Math.PI, -Math.PI / 2);
+ context.arc(left + width - r1, top + r1, r1, -Math.PI / 2, 0);
+ context.arc(left + width - r2, top + height - r2, r2, 0, Math.PI / 2);
+ context.arc(left + r3, top + height - r3, r3, Math.PI / 2, Math.PI);
+ context.lineTo(startX + item.width, item.y);
+ context.lineTo(startX + item.width, zeroPoints);
+ context.lineTo(startX, zeroPoints);
+ context.lineTo(startX, item.y);
+ context.setLineWidth(1)
+ context.setStrokeStyle(strokeColor);
+ context.setFillStyle(fillColor);
+ //context.stroke();
+ case 'stack':
+ // 绘制堆叠数据图
+ var points = getStackDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, seriesIndex, series, process);
+ calPoints.push(points);
+ points = fixColumeStackData(points, eachSpacing, series.length, seriesIndex, config, opts, series);
+ var fillColor = item.color || eachSeries.color;
+ var startX = item.x - item.width / 2 + 1;
+ var height0 = opts.height - item.y0 - opts.area[2];
+ height -= height0;
+ context.fillRect(startX, item.y, item.width, height);
+ case 'meter':
+ // 绘制温度计数据图
+ var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
+ points = fixColumeMeterData(points, eachSpacing, series.length, seriesIndex, config, opts, columnOption.meterBorder);
+ //画背景颜色
+ if (seriesIndex == 0 && columnOption.meterBorder > 0) {
+ context.setStrokeStyle(eachSeries.color);
+ context.setLineWidth(columnOption.meterBorder * opts.pix);
+ if(seriesIndex == 0){
+ context.setFillStyle(columnOption.meterFillColor);
+ context.setFillStyle(item.color || eachSeries.color);
+ const top = item.y;
+ const height = zeroPoints - item.y;
+ if (opts.dataLabel !== false && process === 1) {
+ var points = getColumnDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
+ drawColumePointText(points, eachSeries, config, context, opts);
+ context.restore();
+ calPoints: calPoints,
+function drawMountDataPoints(series, opts, config, context) {
+ let mountOption = assign({}, {
+ type: 'mount',
+ widthRatio: 1,
+ borderWidth: 1,
+ }, opts.extra.mount);
+ mountOption.widthRatio = mountOption.widthRatio <= 0 ? 0 : mountOption.widthRatio;
+ mountOption.widthRatio = mountOption.widthRatio >= 2 ? 2 : mountOption.widthRatio;
+ mountOption.customColor = fillCustomColor(mountOption.linearType, mountOption.customColor, series, config);
+ ranges = [].concat(opts.chartData.yAxisData.ranges[0]);
+ var points = getMountDataPoints(series, minRange, maxRange, xAxisPoints, eachSpacing, opts, mountOption, zeroPoints, process);
+ switch (mountOption.type) {
+ var startX = item.x - eachSpacing*mountOption.widthRatio/2;
+ var fillColor = item.color || series[i].color
+ var strokeColor = item.color || series[i].color
+ if (mountOption.linearType !== 'none') {
+ if (mountOption.linearType == 'opacity') {
+ grd.addColorStop(0, hexToRgb(fillColor, mountOption.linearOpacity));
+ grd.addColorStop(0, hexToRgb(mountOption.customColor[series[i].linearIndex], mountOption.linearOpacity));
+ grd.addColorStop(mountOption.colorStop, hexToRgb(mountOption.customColor[series[i].linearIndex],mountOption.linearOpacity));
+ if ((mountOption.barBorderRadius && mountOption.barBorderRadius.length === 4) || mountOption.barBorderCircle === true) {
+ if (mountOption.barBorderCircle) {
+ mountOption.barBorderRadius = [width / 2, width / 2, 0, 0];
+ mountOption.barBorderRadius = [0, 0,width / 2, width / 2];
+ let [r0, r1, r2, r3] = mountOption.barBorderRadius;
+ if(mountOption.borderWidth > 0){
+ context.setLineWidth(mountOption.borderWidth * opts.pix);
+ context.moveTo(startX, zeroPoints);
+ context.lineTo(item.x, item.y);
+ context.bezierCurveTo(item.x - item.width/4, zeroPoints, item.x - item.width/4, item.y, item.x, item.y);
+ context.bezierCurveTo(item.x + item.width/4, item.y, item.x + item.width/4, zeroPoints, startX + item.width, zeroPoints);
+ case 'sharp':
+ context.quadraticCurveTo(item.x - 0, zeroPoints - height/4, item.x, item.y);
+ context.quadraticCurveTo(item.x + 0, zeroPoints - height/4, startX + item.width, zeroPoints)
+ drawMountPointText(points, series, config, context, opts, zeroPoints);
+ calPoints: points,
+function drawBarDataPoints(series, opts, config, context) {
+ let yAxisPoints = [];
+ let eachSpacing = (opts.height - opts.area[0] - opts.area[2])/opts.categories.length;
+ for (let i = 0; i < opts.categories.length; i++) {
+ yAxisPoints.push(opts.area[0] + eachSpacing / 2 + eachSpacing * i);
+ let rightNum = yAxisPoints.length + 2;
+ drawBarToolTipSplitArea(opts.tooltip.offset.y, opts, config, context, eachSpacing);
+ ranges = [].concat(opts.chartData.xAxisData.ranges);
+ maxRange = ranges.pop();
+ minRange = ranges.shift();
+ var points = getBarDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, process);
+ var tooltipPoints = getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, series, process);
+ points = fixBarData(points, eachSpacing, series.length, seriesIndex, config, opts);
+ //var startX = item.x - item.width / 2;
+ var startY = item.y - item.width / 2;
+ var height = item.height;
+ var grd = context.createLinearGradient(startX, item.y, item.x, item.y);
+ const top = item.y - item.width / 2;
+ const height = item.height;
+ context.arc(left + r3, top + r3, r3, -Math.PI, -Math.PI / 2);
+ context.arc(item.x - r0, top + r0, r0, -Math.PI / 2, 0);
+ context.arc(item.x - r1, top + width - r1, r1, 0, Math.PI / 2);
+ context.arc(left + r2, top + width - r2, r2, Math.PI / 2, Math.PI);
+ context.moveTo(startX, startY);
+ context.lineTo(item.x, startY);
+ context.lineTo(item.x, startY + item.width);
+ context.lineTo(startX, startY + item.width);
+ context.lineTo(startX, startY);
+ var points = getBarStackDataPoints(data, minRange, maxRange, yAxisPoints, eachSpacing, opts, config, seriesIndex, series, process);
+ points = fixBarStackData(points, eachSpacing, series.length, seriesIndex, config, opts, series);
+ var startX = item.x0;
+ context.moveTo(startX, item.y - item.width/2);
+ context.fillRect(startX, item.y - item.width/2, item.height , item.width);
+ drawBarPointText(points, eachSeries, config, context, opts);
+ yAxisPoints: yAxisPoints,
+function drawCandleDataPoints(series, seriesMA, opts, config, context) {
+ var candleOption = assign({}, {
+ color: {},
+ average: {}
+ }, opts.extra.candle);
+ candleOption.color = assign({}, {
+ upLine: '#f04864',
+ upFill: '#f04864',
+ downLine: '#2fc25b',
+ downFill: '#2fc25b'
+ }, candleOption.color);
+ candleOption.average = assign({}, {
+ show: false,
+ name: [],
+ day: [],
+ color: config.color
+ }, candleOption.average);
+ opts.extra.candle = candleOption;
+ let leftSpace = 0;
+ let rightSpace = opts.width + eachSpacing;
+ leftSpace = -opts._scrollDistance_ - eachSpacing * 2 + opts.area[3];
+ rightSpace = leftSpace + (opts.xAxis.itemCount + 4) * eachSpacing;
+ //画均线
+ if (candleOption.average.show || seriesMA) { //Merge pull request !12 from 邱贵翔
+ seriesMA.forEach(function(eachSeries, seriesIndex) {
+ var splitPointList = splitPoints(points,eachSeries);
+ for (let i = 0; i < splitPointList.length; i++) {
+ let points = splitPointList[i];
+ context.setLineWidth(1);
+ if (points.length === 1) {
+ context.moveTo(points[0].x, points[0].y);
+ context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI);
+ let startPoint = 0;
+ for (let j = 0; j < points.length; j++) {
+ let item = points[j];
+ if (startPoint == 0 && item.x > leftSpace) {
+ context.moveTo(item.x, item.y);
+ startPoint = 1;
+ if (j > 0 && item.x > leftSpace && item.x < rightSpace) {
+ var ctrlPoint = createCurveControlPoints(points, j - 1);
+ context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x,
+ item.y);
+ //画K线
+ var points = getCandleDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
+ for (let i = 0; i < splitPointList[0].length; i++) {
+ if (i > leftNum && i < rightNum) {
+ let item = splitPointList[0][i];
+ //如果上涨
+ if (data[i][1] - data[i][0] > 0) {
+ context.setStrokeStyle(candleOption.color.upLine);
+ context.setFillStyle(candleOption.color.upFill);
+ context.moveTo(item[3].x, item[3].y); //顶点
+ context.lineTo(item[1].x, item[1].y); //收盘中间点
+ context.lineTo(item[1].x - eachSpacing / 4, item[1].y); //收盘左侧点
+ context.lineTo(item[0].x - eachSpacing / 4, item[0].y); //开盘左侧点
+ context.lineTo(item[0].x, item[0].y); //开盘中间点
+ context.lineTo(item[2].x, item[2].y); //底点
+ context.lineTo(item[0].x + eachSpacing / 4, item[0].y); //开盘右侧点
+ context.lineTo(item[1].x + eachSpacing / 4, item[1].y); //收盘右侧点
+ context.setStrokeStyle(candleOption.color.downLine);
+ context.setFillStyle(candleOption.color.downFill);
+function drawAreaDataPoints(series, opts, config, context) {
+ var areaOption = assign({}, {
+ type: 'straight',
+ opacity: 0.2,
+ addLine: false,
+ width: 2,
+ gradient: false,
+ activeType: 'none'
+ }, opts.extra.area);
+ let endY = opts.height - opts.area[2];
+ let data = eachSeries.data;
+ let points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process);
+ let splitPointList = splitPoints(points,eachSeries);
+ // 绘制区域数
+ context.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity));
+ if (areaOption.gradient) {
+ let gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]);
+ gradient.addColorStop('0', hexToRgb(eachSeries.color, areaOption.opacity));
+ gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1));
+ context.setFillStyle(gradient);
+ context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity));
+ context.setLineWidth(areaOption.width * opts.pix);
+ if (points.length > 1) {
+ let firstPoint = points[0];
+ let lastPoint = points[points.length - 1];
+ context.moveTo(firstPoint.x, firstPoint.y);
+ if (areaOption.type === 'curve') {
+ let ctrlPoint = createCurveControlPoints(points, j - 1);
+ context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y);
+ if (areaOption.type === 'straight') {
+ if (areaOption.type === 'step') {
+ context.lineTo(item.x, points[j - 1].y);
+ context.lineTo(lastPoint.x, endY);
+ context.lineTo(firstPoint.x, endY);
+ context.lineTo(firstPoint.x, firstPoint.y);
+ let item = points[0];
+ context.moveTo(item.x - eachSpacing / 2, item.y);
+ // context.lineTo(item.x + eachSpacing / 2, item.y);
+ // context.lineTo(item.x + eachSpacing / 2, endY);
+ // context.lineTo(item.x - eachSpacing / 2, endY);
+ // context.moveTo(item.x - eachSpacing / 2, item.y);
+ //画连线
+ if (areaOption.addLine) {
+ if (eachSeries.lineType == 'dash') {
+ let dashLength = eachSeries.dashLength ? eachSeries.dashLength : 8;
+ dashLength *= opts.pix;
+ context.setLineDash([dashLength, dashLength]);
+ // context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI);
+ //画点
+ if (opts.dataPointShape !== false) {
+ drawPointShape(points, eachSeries.color, eachSeries.pointShape, context, opts);
+ drawActivePoint(points, eachSeries.color, eachSeries.pointShape, context, opts, areaOption,seriesIndex);
+ drawPointText(points, eachSeries, config, context, opts);
+function drawScatterDataPoints(series, opts, config, context) {
+ var scatterOption = assign({}, {
+ type: 'circle'
+ }, opts.extra.scatter);
+ var calPoints = [];
+ context.setFillStyle(eachSeries.color);
+ var shape = eachSeries.pointShape;
+function drawBubbleDataPoints(series, opts, config, context) {
+ var bubbleOption = assign({}, {
+ opacity: 1,
+ border:2
+ }, opts.extra.bubble);
+ context.setLineWidth(bubbleOption.border * opts.pix);
+ context.setFillStyle(hexToRgb(eachSeries.color, bubbleOption.opacity));
+ context.moveTo(item.x + item.r, item.y);
+ context.arc(item.x, item.y, item.r * opts.pix, 0, 2 * Math.PI, false);
+ var fontSize = eachSeries.textSize * opts.pix || config.fontSize;
+ context.setFillStyle(eachSeries.textColor || "#FFFFFF");
+ context.fillText(String(item.t), item.x, item.y + fontSize/2);
+function drawLineDataPoints(series, opts, config, context) {
+ var lineOption = assign({}, {
+ activeType: 'none',
+ onShadow: false,
+ animation: 'vertical',
+ }, opts.extra.line);
+ lineOption.width *= opts.pix;
+ // 这段很神奇的代码用于解决ios16的setStrokeStyle失效的bug
+ context.moveTo(-10000, -10000);
+ context.lineTo(-10001, -10001);
+ var points = getLineDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, lineOption, process);
+ var strokeColor = eachSeries.color;
+ if (lineOption.linearType !== 'none' && eachSeries.linearColor && eachSeries.linearColor.length > 0) {
+ var grd = context.createLinearGradient(opts.chartData.xAxisData.startX, opts.height/2, opts.chartData.xAxisData.endX, opts.height/2);
+ for (var i = 0; i < eachSeries.linearColor.length; i++) {
+ grd.addColorStop(eachSeries.linearColor[i][0], hexToRgb(eachSeries.linearColor[i][1], 1));
+ strokeColor = grd
+ if (lineOption.onShadow == true && eachSeries.setShadow && eachSeries.setShadow.length > 0) {
+ context.setShadow(eachSeries.setShadow[0], eachSeries.setShadow[1], eachSeries.setShadow[2], eachSeries.setShadow[3]);
+ context.setShadow(0, 0, 0, 'rgba(0,0,0,0)');
+ context.setLineWidth(lineOption.width);
+ splitPointList.forEach(function(points, index) {
+ if (lineOption.type === 'curve') {
+ if (lineOption.type === 'straight') {
+ if (lineOption.type === 'step') {
+ drawActivePoint(points, eachSeries.color, eachSeries.pointShape, context, opts, lineOption);
+function drawMixDataPoints(series, opts, config, context) {
+ }, opts.extra.mix.column);
+ let areaOption = assign({}, {
+ gradient: false
+ }, opts.extra.mix.area);
+ let lineOption = assign({}, {
+ width: 2
+ }, opts.extra.mix.line);
+ var columnIndex = 0;
+ var columnLength = 0;
+ if (eachSeries.type == 'column') {
+ columnLength += 1;
+ // 绘制柱状数据图
+ points = fixColumeData(points, eachSpacing, columnLength, columnIndex, config, opts);
+ var grd = context.createLinearGradient(startX, item.y, startX, opts.height - opts.area[2]);
+ grd.addColorStop(columnOption.colorStop, hexToRgb(columnOption.customColor[eachSeries.linearIndex], columnOption.linearOpacity));
+ if ((columnOption.barBorderRadius && columnOption.barBorderRadius.length === 4) || columnOption.barBorderCircle) {
+ const height = opts.height - opts.area[2] - item.y;
+ context.lineTo(startX + item.width, opts.height - opts.area[2]);
+ context.lineTo(startX, opts.height - opts.area[2]);
+ columnIndex += 1;
+ //绘制区域图数据
+ if (eachSeries.type == 'area') {
+ // 绘制区域数据
+ var firstPoint = points[0];
+ if (eachSeries.style === 'curve') {
+ // 绘制折线数据图
+ if (eachSeries.type == 'line') {
+ context.setLineWidth(lineOption.width * opts.pix);
+ if (eachSeries.style == 'curve') {
+ context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y,
+ item.x, item.y);
+ // 绘制点数据图
+ if (eachSeries.type == 'point') {
+ eachSeries.addPoint = true;
+ if (eachSeries.addPoint == true && eachSeries.type !== 'column') {
+ if (eachSeries.type !== 'column') {
+ eachSpacing: eachSpacing,
+function drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints) {
+ if (toolTipOption.horizentalLine && opts.tooltip && process === 1 && (opts.type == 'line' || opts.type == 'area' || opts.type == 'column' || opts.type == 'mount' || opts.type == 'candle' || opts.type == 'mix')) {
+ drawToolTipHorizentalLine(opts, config, context, eachSpacing, xAxisPoints)
+ drawToolTip(opts.tooltip.textList, opts.tooltip.offset, opts, config, context, eachSpacing, xAxisPoints);
+function drawXAxis(categories, opts, config, context) {
+ startX = xAxisData.startX,
+ endX = xAxisData.endX,
+ if (opts.type == 'bar' || opts.type == 'line' || opts.type == 'area'|| opts.type == 'scatter' || opts.type == 'bubble') {
+ var startY = opts.height - opts.area[2];
+ var endY = opts.area[0];
+ //绘制滚动条
+ var scrollY = opts.height - opts.area[2] + config.xAxisHeight;
+ var scrollScreenWidth = endX - startX;
+ var scrollTotalWidth = eachSpacing * (xAxisPoints.length - 1);
+ scrollTotalWidth += (opts.extra.mount.widthRatio - 1)*eachSpacing;
+ var scrollWidth = scrollScreenWidth * scrollScreenWidth / scrollTotalWidth;
+ var scrollLeft = 0;
+ if (opts._scrollDistance_) {
+ scrollLeft = -opts._scrollDistance_ * (scrollScreenWidth) / scrollTotalWidth;
+ context.setLineCap('round');
+ context.setLineWidth(6 * opts.pix);
+ context.setStrokeStyle(opts.xAxis.scrollBackgroundColor || "#EFEBEF");
+ context.moveTo(startX, scrollY);
+ context.lineTo(endX, scrollY);
+ context.setStrokeStyle(opts.xAxis.scrollColor || "#A6A6A6");
+ context.moveTo(startX + scrollLeft, scrollY);
+ context.lineTo(startX + scrollLeft + scrollWidth, scrollY);
+ context.setLineCap('butt');
+ if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
+ //绘制X轴刻度线
+ if (opts.xAxis.calibration === true) {
+ context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc");
+ context.moveTo(item - eachSpacing / 2, startY);
+ context.lineTo(item - eachSpacing / 2, startY + 3 * opts.pix);
+ //绘制X轴网格
+ if (opts.xAxis.disableGrid !== true) {
+ if (opts.xAxis.gridType == 'dash') {
+ context.setLineDash([opts.xAxis.dashLength * opts.pix, opts.xAxis.dashLength * opts.pix]);
+ opts.xAxis.gridEval = opts.xAxis.gridEval || 1;
+ if (index % opts.xAxis.gridEval == 0) {
+ context.moveTo(item, startY);
+ context.lineTo(item, endY);
+ //绘制X轴文案
+ if (opts.xAxis.disabled !== true) {
+ // 对X轴列表做抽稀处理
+ //默认全部显示X轴标签
+ let maxXAxisListLength = categories.length;
+ //如果设置了X轴单屏数量
+ if (opts.xAxis.labelCount) {
+ //如果设置X轴密度
+ if (opts.xAxis.itemCount) {
+ maxXAxisListLength = Math.ceil(categories.length / opts.xAxis.itemCount * opts.xAxis.labelCount);
+ maxXAxisListLength = opts.xAxis.labelCount;
+ maxXAxisListLength -= 1;
+ let ratio = Math.ceil(categories.length / maxXAxisListLength);
+ let newCategories = [];
+ let cgLength = categories.length;
+ for (let i = 0; i < cgLength; i++) {
+ if (i % ratio !== 0) {
+ newCategories.push("");
+ newCategories.push(categories[i]);
+ newCategories[cgLength - 1] = categories[cgLength - 1];
+ var xAxisFontSize = opts.xAxis.fontSize * opts.pix || config.fontSize;
+ if (config._xAxisTextAngle_ === 0) {
+ newCategories.forEach(function(item, index) {
+ var offset = -measureText(String(xitem), xAxisFontSize, context) / 2;
+ offset += eachSpacing / 2;
+ var scrollHeight = 0;
+ if (opts.xAxis.scrollShow) {
+ scrollHeight = 6 * opts.pix;
+ // 如果在主视图区域内
+ var _scrollDistance_ = opts._scrollDistance_ || 0;
+ var truePoints = boundaryGap == 'center' ? xAxisPoints[index] + eachSpacing / 2 : xAxisPoints[index];
+ if((truePoints - Math.abs(_scrollDistance_)) >= (opts.area[3] - 1) && (truePoints - Math.abs(_scrollDistance_)) <= (opts.width - opts.area[1] + 1)){
+ context.setFontSize(xAxisFontSize);
+ context.setFillStyle(opts.xAxis.fontColor || opts.fontColor);
+ context.fillText(String(xitem), xAxisPoints[index] + offset, startY + opts.xAxis.marginTop * opts.pix + (opts.xAxis.lineHeight - opts.xAxis.fontSize) * opts.pix / 2 + opts.xAxis.fontSize * opts.pix);
+ var xitem = opts.xAxis.formatter ? opts.xAxis.formatter(item) : item;
+ var textWidth = measureText(String(xitem), xAxisFontSize, context);
+ var offsetX = xAxisPoints[index];
+ offsetX = xAxisPoints[index] + eachSpacing / 2;
+ var offsetY = startY + opts.xAxis.marginTop * opts.pix + xAxisFontSize - xAxisFontSize * Math.abs(Math.sin(config._xAxisTextAngle_));
+ if(opts.xAxis.rotateAngle < 0){
+ offsetX -= xAxisFontSize / 2;
+ textWidth = 0;
+ offsetX += xAxisFontSize / 2;
+ textWidth = -textWidth;
+ context.translate(offsetX, offsetY);
+ context.rotate(-1 * config._xAxisTextAngle_);
+ context.fillText(String(xitem), textWidth , 0 );
+ //画X轴标题
+ if (opts.xAxis.title) {
+ context.setFontSize(opts.xAxis.titleFontSize * opts.pix);
+ context.setFillStyle(opts.xAxis.titleFontColor);
+ context.fillText(String(opts.xAxis.title), opts.width - opts.area[1] + opts.xAxis.titleOffsetX * opts.pix,opts.height - opts.area[2] + opts.xAxis.marginTop * opts.pix + (opts.xAxis.lineHeight - opts.xAxis.titleFontSize) * opts.pix / 2 + (opts.xAxis.titleFontSize + opts.xAxis.titleOffsetY) * opts.pix);
+ //绘制X轴轴线
+ if (opts.xAxis.axisLine) {
+ context.setStrokeStyle(opts.xAxis.axisLineColor);
+ context.moveTo(startX, opts.height - opts.area[2]);
+ context.lineTo(endX, opts.height - opts.area[2]);
+function drawYAxisGrid(categories, opts, config, context) {
+ if (opts.yAxis.disableGrid === true) {
+ let eachSpacing = spacingValid / opts.yAxis.splitNumber;
+ let xAxisPoints = opts.chartData.xAxisData.xAxisPoints,
+ xAxiseachSpacing = opts.chartData.xAxisData.eachSpacing;
+ let TotalWidth = xAxiseachSpacing * (xAxisPoints.length - 1);
+ if(opts.type == 'mount' && opts.extra && opts.extra.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1 ){
+ TotalWidth += (opts.extra.mount.widthRatio - 1) * xAxiseachSpacing;
+ let endX = startX + TotalWidth;
+ let points = [];
+ let startY = 1
+ if (opts.xAxis.axisLine === false) {
+ startY = 0
+ for (let i = startY; i < opts.yAxis.splitNumber + 1; i++) {
+ points.push(opts.height - opts.area[2] - eachSpacing * i);
+ if (opts.yAxis.gridType == 'dash') {
+ context.setLineDash([opts.yAxis.dashLength * opts.pix, opts.yAxis.dashLength * opts.pix]);
+ context.setStrokeStyle(opts.yAxis.gridColor);
+ context.moveTo(startX, item);
+ context.lineTo(endX, item);
+function drawYAxis(series, opts, config, context) {
+ var spacingValid = opts.height - opts.area[0] - opts.area[2];
+ var eachSpacing = spacingValid / opts.yAxis.splitNumber;
+ // set YAxis background
+ if (opts.enableScroll == true && opts.xAxis.scrollPosition && opts.xAxis.scrollPosition !== 'left') {
+ context.fillRect(0, 0, startX, endY + 2 * opts.pix);
+ if (opts.enableScroll == true && opts.xAxis.scrollPosition && opts.xAxis.scrollPosition !== 'right') {
+ context.fillRect(endX, 0, opts.width, endY + 2 * opts.pix);
+ let tStartCenter = opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2;
+ if (opts.yAxis.data) {
+ for (let i = 0; i < opts.yAxis.data.length; i++) {
+ for (let i = 0; i <= yData.categories.length; i++) {
+ points.push(opts.area[0] + spacingValid / yData.categories.length / 2 + spacingValid / yData.categories.length * i);
+ for (let i = 0; i <= opts.yAxis.splitNumber; i++) {
+ points.push(opts.area[0] + eachSpacing * i);
+ if (yData.disabled !== true) {
+ let rangesFormat = opts.chartData.yAxisData.rangesFormat[i];
+ let yAxisFontSize = yData.fontSize ? yData.fontSize * opts.pix : config.fontSize;
+ let yAxisWidth = opts.chartData.yAxisData.yAxisWidth[i];
+ let textAlign = yData.textAlign || "right";
+ //画Y轴刻度及文案
+ rangesFormat.forEach(function(item, index) {
+ var pos = points[index];
+ context.setFontSize(yAxisFontSize);
+ context.setStrokeStyle(yData.axisLineColor || '#cccccc');
+ context.setFillStyle(yData.fontColor || opts.fontColor);
+ let tmpstrat = 0;
+ let gapwidth = 4 * opts.pix;
+ if (yAxisWidth.position == 'left') {
+ //画刻度线
+ if (yData.calibration == true) {
+ context.moveTo(tStartLeft, pos);
+ context.lineTo(tStartLeft - 3 * opts.pix, pos);
+ gapwidth += 3 * opts.pix;
+ //画文字
+ switch (textAlign) {
+ case "left":
+ tmpstrat = tStartLeft - yAxisWidth.width
+ case "right":
+ context.setTextAlign('right');
+ tmpstrat = tStartLeft - gapwidth
+ tmpstrat = tStartLeft - yAxisWidth.width / 2
+ context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
+ } else if (yAxisWidth.position == 'right') {
+ context.moveTo(tStartRight, pos);
+ context.lineTo(tStartRight + 3 * opts.pix, pos);
+ tmpstrat = tStartRight + gapwidth
+ tmpstrat = tStartRight + yAxisWidth.width
+ tmpstrat = tStartRight + yAxisWidth.width / 2
+ } else if (yAxisWidth.position == 'center') {
+ context.moveTo(tStartCenter, pos);
+ context.lineTo(tStartCenter - 3 * opts.pix, pos);
+ tmpstrat = tStartCenter - yAxisWidth.width
+ tmpstrat = tStartCenter - gapwidth
+ tmpstrat = tStartCenter - yAxisWidth.width / 2
+ //画Y轴轴线
+ if (yData.axisLine !== false) {
+ context.moveTo(tStartLeft, opts.height - opts.area[2]);
+ context.lineTo(tStartLeft, opts.area[0]);
+ context.moveTo(tStartRight, opts.height - opts.area[2]);
+ context.lineTo(tStartRight, opts.area[0]);
+ context.moveTo(tStartCenter, opts.height - opts.area[2]);
+ context.lineTo(tStartCenter, opts.area[0]);
+ //画Y轴标题
+ if (opts.yAxis.showTitle) {
+ let titleFontSize = yData.titleFontSize * opts.pix || config.fontSize;
+ let title = yData.title;
+ context.setFontSize(titleFontSize);
+ context.setFillStyle(yData.titleFontColor || opts.fontColor);
+ context.fillText(title, tStartLeft - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
+ context.fillText(title, tStartRight - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
+ context.fillText(title, tStartCenter - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area[0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
+ tStartLeft -= (yAxisWidth.width + opts.yAxis.padding * opts.pix);
+ tStartRight += yAxisWidth.width + opts.yAxis.padding * opts.pix;
+function drawLegend(series, opts, config, context, chartData) {
+ let legendData = chartData.legendData;
+ let legendList = legendData.points;
+ let legendArea = legendData.area;
+ let fontSize = opts.legend.fontSize * opts.pix;
+ let itemGap = opts.legend.itemGap * opts.pix;
+ //画背景及边框
+ context.setLineWidth(opts.legend.borderWidth * opts.pix);
+ context.setStrokeStyle(opts.legend.borderColor);
+ context.setFillStyle(opts.legend.backgroundColor);
+ context.moveTo(legendArea.start.x, legendArea.start.y);
+ context.rect(legendArea.start.x, legendArea.start.y, legendArea.width, legendArea.height);
+ legendList.forEach(function(itemList, listIndex) {
+ let width = 0;
+ let height = 0;
+ width = legendData.widthArr[listIndex];
+ height = legendData.heightArr[listIndex];
+ let startX = 0;
+ let startY = 0;
+ startX = legendArea.start.x + padding;
+ startX = legendArea.start.x + legendArea.width - width;
+ startX = legendArea.start.x + (legendArea.width - width) / 2;
+ startY = legendArea.start.y + padding + listIndex * lineHeight;
+ if (listIndex == 0) {
+ width = 0;
+ width = legendData.widthArr[listIndex - 1];
+ startX = legendArea.start.x + padding + width;
+ startY = legendArea.start.y + padding + (legendArea.height - height) / 2;
+ for (let i = 0; i < itemList.length; i++) {
+ let item = itemList[i];
+ item.area = [0, 0, 0, 0];
+ item.area[0] = startX;
+ item.area[1] = startY;
+ item.area[3] = startY + lineHeight;
+ context.setStrokeStyle(item.show ? item.color : opts.legend.hiddenColor);
+ context.setFillStyle(item.show ? item.color : opts.legend.hiddenColor);
+ context.moveTo(startX, startY + 0.5 * lineHeight - 2 * opts.pix);
+ context.fillRect(startX, startY + 0.5 * lineHeight - 2 * opts.pix, 15 * opts.pix, 4 * opts.pix);
+ context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
+ context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
+ context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
+ context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
+ context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight);
+ context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
+ context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight);
+ context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight);
+ context.arc(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight, 5 * opts.pix, 0, 2 * Math.PI);
+ context.moveTo(startX, startY + 0.5 * lineHeight - 5 * opts.pix);
+ context.fillRect(startX, startY + 0.5 * lineHeight - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix);
+ context.moveTo(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
+ context.fillRect(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix, 10 * opts.pix, 10 * opts.pix);
+ case 'none':
+ startX += shapeWidth + shapeRight;
+ let fontTrans = 0.5 * lineHeight + 0.5 * fontSize - 2;
+ context.setFillStyle(item.show ? opts.legend.fontColor : opts.legend.hiddenColor);
+ context.fillText(legendText, startX, startY + fontTrans);
+ startX += measureText(legendText, fontSize, context) + itemGap;
+ item.area[2] = startX;
+ item.area[2] = startX + measureText(legendText, fontSize, context) + itemGap;;
+ startX -= shapeWidth + shapeRight;
+ startY += lineHeight;
+function drawPieDataPoints(series, opts, config, context) {
+ var pieOption = assign({}, {
+ activeOpacity: 0.5,
+ activeRadius: 10,
+ offsetAngle: 0,
+ labelWidth: 15,
+ ringWidth: 30,
+ customRadius: 0,
+ border: false,
+ borderWidth: 2,
+ borderColor: '#FFFFFF',
+ centerColor: '#FFFFFF',
+ }, opts.type == "pie" ? opts.extra.pie : opts.extra.ring);
+ var centerPosition = {
+ x: opts.area[3] + (opts.width - opts.area[1] - opts.area[3]) / 2,
+ y: opts.area[0] + (opts.height - opts.area[0] - opts.area[2]) / 2
+ if (config.pieChartLinePadding == 0) {
+ config.pieChartLinePadding = pieOption.activeRadius * opts.pix;
+ var radius = Math.min((opts.width - opts.area[1] - opts.area[3]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding - config._pieTextMaxLength_, (opts.height - opts.area[0] - opts.area[2]) / 2 - config.pieChartLinePadding - config.pieChartTextPadding);
+ if (pieOption.customRadius > 0) {
+ radius = pieOption.customRadius * opts.pix;
+ series = getPieDataPoints(series, radius, process);
+ var activeRadius = pieOption.activeRadius * opts.pix;
+ pieOption.customColor = fillCustomColor(pieOption.linearType, pieOption.customColor, series, config);
+ series = series.map(function(eachSeries) {
+ eachSeries._start_ += (pieOption.offsetAngle) * Math.PI / 180;
+ return eachSeries;
+ if (opts.tooltip) {
+ if (opts.tooltip.index == seriesIndex) {
+ context.setFillStyle(hexToRgb(eachSeries.color, pieOption.activeOpacity || 0.5));
+ context.moveTo(centerPosition.x, centerPosition.y);
+ context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_ + activeRadius, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI);
+ context.setLineWidth(pieOption.borderWidth * opts.pix);
+ context.lineJoin = "round";
+ context.setStrokeStyle(pieOption.borderColor);
+ var fillcolor = eachSeries.color;
+ if (pieOption.linearType == 'custom') {
+ var grd;
+ if(context.createCircularGradient){
+ grd = context.createCircularGradient(centerPosition.x, centerPosition.y, eachSeries._radius_)
+ grd = context.createRadialGradient(centerPosition.x, centerPosition.y, 0,centerPosition.x, centerPosition.y, eachSeries._radius_)
+ grd.addColorStop(0, hexToRgb(pieOption.customColor[eachSeries.linearIndex], 1))
+ grd.addColorStop(1, hexToRgb(eachSeries.color, 1))
+ fillcolor = grd
+ context.setFillStyle(fillcolor);
+ context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI);
+ if (pieOption.border == true) {
+ if (opts.type === 'ring') {
+ var innerPieWidth = radius * 0.6;
+ if (typeof pieOption.ringWidth === 'number' && pieOption.ringWidth > 0) {
+ innerPieWidth = Math.max(0, radius - pieOption.ringWidth * opts.pix);
+ context.setFillStyle(pieOption.centerColor);
+ context.arc(centerPosition.x, centerPosition.y, innerPieWidth, 0, 2 * Math.PI);
+ drawPieText(series, opts, config, context, radius, centerPosition);
+ if (process === 1 && opts.type === 'ring') {
+ drawRingTitle(opts, config, context, centerPosition);
+ center: centerPosition,
+ series: series
+function drawRoseDataPoints(series, opts, config, context) {
+ var roseOption = assign({}, {
+ type: 'area',
+ }, opts.extra.rose);
+ config.pieChartLinePadding = roseOption.activeRadius * opts.pix;
+ var minRadius = roseOption.minRadius || radius * 0.5;
+ if(radius < minRadius){
+ radius = minRadius + 10;
+ series = getRoseDataPoints(series, roseOption.type, minRadius, radius, process);
+ var activeRadius = roseOption.activeRadius * opts.pix;
+ roseOption.customColor = fillCustomColor(roseOption.linearType, roseOption.customColor, series, config);
+ eachSeries._start_ += (roseOption.offsetAngle || 0) * Math.PI / 180;
+ context.setFillStyle(hexToRgb(eachSeries.color, roseOption.activeOpacity || 0.5));
+ context.arc(centerPosition.x, centerPosition.y, activeRadius + eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._rose_proportion_ * Math.PI);
+ context.setLineWidth(roseOption.borderWidth * opts.pix);
+ context.setStrokeStyle(roseOption.borderColor);
+ if (roseOption.linearType == 'custom') {
+ grd.addColorStop(0, hexToRgb(roseOption.customColor[eachSeries.linearIndex], 1))
+ context.arc(centerPosition.x, centerPosition.y, eachSeries._radius_, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._rose_proportion_ * Math.PI);
+ if (roseOption.border == true) {
+function drawArcbarDataPoints(series, opts, config, context) {
+ var arcbarOption = assign({}, {
+ startAngle: 0.75,
+ endAngle: 0.25,
+ type: 'default',
+ direction: 'cw',
+ lineCap: 'round',
+ width: 12 ,
+ gap: 2 ,
+ }, opts.extra.arcbar);
+ series = getArcbarDataPoints(series, arcbarOption, process);
+ var centerPosition;
+ if (arcbarOption.centerX || arcbarOption.centerY) {
+ centerPosition = {
+ x: arcbarOption.centerX ? arcbarOption.centerX : opts.width / 2,
+ y: arcbarOption.centerY ? arcbarOption.centerY : opts.height / 2
+ x: opts.width / 2,
+ y: opts.height / 2
+ var radius;
+ if (arcbarOption.radius) {
+ radius = arcbarOption.radius;
+ radius = Math.min(centerPosition.x, centerPosition.y);
+ radius -= 5 * opts.pix;
+ radius -= arcbarOption.width / 2;
+ arcbarOption.customColor = fillCustomColor(arcbarOption.linearType, arcbarOption.customColor, series, config);
+ let eachSeries = series[i];
+ //背景颜色
+ context.setLineWidth(arcbarOption.width * opts.pix);
+ context.setStrokeStyle(arcbarOption.backgroundColor || '#E9E9E9');
+ context.setLineCap(arcbarOption.lineCap);
+ if (arcbarOption.type == 'default') {
+ context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, arcbarOption.startAngle * Math.PI, arcbarOption.endAngle * Math.PI, arcbarOption.direction == 'ccw');
+ context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, 0, 2 * Math.PI, arcbarOption.direction == 'ccw');
+ //进度条
+ var fillColor = eachSeries.color
+ if(arcbarOption.linearType == 'custom'){
+ var grd = context.createLinearGradient(centerPosition.x - radius, centerPosition.y, centerPosition.x + radius, centerPosition.y);
+ grd.addColorStop(1, hexToRgb(arcbarOption.customColor[eachSeries.linearIndex], 1))
+ grd.addColorStop(0, hexToRgb(eachSeries.color, 1))
+ fillColor = grd;
+ context.setStrokeStyle(fillColor);
+ context.arc(centerPosition.x, centerPosition.y, radius - (arcbarOption.width * opts.pix + arcbarOption.gap * opts.pix) * i, arcbarOption.startAngle * Math.PI, eachSeries._proportion_ * Math.PI, arcbarOption.direction == 'ccw');
+function drawGaugeDataPoints(categories, series, opts, config, context) {
+ var gaugeOption = assign({}, {
+ width: 15,
+ labelOffset:13,
+ splitLine: {
+ fixRadius: 0,
+ splitNumber: 10,
+ color: '#FFFFFF',
+ childNumber: 5,
+ childWidth: 5
+ pointer: {
+ color: 'auto'
+ }, opts.extra.gauge);
+ if (gaugeOption.oldAngle == undefined) {
+ gaugeOption.oldAngle = gaugeOption.startAngle;
+ if (gaugeOption.oldData == undefined) {
+ gaugeOption.oldData = 0;
+ categories = getGaugeAxisPoints(categories, gaugeOption.startAngle, gaugeOption.endAngle);
+ var radius = Math.min(centerPosition.x, centerPosition.y);
+ radius -= gaugeOption.width / 2;
+ var innerRadius = radius - gaugeOption.width;
+ var totalAngle = 0;
+ //判断仪表盘的样式:default百度样式,progress新样式
+ if (gaugeOption.type == 'progress') {
+ //## 第一步画中心圆形背景和进度条背景
+ //中心圆形背景
+ var pieRadius = radius - gaugeOption.width * 3;
+ let gradient = context.createLinearGradient(centerPosition.x, centerPosition.y - pieRadius, centerPosition.x, centerPosition.y + pieRadius);
+ //配置渐变填充(起点:中心点向上减半径;结束点中心点向下加半径)
+ gradient.addColorStop('0', hexToRgb(series[0].color, 0.3));
+ context.arc(centerPosition.x, centerPosition.y, pieRadius, 0, 2 * Math.PI, false);
+ //画进度条背景
+ context.setLineWidth(gaugeOption.width);
+ context.setStrokeStyle(hexToRgb(series[0].color, 0.3));
+ context.arc(centerPosition.x, centerPosition.y, innerRadius, gaugeOption.startAngle * Math.PI, gaugeOption.endAngle * Math.PI, false);
+ //## 第二步画刻度线
+ let childAngle = totalAngle / gaugeOption.splitLine.splitNumber / gaugeOption.splitLine.childNumber;
+ let startX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius;
+ let endX = -radius - gaugeOption.width - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.width;
+ context.translate(centerPosition.x, centerPosition.y);
+ context.rotate((gaugeOption.startAngle - 1) * Math.PI);
+ let len = gaugeOption.splitLine.splitNumber * gaugeOption.splitLine.childNumber + 1;
+ let proc = series[0].data * process;
+ for (let i = 0; i < len; i++) {
+ //刻度线随进度变色
+ if (proc > (i / len)) {
+ context.setStrokeStyle(hexToRgb(series[0].color, 1));
+ context.setLineWidth(3 * opts.pix);
+ context.moveTo(startX, 0);
+ context.lineTo(endX, 0);
+ context.rotate(childAngle * Math.PI);
+ //## 第三步画进度条
+ series = getGaugeArcbarDataPoints(series, gaugeOption, process);
+ context.setStrokeStyle(series[0].color);
+ context.arc(centerPosition.x, centerPosition.y, innerRadius, gaugeOption.startAngle * Math.PI, series[0]._proportion_ * Math.PI, false);
+ //## 第四步画指针
+ let pointerRadius = radius - gaugeOption.width * 2.5;
+ context.rotate((series[0]._proportion_ - 1) * Math.PI);
+ context.setLineWidth(gaugeOption.width / 3);
+ let gradient3 = context.createLinearGradient(0, -pointerRadius * 0.6, 0, pointerRadius * 0.6);
+ gradient3.addColorStop('0', hexToRgb('#FFFFFF', 0));
+ gradient3.addColorStop('0.5', hexToRgb(series[0].color, 1));
+ gradient3.addColorStop('1.0', hexToRgb('#FFFFFF', 0));
+ context.setStrokeStyle(gradient3);
+ context.arc(0, 0, pointerRadius, 0.85 * Math.PI, 1.15 * Math.PI, false);
+ context.setFillStyle(series[0].color);
+ context.moveTo(-pointerRadius - gaugeOption.width / 3 / 2, -4);
+ context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2 - 4, 0);
+ context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2, 4);
+ context.lineTo(-pointerRadius - gaugeOption.width / 3 / 2, -4);
+ //default百度样式
+ //画背景
+ let eachCategories = categories[i];
+ context.setStrokeStyle(eachCategories.color);
+ context.arc(centerPosition.x, centerPosition.y, radius, eachCategories._startAngle_ * Math.PI, eachCategories._endAngle_ * Math.PI, false);
+ let endX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.width;
+ let childendX = -radius - gaugeOption.width * 0.5 - gaugeOption.splitLine.fixRadius + gaugeOption.splitLine.childWidth;
+ context.setStrokeStyle(gaugeOption.splitLine.color);
+ context.rotate(splitAngle * Math.PI);
+ for (let i = 0; i < gaugeOption.splitLine.splitNumber * gaugeOption.splitLine.childNumber + 1; i++) {
+ context.lineTo(childendX, 0);
+ //画指针
+ series = getGaugeDataPoints(series, categories, gaugeOption, process);
+ context.rotate((eachSeries._proportion_ - 1) * Math.PI);
+ context.moveTo(gaugeOption.pointer.width, 0);
+ context.lineTo(0, -gaugeOption.pointer.width / 2);
+ context.lineTo(-innerRadius, 0);
+ context.lineTo(0, gaugeOption.pointer.width / 2);
+ context.lineTo(gaugeOption.pointer.width, 0);
+ context.setFillStyle('#FFFFFF');
+ context.arc(0, 0, gaugeOption.pointer.width / 6, 0, 2 * Math.PI, false);
+ if (opts.dataLabel !== false) {
+ drawGaugeLabel(gaugeOption, radius, centerPosition, opts, config, context);
+ //画仪表盘标题,副标题
+ if (process === 1 && opts.type === 'gauge') {
+ opts.extra.gauge.oldAngle = series[0]._proportion_;
+ opts.extra.gauge.oldData = series[0].data;
+ innerRadius: innerRadius,
+ categories: categories,
+ totalAngle: totalAngle
+function drawRadarDataPoints(series, opts, config, context) {
+ var radarOption = assign({}, {
+ gridColor: '#cccccc',
+ gridType: 'radar',
+ gridEval:1,
+ axisLabel:false,
+ axisLabelTofix:0,
+ labelShow:true,
+ labelColor:'#666666',
+ labelPointShow:false,
+ labelPointRadius:3,
+ labelPointColor:'#cccccc',
+ gridCount: 3,
+ border:false,
+ borderWidth:2,
+ }, opts.extra.radar);
+ var coordinateAngle = getRadarCoordinateSeries(opts.categories.length);
+ var xr = (opts.width - opts.area[1] - opts.area[3]) / 2
+ var yr = (opts.height - opts.area[0] - opts.area[2]) / 2
+ var radius = Math.min(xr - (getMaxTextListLength(opts.categories, config.fontSize, context) + config.radarLabelTextMargin), yr - config.radarLabelTextMargin);
+ radius -= config.radarLabelTextMargin * opts.pix;
+ radius = radarOption.radius ? radarOption.radius : radius;
+ // 画分割线
+ context.setStrokeStyle(radarOption.gridColor);
+ coordinateAngle.forEach(function(angle,index) {
+ var pos = convertCoordinateOrigin(radius * Math.cos(angle), radius * Math.sin(angle), centerPosition);
+ if (index % radarOption.gridEval == 0) {
+ context.lineTo(pos.x, pos.y);
+ // 画背景网格
+ var _loop = function _loop(i) {
+ var startPos = {};
+ if (radarOption.gridType == 'radar') {
+ coordinateAngle.forEach(function(angle, index) {
+ var pos = convertCoordinateOrigin(radius / radarOption.gridCount * i * Math.cos(angle), radius /
+ radarOption.gridCount * i * Math.sin(angle), centerPosition);
+ if (index === 0) {
+ startPos = pos;
+ context.moveTo(pos.x, pos.y);
+ context.lineTo(startPos.x, startPos.y);
+ var pos = convertCoordinateOrigin(radius / radarOption.gridCount * i * Math.cos(1.5), radius / radarOption.gridCount * i * Math.sin(1.5), centerPosition);
+ context.arc(centerPosition.x, centerPosition.y, centerPosition.y - pos.y, 0, 2 * Math.PI, false);
+ for (var i = 1; i <= radarOption.gridCount; i++) {
+ _loop(i);
+ radarOption.customColor = fillCustomColor(radarOption.linearType, radarOption.customColor, series, config);
+ var radarDataPoints = getRadarDataPoints(coordinateAngle, centerPosition, radius, series, opts, process);
+ radarDataPoints.forEach(function(eachSeries, seriesIndex) {
+ context.setLineWidth(radarOption.borderWidth * opts.pix);
+ var fillcolor = hexToRgb(eachSeries.color, radarOption.opacity);
+ if (radarOption.linearType == 'custom') {
+ grd = context.createCircularGradient(centerPosition.x, centerPosition.y, radius)
+ grd = context.createRadialGradient(centerPosition.x, centerPosition.y, 0,centerPosition.x, centerPosition.y, radius)
+ grd.addColorStop(0, hexToRgb(radarOption.customColor[series[seriesIndex].linearIndex], radarOption.opacity))
+ grd.addColorStop(1, hexToRgb(eachSeries.color, radarOption.opacity))
+ eachSeries.data.forEach(function(item, index) {
+ context.moveTo(item.position.x, item.position.y);
+ context.lineTo(item.position.x, item.position.y);
+ if(radarOption.border === true){
+ var points = eachSeries.data.map(function(item) {
+ return item.position;
+ // 画刻度值
+ if(radarOption.axisLabel === true){
+ const maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series)));
+ const stepLength = radius / radarOption.gridCount;
+ const fontSize = opts.fontSize * opts.pix;
+ context.setFillStyle(opts.fontColor);
+ for (var i = 0; i < radarOption.gridCount + 1; i++) {
+ let label = i * maxData / radarOption.gridCount;
+ label = label.toFixed(radarOption.axisLabelTofix);
+ context.fillText(String(label), centerPosition.x + 3 * opts.pix, centerPosition.y - i * stepLength + fontSize / 2);
+ // draw label text
+ drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context);
+ // draw dataLabel
+ context.setFillStyle(eachSeries.textColor || opts.fontColor);
+ //如果是中心点垂直的上下点位
+ if(Math.abs(item.position.x - centerPosition.x)<2){
+ //如果在上面
+ if(item.position.y < centerPosition.y){
+ context.fillText(item.value, item.position.x, item.position.y - 4);
+ context.fillText(item.value, item.position.x, item.position.y + fontSize + 2);
+ //如果在左侧
+ if(item.position.x < centerPosition.x){
+ context.fillText(item.value, item.position.x - 4, item.position.y + fontSize / 2 - 2);
+ context.fillText(item.value, item.position.x + 4, item.position.y + fontSize / 2 - 2);
+ angleList: coordinateAngle
+// 经纬度转墨卡托
+function lonlat2mercator(longitude, latitude) {
+ var mercator = Array(2);
+ var x = longitude * 20037508.34 / 180;
+ var y = Math.log(Math.tan((90 + latitude) * Math.PI / 360)) / (Math.PI / 180);
+ y = y * 20037508.34 / 180;
+ mercator[0] = x;
+ mercator[1] = y;
+ return mercator;
+// 墨卡托转经纬度
+function mercator2lonlat(longitude, latitude) {
+ var lonlat = Array(2)
+ var x = longitude / 20037508.34 * 180;
+ var y = latitude / 20037508.34 * 180;
+ y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);
+ lonlat[0] = x;
+ lonlat[1] = y;
+ return lonlat;
+function getBoundingBox(data) {
+ var bounds = {},coords;
+ bounds.xMin = 180;
+ bounds.xMax = 0;
+ bounds.yMin = 90;
+ bounds.yMax = 0
+ for (var i = 0; i < data.length; i++) {
+ var coorda = data[i].geometry.coordinates
+ for (var k = 0; k < coorda.length; k++) {
+ coords = coorda[k];
+ if (coords.length == 1) {
+ coords = coords[0]
+ for (var j = 0; j < coords.length; j++) {
+ var longitude = coords[j][0];
+ var latitude = coords[j][1];
+ var point = {
+ x: longitude,
+ y: latitude
+ bounds.xMin = bounds.xMin < point.x ? bounds.xMin : point.x;
+ bounds.xMax = bounds.xMax > point.x ? bounds.xMax : point.x;
+ bounds.yMin = bounds.yMin < point.y ? bounds.yMin : point.y;
+ bounds.yMax = bounds.yMax > point.y ? bounds.yMax : point.y;
+ return bounds;
+function coordinateToPoint(latitude, longitude, bounds, scale, xoffset, yoffset) {
+ x: (longitude - bounds.xMin) * scale + xoffset,
+ y: (bounds.yMax - latitude) * scale + yoffset
+function pointToCoordinate(pointY, pointX, bounds, scale, xoffset, yoffset) {
+ x: (pointX - xoffset) / scale + bounds.xMin,
+ y: bounds.yMax - (pointY - yoffset) / scale
+function isRayIntersectsSegment(poi, s_poi, e_poi) {
+ if (s_poi[1] == e_poi[1]) {
+ if (s_poi[1] > poi[1] && e_poi[1] > poi[1]) {
+ if (s_poi[1] < poi[1] && e_poi[1] < poi[1]) {
+ if (s_poi[1] == poi[1] && e_poi[1] > poi[1]) {
+ if (e_poi[1] == poi[1] && s_poi[1] > poi[1]) {
+ if (s_poi[0] < poi[0] && e_poi[1] < poi[1]) {
+ let xseg = e_poi[0] - (e_poi[0] - s_poi[0]) * (e_poi[1] - poi[1]) / (e_poi[1] - s_poi[1]);
+ if (xseg < poi[0]) {
+ return true;
+function isPoiWithinPoly(poi, poly, mercator) {
+ let sinsc = 0;
+ for (let i = 0; i < poly.length; i++) {
+ let epoly = poly[i][0];
+ if (poly.length == 1) {
+ epoly = poly[i][0]
+ for (let j = 0; j < epoly.length - 1; j++) {
+ let s_poi = epoly[j];
+ let e_poi = epoly[j + 1];
+ if (mercator) {
+ s_poi = lonlat2mercator(epoly[j][0], epoly[j][1]);
+ e_poi = lonlat2mercator(epoly[j + 1][0], epoly[j + 1][1]);
+ if (isRayIntersectsSegment(poi, s_poi, e_poi)) {
+ sinsc += 1;
+ if (sinsc % 2 == 1) {
+function drawMapDataPoints(series, opts, config, context) {
+ var mapOption = assign({}, {
+ border: true,
+ mercator: false,
+ active:true,
+ borderColor: '#666666',
+ fillOpacity: 0.6,
+ activeBorderColor: '#f04864',
+ activeFillColor: '#facc14',
+ activeFillOpacity: 1
+ }, opts.extra.map);
+ var coords, point;
+ var data = series;
+ var bounds = getBoundingBox(data);
+ if (mapOption.mercator) {
+ var max = lonlat2mercator(bounds.xMax, bounds.yMax)
+ var min = lonlat2mercator(bounds.xMin, bounds.yMin)
+ bounds.xMax = max[0]
+ bounds.yMax = max[1]
+ bounds.xMin = min[0]
+ bounds.yMin = min[1]
+ var xScale = opts.width / Math.abs(bounds.xMax - bounds.xMin);
+ var yScale = opts.height / Math.abs(bounds.yMax - bounds.yMin);
+ var scale = xScale < yScale ? xScale : yScale;
+ var xoffset = opts.width / 2 - Math.abs(bounds.xMax - bounds.xMin) / 2 * scale;
+ var yoffset = opts.height / 2 - Math.abs(bounds.yMax - bounds.yMin) / 2 * scale;
+ context.setLineWidth(mapOption.borderWidth * opts.pix);
+ context.setStrokeStyle(mapOption.borderColor);
+ context.setFillStyle(hexToRgb(series[i].color, series[i].fillOpacity||mapOption.fillOpacity));
+ if (mapOption.active == true && opts.tooltip) {
+ if (opts.tooltip.index == i) {
+ context.setStrokeStyle(mapOption.activeBorderColor);
+ context.setFillStyle(hexToRgb(mapOption.activeFillColor, mapOption.activeFillOpacity));
+ var gaosi = Array(2);
+ gaosi = lonlat2mercator(coords[j][0], coords[j][1])
+ gaosi = coords[j]
+ point = coordinateToPoint(gaosi[1], gaosi[0], bounds, scale, xoffset, yoffset)
+ if (j === 0) {
+ context.moveTo(point.x, point.y);
+ context.lineTo(point.x, point.y);
+ if (mapOption.border == true) {
+ if (opts.dataLabel == true) {
+ var centerPoint = data[i].properties.centroid;
+ if (centerPoint) {
+ centerPoint = lonlat2mercator(data[i].properties.centroid[0], data[i].properties.centroid[1])
+ point = coordinateToPoint(centerPoint[1], centerPoint[0], bounds, scale, xoffset, yoffset);
+ let fontSize = data[i].textSize * opts.pix || config.fontSize;
+ let fontColor = data[i].textColor || opts.fontColor;
+ if(mapOption.active && mapOption.activeTextColor && opts.tooltip && opts.tooltip.index == i){
+ fontColor = mapOption.activeTextColor;
+ let text = data[i].properties.name;
+ context.setFontSize(fontSize)
+ context.setFillStyle(fontColor)
+ context.fillText(text, point.x - measureText(text, fontSize, context) / 2, point.y + fontSize / 2);
+ opts.chartData.mapData = {
+ bounds: bounds,
+ scale: scale,
+ xoffset: xoffset,
+ yoffset: yoffset,
+ mercator: mapOption.mercator
+ drawToolTipBridge(opts, config, context, 1);
+ context.draw();
+function normalInt(min, max, iter) {
+ iter = iter == 0 ? 1 : iter;
+ var arr = [];
+ for (var i = 0; i < iter; i++) {
+ arr[i] = Math.random();
+ return Math.floor(arr.reduce(function(i, j) {
+ return i + j
+ }) / iter * (max - min)) + min;
+function collisionNew(area, points, width, height) {
+ var isIn = false;
+ if (points[i].area) {
+ if (area[3] < points[i].area[1] || area[0] > points[i].area[2] || area[1] > points[i].area[3] || area[2] < points[i].area[0]) {
+ if (area[0] < 0 || area[1] < 0 || area[2] > width || area[3] > height) {
+ isIn = true;
+ isIn = false;
+ return isIn;
+function getWordCloudPoint(opts, type, context) {
+ let points = opts.series;
+ switch (type) {
+ case 'normal':
+ let text = points[i].name;
+ let tHeight = points[i].textSize * opts.pix;
+ let tWidth = measureText(text, tHeight, context);
+ let area;
+ let breaknum = 0;
+ while (true) {
+ breaknum++;
+ x = normalInt(-opts.width / 2, opts.width / 2, 5) - tWidth / 2;
+ y = normalInt(-opts.height / 2, opts.height / 2, 5) + tHeight / 2;
+ area = [x - 5 + opts.width / 2, y - 5 - tHeight + opts.height / 2, x + tWidth + 5 + opts.width / 2, y + 5 +
+ opts.height / 2
+ let isCollision = collisionNew(area, points, opts.width, opts.height);
+ if (!isCollision) break;
+ if (breaknum == 1000) {
+ area = [-100, -100, -100, -100];
+ points[i].area = area;
+ case 'vertical':
+ function Spin() {
+ //获取均匀随机值,是否旋转,旋转的概率为(1-0.5)
+ if (Math.random() > 0.7) {
+ return false
+ let isSpin = Spin();
+ let x, y, area, areav;
+ let isCollision;
+ if (isSpin) {
+ area = [y - 5 - tWidth + opts.width / 2, (-x - 5 + opts.height / 2), y + 5 + opts.width / 2, (-x + tHeight + 5 + opts.height / 2)];
+ areav = [opts.width - (opts.width / 2 - opts.height / 2) - (-x + tHeight + 5 + opts.height / 2) - 5, (opts.height / 2 - opts.width / 2) + (y - 5 - tWidth + opts.width / 2) - 5, opts.width - (opts.width / 2 - opts.height / 2) - (-x + tHeight + 5 + opts.height / 2) + tHeight, (opts.height / 2 - opts.width / 2) + (y - 5 - tWidth + opts.width / 2) + tWidth + 5];
+ isCollision = collisionNew(areav, points, opts.height, opts.width);
+ area = [x - 5 + opts.width / 2, y - 5 - tHeight + opts.height / 2, x + tWidth + 5 + opts.width / 2, y + 5 + opts.height / 2];
+ isCollision = collisionNew(area, points, opts.width, opts.height);
+ area = [-1000, -1000, -1000, -1000];
+ points[i].area = areav;
+ points[i].areav = area;
+ points[i].rotate = isSpin;
+function drawWordCloudDataPoints(series, opts, config, context) {
+ let wordOption = assign({}, {
+ type: 'normal',
+ autoColors: true
+ }, opts.extra.word);
+ if (!opts.chartData.wordCloudData) {
+ opts.chartData.wordCloudData = getWordCloudPoint(opts, wordOption.type, context);
+ context.rect(0, 0, opts.width, opts.height);
+ let points = opts.chartData.wordCloudData;
+ context.translate(opts.width / 2, opts.height / 2);
+ if (points[i].rotate) {
+ context.setStrokeStyle(points[i].color);
+ context.setFillStyle(points[i].color);
+ context.setFontSize(tHeight);
+ if (points[i].areav[0] > 0) {
+ context.strokeText(text, (points[i].areav[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].areav[1] + 5 + tHeight - opts.height / 2) * process);
+ context.fillText(text, (points[i].areav[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].areav[1] + 5 + tHeight - opts.height / 2) * process);
+ if (points[i].area[0] > 0) {
+ context.strokeText(text, (points[i].area[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].area[1] + 5 + tHeight - opts.height / 2) * process);
+ context.fillText(text, (points[i].area[0] + 5 - opts.width / 2) * process - tWidth * (1 - process) / 2, (points[i].area[1] + 5 + tHeight - opts.height / 2) * process);
+function drawFunnelDataPoints(series, opts, config, context) {
+ let funnelOption = assign({}, {
+ type:'funnel',
+ activeWidth: 10,
+ activeOpacity: 0.3,
+ fillOpacity: 1,
+ minSize: 0,
+ labelAlign: 'right',
+ }, opts.extra.funnel);
+ let eachSpacing = (opts.height - opts.area[0] - opts.area[2]) / series.length;
+ let centerPosition = {
+ y: opts.height - opts.area[2]
+ let activeWidth = funnelOption.activeWidth * opts.pix;
+ let radius = Math.min((opts.width - opts.area[1] - opts.area[3]) / 2 - activeWidth, (opts.height - opts.area[0] - opts.area[2]) / 2 - activeWidth);
+ let seriesNew = getFunnelDataPoints(series, radius, funnelOption, eachSpacing, process);
+ funnelOption.customColor = fillCustomColor(funnelOption.linearType, funnelOption.customColor, series, config);
+ if(funnelOption.type == 'pyramid'){
+ for (let i = 0; i < seriesNew.length; i++) {
+ if (i == seriesNew.length -1) {
+ context.setFillStyle(hexToRgb(seriesNew[i].color, funnelOption.activeOpacity));
+ context.moveTo(-activeWidth, -eachSpacing);
+ context.lineTo(-seriesNew[i].radius - activeWidth, 0);
+ context.lineTo(seriesNew[i].radius + activeWidth, 0);
+ context.lineTo(activeWidth, -eachSpacing);
+ context.lineTo(-activeWidth, -eachSpacing);
+ seriesNew[i].funnelArea = [centerPosition.x - seriesNew[i].radius, centerPosition.y - eachSpacing * (i + 1), centerPosition.x + seriesNew[i].radius, centerPosition.y - eachSpacing * i];
+ context.setLineWidth(funnelOption.borderWidth * opts.pix);
+ context.setStrokeStyle(funnelOption.borderColor);
+ var fillColor = hexToRgb(seriesNew[i].color, funnelOption.fillOpacity);
+ if (funnelOption.linearType == 'custom') {
+ var grd = context.createLinearGradient(seriesNew[i].radius, -eachSpacing, -seriesNew[i].radius, -eachSpacing);
+ grd.addColorStop(0, hexToRgb(seriesNew[i].color, funnelOption.fillOpacity));
+ grd.addColorStop(0.5, hexToRgb(funnelOption.customColor[seriesNew[i].linearIndex], funnelOption.fillOpacity));
+ grd.addColorStop(1, hexToRgb(seriesNew[i].color, funnelOption.fillOpacity));
+ context.moveTo(0, -eachSpacing);
+ context.lineTo(-seriesNew[i].radius, 0);
+ context.lineTo(seriesNew[i].radius, 0);
+ context.lineTo(0, -eachSpacing);
+ if (funnelOption.border == true) {
+ context.moveTo(0, 0);
+ context.lineTo(-seriesNew[i + 1].radius - activeWidth, -eachSpacing);
+ context.lineTo(seriesNew[i + 1].radius + activeWidth, -eachSpacing);
+ context.lineTo(0, 0);
+ context.lineTo(-seriesNew[i + 1].radius, -eachSpacing);
+ context.lineTo(seriesNew[i + 1].radius, -eachSpacing);
+ context.translate(0, -eachSpacing)
+ context.translate(0, - (seriesNew.length - 1) * eachSpacing);
+ if (i == seriesNew.length - 1) {
+ context.moveTo(-activeWidth - funnelOption.minSize/2, 0);
+ context.lineTo(-seriesNew[i].radius - activeWidth, -eachSpacing);
+ context.lineTo(seriesNew[i].radius + activeWidth, -eachSpacing);
+ context.lineTo(activeWidth + funnelOption.minSize/2, 0);
+ context.lineTo(-activeWidth - funnelOption.minSize/2, 0);
+ seriesNew[i].funnelArea = [centerPosition.x - seriesNew[i].radius, centerPosition.y - eachSpacing, centerPosition.x + seriesNew[i].radius, centerPosition.y ];
+ context.lineTo(-funnelOption.minSize/2, 0);
+ context.lineTo(-seriesNew[i].radius, -eachSpacing);
+ context.lineTo(seriesNew[i].radius, -eachSpacing);
+ context.lineTo(funnelOption.minSize/2, 0);
+ context.lineTo(-seriesNew[i + 1].radius - activeWidth, 0);
+ context.lineTo(seriesNew[i + 1].radius + activeWidth, 0);
+ seriesNew[i].funnelArea = [centerPosition.x - seriesNew[i].radius, centerPosition.y - eachSpacing * (seriesNew.length - i), centerPosition.x + seriesNew[i].radius, centerPosition.y - eachSpacing * (seriesNew.length - i - 1)];
+ context.lineTo(-seriesNew[i + 1].radius, 0);
+ context.lineTo(seriesNew[i + 1].radius, 0);
+ context.translate(0, eachSpacing)
+ drawFunnelText(seriesNew, opts, context, eachSpacing, funnelOption.labelAlign, activeWidth, centerPosition);
+ if (process === 1) {
+ drawFunnelCenterText(seriesNew, opts, context, eachSpacing, funnelOption.labelAlign, activeWidth, centerPosition);
+ series: seriesNew
+function drawFunnelText(series, opts, context, eachSpacing, labelAlign, activeWidth, centerPosition) {
+ if(item.labelShow === false){
+ let startX, endX, startY, fontSize;
+ let text = item.formatter ? item.formatter(item,i,series,opts) : util.toFixed(item._proportion_ * 100) + '%';
+ if (labelAlign == 'right') {
+ if (i == series.length -1) {
+ startX = (item.funnelArea[2] + centerPosition.x) / 2;
+ startX = (item.funnelArea[2] + series[i + 1].funnelArea[2]) / 2;
+ endX = startX + activeWidth * 2;
+ startY = item.funnelArea[1] + eachSpacing / 2;
+ fontSize = item.textSize * opts.pix || opts.fontSize * opts.pix;
+ context.lineTo(endX, startY);
+ context.moveTo(endX, startY);
+ context.arc(endX, startY, 2 * opts.pix, 0, 2 * Math.PI);
+ context.fillText(text, endX + 5, startY + fontSize / 2 - 2);
+ if (labelAlign == 'left') {
+ startX = (item.funnelArea[0] + centerPosition.x) / 2;
+ startX = (item.funnelArea[0] + series[i + 1].funnelArea[0]) / 2;
+ endX = startX - activeWidth * 2;
+ context.arc(endX, startY, 2, 0, 2 * Math.PI);
+ context.fillText(text, endX - 5 - measureText(text, fontSize, context), startY + fontSize / 2 - 2);
+function drawFunnelCenterText(series, opts, context, eachSpacing, labelAlign, activeWidth, centerPosition) {
+ let startY, fontSize;
+ if (item.centerText) {
+ fontSize = item.centerTextSize * opts.pix || opts.fontSize * opts.pix;
+ context.setFillStyle(item.centerTextColor || "#FFFFFF");
+ context.fillText(item.centerText, centerPosition.x - measureText(item.centerText, fontSize, context) / 2, startY + fontSize / 2 - 2);
+function drawCanvas(opts, context) {
+ context.translate(0, 0.5);
+var Timing = {
+ easeIn: function easeIn(pos) {
+ return Math.pow(pos, 3);
+ easeOut: function easeOut(pos) {
+ return Math.pow(pos - 1, 3) + 1;
+ easeInOut: function easeInOut(pos) {
+ if ((pos /= 0.5) < 1) {
+ return 0.5 * Math.pow(pos, 3);
+ return 0.5 * (Math.pow(pos - 2, 3) + 2);
+ linear: function linear(pos) {
+ return pos;
+function Animation(opts) {
+ this.isStop = false;
+ opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration;
+ opts.timing = opts.timing || 'easeInOut';
+ var delay = 17;
+ function createAnimationFrame() {
+ if (typeof setTimeout !== 'undefined') {
+ return function(step, delay) {
+ var timeStamp = +new Date();
+ step(timeStamp);
+ }, delay);
+ } else if (typeof requestAnimationFrame !== 'undefined') {
+ return requestAnimationFrame;
+ return function(step) {
+ step(null);
+ var animationFrame = createAnimationFrame();
+ var startTimeStamp = null;
+ var _step = function step(timestamp) {
+ if (timestamp === null || this.isStop === true) {
+ opts.onProcess && opts.onProcess(1);
+ opts.onAnimationFinish && opts.onAnimationFinish();
+ if (startTimeStamp === null) {
+ startTimeStamp = timestamp;
+ if (timestamp - startTimeStamp < opts.duration) {
+ var process = (timestamp - startTimeStamp) / opts.duration;
+ var timingFunction = Timing[opts.timing];
+ process = timingFunction(process);
+ opts.onProcess && opts.onProcess(process);
+ animationFrame(_step, delay);
+ _step = _step.bind(this);
+Animation.prototype.stop = function() {
+ this.isStop = true;
+function drawCharts(type, opts, config, context) {
+ var _this = this;
+ var series = opts.series;
+ //兼容ECharts饼图类数据格式
+ if (type === 'pie' || type === 'ring' || type === 'mount' || type === 'rose' || type === 'funnel') {
+ series = fixPieSeries(series, opts, config);
+ var categories = opts.categories;
+ if (type === 'mount') {
+ categories = [];
+ if(series[j].show !== false) categories.push(series[j].name)
+ opts.categories = categories;
+ series = fillSeries(series, opts, config);
+ var duration = opts.animation ? opts.duration : 0;
+ _this.animationInstance && _this.animationInstance.stop();
+ var seriesMA = null;
+ if (type == 'candle') {
+ let average = assign({}, opts.extra.candle.average);
+ if (average.show) {
+ seriesMA = calCandleMA(average.day, average.name, average.color, series[0].data);
+ seriesMA = fillSeries(seriesMA, opts, config);
+ opts.seriesMA = seriesMA;
+ } else if (opts.seriesMA) {
+ seriesMA = opts.seriesMA = fillSeries(opts.seriesMA, opts, config);
+ seriesMA = series;
+ /* 过滤掉show=false的series */
+ opts._series_ = series = filterSeries(series);
+ //重新计算图表区域
+ opts.area = new Array(4);
+ //复位绘图区域
+ for (let j = 0; j < 4; j++) {
+ opts.area[j] = opts.padding[j] * opts.pix;
+ //通过计算三大区域:图例、X轴、Y轴的大小,确定绘图区域
+ var _calLegendData = calLegendData(seriesMA, opts, config, opts.chartData, context),
+ legendHeight = _calLegendData.area.wholeHeight,
+ legendWidth = _calLegendData.area.wholeWidth;
+ opts.area[0] += legendHeight;
+ opts.area[2] += legendHeight;
+ opts.area[3] += legendWidth;
+ opts.area[1] += legendWidth;
+ let _calYAxisData = {},
+ yAxisWidth = 0;
+ if (opts.type === 'line' || opts.type === 'column'|| opts.type === 'mount' || opts.type === 'area' || opts.type === 'mix' || opts.type === 'candle' || opts.type === 'scatter' || opts.type === 'bubble' || opts.type === 'bar') {
+ _calYAxisData = calYAxisData(series, opts, config, context);
+ yAxisWidth = _calYAxisData.yAxisWidth;
+ //如果显示Y轴标题
+ let maxTitleHeight = 0;
+ maxTitleHeight = Math.max(maxTitleHeight, opts.yAxis.data[i].titleFontSize ? opts.yAxis.data[i].titleFontSize * opts.pix : config.fontSize)
+ opts.area[0] += maxTitleHeight;
+ let rightIndex = 0,
+ leftIndex = 0;
+ //计算主绘图区域左右位置
+ for (let i = 0; i < yAxisWidth.length; i++) {
+ if (yAxisWidth[i].position == 'left') {
+ if (leftIndex > 0) {
+ opts.area[3] += yAxisWidth[i].width + opts.yAxis.padding * opts.pix;
+ opts.area[3] += yAxisWidth[i].width;
+ leftIndex += 1;
+ } else if (yAxisWidth[i].position == 'right') {
+ if (rightIndex > 0) {
+ opts.area[1] += yAxisWidth[i].width + opts.yAxis.padding * opts.pix;
+ opts.area[1] += yAxisWidth[i].width;
+ rightIndex += 1;
+ config.yAxisWidth = yAxisWidth;
+ opts.chartData.yAxisData = _calYAxisData;
+ if (opts.categories && opts.categories.length && opts.type !== 'radar' && opts.type !== 'gauge' && opts.type !== 'bar') {
+ opts.chartData.xAxisData = getXAxisPoints(opts.categories, opts, config);
+ let _calCategoriesData = calCategoriesData(opts.categories, opts, config, opts.chartData.xAxisData.eachSpacing, context),
+ xAxisHeight = _calCategoriesData.xAxisHeight,
+ angle = _calCategoriesData.angle;
+ config.xAxisHeight = xAxisHeight;
+ config._xAxisTextAngle_ = angle;
+ opts.area[2] += xAxisHeight;
+ opts.chartData.categoriesData = _calCategoriesData;
+ if (opts.type === 'line' || opts.type === 'area' || opts.type === 'scatter' || opts.type === 'bubble' || opts.type === 'bar') {
+ opts.chartData.xAxisData = calXAxisData(series, opts, config, context);
+ categories = opts.chartData.xAxisData.rangesFormat;
+ let _calCategoriesData = calCategoriesData(categories, opts, config, opts.chartData.xAxisData.eachSpacing, context),
+ opts.chartData.xAxisData = {
+ xAxisPoints: []
+ //计算右对齐偏移距离
+ if (opts.enableScroll && opts.xAxis.scrollAlign == 'right' && opts._scrollDistance_ === undefined) {
+ let offsetLeft = 0,
+ xAxisPoints = opts.chartData.xAxisData.xAxisPoints,
+ startX = opts.chartData.xAxisData.startX,
+ endX = opts.chartData.xAxisData.endX,
+ eachSpacing = opts.chartData.xAxisData.eachSpacing;
+ let totalWidth = eachSpacing * (xAxisPoints.length - 1);
+ let screenWidth = endX - startX;
+ offsetLeft = screenWidth - totalWidth;
+ _this.scrollOption.currentOffset = offsetLeft;
+ _this.scrollOption.startTouchX = offsetLeft;
+ _this.scrollOption.distance = 0;
+ _this.scrollOption.lastMoveTime = 0;
+ opts._scrollDistance_ = offsetLeft;
+ if (type === 'pie' || type === 'ring' || type === 'rose') {
+ config._pieTextMaxLength_ = opts.dataLabel === false ? 0 : getPieTextMaxLength(seriesMA, config, context, opts);
+ case 'word':
+ this.animationInstance = new Animation({
+ timing: opts.timing,
+ duration: duration,
+ onProcess: function(process) {
+ context.clearRect(0, 0, opts.width, opts.height);
+ contextRotate(context, opts);
+ drawWordCloudDataPoints(series, opts, config, context, process);
+ drawCanvas(opts, context);
+ onAnimationFinish: function onAnimationFinish() {
+ _this.uevent.trigger('renderComplete');
+ case 'map':
+ drawMapDataPoints(series, opts, config, context);
+ this.uevent.trigger('renderComplete');
+ },50)
+ case 'funnel':
+ opts.chartData.funnelData = drawFunnelDataPoints(series, opts, config, context, process);
+ drawLegend(opts.series, opts, config, context, opts.chartData);
+ drawToolTipBridge(opts, config, context, process);
+ onProcess: function onProcess(process) {
+ drawYAxisGrid(categories, opts, config, context);
+ drawXAxis(categories, opts, config, context);
+ var _drawLineDataPoints = drawLineDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawLineDataPoints.xAxisPoints,
+ calPoints = _drawLineDataPoints.calPoints,
+ eachSpacing = _drawLineDataPoints.eachSpacing;
+ opts.chartData.xAxisPoints = xAxisPoints;
+ opts.chartData.calPoints = calPoints;
+ opts.chartData.eachSpacing = eachSpacing;
+ drawYAxis(series, opts, config, context);
+ if (opts.enableMarkLine !== false && process === 1) {
+ drawMarkLine(opts, config, context);
+ drawToolTipBridge(opts, config, context, process, eachSpacing, xAxisPoints);
+ case 'scatter':
+ var _drawScatterDataPoints = drawScatterDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawScatterDataPoints.xAxisPoints,
+ calPoints = _drawScatterDataPoints.calPoints,
+ eachSpacing = _drawScatterDataPoints.eachSpacing;
+ case 'bubble':
+ var _drawBubbleDataPoints = drawBubbleDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawBubbleDataPoints.xAxisPoints,
+ calPoints = _drawBubbleDataPoints.calPoints,
+ eachSpacing = _drawBubbleDataPoints.eachSpacing;
+ case 'mix':
+ var _drawMixDataPoints = drawMixDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawMixDataPoints.xAxisPoints,
+ calPoints = _drawMixDataPoints.calPoints,
+ eachSpacing = _drawMixDataPoints.eachSpacing;
+ var _drawColumnDataPoints = drawColumnDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawColumnDataPoints.xAxisPoints,
+ calPoints = _drawColumnDataPoints.calPoints,
+ eachSpacing = _drawColumnDataPoints.eachSpacing;
+ var _drawMountDataPoints = drawMountDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawMountDataPoints.xAxisPoints,
+ calPoints = _drawMountDataPoints.calPoints,
+ eachSpacing = _drawMountDataPoints.eachSpacing;
+ var _drawBarDataPoints = drawBarDataPoints(series, opts, config, context, process),
+ yAxisPoints = _drawBarDataPoints.yAxisPoints,
+ calPoints = _drawBarDataPoints.calPoints,
+ eachSpacing = _drawBarDataPoints.eachSpacing;
+ opts.chartData.yAxisPoints = yAxisPoints;
+ opts.chartData.xAxisPoints = opts.chartData.xAxisData.xAxisPoints;
+ drawToolTipBridge(opts, config, context, process, eachSpacing, yAxisPoints);
+ var _drawAreaDataPoints = drawAreaDataPoints(series, opts, config, context, process),
+ xAxisPoints = _drawAreaDataPoints.xAxisPoints,
+ calPoints = _drawAreaDataPoints.calPoints,
+ eachSpacing = _drawAreaDataPoints.eachSpacing;
+ case 'ring':
+ opts.chartData.pieData = drawPieDataPoints(series, opts, config, context, process);
+ case 'pie':
+ case 'rose':
+ opts.chartData.pieData = drawRoseDataPoints(series, opts, config, context, process);
+ case 'radar':
+ opts.chartData.radarData = drawRadarDataPoints(series, opts, config, context, process);
+ case 'arcbar':
+ opts.chartData.arcbarData = drawArcbarDataPoints(series, opts, config, context, process);
+ case 'gauge':
+ opts.chartData.gaugeData = drawGaugeDataPoints(categories, series, opts, config, context, process);
+ case 'candle':
+ var _drawCandleDataPoints = drawCandleDataPoints(series, seriesMA, opts, config, context, process),
+ xAxisPoints = _drawCandleDataPoints.xAxisPoints,
+ calPoints = _drawCandleDataPoints.calPoints,
+ eachSpacing = _drawCandleDataPoints.eachSpacing;
+ if (seriesMA) {
+ drawLegend(seriesMA, opts, config, context, opts.chartData);
+function uChartsEvent() {
+ this.events = {};
+uChartsEvent.prototype.addEventListener = function(type, listener) {
+ this.events[type] = this.events[type] || [];
+ this.events[type].push(listener);
+uChartsEvent.prototype.delEventListener = function(type) {
+ this.events[type] = [];
+uChartsEvent.prototype.trigger = function() {
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ var type = args[0];
+ var params = args.slice(1);
+ if (!!this.events[type]) {
+ this.events[type].forEach(function(listener) {
+ listener.apply(null, params);
+ //console.log('[uCharts] '+e);
+var uCharts = function uCharts(opts) {
+ opts.pix = opts.pixelRatio ? opts.pixelRatio : 1;
+ opts.fontSize = opts.fontSize ? opts.fontSize : 13;
+ opts.fontColor = opts.fontColor ? opts.fontColor : config.fontColor;
+ if (opts.background == "" || opts.background == "none") {
+ opts.background = "#FFFFFF"
+ opts.title = assign({}, opts.title);
+ opts.subtitle = assign({}, opts.subtitle);
+ opts.duration = opts.duration ? opts.duration : 1000;
+ opts.yAxis = assign({}, {
+ showTitle: false,
+ disableGrid: false,
+ gridSet: 'number',
+ splitNumber: 5,
+ dashLength: 4 * opts.pix,
+ padding: 10,
+ fontColor: '#666666'
+ }, opts.yAxis);
+ opts.xAxis = assign({}, {
+ rotateLabel: false,
+ rotateAngle:45,
+ calibration:false,
+ marginTop: 0,
+ scrollAlign: 'left',
+ boundaryGap: 'center',
+ axisLine: true,
+ axisLineColor: '#cccccc',
+ titleFontSize: 13,
+ titleOffsetY: 0,
+ titleOffsetX: 0,
+ titleFontColor: '#666666'
+ }, opts.xAxis);
+ opts.xAxis.scrollPosition = opts.xAxis.scrollAlign;
+ opts.legend = assign({}, {
+ position: 'bottom',
+ float: 'center',
+ backgroundColor: 'rgba(0,0,0,0)',
+ borderColor: 'rgba(0,0,0,0)',
+ padding: 5,
+ margin: 5,
+ itemGap: 10,
+ fontSize: opts.fontSize,
+ lineHeight: opts.fontSize,
+ fontColor: opts.fontColor,
+ formatter: {},
+ hiddenColor: '#CECECE'
+ }, opts.legend);
+ opts.extra = assign({
+ legendShape: 'auto'
+ }, opts.extra);
+ opts.rotate = opts.rotate ? true : false;
+ opts.animation = opts.animation ? true : false;
+ opts.canvas2d = opts.canvas2d ? true : false;
+ let config$$1 = assign({}, config);
+ config$$1.color = opts.color ? opts.color : config$$1.color;
+ if (opts.type == 'pie') {
+ config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.pie.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix;
+ if (opts.type == 'ring') {
+ config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.ring.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix;
+ if (opts.type == 'rose') {
+ config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : opts.extra.rose.labelWidth * opts.pix || config$$1.pieChartLinePadding * opts.pix;
+ config$$1.pieChartTextPadding = opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding * opts.pix;
+ //屏幕旋转
+ config$$1.rotate = opts.rotate;
+ let tempWidth = opts.width;
+ let tempHeight = opts.height;
+ opts.width = tempHeight;
+ opts.height = tempWidth;
+ //适配高分屏
+ opts.padding = opts.padding ? opts.padding : config$$1.padding;
+ config$$1.yAxisWidth = config.yAxisWidth * opts.pix;
+ config$$1.fontSize = opts.fontSize * opts.pix;
+ config$$1.titleFontSize = config.titleFontSize * opts.pix;
+ config$$1.subtitleFontSize = config.subtitleFontSize * opts.pix;
+ if(!opts.context){
+ throw new Error('[uCharts] 未获取到context!注意:v2.0版本后,需要自行获取canvas的绘图上下文并传入opts.context!');
+ this.context = opts.context;
+ if (!this.context.setTextAlign) {
+ this.context.setStrokeStyle = function(e) {
+ return this.strokeStyle = e;
+ this.context.setLineWidth = function(e) {
+ return this.lineWidth = e;
+ this.context.setLineCap = function(e) {
+ return this.lineCap = e;
+ this.context.setFontSize = function(e) {
+ return this.font = e + "px sans-serif";
+ this.context.setFillStyle = function(e) {
+ return this.fillStyle = e;
+ this.context.setTextAlign = function(e) {
+ return this.textAlign = e;
+ this.context.setTextBaseline = function(e) {
+ return this.textBaseline = e;
+ this.context.setShadow = function(offsetX,offsetY,blur,color) {
+ this.shadowColor = color;
+ this.shadowOffsetX = offsetX;
+ this.shadowOffsetY = offsetY;
+ this.shadowBlur = blur;
+ this.context.draw = function() {}
+ //兼容NVUEsetLineDash
+ if(!this.context.setLineDash){
+ this.context.setLineDash = function(e) {}
+ opts.chartData = {};
+ this.uevent = new uChartsEvent();
+ this.scrollOption = {
+ currentOffset: 0,
+ startTouchX: 0,
+ distance: 0,
+ lastMoveTime: 0
+ this.opts = opts;
+ this.config = config$$1;
+ drawCharts.call(this, opts.type, opts, config$$1, this.context);
+uCharts.prototype.updateData = function() {
+ let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ this.opts = assign({}, this.opts, data);
+ this.opts.updateData = true;
+ let scrollPosition = data.scrollPosition || 'current';
+ switch (scrollPosition) {
+ case 'current':
+ this.opts._scrollDistance_ = this.scrollOption.currentOffset;
+ this.opts._scrollDistance_ = 0;
+ let _calYAxisData = calYAxisData(this.opts.series, this.opts, this.config, this.context), yAxisWidth = _calYAxisData.yAxisWidth;
+ this.config.yAxisWidth = yAxisWidth;
+ let offsetLeft = 0;
+ let _getXAxisPoints0 = getXAxisPoints(this.opts.categories, this.opts, this.config), xAxisPoints = _getXAxisPoints0.xAxisPoints,
+ startX = _getXAxisPoints0.startX,
+ endX = _getXAxisPoints0.endX,
+ eachSpacing = _getXAxisPoints0.eachSpacing;
+ currentOffset: offsetLeft,
+ startTouchX: offsetLeft,
+ this.opts._scrollDistance_ = offsetLeft;
+ drawCharts.call(this, this.opts.type, this.opts, this.config, this.context);
+uCharts.prototype.zoom = function() {
+ var val = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.opts.xAxis.itemCount;
+ if (this.opts.enableScroll !== true) {
+ console.log('[uCharts] 请启用滚动条后使用')
+ //当前屏幕中间点
+ let centerPoint = Math.round(Math.abs(this.scrollOption.currentOffset) / this.opts.chartData.eachSpacing) + Math.round(this.opts.xAxis.itemCount / 2);
+ this.opts.animation = false;
+ this.opts.xAxis.itemCount = val.itemCount;
+ //重新计算x轴偏移距离
+ let _calYAxisData = calYAxisData(this.opts.series, this.opts, this.config, this.context),
+ let _getXAxisPoints0 = getXAxisPoints(this.opts.categories, this.opts, this.config),
+ xAxisPoints = _getXAxisPoints0.xAxisPoints,
+ let centerLeft = eachSpacing * centerPoint;
+ let MaxLeft = screenWidth - eachSpacing * (xAxisPoints.length - 1);
+ offsetLeft = screenWidth / 2 - centerLeft;
+ if (offsetLeft > 0) {
+ offsetLeft = 0;
+ if (offsetLeft < MaxLeft) {
+ offsetLeft = MaxLeft;
+ calValidDistance(this, offsetLeft, this.opts.chartData, this.config, this.opts);
+uCharts.prototype.dobuleZoom = function(e) {
+ const tcs = e.changedTouches;
+ if (tcs.length < 2) {
+ for (var i = 0; i < tcs.length; i++) {
+ tcs[i].x = tcs[i].x ? tcs[i].x : tcs[i].clientX;
+ tcs[i].y = tcs[i].y ? tcs[i].y : tcs[i].clientY;
+ const ntcs = [getTouches(tcs[0], this.opts, e),getTouches(tcs[1], this.opts, e)];
+ const xlength = Math.abs(ntcs[0].x - ntcs[1].x);
+ // 记录初始的两指之间的数据
+ if(!this.scrollOption.moveCount){
+ let cts0 = {changedTouches:[{x:tcs[0].x,y:this.opts.area[0] / this.opts.pix + 2}]};
+ let cts1 = {changedTouches:[{x:tcs[1].x,y:this.opts.area[0] / this.opts.pix + 2}]};
+ if(this.opts.rotate){
+ cts0 = {changedTouches:[{x:this.opts.height / this.opts.pix - this.opts.area[0] / this.opts.pix - 2,y:tcs[0].y}]};
+ cts1 = {changedTouches:[{x:this.opts.height / this.opts.pix - this.opts.area[0] / this.opts.pix - 2,y:tcs[1].y}]};
+ const moveCurrent1 = this.getCurrentDataIndex(cts0).index;
+ const moveCurrent2 = this.getCurrentDataIndex(cts1).index;
+ const moveCount = Math.abs(moveCurrent1 - moveCurrent2);
+ this.scrollOption.moveCount = moveCount;
+ this.scrollOption.moveCurrent1 = Math.min(moveCurrent1, moveCurrent2);
+ this.scrollOption.moveCurrent2 = Math.max(moveCurrent1, moveCurrent2);
+ let currentEachSpacing = xlength / this.scrollOption.moveCount;
+ let itemCount = (this.opts.width - this.opts.area[1] - this.opts.area[3]) / currentEachSpacing;
+ itemCount = itemCount <= 2 ? 2 : itemCount;
+ itemCount = itemCount >= this.opts.categories.length ? this.opts.categories.length : itemCount;
+ this.opts.xAxis.itemCount = itemCount;
+ // 重新计算滚动条偏移距离
+ let currentLeft = eachSpacing * this.scrollOption.moveCurrent1;
+ offsetLeft = -currentLeft+Math.min(ntcs[0].x,ntcs[1].x)-this.opts.area[3]-eachSpacing;
+ this.scrollOption.currentOffset= offsetLeft;
+ this.scrollOption.startTouchX= 0;
+ this.scrollOption.distance=0;
+uCharts.prototype.stopAnimation = function() {
+ this.animationInstance && this.animationInstance.stop();
+uCharts.prototype.addEventListener = function(type, listener) {
+ this.uevent.addEventListener(type, listener);
+uCharts.prototype.delEventListener = function(type) {
+ this.uevent.delEventListener(type);
+uCharts.prototype.getCurrentDataIndex = function(e) {
+ var touches = null;
+ if (e.changedTouches) {
+ touches = e.changedTouches[0];
+ touches = e.mp.changedTouches[0];
+ if (touches) {
+ let _touches$ = getTouches(touches, this.opts, e);
+ if (this.opts.type === 'pie' || this.opts.type === 'ring') {
+ return findPieChartCurrentIndex({
+ x: _touches$.x,
+ y: _touches$.y
+ }, this.opts.chartData.pieData, this.opts);
+ } else if (this.opts.type === 'rose') {
+ return findRoseChartCurrentIndex({
+ } else if (this.opts.type === 'radar') {
+ return findRadarChartCurrentIndex({
+ }, this.opts.chartData.radarData, this.opts.categories.length);
+ } else if (this.opts.type === 'funnel') {
+ return findFunnelChartCurrentIndex({
+ }, this.opts.chartData.funnelData);
+ } else if (this.opts.type === 'map') {
+ return findMapChartCurrentIndex({
+ }, this.opts);
+ } else if (this.opts.type === 'word') {
+ return findWordChartCurrentIndex({
+ }, this.opts.chartData.wordCloudData);
+ } else if (this.opts.type === 'bar') {
+ return findBarChartCurrentIndex({
+ }, this.opts.chartData.calPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset));
+ return findCurrentIndex({
+ return -1;
+uCharts.prototype.getLegendDataIndex = function(e) {
+ return findLegendIndex({
+ }, this.opts.chartData.legendData);
+uCharts.prototype.touchLegend = function(e) {
+ var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var _touches$ = getTouches(touches, this.opts, e);
+ var index = this.getLegendDataIndex(e);
+ if (index >= 0) {
+ if (this.opts.type == 'candle') {
+ this.opts.seriesMA[index].show = !this.opts.seriesMA[index].show;
+ this.opts.series[index].show = !this.opts.series[index].show;
+ this.opts.animation = option.animation ? true : false;
+uCharts.prototype.showToolTip = function(e) {
+ if (!touches) {
+ console.log("[uCharts] 未获取到event坐标信息");
+ var currentOffset = this.scrollOption.currentOffset;
+ var opts = assign({}, this.opts, {
+ _scrollDistance_: currentOffset,
+ animation: false
+ if (this.opts.type === 'line' || this.opts.type === 'area' || this.opts.type === 'column' || this.opts.type === 'scatter' || this.opts.type === 'bubble') {
+ var current = this.getCurrentDataIndex(e);
+ var index = option.index == undefined ? current.index : option.index;
+ if (index > -1 || index.length>0) {
+ var seriesData = getSeriesDataItem(this.opts.series, index, current.group);
+ if (seriesData.length !== 0) {
+ var _getToolTipData = getToolTipData(seriesData, this.opts, index, current.group, this.opts.categories, option),
+ textList = _getToolTipData.textList,
+ offset = _getToolTipData.offset;
+ offset.y = _touches$.y;
+ opts.tooltip = {
+ textList: option.textList !== undefined ? option.textList : textList,
+ offset: option.offset !== undefined ? option.offset : offset,
+ option: option,
+ index: index,
+ group: current.group
+ drawCharts.call(this, opts.type, opts, this.config, this.context);
+ if (this.opts.type === 'mount') {
+ var index = option.index == undefined ? this.getCurrentDataIndex(e).index : option.index;
+ var opts = assign({}, this.opts, {animation: false});
+ var seriesData = assign({}, opts._series_[index]);
+ var textList = [{
+ text: option.formatter ? option.formatter(seriesData, undefined, index, opts) : seriesData.name + ': ' + seriesData.data,
+ color: seriesData.color,
+ legendShape: this.opts.extra.tooltip.legendShape == 'auto' ? seriesData.legendShape : this.opts.extra.tooltip.legendShape
+ }];
+ x: opts.chartData.calPoints[index].x,
+ textList: option.textList ? option.textList : textList,
+ index: index
+ if (this.opts.type === 'bar') {
+ offset.x = _touches$.x;
+ if (this.opts.type === 'mix') {
+ var seriesData = getSeriesDataItem(this.opts.series, index);
+ var _getMixToolTipData = getMixToolTipData(seriesData, this.opts, index, this.opts.categories, option),
+ textList = _getMixToolTipData.textList,
+ offset = _getMixToolTipData.offset;
+ if (this.opts.type === 'candle') {
+ var _getToolTipData = getCandleToolTipData(this.opts.series[0].data, seriesData, this.opts, index, this.opts.categories, this.opts.extra.candle, option),
+ if (this.opts.type === 'pie' || this.opts.type === 'ring' || this.opts.type === 'rose' || this.opts.type === 'funnel') {
+ var index = option.index == undefined ? this.getCurrentDataIndex(e) : option.index;
+ if (this.opts.type === 'map') {
+ var seriesData = assign({}, this.opts.series[index]);
+ seriesData.name = seriesData.properties.name
+ text: option.formatter ? option.formatter(seriesData, undefined, index, this.opts) : seriesData.name,
+ opts.updateData = false;
+ if (this.opts.type === 'word') {
+ if (this.opts.type === 'radar') {
+ var textList = seriesData.map((item) => {
+ text: option.formatter ? option.formatter(item, this.opts.categories[index], index, this.opts) : item.name + ': ' + item.data,
+ legendShape: this.opts.extra.tooltip.legendShape == 'auto' ? item.legendShape : this.opts.extra.tooltip.legendShape
+uCharts.prototype.translate = function(distance) {
+ currentOffset: distance,
+ startTouchX: distance,
+ let opts = assign({}, this.opts, {
+ _scrollDistance_: distance,
+ drawCharts.call(this, this.opts.type, opts, this.config, this.context);
+uCharts.prototype.scrollStart = function(e) {
+ if (touches && this.opts.enableScroll === true) {
+ this.scrollOption.startTouchX = _touches$.x;
+uCharts.prototype.scroll = function(e) {
+ if (this.scrollOption.lastMoveTime === 0) {
+ this.scrollOption.lastMoveTime = Date.now();
+ let Limit = this.opts.touchMoveLimit || 60;
+ let duration = currMoveTime - this.scrollOption.lastMoveTime;
+ if (duration < Math.floor(1000 / Limit)) return;
+ if (this.scrollOption.startTouchX == 0) return;
+ this.scrollOption.lastMoveTime = currMoveTime;
+ var _distance;
+ _distance = _touches$.x - this.scrollOption.startTouchX;
+ var validDistance = calValidDistance(this, currentOffset + _distance, this.opts.chartData, this.config, this.opts);
+ this.scrollOption.distance = _distance = validDistance - currentOffset;
+ _scrollDistance_: currentOffset + _distance,
+ return currentOffset + _distance;
+uCharts.prototype.scrollEnd = function(e) {
+ if (this.opts.enableScroll === true) {
+ var _scrollOption = this.scrollOption,
+ currentOffset = _scrollOption.currentOffset,
+ distance = _scrollOption.distance;
+ this.scrollOption.currentOffset = currentOffset + distance;
+ this.scrollOption.distance = 0;
+ this.scrollOption.moveCount = 0;
+export default uCharts;
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+ 1. Definitions.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+ END OF TERMS AND CONDITIONS
+ APPENDIX: How to apply the Apache License to your work.
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+ Copyright [yyyy] [name of copyright owner]
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
@@ -0,0 +1,81 @@
+ "id": "qiun-data-charts",
+ "displayName": "秋云 ucharts echarts 高性能跨全端图表组件",
+ "version": "2.5.0-20230101",
+ "description": "uCharts 新增正负柱状图!支持H5及APP用 ucharts echarts 渲染图表,uniapp可视化首选组件",
+ "ucharts",
+ "echarts",
+ "f2",
+ "图表",
+ "可视化"
+ "repository": "https://gitee.com/uCharts/uCharts",
+ "HBuilderX": "^3.3.8"
+"dcloudext": {
+ "qq": "474119"
+ "data": "插件不采集任何数据",
+ "npmurl": "https://www.npmjs.com/~qiun",
+ "type": "component-vue"
+ "app-nvue": "y"
+ "Vue": {
+ "vue2": "y",
+ "vue3": "y"
@@ -0,0 +1,84 @@
+
+[](https://gitee.com/uCharts/uCharts/stargazers)
+[](https://gitee.com/uCharts/uCharts/members)
+[](https://www.apache.org/licenses/LICENSE-2.0.html)
+[](https://www.npmjs.com/~qiun)
+## uCharts简介
+`uCharts`是一款基于`canvas API`开发的适用于所有前端应用的图表库,开发者编写一套代码,可运行到 Web、iOS、Android(基于 uni-app / taro )、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等更多支持 canvas API 的平台。
+## 官方网站
+## [https://www.ucharts.cn](https://www.ucharts.cn)
+## 快速体验
+一套代码编到多个平台,依次扫描二维码,亲自体验uCharts图表跨平台效果!其他平台请自行编译。
+
+
+## 致开发者
+感谢各位开发者`五年`来对秋云及uCharts的支持,uCharts的进步离不开各位开发者的鼓励与贡献。为更好的帮助各位开发者使用图表工具,我们推出了新版官网,增加了在线定制、问答社区、在线配置等一些增值服务,为确保您能更好的应用图表组件,建议您先`仔细阅读官网指南`以及`常见问题`,而不是下载下来`直接使用`。如仍然不能解决,请到`官网社区`或开通会员后加入`专属VIP会员群`提问将会很快得到回答。
+## 视频教程
+## [uCharts新手入门教程](https://www.bilibili.com/video/BV1qA411Q7se/?share_source=copy_web&vd_source=42a1242f9aaade6427736af69eb2e1d9)
+## 社群支持
+uCharts官方拥有5个2000人的QQ群及专属VIP会员群支持,庞大的用户量证明我们一直在努力,请各位放心使用!uCharts的开源图表组件的开发,团队付出了大量的时间与精力,经过四来的考验,不会有比较明显的bug,请各位放心使用。如果您有更好的想法,可以在`码云提交Pull Requests`以帮助更多开发者完成需求,再次感谢各位对uCharts的鼓励与支持!
+#### 官方交流群
+- 交流群1:371774600(已满)
+- 交流群2:619841586(已满)
+- 交流群3:955340127(已满)
+- 交流群4:641669795(已满)
+- 交流群5:236294809(只能扫码加入)
+
+- 口令`uniapp`
+#### 专属VIP会员群
+- 开通会员后详见【账号详情】页面中顶部的滚动通知
+- 口令`您的用户ID`
+## 版权信息
+uCharts始终坚持开源,遵循 [Apache Licence 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 开源协议,意味着您无需支付任何费用,即可将uCharts应用到您的产品中。
+注意:这并不意味着您可以将uCharts应用到非法的领域,比如涉及赌博,暴力等方面。如因此产生纠纷或法律问题,uCharts相关方及秋云科技不承担任何责任。
+## 合作伙伴
+[](https://www.diygw.com/)
+[](https://gitee.com/howcode/has-chat)
+[](https://www.uviewui.com/)
+[](https://ext.dcloud.net.cn/plugin?id=7088)
+[](https://ext.dcloud.net.cn/publisher?id=202)
+[](https://www.firstui.cn/)
+[](https://ext.dcloud.net.cn/plugin?id=5169)
+[](https://www.graceui.com/)
+## 更新记录
+详见官网指南中说明,[点击此处查看](https://www.ucharts.cn/v2/#/guide/index?id=100)
+## 相关链接
+- [uCharts官网](https://www.ucharts.cn)
+- [DCloud插件市场地址](https://ext.dcloud.net.cn/plugin?id=271)
+- [uCharts码云开源托管地址](https://gitee.com/uCharts/uCharts) [](https://gitee.com/uCharts/uCharts/stargazers)
+- [uCharts npm开源地址](https://www.ucharts.cn)
+- [ECharts官网](https://echarts.apache.org/zh/index.html)
+- [ECharts配置手册](https://echarts.apache.org/zh/option.html)
+- [图表组件在项目中的应用 ReportPlus数据报表](https://www.ucharts.cn/v2/#/layout/info?id=1)