@@ -1,31 +0,0 @@
-/*
- * Eslint config file
- * Documentation: https://eslint.org/docs/user-guide/configuring/
- * Install the Eslint extension before using this feature.
- */
-module.exports = {
- env: {
- es6: true,
- browser: true,
- node: true,
- },
- ecmaFeatures: {
- modules: true,
- parserOptions: {
- ecmaVersion: 2018,
- sourceType: 'module',
- globals: {
- wx: true,
- App: true,
- Page: true,
- getCurrentPages: true,
- getApp: true,
- Component: true,
- requirePlugin: true,
- requireMiniProgram: true,
- // extends: 'eslint:recommended',
- rules: {},
-}
@@ -1 +0,0 @@
-临时项目,作为测试微信小程序登陆之用
@@ -1,19 +0,0 @@
-// app.js
-App({
- onLaunch() {
- // 展示本地存储能力
- const logs = wx.getStorageSync('logs') || []
- logs.unshift(Date.now())
- wx.setStorageSync('logs', logs)
-
- // 登录
- wx.login({
- success: res => {
- // 发送 res.code 到后台换取 openId, sessionKey, unionId
- }
- })
- globalData: {
- userInfo: null
-})
@@ -1,14 +0,0 @@
-{
- "pages":[
- "pages/index/index",
- "pages/logs/logs"
- ],
- "window":{
- "backgroundTextStyle":"light",
- "navigationBarBackgroundColor": "#fff",
- "navigationBarTitleText": "Weixin",
- "navigationBarTextStyle":"black"
- "style": "v2",
- "sitemapLocation": "sitemap.json"
@@ -1,10 +0,0 @@
-/**app.wxss**/
-.container {
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- padding: 200rpx 0;
- box-sizing: border-box;
@@ -1,91 +0,0 @@
-// index.js
-const common=require('../../utils/common.js')
-// 获取应用实例
-const app = getApp()
-Page({
- data: {
- motto: 'Hello World',
- userInfo: {},
- hasUserInfo: false,
- canIUse: wx.canIUse('button.open-type.getUserInfo'),
- canIUseGetUserProfile: false,
- canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName'), // 如需尝试获取用户信息可改为false
- holderText: 'to be auth'
- // 事件处理函数
- bindViewTap() {
- wx.navigateTo({
- url: '../logs/logs'
- onLoad() {
- if (wx.getUserProfile) {
- this.setData({
- canIUseGetUserProfile: true
- getUserProfile(e) {
- // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
- wx.getUserProfile({
- desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
- success: (res) => {
- console.log(res)
- userInfo: res.userInfo,
- hasUserInfo: true
- getUserInfo(e) {
- // 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息
- console.log(e)
- userInfo: e.detail.userInfo,
- // 小程序登录 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
- wxLogin(e){
- let page=this;
- success (res) {
- console.log("res:")
- if (res.code) {
- //发起网络请求
- console.log('发起网络请求'+common.baseurl)
- wx.request({
- url: common.baseurl+'/api/social-login2',
- method: "POST",
- code: res.code,
- state: 'empty',
- type: 33,
- username: '15601691300',
- password: 'admin123'
- header: {
- 'content-type': 'application/json' // 默认值
- success: function(res) {
- console.log(res.data)
- let holder="auth success, token:"+res.data.data.token
- page.setData({holderText: holder})
- fail: function(data){
- console.error("请求出错");
- console.error(data)
- } else {
- console.log('登录失败!' + res.errMsg)
@@ -1,3 +0,0 @@
- "usingComponents": {}
@@ -1,29 +0,0 @@
-<!--index.wxml-->
-<view class="container">
- <view class="userinfo">
- <block wx:if="{{canIUseOpenData}}">
- </block>
- <block wx:elif="{{!hasUserInfo}}">
- <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
- <button wx:elif="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button>
- <view wx:else> 请使用1.4.4及以上版本基础库 </view>
- <block wx:else>
- <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
- <text class="userinfo-nickname">{{userInfo.nickName}}</text>
- </view>
- <view class="usermotto">
- <text style="position: relative; left: 1rpx; top: -476rpx">授权登录测试1024</text>
- <button style="position: relative; left: 0rpx; top: -361rpx" type="primary" id="login-button" bindtap="wxLogin">点击授权登录</button>
- <text style="position: relative; left: 1rpx; top: -272rpx" id="login-user-id">{{holderText}}</text>
-</view>
-/**index.wxss**/
-.userinfo {
- color: #aaa;
-.userinfo-avatar {
- overflow: hidden;
- width: 128rpx;
- height: 128rpx;
- margin: 20rpx;
- border-radius: 50%;
-.usermotto {
- margin-top: 200px;
@@ -1,18 +0,0 @@
-// logs.js
-const util = require('../../utils/util.js')
- logs: []
- logs: (wx.getStorageSync('logs') || []).map(log => {
- return {
- date: util.formatTime(new Date(log)),
- timeStamp: log
@@ -1,4 +0,0 @@
- "navigationBarTitleText": "查看启动日志",
@@ -1,6 +0,0 @@
-<!--logs.wxml-->
-<view class="container log-list">
- <block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
- <text class="log-item">{{index + 1}}. {{log.date}}</text>
@@ -1,8 +0,0 @@
-.log-list {
- padding: 40rpx;
-.log-item {
- margin: 10rpx;
@@ -1,75 +0,0 @@
- "description": "项目配置文件",
- "packOptions": {
- "ignore": [
- {
- "type": "file",
- "value": ".eslintrc.js"
- ]
- "setting": {
- "bundle": false,
- "userConfirmedBundleSwitch": false,
- "urlCheck": true,
- "scopeDataCheck": false,
- "coverView": true,
- "es6": true,
- "postcss": true,
- "compileHotReLoad": false,
- "lazyloadPlaceholderEnable": false,
- "preloadBackgroundData": false,
- "minified": true,
- "autoAudits": false,
- "newFeature": false,
- "uglifyFileName": false,
- "uploadWithSourceMap": true,
- "useIsolateContext": true,
- "nodeModules": false,
- "enhance": true,
- "useMultiFrameRuntime": true,
- "useApiHook": true,
- "useApiHostProcess": true,
- "showShadowRootInWxmlPanel": true,
- "packNpmManually": false,
- "enableEngineNative": false,
- "packNpmRelationList": [],
- "minifyWXSS": true,
- "showES6CompileOption": false,
- "minifyWXML": true
- "compileType": "miniprogram",
- "libVersion": "2.19.4",
- "appid": "wx44d047d87e6284d8",
- "appid1": "wx63c280fe3248a3e7",
- "projectname": "mini-program-test",
- "debugOptions": {
- "hidedInDevtools": []
- "scripts": {},
- "staticServerOptions": {
- "baseURL": "",
- "servePath": ""
- "isGameTourist": false,
- "condition": {
- "search": {
- "list": []
- "conversation": {
- "game": {
- "plugin": {
- "gamePlugin": {
- "miniprogram": {
@@ -1,7 +0,0 @@
- "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
- "rules": [{
- "action": "allow",
- "page": "*"
- }]
- baseurl: "http://127.0.0.1:28080"
-const formatTime = date => {
- const year = date.getFullYear()
- const month = date.getMonth() + 1
- const day = date.getDate()
- const hour = date.getHours()
- const minute = date.getMinutes()
- const second = date.getSeconds()
- return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
-const formatNumber = n => {
- n = n.toString()
- return n[1] ? n : `0${n}`
- formatTime
@@ -1,16 +0,0 @@
-{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
- // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
- "version": "0.0",
- "configurations": [{
- "default" :
- "launchtype" : "local"
- "h5" :
- "type" : "uniCloud"
@@ -1,79 +0,0 @@
-<script>
- import Vue from 'vue'
- import { getAuthToken } from '@/common/js/util.js'
- let __timerId = 0;
- export default {
- uni.getSystemInfo({
- success: e=> {
- this.initSize(e);
- this.initLogin();
- methods: {
- // 初始化登陆状态
- async initLogin(){
- const token = getAuthToken()
- if (!token) {
- return;
- // 通过设置 Token 的方式,触发加载用户信息
- this.$store.commit('setToken', {
- token
- });
- /**
- * 存储设备信息 参考colorUI
- * @param {Object}
- initSize(e){
- const systemInfo = e;
- let navigationBarHeight;
- let custom = {};
- // #ifndef MP
- custom = {height: 36,width: 88};
- navigationBarHeight = 44;
- // #endif
- // #ifdef MP
- custom = wx.getMenuButtonBoundingClientRect();
- navigationBarHeight = custom.bottom + custom.top - e.statusBarHeight * 2;
- systemInfo.custom = custom;
- systemInfo.navigationBarHeight = navigationBarHeight;
- Vue.prototype.systemInfo = systemInfo;
- //打开全局定时器
- openTimer(){
- this.closeTimer();
- __timerId = setInterval(()=>{
- this.$store.commit('setStateAttr', {
- key: 'timerIdent',
- val: !this.$store.state.timerIdent
- }, 1000)
- //关闭定时器
- closeTimer(){
- if(__timerId != 0){
- clearInterval(__timerId);
- __timerId = 0;
- onShow() {
- console.log('app show');
- this.openTimer();
- onHide() {
-</script>
-<style lang="scss">
- /*每个页面公共css */
- @import "@/uni_modules/uview-ui/index.scss";
- @import url("./common/css/common.css");
- @import url("./common/css/icon.css");
-</style>
@@ -1,23 +0,0 @@
-import { request } from '@/common/js/request.js'
-// 获得用户的基本信息
-export function getUserInfo() {
- return request({
- url: 'member/user/profile/get',
- method: 'get'
-// 修改
-export function updateNickname(nickname) {
- url: 'member/user/profile/update-nickname',
- method: 'post',
- "Content-Type": "application/x-www-form-urlencoded"
- nickname
@@ -1,34 +0,0 @@
-// 手机号 + 密码登陆
-export function login(mobile, password) {
- url: 'login',
- mobile, password
-// 手机号 + 验证码登陆
-export function smsLogin(mobile, code) {
- url: 'sms-login',
- mobile, code
-// 发送手机验证码
-export function sendSmsCode(mobile, scene) {
- url: 'send-sms-code',
- mobile, scene
@@ -1,182 +0,0 @@
-/* #ifndef APP-PLUS-NVUE */
-view,
-scroll-view,
-swiper,
-swiper-item,
-cover-view,
-cover-image,
-icon,
-text,
-rich-text,
-progress,
-button,
-checkbox,
-form,
-input,
-label,
-radio,
-slider,
-switch,
-textarea,
-navigator,
-audio,
-camera,
-image,
-video {
-image{
- display: block;
-text{
- line-height: 1;
- /* font-family: Helvetica Neue, Helvetica, sans-serif; */
-button{
- padding: 0;
- margin: 0;
- background-color: rgba(0,0,0,0) !important;
-button:after{
- border: 0;
-.bottom-fill{
- height: constant(safe-area-inset-bottom);
- height: env(safe-area-inset-bottom);
-.fix-bot{
- box-sizing: content-box;
- padding-bottom: constant(safe-area-inset-bottom);
- padding-bottom: env(safe-area-inset-bottom);
-/* 边框 */
-.round{
- position: relative;
- border-radius: 100rpx;
-.round:after{
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- width: 200%;
- height: 200%;
- transform: scale(.5) translate(-50%,-50%);
- border: 1px solid #878787;
-.b-b:after{
- z-index: 3;
- top: auto;
- bottom: 0;
- right: 0;
- height: 0;
- transform: scaleY(.5);
- border-bottom: 1px solid #e0e0e0;
-.b-t:before{
- border-bottom: 1px solid #e5e5e5;
-.b-r:after{
- width: 0;
- transform: scaleX(.5);
- border-right: 1px solid #e5e5e5;
-.b-l:before{
- border-left: 1px solid #e5e5e5;
-.b-b, .b-t, .b-l, .b-r{
-/* 点击态 */
-.hover-gray {
- background: #fafafa !important;
-.hover-dark {
- background: #f0f0f0 !important;
-.hover-opacity {
- opacity: 0.7;
-/* #endif */
-.clamp {
- /* #ifdef APP-PLUS-NVUE */
- lines: 1;
- /* #endif */
- /* #ifndef APP-PLUS-NVUE */
- text-overflow: ellipsis;
- white-space: nowrap;
-.clamp2 {
- lines: 2;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
-/* 布局 */
-.row{
- display:flex;
- flex-direction:row;
-.column{
-.center{
- justify-content: center;
-.fill{
- flex: 1;
-/* input */
-.placeholder{
- color: #999 !important;
@@ -1,271 +0,0 @@
-@font-face {
- font-family: "mix-icon";
- font-weight: normal;
- font-style: normal;
- src: url('https://at.alicdn.com/t/font_1913318_2ui3nitf38x.ttf') format('truetype'); // TODO 芋艿: icon 怎么搞?
-.mix-icon {
- font-family: "mix-icon" !important;
- font-size: 16px;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-.icon-fanhui:before {
- content: "\e7d5";
-.icon-shoujihaoma:before {
- content: "\e7ec";
-.icon-close:before {
- content: "\e60f";
-.icon-xingbie-nv:before {
- content: "\e60e";
-.icon-wuliuyunshu:before {
- content: "\e7ed";
-.icon-jingpin:before {
- content: "\e608";
-.icon-zhangdanmingxi01:before {
- content: "\e637";
-.icon-tixian1:before {
- content: "\e625";
-.icon-chongzhi:before {
- content: "\e605";
-.icon-wodezhanghu_zijinjilu:before {
- content: "\e615";
-.icon-tixian:before {
- content: "\e6ab";
-.icon-qianbao:before {
- content: "\e6c4";
-.icon-guanbi1:before {
- content: "\e61a";
-.icon-daipingjia:before {
- content: "\e604";
-.icon-daifahuo:before {
- content: "\e6bd";
-.icon-yue:before {
- content: "\e600";
-.icon-wxpay:before {
- content: "\e602";
-.icon-alipay:before {
- content: "\e603";
-.icon-tishi:before {
- content: "\e662";
-.icon-shoucang-1:before {
- content: "\e607";
-.icon-gouwuche:before {
- content: "\e657";
-.icon-shoucang:before {
- content: "\e645";
-.icon-home:before {
- content: "\e60c";
-/* .icon-bangzhu1:before {
- content: "\e63d"; // 帮助
-} */
-.icon-xingxing:before {
- content: "\e70b";
-.icon-shuxiangliebiao:before {
- content: "\e635";
-.icon-hengxiangliebiao:before {
- content: "\e636";
-.icon-guanbi2:before {
- content: "\e7be";
-.icon-down:before {
- content: "\e65c";
-.icon-arrow-top:before {
- content: "\e63e";
-.icon-xiaoxi:before {
- content: "\e634";
-.icon-saoma:before {
- content: "\e655";
-.icon-dizhi1:before {
- content: "\e618";
-.icon-ditu-copy:before {
- content: "\e609";
-.icon-lajitong:before {
- content: "\e682";
-.icon-bianji:before {
- content: "\e60d"; // 编辑
-.icon-yanzhengma1:before {
- content: "\e613";
-.icon-yanjing:before {
- content: "\e65b";
-.icon-mima:before {
- content: "\e628";
-.icon-biyan:before {
- content: "\e633";
-.icon-iconfontweixin:before {
- content: "\e611";
-.icon-shouye:before {
- content: "\e626";
-.icon-daifukuan:before {
- content: "\e68f";
-.icon-pinglun-copy:before {
- content: "\e612";
-.icon-lishijilu:before {
- content: "\e6b9";
-.icon-shoucang_xuanzhongzhuangtai:before {
- content: "\e6a9";
-.icon-share:before {
- content: "\e656";
-.icon-shezhi1:before {
- content: "\e61d";
-.icon-shouhoutuikuan:before {
- content: "\e631";
-.icon-dizhi:before {
- content: "\e614";
-.icon-yishouhuo:before {
- content: "\e71a";
-.icon-xuanzhong:before {
- content: "\e632";
-.icon-xiangzuo:before {
- content: "\e653";
-.icon-iconfontxingxing:before {
- content: "\e6b0";
-.icon-jia2:before {
- content: "\e60a";
-.icon-sousuo:before {
- content: "\e7ce";
-.icon-xiala:before {
- content: "\e644";
-.icon-xia:before {
- content: "\e62d";
-.icon--jianhao:before {
- content: "\e60b";
-.icon-you:before {
- content: "\e606";
-.icon-yk_yuanquan:before {
- content: "\e601";
-.icon-xing:before {
- content: "\e627";
-.icon-guanbi:before {
- content: "\e71d";
-.icon-loading:before {
- content: "\e646";
@@ -1,59 +0,0 @@
-import store from '@/store'
-import { msg, getAuthToken } from './util'
-const BASE_URL = 'http://127.0.0.1:28080/api/';
-export const request = (options) => {
- return new Promise((resolve, reject) => {
- // 发起请求
- const authToken = getAuthToken();
- uni.request({
- url: BASE_URL + options.url,
- method: options.method || 'GET',
- data: options.data || {},
- ...options.header,
- 'Authorization': authToken ? `Bearer ${authToken}` : ''
- }).then(res => {
- res = res[1];
- const statusCode = res.statusCode;
- if (statusCode !== 200) {
- msg('请求失败,请重试');
- const code = res.data.code;
- const message = res.data.msg;
- // Token 过期,引导重新登陆
- if (code === 401) {
- msg('登录信息已过期,请重新登录');
- store.commit('logout');
- // reject('无效的登录信息');
- // 系统异常
- if (code === 500) {
- msg('系统异常,请稍后重试');
- reject(new Error(message));
- // 其它失败情况
- if (code > 0) {
- msg(message);
- // 提供 code + msg,可以基于 code 做进一步的处理。当然,一般情况下是不需要的。
- // 不需要的场景:手机登录时,密码不正确;
- // 需要的场景:微信登录时,未绑定手机,后端会返回一个 code 码,前端需要基于它跳转到绑定手机界面;
- reject({
- 'code': code,
- 'msg': message
- // 处理成功,则只返回成功的 data 数据,不返回 code 和 msg
- resolve(res.data.data);
- }).catch((err) => {
- reject(err);
@@ -1,136 +0,0 @@
-let _debounceTimeout = null,
- _throttleRunning = false
-/**
- * 防抖
- * 参考文章 https://juejin.cn/post/6844903669389885453
- *
- * @param {Function} 执行函数
- * @param {Number} delay 延时ms
-export const debounce = (fn, delay=500) => {
- clearTimeout(_debounceTimeout);
- _debounceTimeout = setTimeout(() => {
- fn();
- }, delay);
- * 节流
-export const throttle = (fn, delay=500) => {
- if(_throttleRunning){
- _throttleRunning = true;
- setTimeout(() => {
- _throttleRunning = false;
- * toast 提示
- * @param {String} title 标题
- * @param {Object} param 拓展参数
- * @param {Integer} param.duration 持续时间
- * @param {Boolean} param.mask 是否遮罩
- * @param {Boolean} param.icon 图标
-export const msg = (title = '', param={}) => {
- if (!title) {
- uni.showToast({
- title,
- duration: param.duration || 1500,
- mask: param.mask || false,
- icon: param.icon || 'none' // TODO 芋艿:是否要区分下 error 的提示,或者专门的封装
- * 检查登录
- * @param {Boolean} options.nav 如果未登陆,是否跳转到登陆页。默认为 true
- * @return {Boolean} 是否登陆
-export const isLogin = (options = {}) => {
- const token = getAuthToken();
- if (token) {
- return true;
- // 若 nav 不为 false,则进行跳转登陆页
- if (options.nav !== false) {
- uni.navigateTo({
- url: '/pages/auth/login'
- return false;
- * 获得认证 Token
- * @return 认证 Token
-export const getAuthToken = () => {
- return uni.getStorageSync('token');
- * 校验参数
- * @param {String} 字符串
- * @param {String} 数据的类型。例如说 mobile 手机号、tel 座机 TODO 芋艿:是否组件里解决
-export const checkStr = (str, type) => {
- switch (type) {
- case 'mobile': //手机号码
- return /^1[3|4|5|6|7|8|9][0-9]{9}$/.test(str);
- case 'tel': //座机
- return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
- case 'card': //身份证
- return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(str);
- case 'mobileCode': //6位数字验证码
- return /^[0-9]{6}$/.test(str)
- case 'pwd': //密码以字母开头,长度在6~18之间,只能包含字母、数字和下划线
- return /^([a-zA-Z0-9_]){6,18}$/.test(str)
- case 'payPwd': //支付密码 6位纯数字
- case 'postal': //邮政编码
- return /[1-9]\d{5}(?!\d)/.test(str);
- case 'QQ': //QQ号
- return /^[1-9][0-9]{4,9}$/.test(str);
- case 'email': //邮箱
- return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
- case 'money': //金额(小数点2位)
- return /^\d*(?:\.\d{0,2})?$/.test(str);
- case 'URL': //网址
- return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str)
- case 'IP': //IP
- return /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/.test(str);
- case 'date': //日期时间
- return /^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})(?:\:\d{2}|:(\d{2}):(\d{2}))$/.test(str) || /^(\d{4})\-(\d{2})\-(\d{2})$/
- .test(str)
- case 'number': //数字
- return /^[0-9]$/.test(str);
- case 'english': //英文
- return /^[a-zA-Z]+$/.test(str);
- case 'chinese': //中文
- return /^[\\u4E00-\\u9FA5]+$/.test(str);
- case 'lower': //小写
- return /^[a-z]+$/.test(str);
- case 'upper': //大写
- return /^[A-Z]+$/.test(str);
- case 'HTML': //HTML标记
- return /<("[^"]*"|'[^']*'|[^'">])*>/.test(str);
- default:
@@ -1,96 +0,0 @@
-// import {request} from '@/common/js/request'
-export default{
- data() {
- page: 0, // 页码
- pageNum: 6, // 每页加载数据量
- loadingType: 1, // 加载类型。0 加载前;1 加载中;2 没有更多
- isLoading: false, // 刷新数据
- loaded: false, // 加载完毕
- * 打印日志,方便调试
- * @param {Object} data 数据
- log(data) {
- console.log(JSON.parse(JSON.stringify(data)))
- * navigatorTo 跳转页面
- * @param {String} url
- * @param {Object} options 可选参数
- * @param {Boolean} options.login 是否检测登录
- navTo(url, options={}) {
- this.$util.throttle(() => {
- if (!url) {
- // 如果需要登陆,并且未登陆,则跳转到登陆界面
- if ((~url.indexOf('login=1') || options.login) && !this.$store.getters.hasLogin){
- url = '/pages/auth/login';
- // 跳转到指定 url 地址
- url
- }, 300)
- * $request云函数请求 TODO 芋艿:需要改成自己的
- * @param {String} module
- * @param {String} operation
- * @param {Boolean} data 请求参数
- * @param {Boolean} ext 附加参数
- * @param {Boolean} ext.showLoading 是否显示loading状态,默认不显示
- * @param {Boolean} ext.hideLoading 是否关闭loading状态,默认关闭
- * @param {Boolean} ext.login 未登录拦截
- * @param {Boolean} ext.setLoaded 加载完成是设置页面加载完毕
- $request(module, operation, data={}, ext={}){
- if(ext.login && !this.$util.isLogin()){
- if(ext.showLoading){
- this.isLoading = true;
- return new Promise((resolve, reject)=> {
- request(module, operation, data, ext).then(result => {
- if(ext.hideLoading !== false){
- this.isLoading = false;
- setTimeout(()=>{
- if(this.setLoaded !== false){
- this.loaded = true;
- }, 100)
- this.$refs.confirmBtn && this.$refs.confirmBtn.stop();
- resolve(result);
- imageOnLoad(data, key){ // TODO 芋艿:需要改成自己的
- this.$set(data, 'loaded', true);
- showPopup(key){ // TODO 芋艿:需要改成自己的
- this.$util.throttle(()=>{
- this.$refs[key].open();
- }, 200)
- hidePopup(key){ // TODO 芋艿:需要改成自己的
- this.$refs[key].close();
- stopPrevent(){}, // TODO 芋艿:需要改成自己的
@@ -1,630 +0,0 @@
-<template>
- <view>
- <slot v-if="!nodes.length" />
- <!--#ifdef APP-PLUS-NVUE-->
- <web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
- <!--#endif-->
- <!--#ifndef APP-PLUS-NVUE-->
- <view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
- <!--#ifdef H5 || MP-360-->
- <div :id="'rtf'+uid"></div>
- <!--#ifndef H5 || MP-360-->
- <trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
-</template>
- // #ifndef H5 || APP-PLUS-NVUE || MP-360
- import trees from './libs/trees';
- var cache = {},
- // #ifdef MP-WEIXIN || MP-TOUTIAO
- fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
- Parser = require('./libs/MpHtmlParser.js');
- var dom;
- // 计算 cache 的 key
- function hash(str) {
- for (var i = str.length, val = 5381; i--;)
- val += (val << 5) + str.charCodeAt(i);
- return val;
- // #ifdef H5 || APP-PLUS-NVUE || MP-360
- var windowWidth = uni.getSystemInfoSync().windowWidth,
- cfg = require('./libs/config.js');
- // #ifdef APP-PLUS-NVUE
- var weexDom = weex.requireModule('dom');
- * Parser 富文本组件
- * @tutorial https://github.com/jin-yufeng/Parser
- * @property {String} html 富文本数据
- * @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
- * @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
- * @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
- * @property {Number} compress 压缩等级
- * @property {String} domain 图片、视频等链接的主域名
- * @property {Boolean} lazyLoad 是否开启图片懒加载
- * @property {String} loadingImg 图片加载完成前的占位图
- * @property {Boolean} selectable 是否开启长按复制
- * @property {Object} tagStyle 标签的默认样式
- * @property {Boolean} showWithAnimation 是否使用渐显动画
- * @property {Boolean} useAnchor 是否使用锚点
- * @property {Boolean} useCache 是否缓存解析结果
- * @event {Function} parse 解析完成事件
- * @event {Function} load dom 加载完成事件
- * @event {Function} ready 所有图片加载完毕事件
- * @event {Function} error 错误事件
- * @event {Function} imgtap 图片点击事件
- * @event {Function} linkpress 链接点击事件
- * @author JinYufeng
- * @version 20200719
- * @listens MIT
- name: 'parser',
- // #ifdef H5 || MP-360
- uid: this._uid,
- height: 1,
- // #ifndef APP-PLUS-NVUE
- showAm: '',
- nodes: []
- components: {
- trees
- props: {
- html: String,
- autopause: {
- type: Boolean,
- default: true
- autoscroll: Boolean,
- autosetTitle: {
- compress: Number,
- loadingImg: String,
- useCache: Boolean,
- domain: String,
- lazyLoad: Boolean,
- selectable: Boolean,
- tagStyle: Object,
- showWithAnimation: Boolean,
- useAnchor: Boolean
- watch: {
- html(html) {
- this.setContent(html);
- created() {
- // 图片数组
- this.imgList = [];
- this.imgList.each = function(f) {
- for (var i = 0, len = this.length; i < len; i++)
- this.setItem(i, f(this[i], i, this));
- this.imgList.setItem = function(i, src) {
- if (i == void 0 || !src) return;
- // #ifndef MP-ALIPAY || APP-PLUS
- // 去重
- if (src.indexOf('http') == 0 && this.includes(src)) {
- var newSrc = src.split('://')[0];
- for (var j = newSrc.length, c; c = src[j]; j++) {
- if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
- newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
- newSrc += src.substr(j);
- return this[i] = newSrc;
- this[i] = src;
- // 暂存 data src
- if (src.includes('data:image')) {
- var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
- if (!info) return;
- filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
- fs && fs.writeFile({
- filePath,
- data: info[3],
- encoding: info[2],
- success: () => this[i] = filePath
- // #ifdef APP-PLUS
- filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
- var bitmap = new plus.nativeObj.Bitmap();
- bitmap.loadBase64Data(src, () => {
- bitmap.save(filePath, {}, () => {
- bitmap.clear()
- this[i] = filePath;
- mounted() {
- this.document = document.getElementById('rtf' + this._uid);
- if (dom) this.document = new dom(this);
- this.document = this.$refs.web;
- if (this.html) this.setContent(this.html);
- }, 30)
- beforeDestroy() {
- if (this._observer) this._observer.disconnect();
- this.imgList.each(src => {
- if (src && src.includes('_doc')) {
- plus.io.resolveLocalFileSystemURL(src, entry => {
- entry.remove();
- if (src && src.includes(uni.env.USER_DATA_PATH))
- fs && fs.unlink({
- filePath: src
- clearInterval(this._timer);
- // 设置富文本内容
- setContent(html, append) {
- if (!html)
- return this.height = 1;
- if (append)
- this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
- "';document.getElementById('parser').appendChild(b)");
- else {
- html =
- '<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
- this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
- '</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
- (this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
- 'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
- this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
- this.$refs.web.evalJs(
- 'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
- windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
- 'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.src),a.onclick=function(){e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(){var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
- (this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
- ';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
- (this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
- 'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
- )
- this.nodes = [1];
- if (!html) {
- if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
- var div = document.createElement('div');
- if (!append) {
- if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
- this.rtf = div;
- if (!this.rtf) this.rtf = div;
- else this.rtf.appendChild(div);
- div.innerHTML = this._handleHtml(html, append);
- for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
- style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
- style.setAttribute('scoped', 'true');
- // 懒加载
- if (!this._observer && this.lazyLoad && IntersectionObserver) {
- this._observer = new IntersectionObserver(changes => {
- for (let item, i = 0; item = changes[i++];) {
- if (item.isIntersecting) {
- item.target.src = item.target.getAttribute('data-src');
- item.target.removeAttribute('data-src');
- this._observer.unobserve(item.target);
- }, {
- rootMargin: '500px 0px 500px 0px'
- var _ts = this;
- // 获取标题
- var title = this.rtf.getElementsByTagName('title');
- if (title.length && this.autosetTitle)
- uni.setNavigationBarTitle({
- title: title[0].innerText
- // 图片处理
- this.imgList.length = 0;
- var imgs = this.rtf.getElementsByTagName('img');
- for (let i = 0, j = 0, img; img = imgs[i]; i++) {
- if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
- img.style.height = 'auto';
- var src = img.getAttribute('src');
- if (this.domain && src) {
- if (src[0] == '/') {
- if (src[1] == '/')
- img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
- else img.src = this.domain + src;
- } else if (!src.includes('://')) img.src = this.domain + '/' + src;
- if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
- img.i = j++;
- _ts.imgList.push(img.src || img.getAttribute('data-src'));
- img.onclick = function() {
- var preview = true;
- this.ignore = () => preview = false;
- _ts.$emit('imgtap', this);
- if (preview) {
- uni.previewImage({
- current: this.i,
- urls: _ts.imgList
- img.onerror = function() {
- if (cfg.errorImg)
- _ts.imgList[this.i] = this.src = cfg.errorImg;
- _ts.$emit('error', {
- source: 'img',
- target: this
- if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
- img.setAttribute('data-src', img.src);
- img.removeAttribute('src');
- this._observer.observe(img);
- // 链接处理
- var links = this.rtf.getElementsByTagName('a');
- for (var link of links) {
- link.onclick = function() {
- var jump = true,
- href = this.getAttribute('href');
- _ts.$emit('linkpress', {
- href,
- ignore: () => jump = false
- if (jump && href) {
- if (href[0] == '#') {
- if (_ts.useAnchor) {
- _ts.navigateTo({
- id: href.substr(1)
- } else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
- else
- url: href
- // 视频处理
- var videos = this.rtf.getElementsByTagName('video');
- _ts.videoContexts = videos;
- for (let video, i = 0; video = videos[i++];) {
- video.style.maxWidth = '100%';
- video.onerror = function() {
- source: 'video',
- video.onplay = function() {
- if (_ts.autopause)
- for (let item, i = 0; item = _ts.videoContexts[i++];)
- if (item != this) item.pause();
- // 音频处理
- var audios = this.rtf.getElementsByTagName('audio');
- for (var audio of audios)
- audio.onerror = function() {
- source: 'audio',
- // 表格处理
- if (this.autoscroll) {
- var tables = this.rtf.getElementsByTagName('table');
- for (var table of tables) {
- let div = document.createElement('div');
- div.style.overflow = 'scroll';
- table.parentNode.replaceChild(div, table);
- div.appendChild(table);
- if (!append) this.document.appendChild(this.rtf);
- this.$nextTick(() => {
- this.$emit('load');
- setTimeout(() => this.showAm = '', 500);
- // #ifndef H5 || MP-360
- var nodes;
- if (!html) return this.nodes = [];
- var parser = new Parser(html, this);
- // 缓存读取
- if (this.useCache) {
- var hashVal = hash(html);
- if (cache[hashVal])
- nodes = cache[hashVal];
- nodes = parser.parse();
- cache[hashVal] = nodes;
- } else nodes = parser.parse();
- this.$emit('parse', nodes);
- if (append) this.nodes = this.nodes.concat(nodes);
- else this.nodes = nodes;
- if (nodes.length && nodes.title && this.autosetTitle)
- title: nodes.title
- if (this.imgList) this.imgList.length = 0;
- this.videoContexts = [];
- (function f(cs) {
- for (var i = cs.length; i--;) {
- if (cs[i].top) {
- cs[i].controls = [];
- cs[i].init();
- f(cs[i].$children);
- })(this.$children)
- var height;
- this._timer = setInterval(() => {
- this.rect = this.rtf.getBoundingClientRect();
- uni.createSelectorQuery().in(this)
- .select('#_top').boundingClientRect().exec(res => {
- if (!res) return;
- this.rect = res[0];
- if (this.rect.height == height) {
- this.$emit('ready', this.rect)
- height = this.rect.height;
- }, 350);
- if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
- // 获取文本内容
- getText(ns = this.nodes) {
- var txt = '';
- txt = this._text;
- txt = this.rtf.innerText;
- for (var i = 0, n; n = ns[i++];) {
- if (n.type == 'text') txt += n.text.replace(/ /g, '\u00A0').replace(/</g, '<').replace(/>/g, '>')
- .replace(/&/g, '&');
- else if (n.type == 'br') txt += '\n';
- // 块级标签前后加换行
- var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
- '0' && n.name[1] < '7');
- if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
- if (n.children) txt += this.getText(n.children);
- if (block && txt[txt.length - 1] != '\n') txt += '\n';
- else if (n.name == 'td' || n.name == 'th') txt += '\t';
- return txt;
- // 锚点跳转
- in (obj) {
- if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
- navigateTo(obj) {
- if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
- if (!obj.id)
- weexDom.scrollToElement(this.$refs.web);
- this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
- '");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
- obj.success && obj.success();
- var d = ' ';
- // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
- d = '>>>';
- var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
- '#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
- if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
- else selector.selectViewport().scrollOffset();
- selector.exec(res => {
- if (!res[0]) return obj.fail && obj.fail('Label not found')
- var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
- if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
- else uni.pageScrollTo({
- scrollTop,
- duration: 300
- // 获取视频对象
- getVideoContext(id) {
- if (!id) return this.videoContexts;
- for (var i = this.videoContexts.length; i--;)
- if (this.videoContexts[i].id == id) return this.videoContexts[i];
- _handleHtml(html, append) {
- // 处理 tag-style 和 userAgentStyles
- var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
- for (var item in cfg.userAgentStyles)
- style += `${item}{${cfg.userAgentStyles[item]}}`;
- for (item in this.tagStyle)
- style += `${item}{${this.tagStyle[item]}}`;
- style += '</style>';
- html = style + html;
- // 处理 rpx
- if (html.includes('rpx'))
- html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
- return html;
- _message(e) {
- // 接收 web-view 消息
- var d = e.detail.data[0];
- switch (d.action) {
- case 'load':
- this.height = d.height;
- this._text = d.text;
- break;
- case 'getTitle':
- if (this.autosetTitle)
- title: d.title
- case 'getImgList':
- for (var i = d.imgList.length; i--;)
- this.imgList.setItem(i, d.imgList[i]);
- case 'preview':
- d.img.ignore = () => preview = false;
- this.$emit('imgtap', d.img);
- if (preview)
- current: d.img.i,
- urls: this.imgList
- case 'linkpress':
- href = d.href;
- this.$emit('linkpress', {
- if (this.useAnchor)
- weexDom.scrollToElement(this.$refs.web, {
- offset: d.offset
- } else if (href.includes('://'))
- plus.runtime.openWeb(href);
- case 'error':
- if (d.source == 'img' && cfg.errorImg)
- this.imgList.setItem(d.target.i, cfg.errorImg);
- this.$emit('error', {
- source: d.source,
- target: d.target
- case 'ready':
- if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
- this.$emit('ready', res[0]);
- case 'click':
- this.$emit('click');
- this.$emit('tap');
-<style>
- @keyframes _show {
- 0% {
- opacity: 0;
- 100% {
- opacity: 1;
- /* #ifdef MP-WEIXIN */
- :host {
- overflow: scroll;
- -webkit-overflow-scrolling: touch;
@@ -1,97 +0,0 @@
-const cfg = require('./config.js'),
- isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
-function CssHandler(tagStyle) {
- var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
- for (var item in tagStyle)
- styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
- this.styles = styles;
-CssHandler.prototype.getStyle = function(data) {
- this.styles = new parser(data, this.styles).parse();
-CssHandler.prototype.match = function(name, attrs) {
- var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
- if (attrs.class) {
- var items = attrs.class.split(' ');
- for (var i = 0, item; item = items[i]; i++)
- if (tmp = this.styles['.' + item])
- matched += tmp + ';';
- if (tmp = this.styles['#' + attrs.id])
- return matched;
-module.exports = CssHandler;
-function parser(data, init) {
- this.data = data;
- this.floor = 0;
- this.i = 0;
- this.list = [];
- this.res = init;
- this.state = this.Space;
-parser.prototype.parse = function() {
- for (var c; c = this.data[this.i]; this.i++)
- this.state(c);
- return this.res;
-parser.prototype.section = function() {
- return this.data.substring(this.start, this.i);
-// 状态机
-parser.prototype.Space = function(c) {
- if (c == '.' || c == '#' || isLetter(c)) {
- this.start = this.i;
- this.state = this.Name;
- } else if (c == '/' && this.data[this.i + 1] == '*')
- this.Comment();
- else if (!cfg.blankChar[c] && c != ';')
- this.state = this.Ignore;
-parser.prototype.Comment = function() {
- this.i = this.data.indexOf('*/', this.i) + 1;
- if (!this.i) this.i = this.data.length;
-parser.prototype.Ignore = function(c) {
- if (c == '{') this.floor++;
- else if (c == '}' && !--this.floor) this.state = this.Space;
-parser.prototype.Name = function(c) {
- if (cfg.blankChar[c]) {
- this.list.push(this.section());
- this.state = this.NameSpace;
- } else if (c == '{') {
- this.Content();
- } else if (c == ',') {
- this.Comma();
- } else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
-parser.prototype.NameSpace = function(c) {
- if (c == '{') this.Content();
- else if (c == ',') this.Comma();
- else if (!cfg.blankChar[c]) this.state = this.Ignore;
-parser.prototype.Comma = function() {
- while (cfg.blankChar[this.data[++this.i]]);
- if (this.data[this.i] == '{') this.Content();
- this.start = this.i--;
-parser.prototype.Content = function() {
- this.start = ++this.i;
- if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
- var content = this.section();
- for (var i = 0, item; item = this.list[i++];)
- if (this.res[item]) this.res[item] += ';' + content;
- else this.res[item] = content;
@@ -1,534 +0,0 @@
- * html 解析器
- blankChar = cfg.blankChar,
- CssHandler = require('./CssHandler.js'),
- windowWidth = uni.getSystemInfoSync().windowWidth;
-var emoji;
-function MpHtmlParser(data, options = {}) {
- this.attrs = {};
- this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
- this.domain = options.domain;
- this.DOM = [];
- this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
- options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
- this.options = options;
- this.state = this.Text;
- this.STACK = [];
- // 工具函数
- this.bubble = () => {
- for (var i = this.STACK.length, item; item = this.STACK[--i];) {
- if (cfg.richOnlyTags[item.name]) {
- if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
- item.c = 1;
- this.decode = (val, amp) => {
- var i = -1,
- j, en;
- while (1) {
- if ((i = val.indexOf('&', i + 1)) == -1) break;
- if ((j = val.indexOf(';', i + 2)) == -1) break;
- if (val[i + 1] == '#') {
- en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
- if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
- en = val.substring(i + 1, j);
- if (cfg.entities[en] || en == amp)
- val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
- this.getUrl = url => {
- if (url[0] == '/') {
- if (url[1] == '/') url = this.options.prot + ':' + url;
- else if (this.domain) url = this.domain + url;
- } else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
- url = this.domain + '/' + url;
- return url;
- this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
- this.section = () => this.data.substring(this.start, this.i);
- this.parent = () => this.STACK[this.STACK.length - 1];
- this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
-MpHtmlParser.prototype.parse = function() {
- if (emoji) this.data = emoji.parseEmoji(this.data);
- if (this.state == this.Text) this.setText();
- while (this.STACK.length) this.popNode(this.STACK.pop());
- return this.DOM;
-// 设置属性
-MpHtmlParser.prototype.setAttr = function() {
- var name = this.attrName.toLowerCase(),
- val = this.attrVal;
- if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
- else if (val) {
- if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
- else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
- else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
- this.attrVal = '';
- while (blankChar[this.data[this.i]]) this.i++;
- if (this.isClose()) this.setNode();
- this.state = this.AttrName;
-// 设置文本节点
-MpHtmlParser.prototype.setText = function() {
- var back, text = this.section();
- if (!text) return;
- text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
- if (back) {
- this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
- let j = this.start + text.length;
- for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
- if (!this.pre) {
- // 合并空白符
- var tmp = [];
- for (let i = text.length, c; c = text[--i];)
- if (!blankChar[c] || (!blankChar[tmp[0]] && (c = ' '))) tmp.unshift(c);
- text = tmp.join('');
- this.siblings().push({
- type: 'text',
- text: this.decode(text)
-// 设置元素节点
-MpHtmlParser.prototype.setNode = function() {
- var node = {
- name: this.tagName.toLowerCase(),
- attrs: this.attrs
- close = cfg.selfClosingTags[node.name];
- if (!cfg.ignoreTags[node.name]) {
- // 处理属性
- var attrs = node.attrs,
- style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
- styleObj = {};
- if (attrs.id) {
- if (this.options.compress & 1) attrs.id = void 0;
- else if (this.options.useAnchor) this.bubble();
- if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
- switch (node.name) {
- case 'a':
- case 'ad': // #ifdef APP-PLUS
- case 'iframe':
- this.bubble();
- case 'font':
- if (attrs.color) {
- styleObj['color'] = attrs.color;
- attrs.color = void 0;
- if (attrs.face) {
- styleObj['font-family'] = attrs.face;
- attrs.face = void 0;
- if (attrs.size) {
- var size = parseInt(attrs.size);
- if (size < 1) size = 1;
- else if (size > 7) size = 7;
- var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
- styleObj['font-size'] = map[size - 1];
- attrs.size = void 0;
- case 'embed':
- // #ifndef APP-PLUS
- var src = node.attrs.src || '',
- type = node.attrs.type || '';
- if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
- node.name = 'video';
- else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
- '.aac'))
- node.name = 'audio';
- else break;
- if (node.attrs.autostart)
- node.attrs.autoplay = 'T';
- node.attrs.controls = 'T';
- case 'video':
- case 'audio':
- if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
- else this[`${node.name}Num`]++;
- if (node.name == 'video') {
- if (this.videoNum > 3)
- node.lazyLoad = 1;
- if (attrs.width) {
- styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
- attrs.width = void 0;
- if (attrs.height) {
- styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
- attrs.height = void 0;
- attrs.source = [];
- if (attrs.src) {
- attrs.source.push(attrs.src);
- attrs.src = void 0;
- case 'td':
- case 'th':
- if (attrs.colspan || attrs.rowspan)
- for (var k = this.STACK.length, item; item = this.STACK[--k];)
- if (item.name == 'table') {
- item.c = void 0;
- if (attrs.align) {
- styleObj['text-align'] = attrs.align;
- attrs.align = void 0;
- // 压缩 style
- var styles = style.split(';');
- style = '';
- for (var i = 0, len = styles.length; i < len; i++) {
- var info = styles[i].split(':');
- if (info.length < 2) continue;
- let key = info[0].trim().toLowerCase(),
- value = info.slice(1).join(':').trim();
- if (value.includes('-webkit') || value.includes('-moz') || value.includes('-ms') || value.includes('-o') || value.includes(
- 'safe'))
- style += `;${key}:${value}`;
- else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
- styleObj[key] = value;
- if (node.name == 'img') {
- if (attrs.src && !attrs.ignore) {
- if (this.bubble())
- attrs.i = (this.imgNum++).toString();
- else attrs.ignore = 'T';
- if (attrs.ignore) {
- style += ';-webkit-touch-callout:none';
- styleObj['max-width'] = '100%';
- var width;
- if (styleObj.width) width = styleObj.width;
- else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px';
- if (width) {
- styleObj.width = width;
- attrs.width = '100%';
- if (parseInt(width) > windowWidth) {
- styleObj.height = '';
- if (attrs.height) attrs.height = void 0;
- if (styleObj.height) {
- attrs.height = styleObj.height;
- } else if (attrs.height && !attrs.height.includes('%'))
- attrs.height += 'px';
- for (var key in styleObj) {
- var value = styleObj[key];
- if (!value) continue;
- if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
- // 填充链接
- if (value.includes('url')) {
- var j = value.indexOf('(');
- if (j++ != -1) {
- while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
- value = value.substr(0, j) + this.getUrl(value.substr(j));
- // 转换 rpx
- else if (value.includes('rpx'))
- value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
- else if (key == 'white-space' && value.includes('pre') && !close)
- this.pre = node.pre = true;
- style = style.substr(1);
- if (style) attrs.style = style;
- if (!close) {
- node.children = [];
- if (node.name == 'pre' && cfg.highlight) {
- this.remove(node);
- this.siblings().push(node);
- this.STACK.push(node);
- } else if (!cfg.filter || cfg.filter(node, this) != false)
- if (!close) this.remove(node);
- else if (node.name == 'source') {
- var parent = this.parent();
- if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
- parent.attrs.source.push(node.attrs.src);
- } else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
- if (this.data[this.i] == '/') this.i++;
- this.start = this.i + 1;
-// 移除标签
-MpHtmlParser.prototype.remove = function(node) {
- var name = node.name,
- j = this.i;
- // 处理 svg
- var handleSvg = () => {
- var src = this.data.substring(j, this.i + 1);
- if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
- var i = j;
- while (this.data[j] != '<') j--;
- src = this.data.substring(j, i).replace("viewbox", "viewBox") + src;
- if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
- parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
- name: 'img',
- attrs: {
- src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
- style: (/vertical[^;]+/.exec(node.attrs.style) || []).shift(),
- ignore: 'T'
- if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
- if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
- if (name == 'pre' || name == 'svg') this.i = j;
- else this.i = this.data.length;
- this.start = (this.i += 2);
- while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
- if (this.section().toLowerCase() == name) {
- // 代码块高亮
- if (name == 'pre') {
- this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
- .substr(this.i - 5);
- return this.i = j;
- } else if (name == 'style')
- this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
- else if (name == 'title')
- this.DOM.title = this.data.substring(j + 1, this.i - 7);
- if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
- if (name == 'svg') handleSvg();
-// 节点出栈处理
-MpHtmlParser.prototype.popNode = function(node) {
- // 空白符处理
- if (node.pre) {
- node.pre = this.pre = void 0;
- for (let i = this.STACK.length; i--;)
- if (this.STACK[i].pre)
- this.pre = true;
- var siblings = this.siblings(),
- len = siblings.length,
- childs = node.children;
- if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
- return siblings.pop();
- var attrs = node.attrs;
- // 替换一些标签名
- if (cfg.blockTags[node.name]) node.name = 'div';
- else if (!cfg.trustTags[node.name]) node.name = 'span';
- // 去除块标签前后空串
- if (node.name == 'div' || node.name == 'p' || node.name[0] == 't') {
- if (len > 1 && siblings[len - 2].text == ' ')
- siblings.splice(--len - 1, 1);
- if (childs.length && childs[childs.length - 1].text == ' ')
- childs.pop();
- // 处理列表
- if (node.c && (node.name == 'ul' || node.name == 'ol')) {
- if ((node.attrs.style || '').includes('list-style:none')) {
- for (let i = 0, child; child = childs[i++];)
- if (child.name == 'li')
- child.name = 'div';
- } else if (node.name == 'ul') {
- var floor = 1;
- if (this.STACK[i].name == 'ul') floor++;
- if (floor != 1)
- for (let i = childs.length; i--;)
- childs[i].floor = floor;
- for (let i = 0, num = 1, child; child = childs[i++];)
- if (child.name == 'li') {
- child.type = 'ol';
- child.num = ((num, type) => {
- if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
- if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
- if (type == 'i' || type == 'I') {
- num = (num - 1) % 99 + 1;
- var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
- ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
- res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
- if (type == 'i') return res.toLowerCase();
- return res;
- return num;
- })(num++, attrs.type) + '.';
- // 处理表格的边框
- if (node.name == 'table') {
- var padding = attrs.cellpadding,
- spacing = attrs.cellspacing,
- border = attrs.border;
- if (node.c) {
- attrs.style = (attrs.style || '') + ';display:table';
- if (!padding) padding = 2;
- if (!spacing) spacing = 2;
- if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
- if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
- if (border || padding || node.c)
- (function f(ns) {
- for (var i = 0, n; n = ns[i]; i++) {
- if (n.type == 'text') continue;
- var style = n.attrs.style || '';
- if (node.c && n.name[0] == 't') {
- n.c = 1;
- style += ';display:table-' + (n.name == 'th' || n.name == 'td' ? 'cell' : (n.name == 'tr' ? 'row' : 'row-group'));
- if (n.name == 'th' || n.name == 'td') {
- if (border) style = `border:${border}px solid gray;${style}`;
- if (padding) style = `padding:${padding}px;${style}`;
- } else f(n.children || []);
- if (style) n.attrs.style = style;
- })(childs)
- if (this.options.autoscroll) {
- var table = Object.assign({}, node);
- node.name = 'div';
- node.attrs = {
- style: 'overflow:scroll'
- node.children = [table];
- this.CssHandler.pop && this.CssHandler.pop(node);
- // 自动压缩
- if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
- siblings[len - 1] = childs[0];
-MpHtmlParser.prototype.Text = function(c) {
- if (c == '<') {
- var next = this.data[this.i + 1],
- if (isLetter(next)) {
- this.setText();
- this.state = this.TagName;
- } else if (next == '/') {
- if (isLetter(this.data[++this.i + 1])) {
- this.state = this.EndTag;
- } else this.Comment();
- } else if (next == '!' || next == '?') {
-MpHtmlParser.prototype.Comment = function() {
- var key;
- if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
- else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
- else key = '>';
- if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
- else this.i += key.length - 1;
-MpHtmlParser.prototype.TagName = function(c) {
- if (blankChar[c]) {
- this.tagName = this.section();
- } else if (this.isClose()) {
- this.setNode();
-MpHtmlParser.prototype.AttrName = function(c) {
- if (c == '=' || blankChar[c] || this.isClose()) {
- this.attrName = this.section();
- if (blankChar[c])
- while (blankChar[this.data[++this.i]]);
- if (this.data[this.i] == '=') {
- this.state = this.AttrValue;
- } else this.setAttr();
-MpHtmlParser.prototype.AttrValue = function(c) {
- if (c == '"' || c == "'") {
- this.start++;
- if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
- this.attrVal = this.section();
- this.i++;
- for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
- this.setAttr();
-MpHtmlParser.prototype.EndTag = function(c) {
- if (blankChar[c] || c == '>' || c == '/') {
- var name = this.section().toLowerCase();
- for (var i = this.STACK.length; i--;)
- if (this.STACK[i].name == name) break;
- if (i != -1) {
- var node;
- while ((node = this.STACK.pop()).name != name) this.popNode(node);
- this.popNode(node);
- } else if (name == 'p' || name == 'br')
- name,
- attrs: {}
- this.i = this.data.indexOf('>', this.i);
- if (this.i == -1) this.i = this.data.length;
- else this.state = this.Text;
-module.exports = MpHtmlParser;
@@ -1,93 +0,0 @@
-/* 配置文件 */
-// #ifdef MP-WEIXIN
-const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
-// #endif
- // 出错占位图
- errorImg: null,
- // 过滤器函数
- filter: null,
- // 代码高亮函数
- highlight: null,
- // 文本处理函数
- onText: null,
- // 实体编码列表
- entities: {
- quot: '"',
- apos: "'",
- semi: ';',
- nbsp: '\xA0',
- ensp: '\u2002',
- emsp: '\u2003',
- ndash: '–',
- mdash: '—',
- middot: '·',
- lsquo: '‘',
- rsquo: '’',
- ldquo: '“',
- rdquo: '”',
- bull: '•',
- hellip: '…'
- blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
- boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
- // 块级标签,将被转为 div
- blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,section' + (
- // #ifdef MP-WEIXIN
- canIUse ? '' :
- ',pre')),
- // 将被移除的标签
- ignoreTags: makeMap(
- 'area,base,canvas,frame,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'
- + (canIUse ? ',rp' : '')
- + ',iframe'
- ),
- // 只能被 rich-text 显示的标签
- richOnlyTags: makeMap('a,colgroup,fieldset,legend,table'
- + (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
- // 自闭合的标签
- selfClosingTags: makeMap(
- 'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
- // 信任的标签
- trustTags: makeMap(
- 'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
- + (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
- + ',embed,iframe'
- // 默认的标签样式
- userAgentStyles: {
- address: 'font-style:italic',
- big: 'display:inline;font-size:1.2em',
- blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
- caption: 'display:table-caption;text-align:center',
- center: 'text-align:center',
- cite: 'font-style:italic',
- dd: 'margin-left:40px',
- mark: 'background-color:yellow',
- pre: 'font-family:monospace;white-space:pre;overflow:scroll',
- s: 'text-decoration:line-through',
- small: 'display:inline;font-size:0.8em',
- u: 'text-decoration:underline'
-function makeMap(str) {
- var map = Object.create(null),
- list = str.split(',');
- for (var i = list.length; i--;)
- map[list[i]] = true;
- return map;
@@ -1,22 +0,0 @@
-var inline = {
- abbr: 1,
- b: 1,
- big: 1,
- code: 1,
- del: 1,
- em: 1,
- i: 1,
- ins: 1,
- label: 1,
- q: 1,
- small: 1,
- span: 1,
- strong: 1,
- sub: 1,
- sup: 1
- use: function(item) {
- return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
@@ -1,500 +0,0 @@
- <view :class="'interlayer '+(c||'')" :style="s">
- <block v-for="(n, i) in nodes" v-bind:key="i">
- <!--图片-->
- <view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap="imgtap">
- <rich-text v-if="controls[i]!=0" :nodes="[{attrs:{src:loading&&(controls[i]||0)<2?loading:(lazyLoad&&!controls[i]?placeholder:(controls[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
- <image class="_image" :src="lazyLoad&&!controls[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
- :show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
- @error="error" />
- <!--文本-->
- <text v-else-if="n.type=='text'" decode>{{n.text}}</text>
- <!--#ifndef MP-BAIDU-->
- <text v-else-if="n.name=='br'">\n</text>
- <!--视频-->
- <view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&controls[i]==undefined" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
- :style="n.attrs.style" :data-i="i" @tap="_loadVideo" />
- <video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||controls[i]==0"
- :controls="!n.attrs.autoplay||n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[controls[i]||0]"
- :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
- <!--音频-->
- <audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
- :autoplay="n.attrs.autoplay" :controls="!n.attrs.autoplay||n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
- :src="n.attrs.source[controls[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio"
- @error.native="error" @play.native="play" />
- <!--链接-->
- <view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
- :data-attrs="n.attrs" @tap="linkpress">
- <trees class="_span" c="_span" :nodes="n.children" />
- <!--广告-->
- <!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
- <!--列表-->
- <view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex'">
- <view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
- <view v-else class="_ul-bef">
- <view v-if="n.floor%3==0" class="_ul-p1">█</view>
- <view v-else-if="n.floor%3==2" class="_ul-p2" />
- <view v-else class="_ul-p1" style="border-radius:50%">█</view>
- <trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
- <!--表格-->
- <view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
- <view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
- <view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
- <trees v-if="tr.name=='td'" :nodes="tr.children" />
- <trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
- :s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
- <!--#ifdef APP-PLUS-->
- <iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
- :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
- <embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
- <!--富文本-->
- <!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
- <rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
- <!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
- <rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
- <trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
- :style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
-<script module="handler" lang="wxs" src="./handler.wxs"></script>
- global.Parser = {};
- import trees from './trees'
- const errorImg = require('../libs/config.js').errorImg;
- name: 'trees',
- controls: [],
- placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
- errorImg,
- loadVideo: typeof plus == 'undefined',
- // #ifndef MP-ALIPAY
- c: '',
- s: ''
- nodes: Array,
- loading: String,
- // #ifdef MP-ALIPAY
- c: String,
- s: String
- for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
- this.init();
- this.observer && this.observer.disconnect();
- init() {
- for (var i = this.nodes.length, n; n = this.nodes[--i];) {
- if (n.name == 'img') {
- this.top.imgList.setItem(n.attrs.i, n.attrs.src);
- if (this.lazyLoad && !this.observer) {
- this.observer = uni.createIntersectionObserver(this).relativeToViewport({
- top: 500,
- bottom: 500
- this.observer.observe('._img', res => {
- if (res.intersectionRatio) {
- for (var j = this.nodes.length; j--;)
- if (this.nodes[j].name == 'img')
- this.$set(this.controls, j, 1);
- this.observer.disconnect();
- }, 0)
- } else if (n.name == 'video' || n.name == 'audio') {
- var ctx;
- if (n.name == 'video') {
- ctx = uni.createVideoContext(n.attrs.id
- // #ifndef MP-BAIDU
- , this
- );
- } else if (this.$refs[n.attrs.id])
- ctx = this.$refs[n.attrs.id][0];
- if (ctx) {
- ctx.id = n.attrs.id;
- this.top.videoContexts.push(ctx);
- // APP 上避免 video 错位需要延时渲染
- this.loadVideo = true;
- play(e) {
- var contexts = this.top.videoContexts;
- if (contexts.length > 1 && this.top.autopause)
- for (var i = contexts.length; i--;)
- if (contexts[i].id != e.currentTarget.dataset.id)
- contexts[i].pause();
- imgtap(e) {
- var attrs = e.currentTarget.dataset.attrs;
- if (!attrs.ignore) {
- var preview = true,
- data = {
- id: e.target.id,
- src: attrs.src,
- ignore: () => preview = false
- };
- global.Parser.onImgtap && global.Parser.onImgtap(data);
- this.top.$emit('imgtap', data);
- var urls = this.top.imgList,
- current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
- current,
- urls
- loadImg(e) {
- var i = e.currentTarget.dataset.i;
- if (this.lazyLoad && !this.controls[i]) {
- // #ifdef QUICKAPP-WEBVIEW
- this.$set(this.controls, i, 0);
- this.$nextTick(function() {
- this.$set(this.controls, i, 1);
- } else if (this.loading && this.controls[i] != 2) {
- this.$set(this.controls, i, 2);
- linkpress(e) {
- attrs = e.currentTarget.dataset.attrs;
- attrs.ignore = () => jump = false;
- global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
- this.top.$emit('linkpress', attrs);
- if (jump) {
- if (attrs['app-id']) {
- return uni.navigateToMiniProgram({
- appId: attrs['app-id'],
- path: attrs.path
- if (attrs.href) {
- if (attrs.href[0] == '#') {
- if (this.top.useAnchor)
- this.top.navigateTo({
- id: attrs.href.substring(1)
- } else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
- plus.runtime.openWeb(attrs.href);
- uni.setClipboardData({
- data: attrs.href,
- success: () =>
- title: '链接已复制'
- } else
- url: attrs.href,
- fail() {
- uni.switchTab({
- error(e) {
- var target = e.currentTarget,
- source = target.dataset.source,
- i = target.dataset.i;
- if (source == 'video' || source == 'audio') {
- // 加载其他 source
- var index = this.controls[i] ? this.controls[i].i + 1 : 1;
- if (index < this.nodes[i].attrs.source.length)
- this.$set(this.controls, i, index);
- if (e.detail.__args__)
- e.detail = e.detail.__args__[0];
- } else if (errorImg && source == 'img') {
- this.top.imgList.setItem(target.dataset.index, errorImg);
- this.$set(this.controls, i, 3);
- this.top && this.top.$emit('error', {
- source,
- target,
- errMsg: e.detail.errMsg
- _loadVideo(e) {
- this.$set(this.controls, e.target.dataset.i, 0);
- /* 在这里引入自定义样式 */
- /* 链接和图片效果 */
- ._a {
- display: inline;
- padding: 1.5px 0 1.5px 0;
- color: #366092;
- word-break: break-all;
- ._hover {
- text-decoration: underline;
- ._img {
- display: inline-block;
- max-width: 100%;
- /* #ifndef MP-ALIPAY || APP-PLUS */
- .interlayer {
- display: inherit;
- flex-direction: inherit;
- flex-wrap: inherit;
- align-content: inherit;
- align-items: inherit;
- justify-content: inherit;
- width: 100%;
- white-space: inherit;
- ._b,
- ._strong {
- font-weight: bold;
- /* #ifndef MP-ALIPAY */
- ._blockquote,
- ._div,
- ._p,
- ._ol,
- ._ul,
- ._li {
- ._code {
- font-family: monospace;
- ._del {
- text-decoration: line-through;
- ._em,
- ._i {
- font-style: italic;
- ._h1 {
- font-size: 2em;
- ._h2 {
- font-size: 1.5em;
- ._h3 {
- font-size: 1.17em;
- ._h5 {
- font-size: 0.83em;
- ._h6 {
- font-size: 0.67em;
- ._h1,
- ._h2,
- ._h3,
- ._h4,
- ._h5,
- ._image {
- height: 360px;
- margin-top: -360px;
- ._ins {
- ._ol-bef {
- width: 36px;
- margin-right: 5px;
- text-align: right;
- ._ul-bef {
- margin: 0 12px 0 23px;
- line-height: normal;
- ._ol-bef,
- ._ul_bef {
- flex: none;
- user-select: none;
- ._ul-p1 {
- width: 0.3em;
- height: 0.3em;
- line-height: 0.3em;
- ._ul-p2 {
- width: 0.23em;
- height: 0.23em;
- border: 0.05em solid black;
- ._q::before {
- content: '"';
- ._q::after {
- ._sub {
- font-size: smaller;
- vertical-align: sub;
- ._sup {
- vertical-align: super;
- /* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
- ._abbr,
- ._code,
- ._del,
- ._i,
- ._ins,
- ._label,
- ._q,
- ._span,
- ._strong,
- ._sub,
- /* #ifdef MP-WEIXIN || MP-QQ */
- .__bdo,
- .__bdi,
- .__ruby,
- .__rt {
- ._video {
- width: 300px;
- height: 225px;
- background-color: black;
- ._video::after {
- top: 50%;
- left: 50%;
- margin: -15px 0 0 -15px;
- border-color: transparent transparent transparent white;
- border-style: solid;
- border-width: 15px 0 15px 30px;
@@ -1,55 +0,0 @@
-/* 下拉刷新区域 */
-.mescroll-downwarp {
- top: -100%;
- text-align: center;
-/* 下拉刷新--内容区,定位于区域底部 */
-.mescroll-downwarp .downwarp-content {
- min-height: 60rpx;
- padding: 20rpx 0;
-/* 下拉刷新--提示文本 */
-.mescroll-downwarp .downwarp-tip {
- font-size: 28rpx;
- vertical-align: middle;
- margin-left: 16rpx;
- /* color: gray; 已在style设置color,此处删去*/
-/* 下拉刷新--旋转进度条 */
-.mescroll-downwarp .downwarp-progress {
- width: 32rpx;
- height: 32rpx;
- 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);
@@ -1,47 +0,0 @@
-<!-- 下拉刷新区域 -->
- <view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
- <view class="downwarp-content">
- <view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
- <view class="downwarp-tip">{{downText}}</view>
-export default {
- option: Object , // down的配置项
- type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
- rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
- computed: {
- // 支付宝小程序需写成计算属性,prop定义default仍报错
- mOption(){
- return this.option || {}
- // 是否在加载中
- isDownLoading(){
- return this.type === 3
- // 旋转的角度
- downRotate(){
- return 'rotate(' + 360 * this.rate + 'deg)'
- // 文本提示
- downText(){
- 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;
-};
-@import "./mescroll-down.css";
@@ -1,27 +0,0 @@
- <view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
- <mix-empty :type="option.type" :backgroundColor="option.backgroundColor"></mix-empty>
- import mixEmpty from '@/components/mix-empty/mix-empty.vue'
- mixEmpty
- // empty的配置项: 默认为GlobalOption.up.empty
- option: {
- type: Object,
- default() {
- return {};
-<style scoped lang="scss">
@@ -1,95 +0,0 @@
-<!--空布局
-可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
-import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
-<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
--->
- <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="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
-// 引入全局配置
-import GlobalOption from './../mescroll-uni-option.js';
- // 使用computed获取配置,用于支持option的动态配置
- // 图标
- icon() {
- return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
- tip() {
- return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
- // 点击按钮
- emptyClick() {
- this.$emit('emptyclick');
-/* 无任何数据的空布局 */
-.mescroll-empty {
- padding: 30vh 50rpx 100rpx;
-.mescroll-empty.empty-fixed {
- z-index: 99;
- position: absolute; /*transform会使fixed失效,最终会降级为absolute */
- top: 100rpx;
-.mescroll-empty .empty-icon {
- width: 170rpx;
- height: 170rpx;
- transform: translateX(16rpx);
-.mescroll-empty .empty-tip {
- margin-top: 20rpx;
- font-size: 24rpx;
- color: #666;
-.mescroll-empty .empty-btn {
- margin-top: 40rpx;
- min-width: 200rpx;
- padding: 18rpx;
- border: 1rpx solid #e04b28;
- border-radius: 60rpx;
- color: #e04b28;
-.mescroll-empty .empty-btn:active {
- opacity: 0.75;
@@ -1,83 +0,0 @@
-<!-- 回到顶部的按钮 -->
- <image
- 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
- toTopClick() {
- this.$emit('input', false); // 使v-model生效
- this.$emit('click'); // 派发点击事件
-/* 回到顶部的按钮 */
-.mescroll-totop {
- z-index: 9990;
- position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
- right: 20rpx;
- bottom: 120rpx;
- width: 72rpx;
- height: auto;
- transition: opacity 0.5s; /* 过渡 */
- margin-bottom: var(--window-bottom); /* css变量 */
-/* 适配 iPhoneX */
-@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
- .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 .upwarp-progress {
-.mescroll-upwarp .mescroll-rotate {
- animation: mescrollUpRotate 0.6s linear infinite;
-@keyframes mescrollUpRotate {
@@ -1,39 +0,0 @@
-<!-- 上拉加载区域 -->
- <view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
- <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
- <view v-show="isUpLoading">
- <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></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(没有更多了)
- mOption() {
- return this.option || {};
- // 加载中
- isUpLoading() {
- return this.type === 1;
- // 没有更多了
- isUpNoMore() {
- return this.type === 2;
-@import './mescroll-up.css';
-.mescroll-body {
- position: relative; /* 下拉刷新区域相对自身定位 */
- height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
- overflow: hidden; /* 遮住顶部下拉刷新区域 */
- box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
- .mescroll-safearea {
@@ -1,344 +0,0 @@
- <view
- class="mescroll-body mescroll-render-touch"
- :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" :change:prop="renderBiz.propObserver" :prop="wxsProp">
- <!-- <view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> -->
- <view class="downwarp-tip">
- <image v-show="downText === '下拉刷新'" style="width:80rpx;height:86rpx" src="/static/loading/hamster.png"></image>
- <image v-show="downText !== '下拉刷新'" style="width:80rpx;height:86rpx" src="/static/loading/hamster.gif"></image>
- <!-- {{downText}} -->
- <!-- 列表内容 -->
- <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}">
- <view v-show="upLoadType===1" :style="{height: upLoadType===1 ? 'auto' : 0}" style="display: flex;align-items: center;justify-content: center;overflow: hidden">
- <!-- <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> -->
- <image style="width: 64rpx;height: 68rpx" src="/static/loading/hamster.gif"></image>
- <view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
- <!-- <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> -->
- <view v-if="upLoadType===2" class="mix-nodata center">
- <image class="logo" src="/static/logo-b-w.png"></image>
- <text>国云网络提供技术支持</text>
- <!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
- <!-- #ifdef H5 -->
- <!-- <view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> -->
- <!-- #endif -->
- <!-- 适配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> -->
-<!-- 微信小程序, app, h5使用wxs -->
-<!-- #ifdef MP-WEIXIN || APP-PLUS || H5-->
-<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
-<!-- #endif -->
-<!-- app, h5使用renderjs -->
-<!-- #ifdef APP-PLUS || H5 -->
-<script module="renderBiz" lang="renderjs">
- import renderBiz from './wxs/renderjs.js';
- mixins: [renderBiz]
- // 引入mescroll-uni.js,处理核心逻辑
- import MeScroll from './mescroll-uni.js';
- // 引入全局配置
- import GlobalOption from './mescroll-uni-option.js';
- // 引入空布局组件
- import MescrollEmpty from './components/mescroll-empty.vue';
- // 引入回到顶部组件
- import MescrollTop from './components/mescroll-top.vue';
- // 引入兼容wxs(含renderjs)写法的mixins
- import WxsMixin from './wxs/mixins.js';
- mixins: [WxsMixin],
- MescrollEmpty,
- MescrollTop
- 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 // 状态栏高度
- down: Object, // 下拉刷新的参数配置
- up: 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页生效)
- // 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之外
- return this.downLoadType === 3
- return 'rotate(' + 360 * this.downRate + 'deg)'
- 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.optDown.textLoading;
- 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;
- // 点击空布局的按钮回调
- this.$emit('emptyclick', this.mescroll);
- // 点击回到顶部的按钮回调
- 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: {
- auto: false,
- 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组件时,此行不可删)
- 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;
- // 空布局
- empty: {
- onShow(isShow) {
- // 显示隐藏的回调
- vm.isShowEmpty = isShow;
- // 回到顶部
- toTop: {
- vm.isShowToTop = isShow;
- // 派发上拉加载的回调
- vm.$emit('up', mescroll);
- MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
- 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为滚动区域
- // 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必须为元素的id,不带#)
- setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
- uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
- let top = rect.top
- top += vm.mescroll.getScrollTop()
- uni.pageScrollTo({
- scrollTop: top,
- duration: t
- }).exec()
- },30)
- } else{
- // 滚动到指定位置 (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;
- @import "./mescroll-body.css";
- @import "./components/mescroll-down.css";
- @import './components/mescroll-up.css';
- .mix-nodata{
- height: 52rpx;
- font-size: 26rpx;
- color: #999;
- .logo{
- width: 34rpx;
- height: 34rpx;
- margin-right: 12rpx;
@@ -1,65 +0,0 @@
-// mescroll-body 和 mescroll-uni 通用
-// import MescrollUni from "./mescroll-uni.vue";
-// import MescrollBody from "./mescroll-body.vue";
-const MescrollMixin = {
- // components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
- // MescrollUni,
- // MescrollBody
- // },
- 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对象 (兼容字节跳动小程序: http://www.mescroll.com/qa.html?v=20200107#q26)
- 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()
- }else{
- this.mescroll.endSuccess();
- }, 500)
- // 上拉加载的回调
- upCallback() {
- // mixin默认延时500自动结束加载
- this.mescroll.endErr();
- this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
-export default MescrollMixin;
@@ -1,33 +0,0 @@
-// 全局配置
-const GlobalOption = {
- // 其他down的配置参数也可以写,这里只展示了常用的配置:
- textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
- textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
- textLoading: '加载中 ...', // 加载中的提示文本
- offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
- native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
- // 其他up的配置参数也可以写,这里只展示了常用的配置:
- textNoMore: '- 我也是有底线的 -', // 没有更多数据的提示文本
- offset: 80, // 距底部多远时,触发upCallback
- // 回到顶部按钮,需配置src才显示
- src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入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: "/static/empty/hamster.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
- tip: '~ 空空如也 ~' // 提示
-export default GlobalOption
@@ -1,36 +0,0 @@
-.mescroll-uni-warp{
-.mescroll-uni-content{
-.mescroll-uni {
- min-height: 200rpx;
- overflow-y: auto;
-/* 定位的方式固定高度 */
-.mescroll-uni-fixed{
- z-index: 1;
- position: fixed;
- width: auto; /* 使right生效 */
- height: auto; /* 使bottom生效 */
@@ -1,788 +0,0 @@
-/* mescroll
- * version 1.3.0
- * 2020-07-10 wenju
- * http://www.mescroll.com
-export default function MeScroll(options, isScrollBody) {
- let me = this;
- me.version = '1.3.0'; // 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度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
- 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){ }
- page: {
- num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
- size: 10, // 每页数据的数量
- time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
- noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
- textNoMore: '-- 我也是有底线的 --', // 没有更多数据的提示文本
- 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);
-/* 列表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.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();
- return
- // 结束下拉刷新的方法
- 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 (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.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) {
- 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()
@@ -1,408 +0,0 @@
- <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-into-view="scrollToViewId" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true">
- <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">
- <view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
- <view v-show="upLoadType===1">
- <view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
- <view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
- </scroll-view>
- <!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
- <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
- mixins:[renderBiz]
- 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, // 可使用窗口的顶部位置
- statusBarHeight: 0, // 状态栏高度
- scrollToViewId: '' // 滚动到指定view的id
- fixed: { // 是否通过fixed固定mescroll的高度, 默认true
- height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
- // 是否使用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
- // 列表是否可滑动
- scrollable(){
- 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已渲染
- let query = uni.createSelectorQuery();
- query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
- let view = query.select('#' + this.viewId);
- view.boundingClientRect(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)
- }).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,则不使用动画过渡
- if(typeof y === 'string'){ // 第一个参数如果为字符串,则使用scroll-into-view
- // 微信小程序暂不支持slot里面的scroll-into-view,只能计算位置实现
- uni.createSelectorQuery().select('#'+vm.viewId).boundingClientRect(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
- // #ifndef MP-WEIXIN
- if (vm.scrollToViewId != y) {
- vm.scrollToViewId = y;
- vm.scrollToViewId = ''; // scrollToViewId必须变化才会生效,所以此处先置空再赋值
- vm.$nextTick(function(){
- if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
- vm.scrollTop = y
- vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
- vm.scrollTop = step
- }, t)
- // 设置容器的高度
- @import "./mescroll-uni.css";
- * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
- * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
- * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
-const MescrollCompMixin = {
- // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
- let item = this.$refs["mescrollItem"];
- if(item && item.mescroll) item.mescroll.onPageScroll(e);
- if(item && item.mescroll) item.mescroll.onReachBottom();
- // 当down的native: true时, 还需传递此方法进到子组件
- if(item && item.mescroll) item.mescroll.onPullDownRefresh();
-export default MescrollCompMixin;
@@ -1,51 +0,0 @@
- * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
-const MescrollMoreItemMixin = {
- // 支付宝小程序不支持props的mixin,需写在具体的页面中
- props:{
- i: Number, // 每个tab页的专属下标
- index: { // 当前tab的下标
- type: Number,
- default(){
- return 0
- downOption:{
- auto:false // 不自动加载
- upOption:{
- isInit: false // 当前tab是否已初始化
- watch:{
- // 监听下标的变化
- index(val){
- if (this.i === val && !this.isInit) {
- this.isInit = true; // 标记为true
- this.mescroll && this.mescroll.triggerDownScroll();
- // mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
- this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
- // 自动加载当前tab的数据
- if(this.i === this.index){
- this.mescroll.triggerDownScroll();
-export default MescrollMoreItemMixin;
@@ -1,56 +0,0 @@
-const MescrollMoreMixin = {
- tabIndex: 0 // 当前tab下标
- let mescroll = this.getMescroll(this.tabIndex);
- mescroll && mescroll.onPageScroll(e);
- mescroll && mescroll.onReachBottom();
- mescroll && mescroll.onPullDownRefresh();
- methods:{
- // 根据下标获取对应子组件的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;
-// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果, 适用于h5和renderjs (下拉刷新时禁止)
-const bounce = {
- // false: 禁止bounce; true:允许bounce
- setBounce: function(isBounce){
- window.$isMescrollBounce = isBounce
-// 引入即自动初始化 (仅初始化一次)
-if(window && window.$isMescrollBounce == null){
- // 是否允许bounce, 默认允许
- window.$isMescrollBounce = true
- // 每次点击时重置bounce
- window.addEventListener('touchstart', function(){
- }, {passive: true})
- // 滑动中标记是否禁止bounce (如:下拉刷新时禁止)
- window.addEventListener('touchmove', function(e){
- !window.$isMescrollBounce && e.preventDefault() // 禁止bounce
- }, {passive: false})
-export default bounce;
@@ -1,102 +0,0 @@
-// 定义在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 || 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
- }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 || 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的方法 (值改变才触发更新)
- if(this.downLoadType === 4 || this.downLoadType === 0){
- this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
- },320)
- // 初始化wxs的数据
- this.wxsCall({type: 'setWxsProp'})
-export default WxsMixin;
@@ -1,92 +0,0 @@
-// 使用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); // 记录起点
- 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();
-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;
@@ -1,267 +0,0 @@
-// 使用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({
- 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
- 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){
- 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) {
- if (me.disabled()) return true;
- 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
@@ -1,82 +0,0 @@
- <uni-popup ref="popup" type="bottom">
- <view class="content">
- <view v-if="data.title" class="cell b-b center title">
- <text >{{ data.title }}</text>
- <view class="cell b-b center" v-for="(item, index) in data.list" :key="index" @click="confirm(item)">
- <text>{{ item.text }}</text>
- <view class="cell center cancel-btn" @click="close">
- <text>取消</text>
- </uni-popup>
- * 底部选择菜单
- data: {}
- //选择回调
- confirm(item){
- this.$emit('onConfirm', item)
- this.close();
- open(data){
- this.$refs.popup.open();
- close(){
- this.$refs.popup.close();
- .content{
- background-color: #fff;
- border-radius: 16rpx 16rpx 0 0;
- .cell{
- min-height: 88rpx;
- font-size: 32rpx;
- color: #333;
- &:after{
- border-bottom: 1px solid #f5f5f5;
- &:last-child{
- height: 96rpx;
- border-top: 12rpx solid #f7f7f7;
- &.title{
- height: 100rpx;
@@ -1,154 +0,0 @@
- class="mix-btn-content"
- :class="{
- disabled: loading || disabled || dead,
- }"
- :style="[
- {marginTop: marginTop}
- ]"
- @click="confirm"
- <image v-if="loading" class="loading-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAE3UlEQVRoQ82Z7ZEQNwyGrQoCFQQqgFRAqCBcBSEVQCqAqyBJBTkqACoAKsilAo4KQioQ8+zIO16tvbaWM4P/7lrSq2/JkiYeVX2SUnpmLP4SkTez2MkswgbitaN/MQvMTCBo/xcH5IOI/DxDeWEgqvq0cJfLloZV9X1K6VEEiKo+NNpvo5YLAYm4SxSIqt5JKf2TUrqXUvosIncjlosCqbnLTUrpJxH5XDI+AeRlSumF0Qi7YBTIn4VblXLjYgiynggQVcUKWAOrcB6LCK45fKJAYPixQh1rYBWss5wgELIbqZrzSkSIw9AJATEBW1Z5IyIXUSCqShZ7Z/f+Tyk9LBUyiuYMEMyP5n+oMFldYtQiqopLka04OxedBsSsgun/rjC5EZH7o65lqTzT+WTW2CSN00CKtgKCtBXVoGtoHL6/iciVql6llH51glAflliwdEu85QBf7vUEt8SQvPttXMv5a6YJcUy+BrIJUvp2yX9JnQ1apeuVsdZNtwacvi1nx02744GUubwUDuvAGAutpm9ofRXKwOQMdJWta0L9VzAg4123rKGqWBbZyJr5/C4iyLQcD+R5SumPA/NilZci8qpwDx/43cbQ3COncZQD390xRVAkfX8G/+elUj0Q/JWYeNDxVf5BI9cmFFpHW6vWB3ydTEWq3cWFWQyF+nrywRS5i9tq+rVsgtlqKbaUsav9HiD/XVWxABbKSYBfqC9YoJkMmnXEtIJf5sGoJlM3SEeBWOdLhS/jgOuXxKfv5TzdbkE010ETviWH1m0C8YnmrVlhky1biukCyRct8AD0Y0Hs1lzL6hf0yYpPpzaNlqlyYL+PMht1szP/DVvkDPFveWcBYnHg24mWHPRTSx35no5YtqADjZxmEYsQqf1rsYJSy/R7RJaO4BIgrbbkUCYRmeKWqkrrMgoiy7gA6bUlNUCfRMTn+681xnJfVdFwr7PwvC4AAnqWCrU6UROOKvtkZsayVD+qGGL2ZnUPA5QntRYR1jTNLnWU84z/pvj5DEF7NMNArKmjraZ1oAM+NZp6wcwjyFb/nnHbYSCWFmmtyyBfR9eexnrf3ZBGzKKkoT4L2l0gB8MN92c2jdCnNGym0pZCjtp4NM9scLQsO72+aRRDBKcclHMQrssscthN7IAUQ74fbkrerG4gfusPN8afoc63TEyFKK661amNumz9WmmYGsKQ4/e8xA7AYUJr300AqsodVkMIVxt3SSjw8fWtutWJLB92A79VYt/ibLYb1suxh1rrj1sV7fbGpelt7IZHOQdxh9XSSnMECAM/brQrhJUlGzKsQFz7s8aT3Su3L4eLa/sfi+dnhw2fXdZy7Uo3Dip7LVzvXnYtt43cZLhKs9p9SrBxI7t1ex3Uy/XO5MSRb/83a88OEHo8rJxdhonzcUSG8t9uHWkRVlWSQrk429WUIyAWX34ZPrT/rcl0CkjjLXHnGj0gBqZ8NK0+441Y6SwQ1p1lq1IN1kEgfhl+qsiGgVSCtPnKNALErFI+QRym43CL0mghCNDyTYPfmhoMAMG6BH5uTcLviCGLVBYVh69Mo0BqhTW6EwgBMYb5rb078gaBlC8B4a46DMTAEKDXvZ4qAiS7Mu3L1MFqJAVWpr4ytYa1HOF5yiKjDFq91uj9yH9TgZgbLiPB7O3LF2dInShyDo35AAAAAElFTkSuQmCC"></image>
- <text v-if="icon" class="mix-icon" :class="icon" :style="{fontSize: iconSize + 'rpx'}"></text>
- <text class="mix-text">{{ text }}</text>
- * 按钮组件
- * @prop text 按钮显示文字
- * @prop icon 按钮图标
- * @prop iconSize 按钮显示文字
- * @prop isConfirm 点击后是否处理loading状态
- * @prop disabled 按钮禁用
- * @prop marginTop 按钮上边距
- let stopTimer = null;
- name: 'MixButton',
- dead: false,
- loading: false
- text: {
- type: String,
- default: '提交'
- icon: {
- default: ''
- iconSize: {
- default: 32
- isConfirm: {
- disabled: {
- default: false
- marginTop: {
- default: '0rpx'
- stop(){
- if(stopTimer){
- clearTimeout(stopTimer);
- stopTimer = null;
- this.loading = false;
- death(){
- this.dead = true;
- confirm(){
- if(this.dead){
- if(this.loading || this.disabled){
- if(this.isConfirm){
- this.loading = true;
- stopTimer = setTimeout(()=>{
- }, 10000)
- this.$emit('onConfirm');
-<style scoped lang='scss'>
- .mix-btn-content{
- width: 610rpx;
- height: 88rpx;
- margin: 0 auto;
- color: #fff;
- background-color: $base-color;
- top: 25%;
- transform: translateX(-50%);
- width: 85%;
- height: 85%;
- background: linear-gradient(131deg, rgba(255,115,138,1) 0%, rgba(255,83,111,1) 100%);
- opacity: 0.4;
- filter:blur(10rpx);
- &.disabled {
- opacity: .65;
- .mix-text{
- .mix-icon{
- margin-right: 8rpx;
- .loading-icon{
- transform-origin:50% 50%;
- margin-right: 16rpx;
- animation: rotate 2s linear infinite;
- @keyframes rotate{
- from {
- transform:rotate(0deg)
- to {
- transform:rotate(360deg)
@@ -1,113 +0,0 @@
- <view class="mix-get-code" @click="getCode">
- <view v-if="loading" class="loading">
- <mix-icon-loading size="28rpx" color="#0083ff"></mix-icon-loading>
- <text class="text" :class="{disabled: timeDown > 0}">
- {{ timeDown > 0 ? '重新获取 ' + timeDown + 's' : '获取验证码' }}
- </text>
- * 手机验证码
- * @prop mobile 手机号
- * @prop templateCode 短信模版id
- import {checkStr} from '@/common/js/util'
- //获取手机验证码
- name: 'MixMobileCode',
- loading: false,
- timeDown: ''
- mobile: {
- templateCode: {
- action: {
- default: '用户注册' //设置支付密码
- //获取验证码
- async getCode(){
- if(this.timeDown > 0){
- const mobile = this.mobile || this.$store.state.userInfo.username;;
- if(!checkStr(mobile, 'mobile')){
- this.$util.msg('手机号码格式不正确');
- this.$request('smsCode', 'send', {
- mobile,
- action: this.action, //uni短信必填
- TemplateCode: this.templateCode, //阿里云必填
- }).then(response=>{
- this.$util.msg(response.msg);
- if(response.status === 1){
- this.countDown(60);
- }).catch(err=>{
- this.$util.msg('验证码发送失败');
- console.log(err);
- }, 2000)
- //倒计时
- countDown(timer){
- timer --;
- this.timeDown = timer;
- if(timer > 0){
- this.countDown(timer);
- .mix-get-code{
- flex-shrink: 0;
- height: 36rpx;
- &:before{
- height: 40;
- border-right: 1px solid #f0f0f0;
- .loading{
- .text{
- line-height: 28rpx;
- color: #40a2ff;
- &.disabled{
- color: #ccc;
@@ -1,209 +0,0 @@
- <view class="mix-empty" :style="{backgroundColor: backgroundColor}">
- <view v-if="type==='cart'" class="cart column center">
- <image class="icon" src="/static/empty/cart.png"></image>
- <text class="title">{{ hasLogin ? '空空如也' : '先去登录嘛' }}</text>
- <text class="text">别忘了买点什么犒赏一下自己哦</text>
- <view class="btn center" @click="onCartBtnClick">
- <text>{{ hasLogin ? '随便逛逛' : '去登录' }}</text>
- <view v-else-if="type==='address'" class="address column center">
- <image class="icon" src="/static/empty/address.png"></image>
- <text class="text">找不到您的收货地址哦,先去添加一个吧~</text>
- <view class="btn center" @click="navTo('manage')">
- <text class="mix-icon icon-jia2"></text>
- <view v-else-if="type==='favorite'" class="favorite column center">
- <image class="icon" src="/static/empty/favorite.png"></image>
- <text class="text">收藏夹空空的,先去逛逛吧~</text>
- <view class="btn center" @click="switchTab('/pages/tabbar/home')">
- <text>随便逛逛</text>
- <view v-else class="default column center">
- <image class="icon" src="/static/empty/default.png"></image>
- <text class="text">{{ text }}</text>
- * 缺省显示
- * @prop text 缺省文字提示
- * @prop type 缺省类型
- * @prop backgroundColor 缺省页面背景色
- hasLogin(){
- return !!this.$store.getters.hasLogin;
- default: '暂时没有数据'
- type: {
- backgroundColor: {
- default: 'rgba(0,0,0,0)'
- onCartBtnClick(){
- if(this.hasLogin){
- url: '/pages/tabbar/home'
- this.navTo('/pages/auth/login');
- switchTab(url){
- .mix-empty{
- animation: show .5s 1;
- @keyframes show{
- .default{
- padding-top: 26vh;
- /* #ifdef H5 */
- padding-top: 30vh;
- .icon{
- width: 460rpx;
- height: 342rpx;
- margin-top: 10rpx;
- .cart{
- padding-top: 14vh;
- padding-top: 18vh;
- width: 320rpx;
- height: 320rpx;
- .title{
- margin: 50rpx 0 26rpx;
- font-size: 34rpx;
- .btn{
- height: 80rpx;
- margin-top: 80rpx;
- text-indent: 2rpx;
- letter-spacing: 2rpx;
- background: linear-gradient(to bottom right, #ffb2bf, $base-color);
- .address{
- padding-top: 6vh;
- padding-top: 10vh;
- width: 380rpx;
- height: 380rpx;
- width: 400rpx;
- font-size: 30rpx;
- line-height: 1.6;
- width: 110rpx;
- height: 110rpx;
- box-shadow: 2rpx 2rpx 10rpx rgba(255, 83, 111, .5);
- .icon-jia2{
- font-size: 50rpx;
- .favorite{
- width: 360rpx;
- height: 360rpx;
@@ -1,66 +0,0 @@
- <view class="mix-icon-loading">
- class="loading-icon"
- :style="{
- width: size,
- height: size,
- borderRightColor: color
- ></view>
- * 菊花loading小图标
- * @prop size 尺寸,单位rpx
- * @prop color 颜色
- name: 'MixIconLoading',
- size: {
- default: '26rpx'
- color: {
- default: '#999'
- .mix-icon-loading{
- width: auto;
- width: 28rpx;
- height: 28rpx;
- border: 4rpx solid #ddd;
- animation: mix-loading 1.8s steps(12) infinite;
- @keyframes mix-loading{
- transform: rotate(1turn)
@@ -1,117 +0,0 @@
- <view class="mix-list-cell" :class="border" @click="onClick" hover-class="cell-hover" :hover-stay-time="50">
- <text
- v-if="icon"
- class="cell-icon mix-icon"
- :style="[{
- color: iconColor,
- }]"
- :class="icon"
- ></text>
- <text class="cell-tit clamp">{{ title }}</text>
- <text v-if="tips" class="cell-tip" :style="{color: tipsColor}">{{ tips }}</text>
- <text class="cell-more mix-icon"
- :class="typeList[navigateType]"
- * 简单封装了下, 应用范围比较狭窄,可以在此基础上进行扩展使用
- * 比如加入image, iconSize可控等
- typeList: {
- left: 'icon-zuo',
- right: 'icon-you',
- up: 'icon-shang',
- down: 'icon-xia'
- title: {
- default: '标题'
- tips: {
- tipsColor: {
- navigateType: {
- default: 'right'
- border: {
- default: 'b-b'
- hoverClass: {
- default: 'cell-hover'
- iconColor: {
- default: '#333'
- onClick(){
- this.$emit('onClick');
- .mix-list-cell{
- padding: 0 24rpx;
- position:relative;
- &.cell-hover{
- background:#fafafa;
- &.b-b{
- left: 30rpx;
- border-color: #f0f0f0;
- .cell-icon{
- align-self: center;
- width: 60rpx;
- font-size: 38rpx;
- .cell-more{
- .cell-tit{
- margin-right:10rpx;
- .cell-tip{
@@ -1,60 +0,0 @@
- <view v-show="status !== 2" class="mix-load-more center">
- <image class="loading-icon" src="/static/loading/hamster.gif"></image>
- <text class="text">{{ textList[status] }}</text>
- <view v-show="status === 2" class="mix-load-more center">
- <text class="text">国云网络提供技术支持</text>
- * 上划加载更多
- * @prop {Number} status 0加载前,1加载中,2没有更多
- * @prop {Array} textList ['加载前提示', '加载中提示', '加载完提示']
- name: "mix-load-more",
- status: {
- default: 0
- textList: {
- type: Array,
- default () {
- return [
- '上拉显示更多',
- '正在加载 ..',
- '我也是有底线的~'
- ];
- .mix-load-more{
- width: 750rpx;
- width: 64rpx;
- height: 68rpx;
- margin-right: 20rpx;
@@ -1,114 +0,0 @@
- <view class="mix-loading center">
- <view v-if="!isTimeout" class="center">
- <view v-if="mask" class="mask" @click.stop.prevent="stopPrevent" @touchmove.stop.prevent="stopPrevent"></view>
- <!-- 黑底菊花 -->
- <view v-if="type === 1" class="chry column center">
- <view class="icon"></view>
- <text class="tit">{{ title }}</text>
- <!-- 仓鼠 -->
- <image v-else-if="type === 2" class="hamster" src="/static/loading/hamster.gif"></image>
- * loading
- * @prop type 1 黑底菊花 2 小胖仓鼠
- * @prop mask 遮罩层
- * @prop timeout 超时时间(秒) 默认10s
- data(){
- isTimeout: false
- default: 1
- mask: {
- timeout: {
- default: 10
- default: '请稍候'
- this._timer = setTimeout(()=>{
- if(!this.isLoading){
- this.isTimeout = true;
- title: '加载超时,请重试',
- icon: 'none'
- }, this.timeout * 1000)
- destroyed() {
- this._timer && clearTimeout(this._timer);
- .mix-loading{
- left: 50vw;
- top: 46vh;
- z-index: 999;
- .mask{
- .chry{
- min-width: 120rpx;
- min-height: 116rpx;
- border-radius: 10rpx;
- background-color: rgba(17,17,17,.7);
- height: 64rpx;
- background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- animation: mix-loading 1s steps(12) infinite;
- .tit{
- margin: 10rpx 0 6rpx;
- font-size: 20rpx;
- color: #e9e9e9;
- .hamster{
- width: 106rpx;
- height: 120rpx;
@@ -1,105 +0,0 @@
- <uni-popup ref="popup">
- <view class="pop-content">
- <text class="title">{{ title }}</text>
- <view class="con center">
- <view class="btn-group row b-t">
- <view class="btn center" @click="close">
- <text>{{ cancelText }}</text>
- <view class="btn center b-l" @click="confirm">
- <text>{{ confirmText }}</text>
- * 确认对话框
- * @prop title 标题
- * @prop text 提示内容
- * @prop cancelText 取消按钮文字
- * @prop confirmText 确认按钮文字
- * @event onConfirm 确认按钮点击时触发
- title: String,
- text: String,
- cancelText: {
- default: '取消'
- confirmText: {
- default: '确定'
- open(){
- .pop-content{
- width: 540rpx;
- padding-top: 36rpx;
- border-radius: 24rpx;
- line-height: 48rpx;
- font-weight: 700;
- .con{
- padding: 36rpx 40rpx 54rpx;
- line-height: 40rpx;
- .btn-group{
- line-height: 88rpx;
- color: #777;
- color: #007aff;
@@ -1,139 +0,0 @@
- <view class="nav-bar b-b">
- class="nav-item"
- v-for="(item, index) in navs"
- :key="index"
- @click="navChange(index)"
- <view class="tit-wrap">
- <text class="tit" :class="{'active': current == index}">{{ item.name }}</text>
- <text v-if="counts.length > index && counts[index] > 0" class="number">{{ counts[index] }}</text>
- <view class="line" :class="{'line--show': current === index}"></view>
- * 顶部tab栏
- countList: [],
- navs: {
- return [];
- current: {
- counts: {
- navChange(index){
- this.$emit('onChange', index);
- /* #ifndef APP-NVUE */
- view{
- .fill-view{
- height: 84rpx;
- .nav-bar{
- flex-direction: row;
- justify-content: space-around;
- z-index: 90;
- top: var(--window-top);
- border-color: #f7f7f7;
- .nav-item{
- padding-top: 4rpx;
- .tit-wrap{
- .number{
- right: -20rpx;
- top: 0px;
- min-width: 36rpx;
- padding: 0 6rpx;
- border: 4rpx solid #fff;
- .active{
- color: #ff4443;
- .line{
- height: 4rpx;
- background-color: #ff4443;
- &--show{
@@ -1,180 +0,0 @@
- <view class="uni-numbox">
- <view class="uni-numbox-minus"
- @click="_calcValue('subtract')"
- <text class="mix-icon icon--jianhao" :class="minDisabled?'uni-numbox-disabled': ''" ></text>
- <input
- class="uni-numbox-value"
- type="number"
- :disabled="inputDisabled"
- :value="inputValue"
- @blur="_onBlur"
- class="uni-numbox-plus"
- @click="_calcValue('add')"
- <text class="mix-icon icon-jia2" :class="maxDisabled?'uni-numbox-disabled': ''" ></text>
- * index 当前行下标
- * value 默认值
- * min 最小值
- * max 最大值
- * step 步进值
- * disabled 禁用
- name: 'uni-number-box',
- index: {
- value: {
- min: {
- default: -Infinity
- max: {
- default: 99
- step: {
- inputDisabled: {
- inputValue: this.value,
- created(){
- maxDisabled(){
- return this.inputValue >= this.max;
- minDisabled(){
- return this.inputValue <= this.min;
- inputValue(number) {
- const data = {
- number: number,
- index: this.index
- this.$emit('eventChange', data);
- _calcValue(type) {
- let value = this.inputValue;
- let newValue = 0;
- let step = this.step;
- if(type === 'subtract'){
- newValue = value - step;
- if(newValue < this.min){
- newValue = this.min
- if(this.min > 1){
- this.$api.msg(this.limit_message);
- }else if(type === 'add'){
- newValue = value + step;
- if(newValue > this.max){
- newValue = this.max
- if(newValue === value){
- this.inputValue = newValue;
- _onBlur(event) {
- let value = event.detail.value;
- let constValue = value;
- if (!value) {
- this.inputValue = 0;
- value = +value;
- if (value > this.max) {
- value = this.max;
- } else if (value < this.min) {
- value = this.min
- if(constValue != value){
- this.inputValue = constValue;
- this.$nextTick(()=>{
- this.inputValue = value
- .uni-numbox {
- justify-content: flex-start;
- height: 50rpx;
- .uni-numbox-minus,
- .uni-numbox-plus {
- width: 50rpx;
- background-color: #f7f7f7;
- .uni-numbox-minus .mix-icon,
- .uni-numbox-plus .mix-icon{
- .uni-numbox-value {
- min-height: 50rpx;
- .uni-numbox-disabled.mix-icon {
- color: #C0C4CC;
@@ -1,53 +0,0 @@
- <view class="mix-price-view" :style="{fontSize: size - 8 + 'rpx'}">
- <text>¥</text>
- <text class="price" :style="{fontSize: size + 'rpx'}">{{ priceArr[0] }}</text>
- <text>.{{ priceArr[1] }}</text>
- * 价格显示组件
- priceArr: []
- price: {
- default: 36
- price(){
- this.render();
- render(){
- const price = parseFloat(this.price).toFixed(2);
- this.priceArr = (''+price).split('.');
- .mix-price-view{
- color: $base-color;
- .price{
@@ -1,137 +0,0 @@
- <view class="mix-timeline">
- <view class="cell" v-for="(item, index) in list" :key="index">
- <view class="left column center">
- <text class="time">{{ item.time | date('H:i') }}</text>
- <text class="date">{{ item.time | date('m/d') }}</text>
- <view class="cen center">
- <view class="circle center"></view>
- <view class="right column">
- <text class="title">{{ item.title }}</text>
- <text v-if="item.tip" class="tip">{{ item.tip }}</text>
- * 时间轴
- * {
- * title: 标题
- * tip: 小字
- * time: 时间戳
- * }
- list: {
- return []
- .mix-timeline{
- align-items: flex-start;
- padding: 0 30rpx 0;
- &:first-child .circle{
- background-color: #f9e0eb;
- &:last-child .right:before{
- display: none;
- .left{
- .time{
- line-height: 44rpx;
- .date{
- .cen{
- width: 80rpx;
- height: 44rpx;
- .circle{
- width: 16rpx;
- height: 16rpx;
- background-color: #ddd;
- z-index: 5;
- .right{
- padding-bottom: 30rpx;
- min-height: 96rpx;
- width: 2rpx;
- transform: translate(-42rpx, 22rpx);
- .tip{
- margin-top: 6rpx;
- line-height: 36rpx;
- <uni-popup ref="uniPopup" type="bottom">
- <text class="mix-icon icon-guanbi" @click="close"></text>
- <view class="center title">
- <text>输入支付密码</text>
- <view class="input center">
- <view class="item center" :class="{has: pwd.length > index}" v-for="(item, index) in 6" :key="index"></view>
- <view class="reset-btn center" @click="navTo('/pages/auth/payPassword')">
- <text>重置密码</text>
- <number-keyboard ref="keybord" @onChange="onNumberChange"></number-keyboard>
- * 支付密码键盘
- pwd: ''
- pwd(pwd){
- if(pwd.length === 0){
- this.$refs.keybord.val = '';
- this.$refs.uniPopup.open();
- this.$refs.uniPopup.close();
- onNumberChange(pwd){
- this.pwd = pwd;
- if(pwd.length >= 6){
- this.$emit('onConfirm', pwd.substring(0,6));
- border-radius: 20rpx 20rpx 0 0;
- .input{
- padding: 30rpx 0 60rpx;
- .item{
- width: 88rpx;
- margin: 0 10rpx;
- border: 1px solid #ddd;
- border-radius: 4rpx;
- .has:after{
- background-color: #333;
- .reset-btn{
- padding-bottom: 20rpx;
- margin-top: -10rpx;
- margin-bottom: 30rpx;
- .icon-guanbi{
- left: 10rpx;
- top: 24rpx;
- padding: 20rpx;
@@ -1,25 +0,0 @@
-import message from './message.js';
-// 定义 type 类型:弹出类型:top/bottom/center
-const config = {
- // 顶部弹出
- top:'top',
- // 底部弹出
- bottom:'bottom',
- // 居中弹出
- center:'center',
- // 消息提示
- message:'top',
- // 对话框
- dialog:'center',
- // 分享
- share:'bottom',
- config:config
- mixins: [message],
@@ -1,302 +0,0 @@
- <view v-if="showPopupState" class="uni-popup" :class="[popupstyle]" @touchmove.stop.prevent="clear">
- <uni-transition v-if="maskShow" :mode-class="['fade']" :styles="maskClass" :maskBackgroundColor="maskBackgroundColor" :duration="duration" :show="showTrans"
- @click="onTap" />
- <uni-transition :mode-class="ani" :styles="transClass" maskBackgroundColor="rgba(,0,0,0,0)" :duration="duration" :show="showTrans" @click="onTap">
- <view class="uni-popup__wrapper-box" @click.stop="clear">
- <slot />
- </uni-transition>
- import uniTransition from '../uni-transition/uni-transition.vue'
- * PopUp 弹出层
- * @description 弹出层组件,为了解决遮罩弹层的问题
- * @tutorial https://ext.dcloud.net.cn/plugin?id=329
- * @property {String} type = [top|center|bottom] 弹出方式
- * @value top 顶部弹出
- * @value center 中间弹出
- * @value bottom 底部弹出
- * @value message 消息提示
- * @value dialog 对话框
- * @value share 底部分享示例
- * @property {Boolean} animation = [ture|false] 是否开启动画
- * @property {Boolean} maskClick = [ture|false] 蒙版点击是否关闭弹窗
- * @event {Function} change 打开关闭弹窗触发,e={show: false}
- name: 'UniPopup',
- uniTransition
- // 开启动画
- animation: {
- // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
- // message: 消息提示 ; dialog : 对话框
- default: 'center'
- // maskClick
- maskClick: {
- maskBackgroundColor: {
- default: 'rgba(0, 0, 0, .5)'
- provide() {
- popup: this
- * 监听type类型
- handler: function(newVal) {
- this[this.config[newVal]]()
- immediate: true
- * 监听遮罩是否可点击
- * @param {Object} val
- maskClick(val) {
- this.mkclick = val
- duration: 300,
- ani: [],
- showPopupState: false,
- showTrans: false,
- maskClass: {
- 'position': 'fixed',
- 'bottom': 0,
- 'top': 0,
- 'left': 0,
- 'right': 0
- transClass: {
- 'right': 0,
- maskShow: true,
- mkclick: true,
- popupstyle: 'top',
- config: {
- center:'center'
- this.mkclick = this.maskClick
- if (this.animation) {
- this.duration = 300
- this.duration = 0
- clear(e) {
- // TODO nvue 取消冒泡
- e.stopPropagation()
- open() {
- this.showPopupState = true
- new Promise(resolve => {
- clearTimeout(this.timer)
- this.timer = setTimeout(() => {
- this.showTrans = true
- // fixed by mehaotian 兼容 app 端
- resolve();
- }, 50);
- // 自定义打开事件
- clearTimeout(this.msgtimer)
- this.msgtimer = setTimeout(() => {
- this.customOpen && this.customOpen()
- this.$emit('change', {
- show: true,
- type: this.type
- close(type) {
- this.showTrans = false
- show: false,
- // 自定义关闭事件
- this.customOpen && this.customClose()
- this.showPopupState = false
- onTap() {
- if (!this.mkclick) return
- this.close()
- * 顶部弹出样式处理
- top() {
- this.popupstyle = 'top'
- this.ani = ['slide-top']
- this.transClass = {
- * 底部弹出样式处理
- bottom() {
- this.popupstyle = 'bottom'
- this.ani = ['slide-bottom']
- 'bottom': 0
- * 中间弹出样式处理
- center() {
- this.popupstyle = 'center'
- this.ani = ['zoom-out', 'fade']
- 'display': 'flex',
- 'flexDirection': 'column',
- 'justifyContent': 'center',
- 'alignItems': 'center'
-<style lang="scss" scoped>
- .uni-popup {
- .uni-popup__mask {
- background-color: $uni-bg-color-mask;
- .mask-ani {
- transition-property: opacity;
- transition-duration: 0.2s;
- .uni-top-mask {
- .uni-bottom-mask {
- .uni-center-mask {
- .uni-popup__wrapper {
- .top {
- /* #ifndef H5 */
- .bottom {
- .uni-popup__wrapper-box {
- /* iphonex 等安全区设置,底部安全区适配 */
- .content-ani {
- // transition: transform 0.3s;
- transition-property: transform, opacity;
- .uni-top-content {
- transform: translateY(0);
- .uni-bottom-content {
- .uni-center-content {
- transform: scale(1);
@@ -1,245 +0,0 @@
-const BindingX = uni.requireNativePlugin('bindingx');
-const dom = uni.requireNativePlugin('dom');
-const animation = uni.requireNativePlugin('animation');
- right: 0,
- button: [],
- preventGesture: false
- show(newVal) {
- if (!this.position || JSON.stringify(this.position) === '{}') return;
- if (this.autoClose) return
- if (this.isInAnimation) return
- if (newVal) {
- this.open()
- if (this.swipeaction.children !== undefined) {
- this.swipeaction.children.push(this)
- this.boxSelector = this.getEl(this.$refs['selector-box-hock']);
- this.selector = this.getEl(this.$refs['selector-content-hock']);
- this.buttonSelector = this.getEl(this.$refs['selector-button-hock']);
- this.position = {}
- this.x = 0
- this.getSelectorQuery()
- if (this.timing) {
- BindingX.unbind({
- token: this.timing.token,
- eventType: 'timing'
- if (this.eventpan) {
- token: this.eventpan.token,
- eventType: 'pan'
- this.swipeaction.children.forEach((item, index) => {
- if (item === this) {
- this.swipeaction.children.splice(index, 1)
- onClick(index, item) {
- this.$emit('click', {
- content: item,
- index
- touchstart(e) {
- if (this.stop) return
- this.stop = true
- if (this.autoClose) {
- this.swipeaction.closeOther(this)
- let endWidth = this.right
- let boxStep = `(x+${this.x})`
- let pageX = `${boxStep}> ${-endWidth} && ${boxStep} < 0?${boxStep}:(x+${this.x} < 0? ${-endWidth}:0)`
- let props = [{
- element: this.selector,
- property: 'transform.translateX',
- expression: pageX
- let left = 0
- for (let i = 0; i < this.options.length; i++) {
- let buttonSelectors = this.getEl(this.$refs['button-hock'][i]);
- if (this.button.length === 0 || !this.button[i] || !this.button[i].width) return
- let moveMix = endWidth - left
- left += this.button[i].width
- let step = `(${this.x}+x)/${endWidth}`
- let moveX = `(${step}) * ${moveMix}`
- let pageButtonX = `${moveX}&& (x+${this.x} > ${-endWidth})?${moveX}:${-moveMix}`
- props.push({
- element: buttonSelectors,
- expression: pageButtonX
- this.eventpan = this._bind(this.boxSelector, props, 'pan', (e) => {
- if (e.state === 'end') {
- this.x = e.deltaX + this.x;
- if (this.x < -endWidth) {
- this.x = -endWidth
- if (this.x > 0) {
- this.stop = false
- this.bindTiming();
- touchend(e) {
- if (this.isopen && !this.isDrag && !this.isInAnimation) {
- bindTiming() {
- if (this.isopen) {
- this.move(this.x, -this.right)
- this.move(this.x, -40)
- move(left, value) {
- if (left >= value) {
- * 开启swipe
- this.animation(true)
- * 关闭swipe
- close() {
- this.animation(false)
- * 开启关闭动画
- * @param {Object} type
- animation(type) {
- this.isDrag = true
- let time = 200
- this.isInAnimation = true;
- let exit = `t>${time}`;
- let translate_x_expression = `easeOutExpo(t,${this.x},${type?(-endWidth-this.x):(-this.x)},${time})`
- expression: translate_x_expression
- let step = `${this.x}/${endWidth}`
- let pageButtonX = `easeOutExpo(t,${moveX},${type ? -moveMix + '-' + moveX: 0 + '-' + moveX},${time})`
- this.timing = BindingX.bind({
- eventType: 'timing',
- exitExpression: exit,
- props: props
- }, (e) => {
- if (e.state === 'end' || e.state === 'exit') {
- this.x = type ? -endWidth : 0
- this.isInAnimation = false;
- this.isopen = this.isopen || false
- if (this.isopen !== type) {
- this.$emit('change', type)
- this.isopen = type
- this.isDrag = false
- * 绑定 BindingX
- * @param {Object} anchor
- * @param {Object} props
- * @param {Object} fn
- _bind(anchor, props, eventType, fn) {
- return BindingX.bind({
- anchor,
- eventType,
- props
- typeof(fn) === 'function' && fn(e)
- * 获取ref
- * @param {Object} el
- getEl(el) {
- return el.ref
- * 获取节点信息
- getSelectorQuery() {
- dom.getComponentRect(this.$refs['selector-content-hock'], (data) => {
- if (this.position.content) return
- this.position.content = data.size
- dom.getComponentRect(this.$refs['button-hock'][i], (data) => {
- if (!this.button) {
- this.button = []
- if (this.options.length === this.button.length) return
- this.button.push(data.size)
- this.right += data.size.width
- if (this.show) {
@@ -1,204 +0,0 @@
- * 监听页面内值的变化,主要用于动态开关swipe-action
- * @param {Object} newValue
- * @param {Object} oldValue
- * @param {Object} ownerInstance
- * @param {Object} instance
-function sizeReady(newValue, oldValue, ownerInstance, instance) {
- var state = instance.getState()
- state.position = JSON.parse(newValue)
- if (!state.position || state.position.length === 0) return
- var show = state.position[0].show
- state.left = state.left || state.position[0].left;
- // 通过用户变量,开启或关闭
- if (show) {
- openState(true, instance, ownerInstance)
- openState(false, instance, ownerInstance)
- * 开始触摸操作
- * @param {Object} e
- * @param {Object} ins
-function touchstart(e, ins) {
- var instance = e.instance;
- var state = instance.getState();
- var pageX = e.touches[0].pageX;
- // 开始触摸时移除动画类
- instance.removeClass('ani');
- var owner = ins.selectAllComponents('.button-hock')
- for (var i = 0; i < owner.length; i++) {
- owner[i].removeClass('ani');
- // state.position = JSON.parse(instance.getDataset().position);
- // 获取最终按钮组的宽度
- state.width = pageX - state.left;
- ins.callMethod('closeSwipe')
- * 开始滑动操作
-function touchmove(e, ownerInstance) {
- var disabled = instance.getDataset().disabled
- // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复
- disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false;
- if (disabled) return
- move(pageX - state.width, instance, ownerInstance)
- * 结束触摸操作
-function touchend(e, ownerInstance) {
- // 滑动过程中触摸结束,通过阙值判断是开启还是关闭
- // fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13
- moveDirection(state.left, -40, instance, ownerInstance)
- * 设置移动距离
- * @param {Object} value
-function move(value, instance, ownerInstance) {
- // 获取可滑动范围
- var x = Math.max(-state.position[1].width, Math.min((value), 0));
- state.left = x;
- instance.setStyle({
- transform: 'translateX(' + x + 'px)',
- '-webkit-transform': 'translateX(' + x + 'px)'
- // 折叠按钮动画
- buttonFold(x, instance, ownerInstance)
- * 移动方向判断
- * @param {Object} left
-function moveDirection(left, value, ins, ownerInstance) {
- var state = ins.getState()
- var position = state.position
- var isopen = state.isopen
- if (!position[1].width) {
- openState(false, ins, ownerInstance)
- // 如果已经是打开状态,进行判断是否关闭,还是保留打开状态
- if (isopen) {
- if (-left <= position[1].width) {
- openState(true, ins, ownerInstance)
- // 如果是关闭状态,进行判断是否打开,还是保留关闭状态
- if (left <= value) {
- * 设置按钮移动距离
-function buttonFold(value, instance, ownerInstance) {
- var ins = ownerInstance.selectAllComponents('.button-hock');
- var position = state.position;
- var arr = [];
- var w = 0;
- for (var i = 0; i < ins.length; i++) {
- if (!ins[i].getDataset().button) return
- var btnData = JSON.parse(ins[i].getDataset().button)
- // fix by mehaotian TODO 在 app-vue 中,字符串转对象,需要转两次,这里先这么兼容
- if (typeof(btnData) === 'string') {
- btnData = JSON.parse(btnData)
- var button = btnData[i] && btnData[i].width || 0
- w += button
- arr.push(-w)
- // 动态计算按钮组每个按钮的折叠动画移动距离
- var distance = arr[i - 1] + value * (arr[i - 1] / position[1].width)
- if (i != 0) {
- ins[i].setStyle({
- transform: 'translateX(' + distance + 'px)',
- * 开启状态
- * @param {Boolean} type
-function openState(type, ins, ownerInstance) {
- if (state.isopen === undefined) {
- state.isopen = false
- // 只有状态有改变才会通知页面改变状态
- if (state.isopen !== type) {
- // 通知页面,已经打开
- ownerInstance.callMethod('change', {
- open: type
- // 设置打开和移动状态
- state.isopen = type
- // 添加动画类
- ins.addClass('ani');
- var owner = ownerInstance.selectAllComponents('.button-hock')
- owner[i].addClass('ani');
- // 设置最终移动位置
- move(type ? -position[1].width : 0, ins, ownerInstance)
- sizeReady: sizeReady,
- touchstart: touchstart,
- touchmove: touchmove,
- touchend: touchend
@@ -1,160 +0,0 @@
- isshow: false,
- viewWidth: 0,
- buttonWidth: 0,
- disabledView: false,
- transition: false
- this.isopen = false
- this.transition = true
- this.getQuerySelect()
- }, 50)
- let {
- pageX,
- pageY
- } = e.changedTouches[0]
- this.transition = false
- this.startX = pageX
- touchmove(e) {
- this.slide = this.getSlide(pageX)
- if (this.slide === 0) {
- this.disabledView = false
- if (this.moveX === -this.buttonWidth) {
- this.move()
- if (this.moveX === 0) {
- this.x = this.moveX
- this.x = -this.buttonWidth
- this.moveX = this.x
- if(!this.isopen){
- this.isopen = true
- this.$emit('change', true)
- if(this.isopen){
- this.$emit('change', false)
- move() {
- onChange(e) {
- let x = e.detail.x
- this.moveX = x
- if (x >= this.buttonWidth) {
- this.disabledView = true
- this.x = this.buttonWidth
- getSlide(x) {
- if (x >= this.startX) {
- this.startX = x
- return 1
- getQuerySelect() {
- const query = uni.createSelectorQuery().in(this);
- query.selectAll('.viewWidth-hook').boundingClientRect(data => {
- this.viewWidth = data[0].width
- this.buttonWidth = data[1].width
- if (!this.buttonWidth) {
@@ -1,158 +0,0 @@
-// #ifdef APP-NVUE
-const dom = weex.requireModule('dom');
- uniShow: false,
- left: 0
- moveLeft() {
- return `translateX(${this.left}px)`
- beforeDestoy() {
- const {
- pageX
- } = e.touches[0]
- if (this.disabled) return
- const left = this.position.content.left
- this.width = pageX - left
- if (this.isopen) return
- if (this.uniShow) {
- this.uniShow = false
- this.openleft = this.left + this.position.button.width
- touchmove(e, index) {
- this.setPosition(pageX)
- touchend() {
- this.move(this.openleft, 0)
- this.move(this.left, -40)
- setPosition(x, y) {
- if (!this.position.button.width) {
- // this.left = x - this.width
- this.setValue(x - this.width)
- setValue(value) {
- // 设置最大最小值
- this.left = Math.max(-this.position.button.width, Math.min(parseInt(value), 0))
- this.position.content.left = this.left
- this.uniShow = true
- this.left = -this.position.button.width
- this.setValue(-this.position.button.width)
- this.setValue(0)
- // #ifndef APP-NVUE
- const views = uni.createSelectorQuery()
- .in(this)
- views
- .selectAll('.selector-query-hock')
- .boundingClientRect(data => {
- this.position.content = data[1]
- this.position.button = data[0]
- .exec()
- // #ifdef APP-NVUE
- dom.getComponentRect(this.$refs['selector-button-hock'], (data) => {
- if (this.position.button) return
- this.position.button = data.size
- position: [],
- button: []
- pos() {
- return JSON.stringify(this.position)
- btn() {
- return JSON.stringify(this.button)
- let valueObj = this.position[0]
- if (!valueObj) {
- this.init()
- valueObj.show = newVal
- this.$set(this.position, 0, valueObj)
- this.getSize()
- this.getButtonSize()
- closeSwipe(e) {
- if (!this.autoClose) return
- change(e) {
- this.$emit('change', e.open)
- if (valueObj.show !== e.open) {
- valueObj.show = e.open
- appTouchStart(){},
- appTouchEnd(){},
- getSize() {
- const views = uni.createSelectorQuery().in(this)
- data[0].show = false
- data[0].show = this.show
- this.position = data
- getButtonSize() {
- .selectAll('.button-hock')
- this.button = data
@@ -1,270 +0,0 @@
- <view class="uni-swipe">
- <!-- 在微信小程序 app vue端 h5 使用wxs 实现-->
- <!-- #ifdef APP-VUE || MP-WEIXIN || H5 -->
- <view class="uni-swipe_content">
- <view :data-disabled="disabled" :data-position="pos" :change:prop="swipe.sizeReady" :prop="pos" class="uni-swipe_move-box selector-query-hock move-hock"
- @touchstart="swipe.touchstart" @touchmove="swipe.touchmove" @touchend="swipe.touchend" @change="change">
- <view class="uni-swipe_box">
- <view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock">
- <!-- 使用 touchend 解决 ios 13 不触发按钮事件的问题-->
- <view v-for="(item,index) in options" :data-button="btn" :key="index" :style="{
- backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
- fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'
- class="uni-swipe_button button-hock" @touchend="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',}">{{ item.text }}</text></view>
- <!-- app nvue端 使用 bindingx -->
- <!-- #ifdef APP-NVUE -->
- <view ref="selector-box-hock" class="uni-swipe_content" @horizontalpan="touchstart" @touchend="touchend">
- <view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock" :style="{width:right+'px'}">
- <view ref="button-hock" v-for="(item,index) in options" :key="index" :style="{
- backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',left: right+'px'}"
- class="uni-swipe_button " @click.stop="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px'}">{{ item.text }}</text></view>
- <view ref='selector-content-hock' class="uni-swipe_move-box selector-query-hock">
- <!-- 在非 app 端、非微信小程序、支付宝小程序、h5端使用 js -->
- <!-- #ifndef APP-PLUS || MP-WEIXIN || MP-ALIPAY || H5 -->
- class="uni-swipe_button button-hock" @click.stop="onClick(index,item)"><text class="uni-swipe_button-text" :style="{color: item.style && item.style.color ? item.style.color : '#FFFFFF',}">{{ item.text }}</text></view>
- <view ref='selector-content-hock' class="selector-query-hock" @touchstart="touchstart" @touchmove="touchmove"
- @touchend="touchend" :class="{'ani':uniShow}" :style="{transform:moveLeft}">
- <view class="uni-swipe_move-box" >
- <!-- #ifdef MP-ALIPAY -->
- <view class="uni-swipe-box" @touchstart="touchstart" @touchmove="touchmove" @touchend="touchend">
- <view class="viewWidth-hook">
- <movable-area v-if="viewWidth !== 0" class="movable-area" :style="{width:(viewWidth-buttonWidth)+'px'}">
- <movable-view class="movable-view" direction="horizontal" :animation="!transition" :style="{width:viewWidth+'px'}"
- :class="[transition?'transition':'']" :x="x" :disabled="disabledView" @change="onChange">
- <view class="movable-view-box">
- </movable-view>
- </movable-area>
- <view ref="selector-button-hock" class="uni-swipe_button-group viewWidth-hook">
-<script src="./index.wxs" module="swipe" lang="wxs"></script>
- // #ifdef APP-VUE|| MP-WEIXIN || H5
- import mpwxs from './mpwxs'
- import bindingx from './bindingx.js'
- // #ifndef APP-PLUS|| MP-WEIXIN || MP-ALIPAY || H5
- import mixins from './mpother'
- import mpalipay from './mpalipay'
- * SwipeActionItem 滑动操作子组件
- * @description 通过滑动触发选项的容器
- * @tutorial https://ext.dcloud.net.cn/plugin?id=181
- * @property {Boolean} show = [true|false] 开启关闭组件,auto-close = false 时生效
- * @property {Boolean} disabled = [true|false] 是否禁止滑动
- * @property {Boolean} autoClose = [true|false] 其他组件开启的时候,当前组件是否自动关闭
- * @property {Array} options 组件选项内容及样式
- * @event {Function} click 点击选项按钮时触发事件,e = {content,index} ,content(点击内容)、index(下标)
- * @event {Function} change 组件打开或关闭时触发,true:开启状态;false:关闭状态
- // #ifdef APP-VUE|| MP-WEIXIN||H5
- mixins: [mpwxs],
- mixins: [bindingx],
- mixins: [mixins],
- mixins: [mpalipay],
- * 按钮内容
- options: {
- * 禁用
- * 变量控制开关
- show: {
- * 是否自动关闭
- autoClose: {
- inject: ['swipeaction']
- .uni-swipe {
- .uni-swipe-box {
- .uni-swipe_content {
- .uni-swipe_move-box {
- .uni-swipe_box {
- /* #ifdef APP-NVUE */
- font-size: 14px;
- .uni-swipe_button-group {
- /* #ifndef APP-VUE|| MP-WEIXIN||H5 */
- z-index: 0;
- .uni-swipe_button {
- padding: 0 20px;
- .uni-swipe_button-text {
- .ani {
- transition-property: transform;
- transition-duration: 0.3s;
- transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1);
- /* #ifdef MP-ALIPAY */
- .movable-area {
- height: 45px;
- .movable-view {
- width: 160%;
- z-index: 2;
- .transition {
- transition: all 0.3s;
- .movable-view-box {
@@ -1,58 +0,0 @@
- * SwipeAction 滑动操作
- swipeaction: this
- this.children = []
- closeOther(vm) {
- let children = this.children
- children.forEach((item, index) => {
- if (vm === item) return
- // 支付宝执行以下操作
- if (item.isopen) {
- item.close()
- // app vue 端、h5 、微信、支付宝 执行以下操作
- // #ifdef APP-VUE || H5 || MP-WEIXIN
- let position = item.position[0]
- let show = position.show
- position.show = false
- // nvue 执行以下操作
- // #ifdef APP-NVUE || MP-BAIDU || MP-QQ || MP-TOUTIAO
@@ -1,290 +0,0 @@
- v-if="isShow"
- ref="ani"
- class="uni-transition"
- :class="[ani.in]"
- :style="'transform:' +transform+';'+stylesObject"
- @click="change"
- const animation = uni.requireNativePlugin('animation');
- * Transition 过渡动画
- * @description 简单过渡动画组件
- * @tutorial https://ext.dcloud.net.cn/plugin?id=985
- * @property {Boolean} show = [false|true] 控制组件显示或隐藏
- * @property {Array} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
- * @value fade 渐隐渐出过渡
- * @value slide-top 由上至下过渡
- * @value slide-right 由右至左过渡
- * @value slide-bottom 由下至上过渡
- * @value slide-left 由左至右过渡
- * @value zoom-in 由小到大过渡
- * @value zoom-out 由大到小过渡
- * @property {Number} duration 过渡动画持续时间
- * @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
- name: 'uniTransition',
- modeClass: {
- duration: {
- default: 300
- styles: {
- return {}
- default: 'rgba(0, 0, 0, 0.4)'
- isShow: false,
- ani: { in: '',
- active: ''
- handler(newVal) {
- stylesObject() {
- let styles = {
- ...this.styles,
- backgroundColor: this.maskBackgroundColor,
- 'transition-duration': this.duration / 1000 + 's'
- let transfrom = ''
- for (let i in styles) {
- let line = this.toLine(i)
- transfrom += line + ':' + styles[i] + ';'
- return transfrom
- // this.timer = null
- // this.nextTick = (time = 50) => new Promise(resolve => {
- // clearTimeout(this.timer)
- // this.timer = setTimeout(resolve, time)
- // return this.timer
- // });
- change() {
- detail: this.isShow
- this.isShow = true
- this.transform = ''
- this.ani.in = ''
- for (let i in this.getTranfrom(false)) {
- if (i === 'opacity') {
- this.ani.in = 'fade-in'
- this.transform += `${this.getTranfrom(false)[i]} `
- this._animation(true)
- this._animation(false)
- _animation(type) {
- let styles = this.getTranfrom(type)
- if(!this.$refs['ani']) return
- animation.transition(this.$refs['ani'].ref, {
- styles,
- duration: this.duration, //ms
- timingFunction: 'ease',
- needLayout: false,
- delay: 0 //ms
- }, () => {
- if (!type) {
- this.isShow = false
- this.ani.in = `fade-${type?'out':'in'}`
- this.transform += `${styles[i]} `
- }, this.duration)
- getTranfrom(type) {
- transform: ''
- this.modeClass.forEach((mode) => {
- switch (mode) {
- case 'fade':
- styles.opacity = type ? 1 : 0
- case 'slide-top':
- styles.transform += `translateY(${type?'0':'-100%'}) `
- case 'slide-right':
- styles.transform += `translateX(${type?'0':'100%'}) `
- case 'slide-bottom':
- styles.transform += `translateY(${type?'0':'100%'}) `
- case 'slide-left':
- styles.transform += `translateX(${type?'0':'-100%'}) `
- case 'zoom-in':
- styles.transform += `scale(${type?1:0.8}) `
- case 'zoom-out':
- styles.transform += `scale(${type?1:1.2}) `
- return styles
- _modeClassArr(type) {
- let mode = this.modeClass
- if (typeof(mode) !== "string") {
- let modestr = ''
- mode.forEach((item) => {
- modestr += (item + '-' + type + ',')
- return modestr.substr(0, modestr.length - 1)
- return mode + '-' + type
- // getEl(el) {
- // console.log(el || el.ref || null);
- // return el || el.ref || null
- toLine(name) {
- return name.replace(/([A-Z])/g, "-$1").toLowerCase();
- .uni-transition {
- transition-timing-function: ease;
- .fade-in {
- .fade-active {
- .slide-top-in {
- /* transition-property: transform, opacity; */
- transform: translateY(-100%);
- .slide-top-active {
- /* opacity: 1; */
- .slide-right-in {
- transform: translateX(100%);
- .slide-right-active {
- transform: translateX(0);
- .slide-bottom-in {
- transform: translateY(100%);
- .slide-bottom-active {
- .slide-left-in {
- transform: translateX(-100%);
- .slide-left-active {
- .zoom-in-in {
- transform: scale(0.8);
- .zoom-out-active {
- .zoom-out-in {
- transform: scale(1.2);
@@ -1,1250 +0,0 @@
-div,a,img,span,page,view,navigator,image,text,input,textarea,button,form{
-a{
- text-decoration: none;
- color: $main;
-form{
-image{will-change: transform}
-input,textarea,form{
- min-height: 10px;
- font-size: inherit;
- color: inherit;
- line-height: inherit;
- background-color: transparent;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-switch .uni-switch-input{
- margin-right: 0;
-.wx-switch-input,.uni-switch-input{width:42px !important;height:22px !important;}
- .wx-switch-input::before,.uni-switch-input::before{width:40px !important;height: 20px !important;}
- .wx-switch-input::after,.uni-switch-input::after{width: 20px !important;height: 20px !important;}
-/**背景颜色**/
-.bg{
- background-color: $main;
- color: $mainInverse;
-.gradualBg{
- background-image: $mainGradual;
- color: $mainGradualInverse ;
-.grayBg{
- color: #30302f;
-.whiteBg{
- color: #000;
-.blackBg{
- background-color: #000;
-.orangeBg{
- background-color: $orange;
-.redBg{
- background-color: $red;
-.yellowBg{
- background-color: $yellow;
-.greenBg{
- background-color: $green;
-.brownBg{
- background-color: $brown;
-.blueBg{
- background-color: $blue;
-.purpleBg{
- background-color: $purple;
-/* 文字颜色 */
-.main{
-.green{
- color: $green;
-.red{
- color: $red;
-.yellow{
- color: $yellow;
-.black{
- color: $black;
-.white{
- color: $white;
-.gray{
- color: $gray;
-.grey{
- color: $grey;
-.orange{
- color: $orange;
-.brown{
- color: $brown;
-.blue{
- color: $blue;
-.purple{
- color: $purple;
-.hoverMain{
- &:hover{
-.hoverGreen{
-.hoverRed{
-.hoverBlue{
-.hoverGray{
-.hoverWhite{
-.hoverBlack{
-.hoverOrange{
-.hoverYellow{
-.hoverBrown{
-.hoverPurple{
-/* 宽度 高度 */
-$w:0;
-@while $w <= 500 {
- @if $w <= 100 {
- .w#{$w}p{
- width: $w*1%;
- .h#{$w}p{
- height: $w*1%;
- @if $w == 100 {
- .100p{
- .ww{
- width: 100vw;
- .hh{
- height: 100vh;
- .w#{$w}{
- width: $w*2upx;
- .h#{$w}{
- height: $w*2upx;
- @if $w < 10 {
- $w : $w + 1 ;
- } @else{
- $w : $w + 5 ;
-/* 字号 */
-@for $fz from 12 through 100 {
- .fz#{$fz}{
- font-size: $fz*2upx !important;
-/* 边距 - 覆盖顺序是小的尺寸覆盖大的尺寸 少的方向覆盖多的方向 */
-$s : 0 ;
-@while $s <= 500 {
- .pd#{$s}{
- padding: $s*2upx!important;
- .m#{$s}{
- margin: $s*2upx!important;
- @if $s == 15 {
- .pd{
- padding: 30upx!important;
- .m{
- margin: 30upx!important;
- @if $s < 10 {
- $s : $s + 1 ;
- } @else if($s < 100){
- $s : $s + 5 ;
- } @else if($s < 300){
- $s : $s + 10 ;
- $s : $s + 50 ;
- .ptb#{$s}{
- padding-top: $s*2upx!important;
- padding-bottom: $s*2upx!important;
- .plr#{$s}{
- padding-left: $s*2upx!important;
- padding-right: $s*2upx!important;
- .mtb#{$s}{
- margin-top: $s*2upx!important;
- margin-bottom: $s*2upx!important;
- .mlr#{$s}{
- margin-left: $s*2upx!important;
- margin-right: $s*2upx!important;
- .ptb{
- padding-top: 30upx!important;
- padding-bottom: 30upx!important;
- .plr{
- padding-left: 30upx!important;
- padding-right: 30upx!important;
- .mlr{
- margin-left: 30upx!important;
- margin-right: 30upx!important;
- .mtb{
- margin-top: 30upx!important;
- margin-bottom: 30upx!important;
- .pl#{$s}{
- .pr#{$s}{
- .pt#{$s}{
- .pb#{$s}{
- .ml#{$s}{
- .mr#{$s}{
- .mt#{$s}{
- .mb#{$s}{
- .pt{
- .pb{
- .pl{
- .pr{
- .mt{
- .mr{
- .ml{
- .mb{
-/* 文字溢出隐藏 */
-.clip{
- -webkit-line-clamp: 1;
- @for $i from 2 through 10{
- &.c#{$i}{
- -webkit-line-clamp: $i;
-.cut{
-/* 价格 */
-.price{
- font-size: inherit ;
- content: "¥";
- font-size: 70%;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif ;
- .point{
- font-weight: inherit;
- letter-spacing: 1px;
- &.noPrefix{
-.grid,.gridNoPd,.gridSmPd,.gridNoMb{
- display: -webkit-flex;
- display: -ms-flexbox;
- -webkit-flex-wrap: wrap;
- -ms-flex-wrap: wrap;
- flex-wrap: wrap;
- padding: 20upx 20upx 0 20upx;
- >.item,>image,>view,>navigator,>text,>button{
- width: 50%;
- padding: 0 10upx;
- margin-bottom: 20upx;
- @for $i from 1 through 50{
- &.g#{$i}{
- width: 100%/$i;
-.gridNoMb{
- margin-bottom: 0;
-.gridNoPd{
-.gridSmPd{
- padding-right: 0;
- &:first-child{
- padding-left: 0;
- padding-right: 10upx;
-/* flex布局 */
-.flex{
- &.t{
- &.b{
- align-items: flex-end;
- &.cv{ //垂直方向铺满
- align-items: stretch;
- &.bk{ //水平方向铺满
- &.lt{
- &.ct{
- &.rt{
- justify-content: flex-end;
- &.ar{
- &.av{
- >.item,view,button,navigator,image,text{
- flex:1;
-/* 定位布局 */
-.father{
-.abs,.fixed{
- &image{
- &.top{
- bottom: auto;
- &.bottom{
- &.left{
- right: auto;
- &.right{
- left: auto;
-@for $i from 0 through 20 {
- .z#{$i}{
- z-index: $i !important;
-@for $i from 1 through 200 {
- .top-#{$i}{
- top: $i * -2upx;
- .left-#{$i}{
- left: $i * -2upx;
- .bottom-#{$i}{
- bottom: $i * -2upx;
- .right-#{$i}{
- right: $i * -2upx;
- .top#{$i}{
- top: $i * 2upx;
- .left#{$i}{
- left: $i * 2upx;
- .bottom#{$i}{
- bottom: $i * 2upx;
- .right#{$i}{
- right: $i * 2upx;
- .top-#{$i}p{
- top: $i * -1%;
- .left-#{$i}p{
- left: $i * -1%;
- .bottom-#{$i}p{
- bottom: $i * -1%;
- .right-#{$i}p{
- right: $i * -1%;
- .top#{$i}p{
- top: $i * 1%;
- .left#{$i}p{
- left: $i * 1%;
- .bottom#{$i}p{
- bottom: $i * 1%;
- .right#{$i}p{
- right: $i * 1%;
-.fixed{
-/* fix-auto布局 */
-.fixAuto,.fixAutoNoPd,.fixAutoSmPd{
- display: table;
- padding: 20upx 10upx;
- >.item,>view,>image,>navigator,>text,>button{
- vertical-align: top;
- display: table-cell ;
- &.middle{
- >.item,>view,>image,>navigator,>text{
- vertical-align: bottom;
-.fixAutoSmPd{
-.fixAutoNoPd{
-/* 浮动组件 */
-.clear{
- content: "";
- visibility: hidden;
-.fl{
- float: left;
-.fr{
- float: right;
-/* 按钮样式类 */
-.btn,.roundBtn{
- cursor: pointer;
- padding: 8upx 24upx;
- color: $mainInverse ;
- font-size: 28upx;
- border: 1px solid $main;
- -webkit-border-radius: 8upx;
- -moz-border-radius: 8upx;
- border-radius: 8upx;
- transition: 0.4s;
- &.gradualBg{
- border-color: transparent;
- color: $mainGradualInverse;
- &.blackBg{
- background-color: $black;
- border-color: $black;
- &.shadow{
- box-shadow: 0px 2px 9px -1px rgba($black , 0.4);
- &.greenBg{
- border-color: $green;
- box-shadow: 0px 2px 9px -1px rgba($green , 0.4);
- &.grayBg{
- border-color: rgba(#30302f,0.2);
- box-shadow: 0px 2px 9px -1px rgba( #30302f , 0.2);
- &.whiteBg{
- border-color: rgba(#fff,0.2);
- &.orangeBg{
- border-color: $orange;
- box-shadow: 0px 2px 9px -1px rgba( $orange , 0.4);
- &.redBg{
- border-color: $red;
- box-shadow: 0px 2px 9px -1px rgba( $red , 0.4);
- &.yellowBg{
- border-color: $yellow;
- box-shadow: 0px 2px 9px -1px rgba( $yellow , 0.4);
- &.line{
- background-image: none;
- border-color: rgba(#fff,0.7);
- &+.btn,&+.roundBtn{
- margin-left: 20upx;
- &.block{
- padding: 20upx 24upx;
- &+.btn{
- margin-left: 0;
- -webkit-transform: scale(0.99);
- -moz-transform: scale(0.99);
- -ms-transform: scale(0.99);
- -o-transform: scale(0.99);
- transform: scale(0.99);
- opacity: 0.8;
- -webkit-transform: scale(1);
- -moz-transform: scale(1);
- -ms-transform: scale(1);
- -o-transform: scale(1);
- cursor: not-allowed;
-[class^="tag"] , [class*=" tag"]{
- font-size: 24upx;
- padding: 4upx 14upx;
- border-radius: 4upx;
- margin-right: 6upx;
- margin-left: 6upx;
-.tag{
- background-color: rgba($main,0.2);
-.tagBlue{
- background-color: rgba($blue,0.2);
-.tagGray{
- background-color: rgba($gray,0.2);
-.tagGradual{
- background-image: linear-gradient(to top right,rgba($main,0.2),rgba($main,0.1));
-.tagBlack{
- background-color: rgba($black,0.2);
-.tagGreen{
- background-color: rgba($green,0.2);
-.tagWhite{
- background-color: rgba($white,0.2);
-.tagOrange{
- background-color: rgba($orange,0.2);
-.tagRed{
- background-color: rgba($red,0.2);
-.tagYellow{
- background-color: rgba($yellow,0.2);
-/* 边线(实线、虚线) */
-.bdn{
- border: none;
-.bd{
- border: 1px solid $borderColor;
- &.dashed{
- border-style: dashed;
-.bt{
- border-top:1px solid $borderColor;
- border-top-style: dashed;
-.bb{
- border-bottom:1px solid $borderColor;
- border-bottom-style: dashed;
-.bl{
- border-left:1px solid $borderColor;
- border-left-style: dashed;
-.br{
- border-right: 1px solid $borderColor;
- border-right-style: dashed;
-$b:2;
-@while $b <= 10 {
- .bd#{$b}{
- border: #{$b}px solid $borderColor;
- .bt#{$b}{
- border-top:#{$b}px solid $borderColor;
- .bb#{$b}{
- border-bottom:#{$b}px solid $borderColor;
- .bl#{$b}{
- border-left:#{$b}px solid $borderColor;
- .br#{$b}{
- border-right: #{$b}px solid $borderColor;
- $b : $b + 1 ;
-/* 边线颜色 */
-.mainBd{
- border-color: $main;
-.greenBd{
-.redBd{
-.yellowBd{
- border-color:$yellow ;
-.blackBd{
-.whiteBd{
- border-color:$white ;
-.grayBd{
- border-color:$gray;
-.greyBd{
- border-color:$grey;
-.orangeBd{
- border-color:$orange;
-/* 圆角 */
-.radius,.rds{
- -webkit-border-radius: 100%!important;
- -moz-border-radius: 100%!important;
- border-radius: 100%!important;
-$r:0;
-@while $r <= 50{
- .rds#{$r},&.radius#{$r}{
- -webkit-border-radius:$r*2upx!important;
- -moz-border-radius:$r*2upx!important;
- border-radius:$r*2upx!important;
- $r : $r + 1;
-.rdsTl,.radiusTopLeft{
- border-top-left-radius:100%!important;
-.rdsTr,.radiusTopRight{
- border-top-right-radius: 100%!important;
-.rdsBl,.radiusBottomLeft{
- border-bottom-left-radius: 100%!important;
-.rdsBr,.radiusBottomRight{
- border-bottom-right-radius: 100%!important;
- .rdsTl#{$r},.radiusTopLeft#{$r}{
- border-top-left-radius: $r*2upx!important;
- .rdsTr#{$r},.radiusTopRight#{$r}{
- border-top-right-radius: $r*2upx!important;
- .rdsBl#{$r},.radiusBottomLeft#{$r}{
- border-bottom-left-radius: $r*2upx!important;
- .rdsBr#{$r},.radiusBottomRight#{$r}{
- border-bottom-right-radius: $r*2upx!important;
-/* 正方形&长方形 */
-[class^="square"] , [class*=" square"]{
-$p : 200 ;
-@while $p > 0 {
- .square#{$p}{
- padding-top: $p*1%;
- @if $p == 100 {
- .square{
- padding-top: 100%;
- $p : $p - 5 ;
-/* 阴影 */
-.shadow{
- box-shadow: 0px 2px 9px -1px rgba(0, 0, 0, 0.1);
-/* 遮罩层 */
-.wrapper-top,.wt{
- background-image: linear-gradient(rgba(0,0,0,0.3) , rgba(0,0,0,0.02));
-.wrapper-bottom,.wb{
- background-image: linear-gradient( rgba(0,0,0,0.02) , rgba(0,0,0,0.3) );
-[class^="wp"],[class*=" wp"] {
- z-index: 10;
-/* 透明度 */
-@for $op from 0 through 10 {
- .op#{$op}{
- opacity: $op * 0.1!important;
- .wp#{$op}{
- background-color: rgba(#000,$op*0.1);
- @if $op == 5 {
- .wp{
- background-color: rgba(#000,0.5);
-/* 分割线 */
-[class*=" split"],[class^="split"] {
- content:"";
- border-left: 1px solid $borderColor;
-$s:10;
-@while $s <= 100 {
- .split#{$s}{
- height: #{$s*2}upx;
- margin-top: -#{$s}upx;
- @if $s == 10 {
- .split{
- height: 20upx;
- margin-top: -10upx;
- $s:$s+2;
-.hover,[class^="hover"],[class*=" hover"]{
- transition: all 0.4s;
- opacity: 0.8 !important;
-.statusBar{
- height: var(--status-bar-height);
-.winBottom{
- height: var(--windown-bottom);
-.safeBottom{
-.disabled{
- opacity:0.8;
-.grid,.gridNoMb,.gridNoPd{
- >.btn,>.roundBtn{
- margin-left: 0 ;
-.roundBtn{
- -webkit-border-radius: 100upx;
- -moz-border-radius: 100upx;
- border-radius: 100upx;
- /* 位置 */
- .text-center,.tc{
- text-align: center!important;
- .text-left,.tl{
- text-align: left!important;
- .text-right,.tr{
- text-align: right!important;
- .text-justify,.tj{
- text-align: justify!important;
- .text-bold,.bold{
- font-weight: bold!important;
- .text-normal,.normal{
- font-weight: normal!important;
- .break{
- white-space: normal;
- .noBreak{
- word-break: keep-all;
- .inline{
- .block{
- .none{
- .center-block{
- .hidden{
- .hiddenX{
- overflow-x: hidden;
- .hiddenY{
- overflow-y: hidden;
- .auto{
- overflow: auto;
- .autoX{
- overflow-x: auto;
- .autoY{
- .showInMb{
- .showInPc{
- table{
- border-collapse: collapse;
- border-spacing: 0;
- border: 1px solid #e6e6e6;
- thead{
- tr{
- background-color: #f2f2f2;
- th{
- color: #8799a3;
- width: 1%;
- td,th{
- -o-text-overflow: ellipsis;
- word-wrap: break-word;
- padding: 5px 10px;
- height: 28px;
- line-height: 28px;
- &.autoWidth{
@@ -1,1811 +0,0 @@
-* BaseCloud APP更新检测组件
-v1.0.4
-*/
- <view class="base-cloud" style="display: inline-block;">
- <view class="father" style="display: flex;align-items: center;" v-if="showVersion" @click.stop.prevent="checkVersion">
- <text class="version-text">版本{{version}}</text>
- <view class="abs top right" v-if="updateData.updated" style="top: -3px;right: -7px;">
- <view class="w7 h7 rds redBg"></view>
- <view class="fixed z20 wp6 flex ct plr50 pb50 " v-if="show">
- <view class="w100p showIn" :animation="inData" style="max-width: 300px;">
- <view class="rds12" :style="{'background-color':color}">
- <view class="father">
- <view class="h120 father hidden">
- <view class="abs top left50p">
- <image src="./static/cloudRight.png" mode="widthFix" class="w500 h120 animated goAway infinite"></image>
- <view class="abs top right50p">
- <image src="./static/cloudLeft.png" mode="widthFix" class="w500 h120 animated goAwayLeft infinite"></image>
- <image class="abs top30 left30 w10 h10 animated infinite fadeOut slow" src="./static/star.png" mode="widthFix"></image>
- <image class="abs top60 left80 w10 h10 animated infinite fadeOut slowest delay-1s" src="./static/star.png" mode="widthFix"></image>
- <image class="abs top20 right20 w10 h10 animated infinite fadeOut slower delay-2s" src="./static/star.png" mode="widthFix"></image>
- <image class="abs top20 right50 w30 h30 animated fadeOutRight infinite slowest" src="./static/smallCloud.png"
- mode="widthFix"></image>
- <image class="abs top30 left50 w30 h30 animated fadeOutRight infinite slow8 " src="./static/smallCloud.png" mode="widthFix"></image>
- <view class="abs bottom animated bounceUp infinite">
- <view class="animated goUp delay-06s">
- <image src="./static/airship.png" mode="widthFix" class="w80 h75 center-block father z3"></image>
- <view class="father" style="top: -5px;">
- <image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block animated infinite splashOut"></image>
- <view class="abs">
- <image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block animated infinite splashOut delay-03s"></image>
- <image src="./static/shipAir.png" mode="widthFix" class="w40 h85 center-block animated infinite splashOut delay-06s"></image>
- <view class="abs bottom" style="bottom: -80upx;">
- <image src="./static/shipGas.png" mode="widthFix" class="w40 h85 center-block animated infinite splash"></image>
- <view class=" whiteBg hidden plr20 father z3 rdsBr12 rdsBl12" :class="{'pb100':progress <= 0 || progress >= 100 || completed}">
- <view class="ptb10 fz16 bold">
- <block v-if="progress == 0">
- {{title}} {{ updateData.version ? 'v' + updateData.version : ''}}
- <block v-else-if="progress <=100 && !completed">
- <view class="fz30 normal avanti pt15 text-center">
- {{ progress }}
- <text class="fz12 ml2">
- %
- <view class="text-center pb40 op8 gray fz14 normal">
- 版本更新中,不要离开...
- <view class="text-center pt15" v-else-if="completed">
- 版本升级成功
- <view class="pb40 op8 gray fz14 normal pt5">
- 更新已完成,请重启应用体验新版
- <scroll-view scroll-y="true" class="scroll-view h60 autoY pb20" v-if="progress == 0">
- <view class="column">
- <text v-if="updateData.description.length === 0">
- {{ defaultContent }}
- <text v-for="(item, index) in updateData.description" :key="index">
- {{ index + 1 }}.{{ item }}
- <view class="pd50 pt25" v-else-if="progress < 100">
- <view class="grayBg bd rds23">
- <view class="grayBg rds23">
- <view class="ptb3 rds23" :style="{width:progress+'%','background-color':color}"></view>
- <view class="abs top left50p roundBox rds text-center" :style="{'background-color':color}">
- <view class="pt30" v-if="!completed">
- <button hover-class="op9" @tap.stop="download" class="btn bd2 whiteBg line rds23 inline plr50 ptb10 fz16">
- 立即升级
- </button>
- <view class="pt30" v-else>
- <button hover-class="op8" @tap.stop="restart" class="btn bd2 whiteBg line rds23 inline plr50 ptb10 fz16">
- 立即重启
- <view class="op9 father" v-if="progress == 0">
- <view class="flex ct ">
- <view class="h30 bl3 whiteBd"></view>
- <view class="close-btn" @click="hide">
- <text class="mix-icon icon-close"></text>
- <mix-loading v-if="isLoading"></mix-loading>
- name: "version-update",
- default: "发现新版本"
- defaultContent: {
- default: "快来升级,体验最新的功能吧!"
- showVersion: {
- autoShow: {
- isCache: {
- updateUrl: {
- default: "api/base-app-version"
- default: "#ff536f"
- version: "1.0.0",
- updateData: {
- description: []
- progress: 0,
- completed: false,
- inData: null
- plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
- this.version = widgetInfo.version;
- async checkVersion(e) {
- console.log(e);
- const res = await this.$request('version', 'check', {
- version: this.version
- showLoading: true
- if (res.status === 0) {
- this.$util.msg(res.msg);
- res.data.description = res.data.description.split(';');
- this.updateData = res.data;
- this.show = true;
- download(e) {
- if(this.isDownloading){
- this.isDownloading = true;
- const task = uni.downloadFile({
- url: this.updateData.downloadLink,
- success: (downloadResult) => {
- this.isDownloading =false;
- uni.hideLoading();
- if (downloadResult.statusCode === 200) {
- plus.runtime.install(downloadResult.tempFilePath, {
- force: false
- this.downloadSuccess(e);
- }, (err) => {
- this.downloadError(err);
- fail: err => {
- task.onProgressUpdate((e) => {
- console.log(e.progress);
- this.progress = e.progress;
- downloadError(e) {
- this.show = false;
- this.progress = 0;
- uni.showModal({
- title: '提示',
- content: '更新失败,请稍后再试',
- showCancel: false,
- confirmColor: '#414cd9'
- downloadSuccess(e) {
- this.completed = true;
- restart(e) {
- this.completed = false;
- plus.runtime.restart();
- hide(e) {
- var animation = uni.createAnimation({
- animation.scale(0).opacity(0).step();
- this.inData = animation.export();
- setTimeout((e) => {
- this.inData = null;
- }, 420);
- .version-text{
- margin-right: 10rpx;
- z-index: -2rpx;
- .close-btn{
- margin-top: -20rpx;
- font-size: 48rpx;
- .scroll-view{
- height: auto !important;
- min-height: 120rpx;
- max-height: 17vh;
- text{
- margin-bottom: 16rpx;
- line-height: 1.5;
- .roundBox {
- width: 5000upx;
- height: 5000upx;
- margin-left: -2500upx;
- .animated {
- -webkit-animation-duration: 1s;
- animation-duration: 1s;
- -webkit-animation-fill-mode: both;
- animation-fill-mode: both;
- animation-timing-function: linear;
- .animated.infinite {
- -webkit-animation-iteration-count: infinite;
- animation-iteration-count: infinite;
- @keyframes goUp {
- -webkit-transform: translate3d(0, 70%, 0);
- transform: translate3d(0, 70%, 0);
- -webkit-transform: translate3d(0, 0, 0);
- transform: translate3d(0, 0, 0);
- .goUp {
- -webkit-animation-name: goUp;
- animation-name: goUp;
- -webkit-animation-duration: 600ms;
- animation-duration: 600ms;
- animation-timing-function: ease-in;
- @keyframes splash {
- 0,
- transform: scaleX(0.9);
- 5%,
- 95% {
- transform: scaleX(1.02);
- 10%,
- 80% {
- transform: scaleX(1.05);
- 25%,
- 75% {
- transform: scaleX(1.08);
- 50% {
- transform: scaleX(1.1);
- .splash {
- -webkit-animation-name: splash;
- animation-name: splash;
- -webkit-animation-duration: 0.6s;
- animation-duration: 0.6s;
- @-webkit-keyframes splashOut {
- transform: scaleX(0);
- transform: scaleX(2);
- .splashOut {
- -webkit-animation-name: splashOut;
- animation-name: splashOut;
- @keyframes bounceUp {
- transform: translate3d(0, -30rpx, 0);
- .bounceUp {
- -webkit-animation-name: bounceUp;
- animation-name: bounceUp;
- -webkit-animation-duration: 1.6s;
- animation-duration: 1.6s;
- @keyframes fadeOut {
- .fadeOut {
- -webkit-animation-name: fadeOut;
- animation-name: fadeOut;
- @keyframes fadeOutRight {
- transform: translate3d(-200%, 0, 0);
- transform: translate3d(200%, 0, 0);
- .fadeOutRight {
- -webkit-animation-name: fadeOutRight;
- animation-name: fadeOutRight;
- .animated.delay-03s {
- -webkit-animation-delay: 0.3s;
- animation-delay: 0.3s;
- .animated.delay-06s {
- -webkit-animation-delay: 0.6s;
- animation-delay: 0.6s;
- .animated.delay-1s {
- -webkit-animation-delay: 1s;
- animation-delay: 1s;
- .animated.delay-2s {
- -webkit-animation-delay: 2s;
- animation-delay: 2s;
- .animated.delay-3s {
- -webkit-animation-delay: 3s;
- animation-delay: 3s;
- .animated.fast {
- -webkit-animation-duration: 800ms;
- animation-duration: 800ms;
- .animated.faster {
- -webkit-animation-duration: 500ms;
- animation-duration: 500ms;
- .animated.fastest {
- -webkit-animation-duration: 200ms;
- animation-duration: 200ms;
- .animated.slow {
- -webkit-animation-duration: 2s;
- animation-duration: 2s;
- .animated.slower {
- -webkit-animation-duration: 3s;
- animation-duration: 3s;
- .animated.slowest {
- -webkit-animation-duration: 10s;
- animation-duration: 10s;
- .animated.slow4 {
- -webkit-animation-duration: 5s;
- animation-duration: 5s;
- .animated.slow5 {
- .animated.slow8 {
- -webkit-animation-duration: 8s;
- animation-duration: 8s;
- .goAway {
- transform: translate3d(-50%, 10%, 0);
- -webkit-animation-name: goAway;
- animation-name: goAway;
- @keyframes goAway {
- transform: translate3d(-1.3%, -17.6%, 0);
- .goAwayLeft {
- transform: translate3d(50%, 10%, 0);
- -webkit-animation-name: goAwayLeft;
- animation-name: goAwayLeft;
- @keyframes goAwayLeft {
- transform: translate3d(2%, -17%, 0);
- @keyframes showIn {
- transform: scale3d(0.5, 0.5, 0.5);
- transform: scale3d(1, 1, 1);
- .showIn {
- animation-duration: 0.4s;
- animation-name: showIn;
- div,a,img,span,page,view,navigator,image,text,input,textarea,button,form{
- a{
- form{
- image{will-change: transform}
- input,textarea,form{
- button{
- switch .uni-switch-input{
- .wx-switch-input,.uni-switch-input{width:42px !important;height:22px !important;}
- /**背景颜色**/
- .bg{
- .gradualBg{
- .grayBg{
- .whiteBg{
- .blackBg{
- .orangeBg{
- .redBg{
- .yellowBg{
- .greenBg{
- .brownBg{
- .blueBg{
- .purpleBg{
- /* 文字颜色 */
- .main{
- .green{
- .red{
- .yellow{
- .black{
- .white{
- .gray{
- .grey{
- .orange{
- .brown{
- .blue{
- .purple{
- .hoverMain{
- .hoverGreen{
- .hoverRed{
- .hoverBlue{
- .hoverGray{
- .hoverWhite{
- .hoverBlack{
- .hoverOrange{
- .hoverYellow{
- .hoverBrown{
- .hoverPurple{
- /* 宽度 高度 */
- $w:0;
- @while $w <= 500 {
- /* 字号 */
- @for $fz from 12 through 100 {
- /* 边距 - 覆盖顺序是小的尺寸覆盖大的尺寸 少的方向覆盖多的方向 */
- $s : 0 ;
- @while $s <= 500 {
- /* 文字溢出隐藏 */
- .clip{
- .cut{
- /* 价格 */
- /* 布局 */
- .grid,.gridNoPd,.gridSmPd,.gridNoMb{
- .gridNoMb{
- .gridNoPd{
- .gridSmPd{
- /* flex布局 */
- .flex{
- /* 定位布局 */
- .father{
- .abs,.fixed{
- @for $i from 0 through 20 {
- @for $i from 1 through 200 {
- .fixed{
- /* fix-auto布局 */
- .fixAuto,.fixAutoNoPd,.fixAutoSmPd{
- .fixAutoSmPd{
- .fixAutoNoPd{
- /* 浮动组件 */
- .clear{
- .fl{
- .fr{
- /* 按钮样式类 */
- .btn,.roundBtn{
- [class^="tag"] , [class*=" tag"]{
- .tag{
- .tagBlue{
- .tagGray{
- .tagGradual{
- .tagBlack{
- .tagGreen{
- .tagWhite{
- .tagOrange{
- .tagRed{
- .tagYellow{
- /* 边线(实线、虚线) */
- .bdn{
- .bd{
- .bt{
- .bb{
- .bl{
- .br{
- $b:2;
- @while $b <= 10 {
- /* 边线颜色 */
- .mainBd{
- .greenBd{
- .redBd{
- .yellowBd{
- .blackBd{
- .whiteBd{
- .grayBd{
- .greyBd{
- .orangeBd{
- /* 圆角 */
- .radius,.rds{
- $r:0;
- @while $r <= 50{
- .rdsTl,.radiusTopLeft{
- .rdsTr,.radiusTopRight{
- .rdsBl,.radiusBottomLeft{
- .rdsBr,.radiusBottomRight{
- /* 正方形&长方形 */
- [class^="square"] , [class*=" square"]{
- $p : 200 ;
- @while $p > 0 {
- /* 阴影 */
- .shadow{
- /* 遮罩层 */
- .wrapper-top,.wt{
- .wrapper-bottom,.wb{
- [class^="wp"],[class*=" wp"] {
- /* 透明度 */
- @for $op from 0 through 10 {
- /* 分割线 */
- [class*=" split"],[class^="split"] {
- $s:10;
- @while $s <= 100 {
- .hover,[class^="hover"],[class*=" hover"]{
- .statusBar{
- .winBottom{
- .safeBottom{
- .disabled{
- .grid,.gridNoMb,.gridNoPd{
- .roundBtn{
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
- <title></title>
- <!--preload-links-->
- <!--app-context-->
- </head>
- <body>
- <div id="app"><!--app-html--></div>
- <script type="module" src="/main.js"></script>
- </body>
-</html>
@@ -1,50 +0,0 @@
-import App from './App'
-import uView from '@/uni_modules/uview-ui'
-Vue.use(uView)
-// 全局 Mixin
-import mixin from './common/mixin/mixin'
-Vue.mixin(mixin)
-// 全局 Util
-import {
- msg,
- isLogin,
- debounce,
- throttle,
- prePage,
- date
-} from '@/common/js/util'
-Vue.prototype.$util = {
-// 全局 Store
-import store from './store'
-Vue.prototype.$store = store
-// #ifndef VUE3
-import Vue from 'vue'
-Vue.config.productionTip = false
-App.mpType = 'app'
-const app = new Vue({
- ...App
-app.$mount()
-// #ifdef VUE3
-import { createSSRApp } from 'vue'
-export function createApp() {
- const app = createSSRApp(App)
- app
@@ -1,72 +0,0 @@
- "name" : "ruoyi-vue-ui",
- "appid" : "__UNI__764D04C",
- "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
- "usingComponents" : true
- "mp-alipay" : {
- "mp-baidu" : {
- "mp-toutiao" : {
- "uniStatistics" : {
- "enable" : false
- "vueVersion" : "2"
- "easycom": {
- "^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue"
- "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
- "path": "pages/index/index",
- "style": {
- "navigationBarTitleText": "uni-app"
- "path": "pages/tabbar/user",
- "navigationBarTitleText": "我的",
- "navigationStyle": "custom"
- "path": "pages/auth/login",
- "navigationBarTitleText": "登录",
- "navigationStyle":"custom",
- "app-plus": {
- "animationType": "slide-in-bottom"
- "path" : "pages/set/userInfo",
- "style" : {
- "navigationBarTitleText": "个人资料"
- "globalStyle": {
- "navigationBarTextStyle": "black",
- "navigationBarTitleText": "uni-app",
- "navigationBarBackgroundColor": "#F8F8F8",
- "backgroundColor": "#F8F8F8"
- "tabBar": {
- "color": "#666",
- "selectedColor": "#FF5A5F",
- "borderStyle": "black",
- "list": [{
- "text": "首页",
- "pagePath": "pages/index/index",
- "iconPath": "static/tarbar/index.png",
- "selectedIconPath": "static/tarbar/index-active.png"
- "text": "商品",
- "pagePath": "pages/product/list",
- "iconPath": "static/tarbar/product.png",
- "selectedIconPath": "static/tarbar/product-active.png"
- "text": "我的",
- "pagePath": "pages/tabbar/user",
- "iconPath": "static/tarbar/ucenter.png",
- "selectedIconPath": "static/tarbar/ucenter-active.png"
@@ -1,326 +0,0 @@
- <view class="app">
- <!-- 左上角的 x 关闭 -->
- <view class="back-btn mix-icon icon-guanbi" @click="navBack"></view>
- <!-- 用户协议 -->
- <view class="agreement center">
- <text class="mix-icon icon-xuanzhong" :class="{active: agreement}" @click="checkAgreement"></text>
- <text @click="checkAgreement">请认真阅读并同意</text>
- <text class="title" @click="navToAgreementDetail(1)">《用户服务协议》</text>
- <text class="title" @click="navToAgreementDetail(2)">《隐私权政策》</text>
- <!-- 登录表单 -->
- <view class="wrapper">
- <view class="left-top-sign">LOGIN</view>
- <view class="welcome">手机登录/注册</view>
- <!-- 手机验证码登录 -->
- <view class="input-content">
- <u--form labelPosition="left" :model="form" :rules="rules" ref="form" errorType="toast">
- <u-form-item prop="mobile" borderBottom>
- <u--input type="number" v-model="form.mobile" placeholder="请输入手机号" border="none"></u--input>
- </u-form-item>
- <!-- 判断使用验证码还是密码 -->
- <u-form-item prop="code" borderBottom v-if="loginType == 'code'">
- <u--input type="number" v-model="form.code" placeholder="请输入验证码" border="none"></u--input>
- <u-button slot="right" @tap="getCode" :text="uCode.tips" type="success" size="mini" :disabled="uCode.disabled"></u-button>
- <u-code ref="uCode" @change="codeChange" seconds="60" @start="uCode.disabled === true" @end="uCode.disabled === false"></u-code>
- <u-form-item prop="password" borderBottom v-else>
- <u--input password v-model="form.password" placeholder="请输入密码" border="none"></u--input>
- </u--form>
- <u-button class="login-button" text="立即登录" type="error" shape="circle" @click="mobileLogin"
- :loading="loading"></u-button>
- <!-- 切换登陆 -->
- <view class="login-type" v-if="loginType == 'code'" @click="setLoginType('password')">账号密码登录</view>
- <view class="login-type" v-else @click="setLoginType('code')">免密登录</view>
- <!-- 快捷登录 -->
- <!-- #ifdef APP-PLUS || MP-WEIXIN -->
- <view class="other-wrapper">
- <view class="line center">
- <text class="title">快捷登录</text>
- <view class="list row">
- <!-- #ifdef MP-WEIXIN -->
- <view class="item column center" @click="mpWxGetUserInfo">
- <image class="icon" src="/static/icon/login-wx.png"></image>
- <!-- #ifdef APP-PLUS -->
- <view class="item column center" style="width: 180rpx;" @click="loginByWxApp">
- <text>微信登录</text>
- import { checkStr } from '@/common/js/util'
- import { login, smsLogin, sendSmsCode } from '@/api/system/auth.js'
- import loginMpWx from './mixin/login-mp-wx.js'
- import loginAppWx from './mixin/login-app-wx.js'
- export default{
- mixins: [loginMpWx, loginAppWx],
- agreement: true,
- loginType: 'code', // 登录方式,code 验证码;password 密码
- loading: false, // 表单提交
- rules: {
- mobile: [{
- required: true,
- message: '请输入手机号'
- validator: (rule, value, callback) => {
- return uni.$u.test.mobile(value);
- message: '手机号码不正确'
- }],
- code: [],
- password: []
- form: {
- mobile: '',
- code: '',
- password: '',
- uCode: {
- tips: '',
- disable: false,
- this.setLoginType(this.loginType);
- // 手机号登录
- mobileLogin() {
- if (!this.agreement) {
- this.$util.msg('请阅读并同意用户服务及隐私协议');
- this.$refs.form.validate().then(() => {
- // 执行登陆
- const { mobile, code, password} = this.form;
- const loginPromise = this.loginType == 'password' ? login(mobile, password) :
- smsLogin(mobile, code);
- loginPromise.then(data => {
- // 登陆成功
- this.loginSuccessCallBack(data);
- }).catch(errors => {
- }).finally(() => {
- // 登陆成功的处理逻辑
- loginSuccessCallBack(data) {
- this.$util.msg('登录成功');
- this.$store.commit('setToken', data);
- // TODO 芋艿:如果当前页是第一页,则无法返回。期望是能够回到首页
- uni.navigateBack();
- navBack() {
- uni.navigateBack({
- delta: 1
- setLoginType(loginType) {
- this.loginType = loginType;
- // 修改校验规则
- this.rules.code = [];
- this.rules.password = [];
- if (loginType == 'code') {
- this.rules.code = [{
- message: '请输入验证码'
- min: 4,
- max: 6,
- message: '验证码不正确'
- }];
- this.rules.password = [{
- message: '请输入密码'
- max: 16,
- message: '密码不正确'
- //同意协议
- checkAgreement(){
- this.agreement = !this.agreement;
- //打开协议
- navToAgreementDetail(type){
- this.navTo('/pages/public/article?param=' + JSON.stringify({
- module: 'article',
- operation: 'getAgreement',
- type
- }))
- codeChange(text) {
- this.uCode.tips = text;
- getCode() {
- // 处于发送中,则跳过
- if (!this.$refs.uCode.canGetCode) {
- // 校验手机号
- this.$refs.form.validateField('mobile', errors => {
- if (errors.length > 0) {
- uni.$u.toast(errors[0].message);
- // 发送验证码 TODO 芋艿,枚举类
- sendSmsCode(this.form.mobile, 1).then(data => {
- uni.$u.toast('验证码已发送');
- // 通知验证码组件内部开始倒计时
- this.$refs.uCode.start();
- }).catch(erros => {
- page {
- background: #fff;
- .app {
- padding-top: 15vh;
- .wrapper {
- padding-bottom: 40rpx;
- .welcome {
- left: 50rpx;
- top: -90rpx;
- font-size: 46rpx;
- color: #555;
- text-shadow: 1px 0px 1px rgba(0,0,0,.3);
- .back-btn {
- position:absolute;
- left: 20rpx;
- top: calc(var(--status-bar-height) + 20rpx);
- color: #606266;
- .left-top-sign {
- font-size: 120rpx;
- color: #f8f8f8;
- left: -12rpx;
- /** 手机登录部分 */
- .input-content {
- padding: 0 60rpx;
- .login-button {
- margin-top: 30rpx;
- .login-type {
- font-size: 13px;
- /* 其他登录方式 */
- .other-wrapper{
- padding-top: 20rpx;
- margin-bottom: 40rpx;
- .title {
- margin: 0 32rpx;
- &:before, &:after{
- width: 160rpx;
- border-top: 1px solid #e0e0e0;
- width: 90rpx;
- height: 90rpx;
- margin: 0 24rpx 16rpx;
- .agreement{
- bottom: 6vh;
- font-size: 36rpx;
- margin-top: 1px;
- &.active{
@@ -1,73 +0,0 @@
- * 微信App登录
- * "openId": "o0yywwGWxtBCvBuE8vH4Naof0cqU",
- * "nickName": "S .",
- * "gender": 1,
- * "city": "临沂",
- * "province": "山东",
- * "country": "中国",
- * "avatarUrl": "http://thirdwx.qlogo.cn/mmopen/vi_32/xqpCtHRBBmdlf201Fykhtx7P7JcicIbgV3Weic1oOvN6iaR3tEbuu74f2fkKQWXvzK3VDgNTZzgf0g8FqPvq8LCNQ/132",
- * "unionId": "oYqy4wmMcs78x9P-tsyMeM3MQ1PU"
- loginByWxApp(userInfoData){
- if(!this.agreement){
- this.$util.throttle(async ()=>{
- let [err, res] = await uni.login({
- provider: 'weixin'
- if(err){
- uni.getUserInfo({
- provider: 'weixin',
- success: async res=>{
- const response = await this.$request('user', 'loginByWeixin', {
- if(response.status === 0){
- if(response.hasBindMobile && response.data.token){
- this.loginSuccessCallBack({
- token: response.data.token,
- tokenExpired: response.data.tokenExpired
- this.navTo('/pages/auth/bindMobile?data='+JSON.stringify(response.data))
- plus.oauth.getServices(oauthRes=>{
- oauthRes[0].logout(logoutRes => {
- console.log(logoutRes);
- }, error => {
- console.log(error);
- fail(err) {
@@ -1,81 +0,0 @@
- mpCodeTimer: 0,
- mpWxCode: '',
- timerIdent(){
- return this.$store.state.timerIdent;
- this.mpCodeTimer ++;
- if(this.mpCodeTimer % 30 === 0){
- this.getMpWxCode();
- onShow(){
- //微信小程序登录
- mpWxGetUserInfo(){
- uni.getUserProfile({
- desc: '用于展示您的头像及昵称',
- success: async profileRes=> {
- const res = await this.$request('user', 'loginByWeixin', {
- code: this.mpWxCode,
- ...profileRes.userInfo
- if(res.status === 0){
- if(res.hasBindMobile && res.data.token){
- token: res.data.token,
- tokenExpired: res.data.tokenExpired
- this.navTo('/pages/auth/bindMobile?data='+JSON.stringify(res.data))
- //获取code
- getMpWxCode(){
- uni.login({
- success: res=> {
- this.mpWxCode = res.code;
@@ -1,52 +0,0 @@
- <image class="logo" src="/static/logo.png"></image>
- <view class="text-area">
- <text class="title">{{title}}</text>
- title: 'Hello'
- .content {
- .logo {
- height: 200rpx;
- width: 200rpx;
- margin-top: 200rpx;
- margin-left: auto;
- margin-right: auto;
- margin-bottom: 50rpx;
- .text-area {
- color: #8f8f94;
@@ -1,633 +0,0 @@
-(function(global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.weCropper = factory());
-}(this, (function() {
- 'use strict';
- var device = void 0;
- var TOUCH_STATE = ['touchstarted', 'touchmoved', 'touchended'];
- function firstLetterUpper(str) {
- return str.charAt(0).toUpperCase() + str.slice(1);
- function setTouchState(instance) {
- for (var _len = arguments.length, arg = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- arg[_key - 1] = arguments[_key];
- TOUCH_STATE.forEach(function(key, i) {
- if (arg[i] !== undefined) {
- instance[key] = arg[i];
- function validator(instance, o) {
- Object.defineProperties(instance, o);
- function getDevice() {
- if (!device) {
- device = wx.getSystemInfoSync();
- return device;
- var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
- return typeof obj;
- } : function(obj) {
- return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" :
- typeof obj;
- var classCallCheck = function(instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- var createClass = function() {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
- return function(Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- }();
- var slicedToArray = function() {
- function sliceIterator(arr, i) {
- var _arr = [];
- var _n = true;
- var _d = false;
- var _e = undefined;
- try {
- for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
- _arr.push(_s.value);
- if (i && _arr.length === i) break;
- } catch (err) {
- _d = true;
- _e = err;
- } finally {
- if (!_n && _i["return"]) _i["return"]();
- if (_d) throw _e;
- return _arr;
- return function(arr, i) {
- if (Array.isArray(arr)) {
- return arr;
- } else if (Symbol.iterator in Object(arr)) {
- return sliceIterator(arr, i);
- throw new TypeError("Invalid attempt to destructure non-iterable instance");
- var tmp = {};
- var DEFAULT = {
- id: {
- default: 'cropper',
- get: function get$$1() {
- return tmp.id;
- set: function set$$1(value) {
- if (typeof value !== 'string') {}
- tmp.id = value;
- width: {
- default: 750,
- return tmp.width;
- tmp.width = value;
- height: {
- return tmp.height;
- tmp.height = value;
- scale: {
- default: 2.5,
- return tmp.scale;
- tmp.scale = value;
- zoom: {
- default: 5,
- return tmp.zoom;
- tmp.zoom = value;
- src: {
- return tmp.src;
- tmp.src = value;
- cut: {
- default: {},
- return tmp.cut;
- tmp.cut = value;
- onReady: {
- default: null,
- return tmp.ready;
- tmp.ready = value;
- onBeforeImageLoad: {
- return tmp.beforeImageLoad;
- tmp.beforeImageLoad = value;
- onImageLoad: {
- return tmp.imageLoad;
- tmp.imageLoad = value;
- onBeforeDraw: {
- return tmp.beforeDraw;
- tmp.beforeDraw = value;
- function prepare() {
- var self = this;
- var _getDevice = getDevice(),
- windowWidth = _getDevice.windowWidth;
- self.attachPage = function() {
- var pages = getCurrentPages();
- var pageContext = pages[pages.length - 1];
- pageContext.wecropper = self;
- self.createCtx = function() {
- var id = self.id;
- if (id) {
- self.ctx = wx.createCanvasContext(id);
- self.deviceRadio = windowWidth / 750;
- self.deviceRadio = self.deviceRadio.toFixed(2)
- function observer() {
- var EVENT_TYPE = ['ready', 'beforeImageLoad', 'beforeDraw', 'imageLoad'];
- self.on = function(event, fn) {
- if (EVENT_TYPE.indexOf(event) > -1) {
- if (typeof fn === 'function') {
- event === 'ready' ? fn(self) : self['on' + firstLetterUpper(event)] = fn;
- return self;
- function methods() {
- var deviceRadio = self.deviceRadio;
- var boundWidth = self.width;
- var boundHeight = self.height;
- var _self$cut = self.cut,
- _self$cut$x = _self$cut.x,
- x = _self$cut$x === undefined ? 0 : _self$cut$x,
- _self$cut$y = _self$cut.y,
- y = _self$cut$y === undefined ? 0 : _self$cut$y,
- _self$cut$width = _self$cut.width,
- width = _self$cut$width === undefined ? boundWidth : _self$cut$width,
- _self$cut$height = _self$cut.height,
- height = _self$cut$height === undefined ? boundHeight : _self$cut$height;
- self.updateCanvas = function() {
- if (self.croperTarget) {
- self.ctx.drawImage(self.croperTarget, self.imgLeft, self.imgTop, self.scaleWidth, self.scaleHeight);
- typeof self.onBeforeDraw === 'function' && self.onBeforeDraw(self.ctx, self);
- self.setBoundStyle();
- self.ctx.draw();
- self.pushOrign = function(src) {
- self.src = src;
- typeof self.onBeforeImageLoad === 'function' && self.onBeforeImageLoad(self.ctx, self);
- uni.getImageInfo({
- src: src,
- success: function success(res) {
- var innerAspectRadio = res.width / res.height;
- self.croperTarget = res.path || src;
- if (innerAspectRadio < width / height) {
- self.rectX = x;
- self.baseWidth = width;
- self.baseHeight = width / innerAspectRadio;
- self.rectY = y - Math.abs((height - self.baseHeight) / 2);
- self.rectY = y;
- self.baseWidth = height * innerAspectRadio;
- self.baseHeight = height;
- self.rectX = x - Math.abs((width - self.baseWidth) / 2);
- self.imgLeft = self.rectX;
- self.imgTop = self.rectY;
- self.scaleWidth = self.baseWidth;
- self.scaleHeight = self.baseHeight;
- self.updateCanvas();
- typeof self.onImageLoad === 'function' && self.onImageLoad(self.ctx, self);
- self.update();
- self.getCropperImage = function() {
- for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
- args[_key] = arguments[_key];
- var ARG_TYPE = toString.call(args[0]);
- switch (ARG_TYPE) {
- case '[object Object]':
- var _args$0$quality = args[0].quality,
- quality = _args$0$quality === undefined ? 10 : _args$0$quality;
- uni.canvasToTempFilePath({
- canvasId: id,
- x: x,
- y: y,
- fileType: "jpg",
- width: width,
- height: height,
- destWidth: width * quality / (deviceRadio * 10),
- destHeight: height * quality / (deviceRadio * 10),
- typeof args[args.length - 1] === 'function' && args[args.length - 1](res.tempFilePath);
- case '[object Function]':
- destWidth: width,
- destHeight: height,
- function update() {
- if (!self.src) return;
- self.__oneTouchStart = function(touch) {
- self.touchX0 = touch.x;
- self.touchY0 = touch.y;
- self.__oneTouchMove = function(touch) {
- var xMove = void 0,
- yMove = void 0;
- if (self.touchended) {
- return self.updateCanvas();
- xMove = touch.x - self.touchX0;
- yMove = touch.y - self.touchY0;
- var imgLeft = self.rectX + xMove;
- var imgTop = self.rectY + yMove;
- self.outsideBound(imgLeft, imgTop);
- self.__twoTouchStart = function(touch0, touch1) {
- yMove = void 0,
- oldDistance = void 0;
- self.touchX1 = self.rectX + self.scaleWidth / 2;
- self.touchY1 = self.rectY + self.scaleHeight / 2;
- xMove = touch1.x - touch0.x;
- yMove = touch1.y - touch0.y;
- oldDistance = Math.sqrt(xMove * xMove + yMove * yMove);
- self.oldDistance = oldDistance;
- self.__twoTouchMove = function(touch0, touch1) {
- newDistance = void 0;
- var scale = self.scale,
- zoom = self.zoom;
- newDistance = Math.sqrt(xMove * xMove + yMove * yMove
- // 使用0.005的缩放倍数具有良好的缩放体验
- self.newScale = self.oldScale + 0.001 * zoom * (newDistance - self.oldDistance);
- // 设定缩放范围
- self.newScale <= 1 && (self.newScale = 1);
- self.newScale >= scale && (self.newScale = scale);
- self.scaleWidth = self.newScale * self.baseWidth;
- self.scaleHeight = self.newScale * self.baseHeight;
- var imgLeft = self.touchX1 - self.scaleWidth / 2;
- var imgTop = self.touchY1 - self.scaleHeight / 2;
- self.__xtouchEnd = function() {
- self.oldScale = self.newScale;
- self.rectX = self.imgLeft;
- self.rectY = self.imgTop;
- var handle = {
- touchStart: function touchStart(e) {
- var _e$touches = slicedToArray(e.touches, 2),
- touch0 = _e$touches[0],
- touch1 = _e$touches[1];
- if (!touch0.x) {
- touch0.x = touch0.clientX;
- touch0.y = touch0.clientY;
- if (touch1) {
- touch1.x = touch1.clientX;
- touch1.y = touch1.clientY;
- setTouchState(self, true, null, null);
- self.__oneTouchStart(touch0);
- if (e.touches.length >= 2) {
- self.__twoTouchStart(touch0, touch1);
- touchMove: function touchMove(e) {
- var _e$touches2 = slicedToArray(e.touches, 2),
- touch0 = _e$touches2[0],
- touch1 = _e$touches2[1];
- setTouchState(self, null, true);
- if (e.touches.length === 1) {
- self.__oneTouchMove(touch0);
- self.__twoTouchMove(touch0, touch1);
- touchEnd: function touchEnd(e) {
- setTouchState(self, false, false, true);
- self.__xtouchEnd();
- function cut() {
- self.outsideBound = function(imgLeft, imgTop) {
- self.imgLeft = imgLeft >= x ? x : self.scaleWidth + imgLeft - x <= width ? x + width - self.scaleWidth : imgLeft;
- self.imgTop = imgTop >= y ? y : self.scaleHeight + imgTop - y <= height ? y + height - self.scaleHeight : imgTop;
- self.setBoundStyle = function() {
- var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
- _ref$color = _ref.color,
- color = _ref$color === undefined ? '#04b00f' : _ref$color,
- _ref$mask = _ref.mask,
- mask = _ref$mask === undefined ? 'rgba(0, 0, 0, 0.5)' : _ref$mask,
- _ref$lineWidth = _ref.lineWidth,
- lineWidth = _ref$lineWidth === undefined ? 1 : _ref$lineWidth;
- self.ctx.beginPath();
- self.ctx.setFillStyle(mask);
- self.ctx.fillRect(0, 0, x, boundHeight);
- self.ctx.fillRect(x, 0, width, y);
- self.ctx.fillRect(x, y + height, width, boundHeight - y - height);
- self.ctx.fillRect(x + width, 0, boundWidth - x - width, boundHeight);
- self.ctx.fill();
- self.ctx.setStrokeStyle(color);
- self.ctx.setLineWidth(lineWidth);
- self.ctx.moveTo(x - lineWidth, y + 10 - lineWidth);
- self.ctx.lineTo(x - lineWidth, y - lineWidth);
- self.ctx.lineTo(x + 10 - lineWidth, y - lineWidth);
- self.ctx.stroke();
- self.ctx.moveTo(x - lineWidth, y + height - 10 + lineWidth);
- self.ctx.lineTo(x - lineWidth, y + height + lineWidth);
- self.ctx.lineTo(x + 10 - lineWidth, y + height + lineWidth);
- self.ctx.moveTo(x + width - 10 + lineWidth, y - lineWidth);
- self.ctx.lineTo(x + width + lineWidth, y - lineWidth);
- self.ctx.lineTo(x + width + lineWidth, y + 10 - lineWidth);
- self.ctx.moveTo(x + width + lineWidth, y + height - 10 + lineWidth);
- self.ctx.lineTo(x + width + lineWidth, y + height + lineWidth);
- self.ctx.lineTo(x + width - 10 + lineWidth, y + height + lineWidth);
- var __version__ = '1.1.4';
- var weCropper = function() {
- function weCropper(params) {
- classCallCheck(this, weCropper);
- var _default = {};
- validator(self, DEFAULT);
- Object.keys(DEFAULT).forEach(function(key) {
- _default[key] = DEFAULT[key].default;
- Object.assign(self, _default, params);
- self.prepare();
- self.attachPage();
- self.createCtx();
- self.observer();
- self.cutt();
- self.methods();
- self.init();
- createClass(weCropper, [{
- key: 'init',
- value: function init() {
- var src = self.src;
- self.version = __version__;
- typeof self.onReady === 'function' && self.onReady(self.ctx, self);
- if (src) {
- self.pushOrign(src);
- setTouchState(self, false, false, false);
- self.oldScale = 1;
- self.newScale = 1;
- }]);
- return weCropper;
- Object.assign(weCropper.prototype, handle);
- weCropper.prototype.prepare = prepare;
- weCropper.prototype.observer = observer;
- weCropper.prototype.methods = methods;
- weCropper.prototype.cutt = cut;
- weCropper.prototype.update = update;
-})));
@@ -1,223 +0,0 @@
- <view class="title-view" :style="{height: navigationBarHeight + 'px'}">
- <navigator open-type="navigateBack" class="back-btn mix-icon icon-xiangzuo"></navigator>
- <text class="title">裁剪</text>
- <text class="empty"></text>
- <view class="cropper-wrapper">
- <canvas
- class="cropper"
- disable-scroll="true"
- @touchstart="touchStart"
- @touchmove="touchMove"
- @touchend="touchEnd"
- :style="{ width: cropperOpt.width, height: cropperOpt.height }"
- canvas-id="cropper"
- ></canvas>
- <view class="cropper-buttons">
- <view class="btn upload" @tap="uploadTap">重选</view>
- <view class="btn getCropperImage" @tap="getCropperImage">确定</view>
- import weCropper from './cut.js';
- const device = uni.getSystemInfoSync();
- const width = device.windowWidth;
- const height = device.windowHeight;
- cropperOpt: {
- id: 'cropper',
- scale: 2.5,
- zoom: 8,
- x: (width - 200) / 2,
- y: (height - this.systemInfo.navigationBarHeight - this.systemInfo.statusBarHeight - 200) / 2,
- width: 200,
- height: 200
- weCropper: ''
- navigationBarHeight(){
- console.log(this.systemInfo.navigationBarHeight);
- return this.systemInfo.navigationBarHeight;
- onLoad(option) {
- // do something
- const cropperOpt = this.cropperOpt;
- const src = option.src;
- console.log(src);
- Object.assign(cropperOpt, {
- src
- this.weCropper = new weCropper(cropperOpt)
- .on('ready', function(ctx) {})
- .on('beforeImageLoad', ctx => {
- /* uni.showToast({
- title: '上传中',
- icon: 'loading',
- duration: 3000
- }); */
- .on('imageLoad', ctx => {
- uni.hideToast();
- touchStart(e) {
- this.weCropper.touchStart(e);
- touchMove(e) {
- this.weCropper.touchMove(e);
- touchEnd(e) {
- this.weCropper.touchEnd(e);
- convertBase64UrlToBlob(dataURI, type) {
- var binary = atob(dataURI.split(',')[1]);
- var array = [];
- for (var i = 0; i < binary.length; i++) {
- array.push(binary.charCodeAt(i));
- return new Blob([new Uint8Array(array)], {
- type: type
- filename: '1111.jpg'
- blobToDataURL(blob) {
- var a = new FileReader();
- a.readAsDataURL(blob); //读取文件保存在result中
- a.onload = function(e) {
- var getRes = e.target.result; //读取的结果在result中
- console.log(getRes);
- getCropperImage() {
- let _this = this;
- this.weCropper.getCropperImage(avatar => {
- if (avatar) {
- this.$util.prePage().setAvatar(avatar);
- console.log('获取图片失败,请稍后重试');
- uploadTap() {
- const self = this;
- uni.chooseImage({
- count: 1, // 默认9
- sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
- sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
- success(res) {
- let src = res.tempFilePaths[0];
- // 获取裁剪图片资源后,给data添加src属性及其值
- self.weCropper.pushOrign(src);
- page, .content{
- padding-top: var(--status-bar-height);
- .title-view{
- background: transparent;
- .back-btn{
- width: 42px;
- height: 40px;
- font-size: 18px;
- font-size: 17px;
- .empty{
- .cropper {
- .cropper-wrapper {
- .cropper-buttons {
- height: 50px;
- background-color: rgba(0, 0, 0, 0.4);
- width: 100px;
- line-height: 50px;
- font-size: 15px;
- &.upload{
- padding-left: 20px;
- &.getCropperImage{
- padding-right: 20px;
- <view class="cell">
- <text class="tit fill">头像</text>
- <view class="avatar-wrap" @click="chooseImage">
- <image class="avatar" :src="tempAvatar || userInfo.avatar || '/static/icon/default-avatar.png'" mode="aspectFill"></image>
- <!-- 进度遮盖 -->
- <view class="progress center"
- 'no-transtion': uploadProgress === 0,
- show: uploadProgress != 100
- width: uploadProgress + '%',
- height: uploadProgress + '%',
- }"></view>
- <view class="cell b-b">
- <text class="tit fill">昵称</text>
- <input class="input" v-model="userInfo.nickname" type="text" maxlength="8" placeholder="请输入昵称" placeholder-class="placeholder">
- <u-cell-group>
- <u-cell title="昵称" :value="userInfo.nickname" isLink @click="nicknameClick()"></u-cell>
- </u-cell-group>
- <u-modal :show="nicknameOpen" title="修改昵称" showCancelButton @confirm="nicknameSubmit" @cancel="nicknameCancel">
- <view class="slot-content">
- <u--form labelPosition="left" :model="nicknameForm" :rules="nicknameRules" ref="nicknameForm" errorType="toast">
- <u-form-item prop="nickname">
- <u--input v-model="nicknameForm.nickname" placeholder="请输入昵称" border="none"></u--input>
- </u-modal>
- uploadProgress: 100, //头像上传进度
- tempAvatar: '',
- nicknameOpen: false,
- nicknameForm: {
- nickname: ''
- nicknameRules: {
- nickname: [{
- message: '请输入昵称'
- curUserInfo(){
- return this.$store.state.userInfo
- curUserInfo(curUserInfo){
- const {avatar, nickname, gender} = curUserInfo;
- this.userInfo = {avatar, nickname, gender,};
- const {avatar, nickname, gender, anonymous} = this.curUserInfo;
- this.userInfo = {avatar, nickname, gender};
- nicknameClick() {
- this.nicknameOpen = true;
- this.nicknameForm.nickname = this.userInfo.nickname;
- nicknameCancel() {
- this.nicknameOpen = false;
- nicknameSubmit() {
- this.$refs.nicknameForm.validate().then(() => {
- // 提交修改
- async confirm() {
- // 校验信息是否变化
- const {uploadProgress, userInfo, curUserInfo} = this;
- let isUpdate = false;
- for (let key in userInfo) {
- if(userInfo[key] !== curUserInfo[key]){
- isUpdate = true;
- if (isUpdate === false) {
- this.$util.msg('信息未修改');
- this.$refs.confirmBtn.stop();
- if (!userInfo.avatar) {
- this.$util.msg('请上传头像');
- if (uploadProgress !== 100) {
- this.$util.msg('请等待头像上传完毕');
- if (!userInfo.nickname) {
- this.$util.msg('请输入您的昵称');
- const res = await this.$request('user', 'update', userInfo);
- if(res.status === 1){
- this.$store.dispatch('getUserInfo'); //刷新用户信息
- // 选择头像
- chooseImage(){
- count: 1,
- url: `./cutImage/cut?src=${res.tempFilePaths[0]}`
- // 裁剪回调
- async setAvatar(filePath){
- this.tempAvatar = filePath;
- this.uploadProgress = 0;
- const result = await uniCloud.uploadFile({
- filePath: filePath,
- cloudPath: + new Date() + ('000000' + Math.floor(Math.random() * 999999)).slice(-6) + '.jpg',
- onUploadProgress: e=> {
- this.uploadProgress = Math.round(
- (e.loaded * 100) / e.total
- if(!result.fileID){
- this.$util.msg('头像上传失败');
- if(typeof uniCloud.getTempFileURL === 'undefined'){
- this.userInfo.avatar = result.fileID;
- const tempFiles = await uniCloud.getTempFileURL({
- fileList: [result.fileID]
- const tempFile = tempFiles.fileList[0];
- if(tempFile.download_url || tempFile.fileID){
- this.userInfo.avatar = tempFile.download_url || tempFile.fileID;
- .app{
- padding-top: 16rpx;
- padding: 0 40rpx;
- margin-bottom: 10rpx;
- left: 40rpx;
- right: 40rpx;
- border-color: #d8d8d8;
- .avatar-wrap{
- width: 120rpx;
- .avatar{
- .progress{
- transform: translate(-50%, -50%);
- width: 100rpx;
- box-shadow: rgba(0,0,0,.6) 0px 0px 0px 2005px;
- transition: .5s;
- &.no-transtion{
- transition: 0s;
- &.show{
- switch{
- transform: scale(0.8) translateX(10rpx);
- transform-origin: center right;
- margin-left: 20rpx;
- .checkbox{
- padding: 12rpx 0 12rpx 40rpx;
- .icon-xuanzhong{