多模块重构 12:修改项目名字,按照新的规则

This commit is contained in:
YunaiV
2022-02-02 22:33:39 +08:00
parent 352a67c530
commit 0773a4c4d7
1040 changed files with 12 additions and 190 deletions

View File

@@ -0,0 +1,16 @@
{ // 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" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

79
yudao-ui-app-v1/App.vue Normal file
View File

@@ -0,0 +1,79 @@
<script>
import Vue from 'vue'
import { getAuthToken } from '@/common/js/util.js'
let __timerId = 0;
export default {
onLaunch() {
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;
// #endif
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() {
this.closeTimer();
}
}
</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>

View File

@@ -0,0 +1,23 @@
import { request } from '@/common/js/request.js'
// 获得用户的基本信息
export function getUserInfo() {
return request({
url: 'member/user/profile/get',
method: 'get'
})
}
// 修改
export function updateNickname(nickname) {
return request({
url: 'member/user/profile/update-nickname',
method: 'post',
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: {
nickname
}
})
}

View File

@@ -0,0 +1,34 @@
import { request } from '@/common/js/request.js'
// 手机号 + 密码登陆
export function login(mobile, password) {
return request({
url: 'login',
method: 'post',
data: {
mobile, password
}
})
}
// 手机号 + 验证码登陆
export function smsLogin(mobile, code) {
return request({
url: 'sms-login',
method: 'post',
data: {
mobile, code
}
})
}
// 发送手机验证码
export function sendSmsCode(mobile, scene) {
return request({
url: 'send-sms-code',
method: 'post',
data: {
mobile, scene
}
})
}

View File

@@ -0,0 +1,182 @@
/* #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 {
box-sizing: border-box;
}
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;
border-radius: 100rpx;
box-sizing: border-box;
}
.b-b:after{
position: absolute;
z-index: 3;
left: 0;
top: auto;
bottom: 0;
right: 0;
height: 0;
content: '';
transform: scaleY(.5);
border-bottom: 1px solid #e0e0e0;
}
.b-t:before{
position: absolute;
z-index: 3;
left: 0;
top: 0;
right: 0;
height: 0;
content: '';
transform: scaleY(.5);
border-bottom: 1px solid #e5e5e5;
}
.b-r:after{
position: absolute;
z-index: 3;
right: 0;
top: 0;
bottom: 0;
width: 0;
content: '';
transform: scaleX(.5);
border-right: 1px solid #e5e5e5;
}
.b-l:before{
position: absolute;
z-index: 3;
left: 0;
top: 0;
bottom: 0;
width: 0;
content: '';
transform: scaleX(.5);
border-left: 1px solid #e5e5e5;
}
.b-b, .b-t, .b-l, .b-r{
position: relative;
}
/* 点击态 */
.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 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
/* #endif */
}
.clamp2 {
/* #ifdef APP-PLUS-NVUE */
lines: 2;
/* #endif */
/* #ifndef APP-PLUS-NVUE */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
/* #endif */
}
/* 布局 */
.row{
/* #ifndef APP-PLUS-NVUE */
display:flex;
/* #endif */
flex-direction:row;
align-items: center;
}
.column{
/* #ifndef APP-PLUS-NVUE */
display:flex;
/* #endif */
flex-direction: column;
}
.center{
/* #ifndef APP-PLUS-NVUE */
display:flex;
/* #endif */
align-items: center;
justify-content: center;
}
.fill{
flex: 1;
}
/* input */
.placeholder{
color: #999 !important;
}

View File

@@ -0,0 +1,271 @@
@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;
font-style: normal;
-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";
}

View File

@@ -0,0 +1,59 @@
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 || {},
header: {
...options.header,
'Authorization': authToken ? `Bearer ${authToken}` : ''
}
}).then(res => {
res = res[1];
const statusCode = res.statusCode;
if (statusCode !== 200) {
msg('请求失败,请重试');
return;
}
const code = res.data.code;
const message = res.data.msg;
// Token 过期,引导重新登陆
if (code === 401) {
msg('登录信息已过期,请重新登录');
store.commit('logout');
// reject('无效的登录信息');
return;
}
// 系统异常
if (code === 500) {
msg('系统异常,请稍后重试');
reject(new Error(message));
return;
}
// 其它失败情况
if (code > 0) {
msg(message);
// 提供 code + msg可以基于 code 做进一步的处理。当然,一般情况下是不需要的。
// 不需要的场景:手机登录时,密码不正确;
// 需要的场景:微信登录时,未绑定手机,后端会返回一个 code 码,前端需要基于它跳转到绑定手机界面;
reject({
'code': code,
'msg': message
});
return;
}
// 处理成功,则只返回成功的 data 数据,不返回 code 和 msg
resolve(res.data.data);
}).catch((err) => {
reject(err);
})
})
}

View File

@@ -0,0 +1,136 @@
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);
}
/**
* 节流
* 参考文章 https://juejin.cn/post/6844903669389885453
*
* @param {Function} 执行函数
* @param {Number} delay 延时ms
*/
export const throttle = (fn, delay=500) => {
if(_throttleRunning){
return;
}
_throttleRunning = true;
fn();
setTimeout(() => {
_throttleRunning = false;
}, delay);
}
/**
* 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) {
return;
}
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位纯数字
return /^[0-9]{6}$/.test(str)
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:
return true;
}
}

View File

@@ -0,0 +1,96 @@
// import {request} from '@/common/js/request'
export default{
data() {
return {
page: 0, // 页码
pageNum: 6, // 每页加载数据量
loadingType: 1, // 加载类型。0 加载前1 加载中2 没有更多
isLoading: false, // 刷新数据
loaded: false, // 加载完毕
}
},
methods: {
/**
* 打印日志,方便调试
*
* @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) {
return;
}
// 如果需要登陆,并且未登陆,则跳转到登陆界面
if ((~url.indexOf('login=1') || options.login) && !this.$store.getters.hasLogin){
url = '/pages/auth/login';
}
// 跳转到指定 url 地址
uni.navigateTo({
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()){
return;
}
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);
}).catch((err) => {
reject(err);
})
})
},
imageOnLoad(data, key){ // TODO 芋艿:需要改成自己的
setTimeout(()=>{
this.$set(data, 'loaded', true);
}, 100)
},
showPopup(key){ // TODO 芋艿:需要改成自己的
this.$util.throttle(()=>{
this.$refs[key].open();
}, 200)
},
hidePopup(key){ // TODO 芋艿:需要改成自己的
this.$refs[key].close();
},
stopPrevent(){}, // TODO 芋艿:需要改成自己的
},
}

View File

@@ -0,0 +1,630 @@
<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>
<!--#endif-->
<!--#ifndef H5 || MP-360-->
<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
<!--#endif-->
</view>
<!--#endif-->
</view>
</template>
<script>
// #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,
// #endif
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;
}
// #endif
// #ifdef H5 || APP-PLUS-NVUE || MP-360
var windowWidth = uni.getSystemInfoSync().windowWidth,
cfg = require('./libs/config.js');
// #endif
// #ifdef APP-PLUS-NVUE
var weexDom = weex.requireModule('dom');
// #endif
/**
* 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
*/
export default {
name: 'parser',
data() {
return {
// #ifdef H5 || MP-360
uid: this._uid,
// #endif
// #ifdef APP-PLUS-NVUE
height: 1,
// #endif
// #ifndef APP-PLUS-NVUE
showAm: '',
// #endif
nodes: []
}
},
// #ifndef H5 || APP-PLUS-NVUE || MP-360
components: {
trees
},
// #endif
props: {
html: String,
autopause: {
type: Boolean,
default: true
},
autoscroll: Boolean,
autosetTitle: {
type: Boolean,
default: true
},
// #ifndef H5 || APP-PLUS-NVUE || MP-360
compress: Number,
loadingImg: String,
useCache: Boolean,
// #endif
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;
}
// #endif
this[i] = src;
// 暂存 data src
if (src.includes('data:image')) {
var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
if (!info) return;
// #ifdef MP-WEIXIN || MP-TOUTIAO
filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
fs && fs.writeFile({
filePath,
data: info[3],
encoding: info[2],
success: () => this[i] = filePath
})
// #endif
// #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;
})
})
// #endif
}
}
},
mounted() {
// #ifdef H5 || MP-360
this.document = document.getElementById('rtf' + this._uid);
// #endif
// #ifndef H5 || APP-PLUS-NVUE || MP-360
if (dom) this.document = new dom(this);
// #endif
// #ifdef APP-PLUS-NVUE
this.document = this.$refs.web;
setTimeout(() => {
// #endif
if (this.html) this.setContent(this.html);
// #ifdef APP-PLUS-NVUE
}, 30)
// #endif
},
beforeDestroy() {
// #ifdef H5 || MP-360
if (this._observer) this._observer.disconnect();
// #endif
this.imgList.each(src => {
// #ifdef APP-PLUS
if (src && src.includes('_doc')) {
plus.io.resolveLocalFileSystemURL(src, entry => {
entry.remove();
});
}
// #endif
// #ifdef MP-WEIXIN || MP-TOUTIAO
if (src && src.includes(uni.env.USER_DATA_PATH))
fs && fs.unlink({
filePath: src
})
// #endif
})
clearInterval(this._timer);
},
methods: {
// 设置富文本内容
setContent(html, append) {
// #ifdef APP-PLUS-NVUE
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];
// #endif
// #ifdef H5 || MP-360
if (!html) {
if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
return;
}
var div = document.createElement('div');
if (!append) {
if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
this.rtf = div;
} else {
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)
return true;
else
uni.navigateTo({
url: href
})
}
return false;
}
}
// 视频处理
var videos = this.rtf.getElementsByTagName('video');
_ts.videoContexts = videos;
for (let video, i = 0; video = videos[i++];) {
video.style.maxWidth = '100%';
video.onerror = function() {
_ts.$emit('error', {
source: 'video',
target: this
});
}
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() {
_ts.$emit('error', {
source: 'audio',
target: this
});
}
// 表格处理
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.nodes = [1];
this.$emit('load');
});
setTimeout(() => this.showAm = '', 500);
// #endif
// #ifndef APP-PLUS-NVUE
// #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];
else {
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)
uni.setNavigationBarTitle({
title: nodes.title
})
if (this.imgList) this.imgList.length = 0;
this.videoContexts = [];
this.$nextTick(() => {
(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)
this.$emit('load');
})
// #endif
var height;
clearInterval(this._timer);
this._timer = setInterval(() => {
// #ifdef H5 || MP-360
this.rect = this.rtf.getBoundingClientRect();
// #endif
// #ifndef H5 || MP-360
uni.createSelectorQuery().in(this)
.select('#_top').boundingClientRect().exec(res => {
if (!res) return;
this.rect = res[0];
// #endif
if (this.rect.height == height) {
this.$emit('ready', this.rect)
clearInterval(this._timer);
}
height = this.rect.height;
// #ifndef H5 || MP-360
});
// #endif
}, 350);
if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
// #endif
},
// 获取文本内容
getText(ns = this.nodes) {
var txt = '';
// #ifdef APP-PLUS-NVUE
txt = this._text;
// #endif
// #ifdef H5 || MP-360
txt = this.rtf.innerText;
// #endif
// #ifndef H5 || APP-PLUS-NVUE || MP-360
for (var i = 0, n; n = ns[i++];) {
if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
else if (n.type == 'br') txt += '\n';
else {
// 块级标签前后加换行
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';
}
}
// #endif
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');
// #ifdef APP-PLUS-NVUE
if (!obj.id)
weexDom.scrollToElement(this.$refs.web);
else
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();
// #endif
// #ifndef APP-PLUS-NVUE
var d = ' ';
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
d = '>>>';
// #endif
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
})
obj.success && obj.success();
})
// #endif
},
// 获取视频对象
getVideoContext(id) {
// #ifndef APP-PLUS-NVUE
if (!id) return this.videoContexts;
else
for (var i = this.videoContexts.length; i--;)
if (this.videoContexts[i].id == id) return this.videoContexts[i];
// #endif
},
// #ifdef H5 || APP-PLUS-NVUE || MP-360
_handleHtml(html, append) {
if (!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;
},
// #endif
// #ifdef APP-PLUS-NVUE
_message(e) {
// 接收 web-view 消息
var d = e.detail.data[0];
switch (d.action) {
case 'load':
this.$emit('load');
this.height = d.height;
this._text = d.text;
break;
case 'getTitle':
if (this.autosetTitle)
uni.setNavigationBarTitle({
title: d.title
})
break;
case 'getImgList':
this.imgList.length = 0;
for (var i = d.imgList.length; i--;)
this.imgList.setItem(i, d.imgList[i]);
break;
case 'preview':
var preview = true;
d.img.ignore = () => preview = false;
this.$emit('imgtap', d.img);
if (preview)
uni.previewImage({
current: d.img.i,
urls: this.imgList
})
break;
case 'linkpress':
var jump = true,
href = d.href;
this.$emit('linkpress', {
href,
ignore: () => jump = false
})
if (jump && href) {
if (href[0] == '#') {
if (this.useAnchor)
weexDom.scrollToElement(this.$refs.web, {
offset: d.offset
})
} else if (href.includes('://'))
plus.runtime.openWeb(href);
else
uni.navigateTo({
url: href
})
}
break;
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
})
break;
case 'ready':
this.height = d.height;
if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
this.rect = res[0];
this.$emit('ready', res[0]);
})
break;
case 'click':
this.$emit('click');
this.$emit('tap');
}
},
// #endif
}
}
</script>
<style>
@keyframes _show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* #ifdef MP-WEIXIN */
:host {
display: block;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
/* #endif */
</style>

View File

@@ -0,0 +1,97 @@
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])
matched += tmp + ';';
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;
this.state = this.Space;
}
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.list.push(this.section());
this.Content();
} else if (c == ',') {
this.list.push(this.section());
this.Comma();
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
this.state = this.Ignore;
}
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();
else {
this.start = this.i--;
this.state = this.Name;
}
}
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;
this.list = [];
this.state = this.Space;
}

View File

@@ -0,0 +1,534 @@
/**
* html 解析器
* @tutorial https://github.com/jin-yufeng/Parser
* @version 20200719
* @author JinYufeng
* @listens MIT
*/
const cfg = require('./config.js'),
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.data = data;
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;
return false;
}
item.c = 1;
}
return true;
}
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);
} else {
en = val.substring(i + 1, j);
if (cfg.entities[en] || en == amp)
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
}
}
return val;
}
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);
for (var c; c = this.data[this.i]; this.i++)
this.state(c);
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();
else {
this.start = this.i;
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]);
return;
}
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];
this.attrs = {};
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':
// #endif
this.bubble();
break;
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;
}
break;
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';
// #endif
// #ifdef APP-PLUS
this.bubble();
break;
// #endif
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;
}
this.bubble();
break;
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;
break;
}
}
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;
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 += `;${key}:${value}`;
}
style = style.substr(1);
if (style) attrs.style = style;
if (!close) {
node.children = [];
if (node.name == 'pre' && cfg.highlight) {
this.remove(node);
this.pre = node.pre = true;
}
this.siblings().push(node);
this.STACK.push(node);
} else if (!cfg.filter || cfg.filter(node, this) != false)
this.siblings().push(node);
} else {
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;
this.state = this.Text;
}
// 移除标签
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;
var parent = this.parent();
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
this.siblings().push({
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++);
while (1) {
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
if (name == 'pre' || name == 'svg') this.i = j;
else this.i = this.data.length;
return;
}
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();
return;
}
}
}
// 节点出栈处理
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;
for (let i = this.STACK.length; i--;)
if (this.STACK[i].name == 'ul') floor++;
if (floor != 1)
for (let i = childs.length; i--;)
childs[i].floor = floor;
} else {
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) {
this.bubble();
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],
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
if (isLetter(next)) {
this.setText();
this.start = this.i + 1;
this.state = this.TagName;
} else if (next == '/') {
this.setText();
if (isLetter(this.data[++this.i + 1])) {
this.start = this.i + 1;
this.state = this.EndTag;
} else this.Comment();
} else if (next == '!' || next == '?') {
this.setText();
this.Comment();
}
}
}
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;
this.start = this.i + 1;
this.state = this.Text;
}
MpHtmlParser.prototype.TagName = function(c) {
if (blankChar[c]) {
this.tagName = this.section();
while (blankChar[this.data[this.i]]) this.i++;
if (this.isClose()) this.setNode();
else {
this.start = this.i;
this.state = this.AttrName;
}
} else if (this.isClose()) {
this.tagName = this.section();
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] == '=') {
while (blankChar[this.data[++this.i]]);
this.start = 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++;
} else {
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
this.attrVal = this.section();
}
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')
this.siblings().push({
name,
attrs: {}
});
this.i = this.data.indexOf('>', this.i);
this.start = this.i + 1;
if (this.i == -1) this.i = this.data.length;
else this.state = this.Text;
}
}
module.exports = MpHtmlParser;

View File

@@ -0,0 +1,93 @@
/* 配置文件 */
// #ifdef MP-WEIXIN
const canIUse = wx.canIUse('editor'); // 高基础库标识,用于兼容
// #endif
module.exports = {
// 出错占位图
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 ? '' :
// #endif
',pre')),
// 将被移除的标签
ignoreTags: makeMap(
'area,base,canvas,frame,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'
// #ifdef MP-WEIXIN
+ (canIUse ? ',rp' : '')
// #endif
// #ifndef APP-PLUS
+ ',iframe'
// #endif
),
// 只能被 rich-text 显示的标签
richOnlyTags: makeMap('a,colgroup,fieldset,legend,table'
// #ifdef MP-WEIXIN
+ (canIUse ? ',bdi,bdo,caption,rt,ruby' : '')
// #endif
),
// 自闭合的标签
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'
// #ifdef MP-WEIXIN
+ (canIUse ? ',bdi,bdo,caption,pre,rt,ruby' : '')
// #endif
// #ifdef APP-PLUS
+ ',embed,iframe'
// #endif
),
// 默认的标签样式
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;
}

View File

@@ -0,0 +1,22 @@
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
}
module.exports = {
use: function(item) {
return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
}
}

View File

@@ -0,0 +1,500 @@
<template>
<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" />
</view>
<!--文本-->
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
<!--#ifndef MP-BAIDU-->
<text v-else-if="n.name=='br'">\n</text>
<!--#endif-->
<!--视频-->
<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" />
</view>
<!--广告-->
<!--<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>
</view>
<trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
</view>
<!--表格-->
<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" />
</view>
</view>
</view>
<!--#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" />
<!--#endif-->
<!--富文本-->
<!--#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]" />
<!--#endif-->
<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
<!--#endif-->
<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" />
</block>
</view>
</template>
<script module="handler" lang="wxs" src="./handler.wxs"></script>
<script>
global.Parser = {};
import trees from './trees'
const errorImg = require('../libs/config.js').errorImg;
export default {
components: {
trees
},
name: 'trees',
data() {
return {
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: ''
// #endif
}
},
props: {
nodes: Array,
lazyLoad: Boolean,
loading: String,
// #ifdef MP-ALIPAY
c: String,
s: String
// #endif
},
mounted() {
for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
this.init();
},
// #ifdef APP-PLUS
beforeDestroy() {
this.observer && this.observer.disconnect();
},
// #endif
methods: {
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);
// #ifdef APP-PLUS
if (this.lazyLoad && !this.observer) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
});
setTimeout(() => {
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)
}
// #endif
} else if (n.name == 'video' || n.name == 'audio') {
var ctx;
if (n.name == 'video') {
ctx = uni.createVideoContext(n.attrs.id
// #ifndef MP-BAIDU
, this
// #endif
);
} 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);
}
}
}
// #ifdef APP-PLUS
// APP 上避免 video 错位需要延时渲染
setTimeout(() => {
this.loadVideo = true;
}, 1000)
// #endif
},
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);
if (preview) {
var urls = this.top.imgList,
current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
uni.previewImage({
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() {
// #endif
// #ifndef APP-PLUS
this.$set(this.controls, i, 1);
// #endif
// #ifdef QUICKAPP-WEBVIEW
})
// #endif
} else if (this.loading && this.controls[i] != 2) {
// #ifdef QUICKAPP-WEBVIEW
this.$set(this.controls, i, 0);
this.$nextTick(function() {
// #endif
this.$set(this.controls, i, 2);
// #ifdef QUICKAPP-WEBVIEW
})
// #endif
}
},
linkpress(e) {
var jump = true,
attrs = e.currentTarget.dataset.attrs;
attrs.ignore = () => jump = false;
global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
this.top.$emit('linkpress', attrs);
if (jump) {
// #ifdef MP
if (attrs['app-id']) {
return uni.navigateToMiniProgram({
appId: attrs['app-id'],
path: attrs.path
})
}
// #endif
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) {
// #ifdef APP-PLUS
plus.runtime.openWeb(attrs.href);
// #endif
// #ifndef APP-PLUS
uni.setClipboardData({
data: attrs.href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
} else
uni.navigateTo({
url: attrs.href,
fail() {
uni.switchTab({
url: attrs.href,
})
}
})
}
}
},
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);
}
}
}
</script>
<style>
/* 在这里引入自定义样式 */
/* 链接和图片效果 */
._a {
display: inline;
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
._hover {
text-decoration: underline;
opacity: 0.7;
}
._img {
display: inline-block;
max-width: 100%;
overflow: hidden;
}
/* #ifdef MP-WEIXIN */
:host {
display: inline;
}
/* #endif */
/* #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;
}
/* #endif */
._b,
._strong {
font-weight: bold;
}
/* #ifndef MP-ALIPAY */
._blockquote,
._div,
._p,
._ol,
._ul,
._li {
display: block;
}
/* #endif */
._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,
._h6 {
display: block;
font-weight: bold;
}
._image {
display: block;
width: 100%;
height: 360px;
margin-top: -360px;
opacity: 0;
}
._ins {
text-decoration: underline;
}
._li {
flex: 1;
width: 0;
}
._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 {
display: inline-block;
width: 0.3em;
height: 0.3em;
overflow: hidden;
line-height: 0.3em;
}
._ul-p2 {
display: inline-block;
width: 0.23em;
height: 0.23em;
border: 0.05em solid black;
border-radius: 50%;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #endif */
/* #ifdef MP-WEIXIN || MP-QQ */
.__bdo,
.__bdi,
.__ruby,
.__rt {
display: inline-block;
}
/* #endif */
._video {
position: relative;
display: inline-block;
width: 300px;
height: 225px;
background-color: black;
}
._video::after {
position: absolute;
top: 50%;
left: 50%;
margin: -15px 0 0 -15px;
content: '';
border-color: transparent transparent transparent white;
border-style: solid;
border-width: 15px 0 15px 30px;
}
</style>

View File

@@ -0,0 +1,55 @@
/* 下拉刷新区域 */
.mescroll-downwarp {
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
text-align: center;
}
/* 下拉刷新--内容区,定位于区域底部 */
.mescroll-downwarp .downwarp-content {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
min-height: 60rpx;
padding: 20rpx 0;
text-align: center;
}
/* 下拉刷新--提示文本 */
.mescroll-downwarp .downwarp-tip {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
margin-left: 16rpx;
/* color: gray; 已在style设置color,此处删去*/
}
/* 下拉刷新--旋转进度条 */
.mescroll-downwarp .downwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-downwarp .mescroll-rotate {
animation: mescrollDownRotate 0.6s linear infinite;
}
@keyframes mescrollDownRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,47 @@
<!-- 下拉刷新区域 -->
<template>
<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>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object , // down的配置项
type: Number, // 下拉状态inOffset1 outOffset2 showLoading3 endDownScroll4
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;
}
}
}
};
</script>
<style>
@import "./mescroll-down.css";
</style>

View File

@@ -0,0 +1,27 @@
<template>
<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>
</view>
</template>
<script>
import mixEmpty from '@/components/mix-empty/mix-empty.vue'
export default {
components: {
mixEmpty
},
props: {
// empty的配置项: 默认为GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
}
};
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,95 @@
<!--空布局
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
</view>
</template>
<script>
// 引入全局配置
import GlobalOption from './../mescroll-uni-option.js';
export default {
props: {
// empty的配置项: 默认为GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
},
// 使用computed获取配置,用于支持option的动态配置
computed: {
// 图标
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; // 此处不使用短路求值, 用于支持传空串不显示文本提示
}
},
methods: {
// 点击按钮
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style>
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
padding: 30vh 50rpx 100rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.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 {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: 28rpx;
border: 1rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 回到顶部的按钮 -->
<template>
<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"
/>
</template>
<script>
export default {
props: {
// up.toTop的配置项
option: Object,
// 是否显示
value: false
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption(){
return this.option || {}
},
// 优先显示左边
left(){
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
},
// 右边距离 (优先显示左边)
right() {
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
}
},
methods: {
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'); // 派发点击事件
}
}
};
</script>
<style>
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 9990;
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 20rpx;
bottom: 120rpx;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
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 {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,47 @@
/* 上拉加载区域 */
.mescroll-upwarp {
box-sizing: border-box;
min-height: 110rpx;
padding: 30rpx 0;
text-align: center;
clear: both;
}
/*提示文本 */
.mescroll-upwarp .upwarp-tip,
.mescroll-upwarp .upwarp-nodata {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
/* color: gray; 已在style设置color,此处删去*/
}
.mescroll-upwarp .upwarp-tip {
margin-left: 16rpx;
}
/*旋转进度条 */
.mescroll-upwarp .upwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-upwarp .mescroll-rotate {
animation: mescrollUpRotate 0.6s linear infinite;
}
@keyframes mescrollUpRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,39 @@
<!-- 上拉加载区域 -->
<template>
<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>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up的配置项
type: Number // 上拉加载的状态0loading前1loading中2没有更多了
},
computed: {
// 支付宝小程序需写成计算属性,prop定义default仍报错
mOption() {
return this.option || {};
},
// 加载中
isUpLoading() {
return this.type === 1;
},
// 没有更多了
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style>
@import './mescroll-up.css';
</style>

View File

@@ -0,0 +1,14 @@
.mescroll-body {
position: relative; /* 下拉刷新区域相对自身定位 */
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
overflow: hidden; /* 遮住顶部下拉刷新区域 */
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,344 @@
<template>
<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>
</view>
<!-- {{downText}} -->
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1" :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>
<!-- 无数据 -->
<!-- <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>
</view>
</view>
</view>
<!-- 底部是否偏移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> -->
</view>
</template>
<!-- 微信小程序, 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';
export default {
mixins: [renderBiz]
}
</script>
<!-- #endif -->
<script>
// 引入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';
export default {
mixins: [WxsMixin],
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
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, // 上拉加载状态0loading前1loading中2没有更多了,显示END文本提示3没有更多了,不显示END文本提示
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
windowHeight: 0, // 可使用窗口的高度
windowBottom: 0, // 可使用窗口的底部位置
statusBarHeight: 0 // 状态栏高度
};
},
props: {
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页生效)
type: Boolean,
default: true
}
},
computed: {
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
minHeight(){
return this.toPx(this.height || '100%') + 'px'
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top)
},
padTop() {
return this.numTop + 'px';
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
// 是否为重置下拉的状态
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
// 过渡
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
},
// 是否在加载中
isDownLoading(){
return this.downLoadType === 3
},
// 旋转的角度
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
// 文本提示
downText(){
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.optDown.textLoading;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//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', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
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组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义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: {
onShow(isShow) {
// 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
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必须为数字)
uni.pageScrollTo({
scrollTop: y,
duration: t
})
}
});
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
}
};
</script>
<style lang="scss">
@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;
}
}
</style>

View File

@@ -0,0 +1,65 @@
// 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
// },
data() {
return {
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();
},
methods: {
// 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{
setTimeout(()=>{
this.mescroll.endSuccess();
}, 500)
}
},
// 上拉加载的回调
upCallback() {
// mixin默认延时500自动结束加载
setTimeout(()=>{
this.mescroll.endErr();
}, 500)
}
},
mounted() {
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
}
}
export default MescrollMixin;

View File

@@ -0,0 +1,33 @@
// 全局配置
// mescroll-body 和 mescroll-uni 通用
const GlobalOption = {
down: {
// 其他down的配置参数也可以写,这里只展示了常用的配置:
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
},
up: {
// 其他up的配置参数也可以写,这里只展示了常用的配置:
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '- 我也是有底线的 -', // 没有更多数据的提示文本
offset: 80, // 距底部多远时,触发upCallback
toTop: {
// 回到顶部按钮,需配置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)
},
empty: {
use: true, // 是否显示空布局
icon: "/static/empty/hamster.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
tip: '~ 空空如也 ~' // 提示
}
}
}
export default GlobalOption

View File

@@ -0,0 +1,36 @@
.mescroll-uni-warp{
height: 100%;
}
.mescroll-uni-content{
height: 100%;
}
.mescroll-uni {
position: relative;
width: 100%;
height: 100%;
min-height: 200rpx;
overflow-y: auto;
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 定位的方式固定高度 */
.mescroll-uni-fixed{
z-index: 1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto; /* 使right生效 */
height: auto; /* 使bottom生效 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,788 @@
/* 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(); // 显示下拉进度,执行下拉回调
} else {
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;
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
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度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
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中使用)
callback: function(mescroll) {
// 下拉刷新的回调;默认重置上拉加载列表为第一页
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条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
offset: 80, // 距底部多远时,触发upCallback
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- 我也是有底线的 --', // 没有更多数据的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 初始化完毕的回调
showLoading: null, // 显示加载中的回调
showNoMore: null, // 显示无更多数据的回调
hideUpScroll: null, // 隐藏上拉加载的回调
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
toTop: {
// 回到顶部按钮,需配置src才显示
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)
},
empty: {
use: true, // 是否显示空布局
icon: null, // 图标路径
tip: '~ 暂无相关数据 ~', // 提示
btnText: '', // 按钮
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
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); // 深度匹配
} else {
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() {
let me = this;
// 配置参数
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
}else{
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
}
me.downHight = 0; // 下拉区域的高度
// 在页面中加入下拉布局
if (me.optDown.use && me.optDown.inited) {
// 初始化完毕的回调
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optDown.inited(me);
}, 0)
}
}
/* 列表touchstart事件 */
MeScroll.prototype.touchstartEvent = function(e) {
if (!this.optDown.use) return;
this.startPoint = this.getPoint(e); // 记录起点
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
this.startAngle = 0; // 初始角度
this.lastPoint = this.startPoint; // 重置上次move的点
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
this.inTouchend = false; // 标记不是touchend
}
/* 列表touchmove事件 */
MeScroll.prototype.touchmoveEvent = function(e) {
if (!this.optDown.use) return;
let me = this;
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
return;
}
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; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
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.optDown.use) return;
// 如果下拉区域高度已改变,则需重置回来
if (this.isMoveDown) {
if (this.downHight >= this.optDown.offset) {
// 符合触发刷新的条件
this.triggerDownScroll();
} else {
// 不符合的话 则重置
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) {
return {
x: 0,
y: 0
}
}
if (e.touches && e.touches[0]) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
}
} else if (e.changedTouches && e.changedTouches[0]) {
return {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY
}
} else {
return {
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则处于完全自定义状态
} else {
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,因为上拉加载用到
} else{
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.isDownScrolling = true; // 标记下拉中
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
/* 结束下拉刷新 */
MeScroll.prototype.endDownScroll = function() {
if (this.optDown.native) { // 结束原生下拉刷新
this.isDownScrolling = false;
this.endDownScrollCall(this);
uni.stopPullDownRefresh();
return
}
let me = this;
// 结束下拉刷新的方法
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);
} else {
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) {
if (isLock == null) isLock = true;
this.optUp.isLock = isLock;
}
/* -------初始化上拉加载------- */
MeScroll.prototype.initUpScroll = function() {
let me = this;
// 配置参数
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) {
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optUp.inited(me);
}, 0)
}
}
/*滚动到底部的事件 (仅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();
} else {
this.hideTopBtn();
}
}
/*列表滚动事件*/
MeScroll.prototype.scroll = function(e, onScroll) {
// 更新滚动条的位置
this.setScrollTop(e.scrollTop);
// 更新滚动内容高度
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);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
// 滑动监听
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,显示无更多数据
} else {
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(); // 不传参,默认显示上拉加载的进度布局
} else {
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
}
}
this.isUpAutoLoad = 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);
}
/* 联网回调成功,结束下拉刷新和上拉加载
* dataSize: 当前页的数据量(必传)
* totalSize: 列表所有数据总数量(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
let hasNext;
if (this.optUp.use && totalSize != null) {
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
hasNext = loadSize < totalSize; // 是否还有下一页
}
this.endSuccess(dataSize, hasNext, systime);
}
/* 联网回调成功,结束下拉刷新和上拉加载
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
*/
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
let me = this;
// 结束下拉刷新
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();
} else {
// 总列表数少于配置的数量,则不显示无更多数据
let allDataSize = (pageNum - 1) * pageSize + dataSize;
if (allDataSize < me.optUp.noMoreSize) {
isShowNoMore = false;
} else {
isShowNoMore = true;
}
me.removeEmpty(); // 移除空布局
}
} else {
// 还有下一页
isShowNoMore = false;
me.optUp.hasNext = true;
me.removeEmpty(); // 移除空布局
}
}
// 隐藏上拉
me.endUpScroll(isShowNoMore);
}
}
/* 回调失败,结束下拉刷新和上拉加载 */
MeScroll.prototype.endErr = function(errDistance) {
// 结束下拉,回调失败重置回原来的页码和时间
if (this.isDownScrolling) {
let page = this.optUp.page;
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);
return;
}
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++;
} else {
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()
}

View File

@@ -0,0 +1,408 @@
<template>
<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"
@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-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">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 底部是否偏移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>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
</view>
</template>
<!-- 微信小程序, 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';
export default {
mixins:[renderBiz]
}
</script>
<!-- #endif -->
<script>
// 引入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';
export default {
mixins: [WxsMixin],
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: {optDown:{},optUp:{}}, // mescroll实例
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
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前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
isShowEmpty: false, // 是否显示空布局
isShowToTop: false, // 是否显示回到顶部按钮
scrollTop: 0, // 滚动条的位置
scrollAnim: false, // 是否开启滚动动画
windowTop: 0, // 可使用窗口的顶部位置
windowBottom: 0, // 可使用窗口的底部位置
windowHeight: 0, // 可使用窗口的高度
statusBarHeight: 0, // 状态栏高度
scrollToViewId: '' // 滚动到指定view的id
}
},
props: {
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时使用)
fixed: { // 是否通过fixed固定mescroll的高度, 默认true
type: Boolean,
default: true
},
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
type: Boolean,
default: true
}
},
computed: {
// 是否使用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'
}else{
return "100%"
}
},
// 下拉布局往下偏移的距离 (px)
numTop() {
return this.toPx(this.top)
},
fixedTop() {
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0
},
// 上拉布局往上偏移 (px)
numBottom() {
return this.toPx(this.bottom)
},
fixedBottom() {
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0
},
// 是否为重置下拉的状态
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之外
},
// 列表是否可滑动
scrollable(){
return this.downLoadType===0 || this.isDownReset
},
// 是否在加载中
isDownLoading(){
return this.downLoadType === 3
},
// 旋转的角度
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
// 文本提示
downText(){
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.optDown.textLoading;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//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', '');
} else { // "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
},
//注册列表滚动事件,用于下拉刷新和上拉加载
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
})
},
// 点击空布局的按钮回调
emptyClick() {
this.$emit('emptyclick', this.mescroll)
},
// 点击回到顶部的按钮回调
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
},
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
setClientHeight() {
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
this.isExec = true; // 避免多次获取
this.$nextTick(() => { // 确保dom已渲染
let query = uni.createSelectorQuery();
// #ifndef MP-ALIPAY
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
// #endif
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;
setTimeout(() => {
this.setClientHeight()
}, this.clientNum * 100)
}
}).exec();
})
}
}
},
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
created() {
let vm = this;
let diyOption = {
// 下拉刷新的配置
down: {
inOffset() {
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
},
outOffset() {
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
},
onMoving(mescroll, rate, downHight) {
// 下拉过程中的回调,滑动过程一直在执行;
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
},
endDownScroll() {
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
vm.downResetTimer && clearTimeout(vm.downResetTimer)
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
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: {
onShow(isShow) { // 显示隐藏的回调
vm.isShowToTop = isShow;
}
},
// 派发上拉加载的回调
callback: function(mescroll) {
vm.$emit('up', mescroll);
// 更新容器的高度 (多mescroll的情况)
vm.setClientHeight()
}
}
}
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);
vm.mescroll.viewId = vm.viewId; // 附带id
// init回调mescroll对象
vm.$emit('init', vm.mescroll);
// 设置高度
const sys = uni.getSystemInfoSync();
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;
// 使down的bottomOffset生效
vm.mescroll.setBodyHeight(sys.windowHeight);
// 因为使用的是scrollview,这里需自定义scrollTo
vm.mescroll.resetScrollTo((y, t) => {
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
if(typeof y === 'string'){ // 第一个参数如果为字符串,则使用scroll-into-view
// #ifdef MP-WEIXIN
// 微信小程序暂不支持slot里面的scroll-into-view,只能计算位置实现
uni.createSelectorQuery().select('#'+vm.viewId).boundingClientRect(function(rect){
let mescrollTop = rect.top // mescroll到顶部的距离
uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
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
})
}).exec()
}).exec()
// #endif
// #ifndef MP-WEIXIN
if (vm.scrollToViewId != y) {
vm.scrollToViewId = y;
} else{
vm.scrollToViewId = ''; // scrollToViewId必须变化才会生效,所以此处先置空再赋值
vm.$nextTick(function(){
vm.scrollToViewId = y;
})
}
// #endif
return;
}
let curY = vm.mescroll.getScrollTop()
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y
})
} else {
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
vm.scrollTop = step
}, t)
}
})
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
},
mounted() {
// 设置容器的高度
this.setClientHeight()
}
}
</script>
<style>
@import "./mescroll-uni.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@@ -0,0 +1,23 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollCompMixin = {
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPageScroll(e);
},
onReachBottom() {
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onReachBottom();
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPullDownRefresh();
}
}
export default MescrollCompMixin;

View File

@@ -0,0 +1,51 @@
/**
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
*/
const MescrollMoreItemMixin = {
// 支付宝小程序不支持props的mixin,需写在具体的页面中
// #ifndef MP-ALIPAY
props:{
i: Number, // 每个tab页的专属下标
index: { // 当前tab的下标
type: Number,
default(){
return 0
}
}
},
// #endif
data() {
return {
downOption:{
auto:false // 不自动加载
},
upOption:{
auto:false // 不自动加载
},
isInit: false // 当前tab是否已初始化
}
},
watch:{
// 监听下标的变化
index(val){
if (this.i === val && !this.isInit) {
this.isInit = true; // 标记为true
this.mescroll && this.mescroll.triggerDownScroll();
}
}
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
mescrollInit(mescroll) {
this.mescroll = mescroll;
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
// 自动加载当前tab的数据
if(this.i === this.index){
this.isInit = true; // 标记为true
this.mescroll.triggerDownScroll();
}
},
}
}
export default MescrollMoreItemMixin;

View File

@@ -0,0 +1,56 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollMoreMixin = {
data() {
return {
tabIndex: 0 // 当前tab下标
}
},
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPageScroll(e);
},
onReachBottom() {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onReachBottom();
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
let mescroll = this.getMescroll(this.tabIndex);
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]
}else{
// 普通的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靠谱一些),确保元素已渲染
setTimeout(()=>{
mescroll.scrollTo(mescroll.getScrollTop(),0)
},30)
}
}
}
}
export default MescrollMoreMixin;

View File

@@ -0,0 +1,23 @@
// 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(){
window.$isMescrollBounce = true
}, {passive: true})
// 滑动中标记是否禁止bounce (如:下拉刷新时禁止)
window.addEventListener('touchmove', function(e){
!window.$isMescrollBounce && e.preventDefault() // 禁止bounce
}, {passive: false})
}
export default bounce;

View File

@@ -0,0 +1,102 @@
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
const WxsMixin = {
data() {
return {
// 传入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: '', // 方法名
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
},
// 不用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的写法
},
// #endif
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
// #ifndef APP-PLUS || H5
renderBiz: {
propObserver(){} // 抹平renderjs的写法
}
// #endif
}
},
methods: {
// 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'){
// 主动触发下拉刷新
this.mescroll.triggerDownScroll();
}else if(msg.type === 'endDownScroll'){
// 结束下拉刷新
this.mescroll.endDownScroll();
}else if(msg.type === 'triggerUpScroll'){
// 主动触发上拉加载
this.mescroll.triggerUpScroll(true);
}
}
},
mounted() {
// #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的方法 (值改变才触发更新)
setTimeout(()=>{
if(this.downLoadType === 4 || this.downLoadType === 0){
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
}
},320)
}
// 初始化wxs的数据
this.wxsCall({type: 'setWxsProp'})
// #endif
}
}
export default WxsMixin;

View File

@@ -0,0 +1,92 @@
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
// https://uniapp.dcloud.io/frame?id=renderjs
// 与wxs的me实例一致
var me = {}
// 初始化window对象的touch事件 (仅初始化一次)
if(window && !window.$mescrollRenderInit){
window.$mescrollRenderInit = true
window.addEventListener('touchstart', function(e){
if (me.disabled()) return;
me.startPoint = me.getPoint(e); // 记录起点
}, {passive: true})
window.addEventListener('touchmove', function(e){
if (me.disabled()) return;
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
break;
}
el = el.parentNode; // 继续检查其父元素
}
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
}
}
}, {passive: false})
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || 0
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
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 = {
data() {
return {
propObserver: propObserver,
}
}
}
export default renderBiz;

View File

@@ -0,0 +1,267 @@
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
// https://uniapp.dcloud.io/frame?id=wxs
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
var me = {}
// ------ 自定义下拉刷新动画 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
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
transform: 'translateY(' + me.downHight + 'px)',
transition: 'transform 300ms'
})
})
}
/* 结束下拉 */
me.endDownScroll = function (ins){
me.downHight = 0;
me.isDownScrolling = false;
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
transform: 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
transition: 'transform 300ms'
})
})
}
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
me.clearTransform = function (ins){
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
transform: '',
transition: ''
})
})
}
// ------ 自定义下拉刷新动画 end ------
/**
* 监听逻辑层数据的变化 (实时更新数据)
*/
function propObserver(wxsProp) {
me.optDown = wxsProp.optDown
me.scrollTop = wxsProp.scrollTop
me.bodyHeight = wxsProp.bodyHeight
me.isDownScrolling = wxsProp.isDownScrolling
me.isUpScrolling = wxsProp.isUpScrolling
me.isUpBoth = wxsProp.isUpBoth
me.isScrollBody = wxsProp.isScrollBody
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
}
/**
* 监听逻辑层数据的变化 (调用wxs的方法)
*/
function callObserver(callProp, oldValue, ins) {
if (me.disabled()) return;
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.startPoint = me.getPoint(e); // 记录起点
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(); // 当前滚动条的距离
var curPoint = me.getPoint(e); // 当前点
var 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.isUpBoth))) {
// 下拉的角度是否在配置的范围内
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
touchendEvent(e, ins); // 提前触发touchend
return isPrevent;
}
isPrevent = false // 小程序是return false
var 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.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
me.downHight = Math.round(me.downHight) // 取整
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
me.onMoving(ins, rate, me.downHight)
}
}
me.lastPoint = curPoint; // 记录本次移动的点
return isPrevent // false表示不往上冒泡相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
}
function touchendEvent(e, ins) {
if (me.disabled()) return true;
// 如果下拉区域高度已改变,则需重置回来
if (me.isMoveDown) {
if (me.downHight >= me.optDown.offset) {
// 符合触发刷新的条件
me.downHight = me.optDown.offset; // 更新下拉区域高度
// me.triggerDownScroll();
me.callMethod(ins, {type: 'triggerDownScroll'})
} else {
// 不符合的话 则重置
me.downHight = 0;
// 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向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
// me.triggerUpScroll(true);
me.callMethod(ins, {type: 'triggerUpScroll'})
}
}
}
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
return {x: e.clientX,y: e.clientY}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
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;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || 0
}
/* 获取body的高度 */
me.getBodyHeight = function() {
return me.bodyHeight || 0;
}
/* 调用逻辑层的方法 */
me.callMethod = function(ins, param) {
if(ins) ins.callMethod('wxsCall', param)
}
/* 导出模块 */
module.exports = {
propObserver: propObserver,
callObserver: callObserver,
touchstartEvent: touchstartEvent,
touchmoveEvent: touchmoveEvent,
touchendEvent: touchendEvent
}

View File

@@ -0,0 +1,82 @@
<template>
<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>
<view class="cell b-b center" v-for="(item, index) in data.list" :key="index" @click="confirm(item)">
<text>{{ item.text }}</text>
</view>
<view class="cell center cancel-btn" @click="close">
<text>取消</text>
</view>
</view>
</uni-popup>
</template>
<script>
/**
* 底部选择菜单
*/
export default {
data() {
return {
data: {}
};
},
methods: {
//选择回调
confirm(item){
this.$util.throttle(()=>{
this.$emit('onConfirm', item)
})
this.close();
},
open(data){
this.data = data;
this.$refs.popup.open();
},
close(){
this.$refs.popup.close();
}
}
}
</script>
<style scoped lang="scss">
.content{
background-color: #fff;
border-radius: 16rpx 16rpx 0 0;
overflow: hidden;
}
.cell{
min-height: 88rpx;
font-size: 32rpx;
color: #333;
position: relative;
&:after{
position: absolute;
z-index: 3;
left: 0;
top: auto;
bottom: 0;
right: 0;
height: 0;
content: '';
transform: scaleY(.5);
border-bottom: 1px solid #f5f5f5;
}
&:last-child{
height: 96rpx;
border-top: 12rpx solid #f7f7f7;
}
&.title{
height: 100rpx;
font-size: 28rpx;
color: #999;
}
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<view
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>
</view>
</template>
<script>
/**
* 按钮组件
* @prop text 按钮显示文字
* @prop icon 按钮图标
* @prop iconSize 按钮显示文字
* @prop isConfirm 点击后是否处理loading状态
* @prop disabled 按钮禁用
* @prop marginTop 按钮上边距
*/
let stopTimer = null;
export default {
name: 'MixButton',
data() {
return {
dead: false,
loading: false
};
},
props: {
text: {
type: String,
default: '提交'
},
icon: {
type: String,
default: ''
},
iconSize: {
type: Number,
default: 32
},
isConfirm: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
marginTop: {
type: String,
default: '0rpx'
}
},
methods: {
stop(){
if(stopTimer){
clearTimeout(stopTimer);
stopTimer = null;
}
this.loading = false;
},
death(){
this.loading = false;
this.dead = true;
},
confirm(){
if(this.dead){
return;
}
if(this.loading || this.disabled){
return;
}
if(this.isConfirm){
this.loading = true;
stopTimer = setTimeout(()=>{
this.loading = false;
clearTimeout(stopTimer);
stopTimer = null;
}, 10000)
}
this.$emit('onConfirm');
}
}
}
</script>
<style scoped lang='scss'>
.mix-btn-content{
display: flex;
align-items: center;
justify-content: center;
width: 610rpx;
height: 88rpx;
margin: 0 auto;
font-size: 32rpx;
color: #fff;
border-radius: 100rpx;
background-color: $base-color;
position: relative;
&:after{
content: '';
position: absolute;
left: 50%;
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%);
border-radius: 100rpx;
opacity: 0.4;
filter:blur(10rpx);
}
&.disabled {
opacity: .65;
}
.mix-text{
position: relative;
z-index: 1;
}
.mix-icon{
position: relative;
z-index: 1;
margin-right: 8rpx;
}
.loading-icon{
width: 34rpx;
height: 34rpx;
transform-origin:50% 50%;
margin-right: 16rpx;
animation: rotate 2s linear infinite;
position: relative;
z-index: 1;
}
}
@keyframes rotate{
from {
transform:rotate(0deg)
}
to {
transform:rotate(360deg)
}
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<view class="mix-get-code" @click="getCode">
<view v-if="loading" class="loading">
<mix-icon-loading size="28rpx" color="#0083ff"></mix-icon-loading>
</view>
<text class="text" :class="{disabled: timeDown > 0}">
{{ timeDown > 0 ? '重新获取 ' + timeDown + 's' : '获取验证码' }}
</text>
</view>
</template>
<script>
/**
* 手机验证码
* @prop mobile 手机号
* @prop templateCode 短信模版id
*/
import {checkStr} from '@/common/js/util'
export default {
//获取手机验证码
name: 'MixMobileCode',
data() {
return {
loading: false,
timeDown: ''
}
},
props: {
mobile: {
type: String,
default: ''
},
templateCode: {
type: String,
default: ''
},
action: {
type: String,
default: '用户注册' //设置支付密码
}
},
methods: {
//获取验证码
async getCode(){
if(this.timeDown > 0){
return;
}
this.$util.throttle(()=>{
const mobile = this.mobile || this.$store.state.userInfo.username;;
if(!checkStr(mobile, 'mobile')){
this.$util.msg('手机号码格式不正确');
return;
}
this.loading = true;
this.$request('smsCode', 'send', {
mobile,
action: this.action, //uni短信必填
TemplateCode: this.templateCode, //阿里云必填
}).then(response=>{
this.$util.msg(response.msg);
this.loading = false;
if(response.status === 1){
this.countDown(60);
}
}).catch(err=>{
this.$util.msg('验证码发送失败');
this.loading = false;
console.log(err);
})
}, 2000)
},
//倒计时
countDown(timer){
timer --;
this.timeDown = timer;
if(timer > 0){
setTimeout(()=>{
this.countDown(timer);
}, 1000)
}
},
}
}
</script>
<style scoped lang="scss">
.mix-get-code{
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
height: 36rpx;
&:before{
content: '';
width: 0;
height: 40;
border-right: 1px solid #f0f0f0;
}
.loading{
margin-right: 8rpx;
}
.text{
line-height: 28rpx;
font-size: 26rpx;
color: #40a2ff;
&.disabled{
color: #ccc;
}
}
}
</style>

View File

@@ -0,0 +1,209 @@
<template>
<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>
</view>
<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>
</view>
<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>
</view>
<view v-else class="default column center">
<image class="icon" src="/static/empty/default.png"></image>
<text class="text">{{ text }}</text>
</view>
</view>
</template>
<script>
/**
* 缺省显示
* @prop text 缺省文字提示
* @prop type 缺省类型
* @prop backgroundColor 缺省页面背景色
*/
export default {
computed: {
hasLogin(){
return !!this.$store.getters.hasLogin;
}
},
props: {
text: {
type: String,
default: '暂时没有数据'
},
type: {
type: String,
default: ''
},
backgroundColor: {
type: String,
default: 'rgba(0,0,0,0)'
}
},
methods: {
onCartBtnClick(){
if(this.hasLogin){
uni.switchTab({
url: '/pages/tabbar/home'
})
}else{
this.navTo('/pages/auth/login');
}
},
switchTab(url){
uni.switchTab({
url
})
}
}
}
</script>
<style scoped lang="scss">
.mix-empty{
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
animation: show .5s 1;
}
@keyframes show{
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.default{
padding-top: 26vh;
/* #ifdef H5 */
padding-top: 30vh;
/* #endif */
.icon{
width: 460rpx;
height: 342rpx;
}
.text{
margin-top: 10rpx;
font-size: 28rpx;
color: #999;
}
}
.cart{
padding-top: 14vh;
/* #ifdef H5 */
padding-top: 18vh;
/* #endif */
.icon{
width: 320rpx;
height: 320rpx;
}
.title{
margin: 50rpx 0 26rpx;
font-size: 34rpx;
color: #333;
}
.text{
font-size: 28rpx;
color: #aaa;
}
.btn{
width: 320rpx;
height: 80rpx;
margin-top: 80rpx;
text-indent: 2rpx;
letter-spacing: 2rpx;
font-size: 32rpx;
color: #fff;
border-radius: 100rpx;
background: linear-gradient(to bottom right, #ffb2bf, $base-color);
}
}
.address{
padding-top: 6vh;
/* #ifdef H5 */
padding-top: 10vh;
/* #endif */
.icon{
width: 380rpx;
height: 380rpx;
}
.text{
width: 400rpx;
margin-top: 40rpx;
font-size: 30rpx;
color: #999;
text-align: center;
line-height: 1.6;
}
.btn{
position: fixed;
left: 50%;
bottom: 120rpx;
width: 110rpx;
height: 110rpx;
background-color: $base-color;
border-radius: 100rpx;
transform: translateX(-50%);
box-shadow: 2rpx 2rpx 10rpx rgba(255, 83, 111, .5);
}
.icon-jia2{
font-size: 50rpx;
color: #fff;
}
}
.favorite{
padding-top: 6vh;
/* #ifdef H5 */
padding-top: 10vh;
/* #endif */
.icon{
width: 360rpx;
height: 360rpx;
}
.text{
width: 400rpx;
margin-top: 40rpx;
font-size: 30rpx;
color: #999;
text-align: center;
line-height: 1.6;
}
.btn{
width: 320rpx;
height: 80rpx;
margin-top: 40rpx;
text-indent: 2rpx;
letter-spacing: 2rpx;
font-size: 32rpx;
color: #fff;
border-radius: 100rpx;
background: linear-gradient(to bottom right, #ffb2bf, $base-color);
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<view class="mix-icon-loading">
<view
class="loading-icon"
:style="{
width: size,
height: size,
borderRightColor: color
}"
></view>
</view>
</template>
<script>
/**
* 菊花loading小图标
* @prop size 尺寸单位rpx
* @prop color 颜色
*/
export default {
name: 'MixIconLoading',
data() {
return {
};
},
props: {
size: {
type: String,
default: '26rpx'
},
color: {
type: String,
default: '#999'
}
},
methods: {
}
}
</script>
<style scoped lang='scss'>
.mix-icon-loading{
display: flex;
align-items: center;
justify-content: center;
width: auto;
height: auto;
}
.loading-icon{
width: 28rpx;
height: 28rpx;
border: 4rpx solid #ddd;
animation: mix-loading 1.8s steps(12) infinite;
border-radius: 100rpx;
}
@keyframes mix-loading{
from {
transform:rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<view class="content">
<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]"
></text>
</view>
</view>
</template>
<script>
/**
* 简单封装了下, 应用范围比较狭窄,可以在此基础上进行扩展使用
* 比如加入image iconSize可控等
*/
export default {
data() {
return {
typeList: {
left: 'icon-zuo',
right: 'icon-you',
up: 'icon-shang',
down: 'icon-xia'
},
}
},
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: '标题'
},
tips: {
type: String,
default: ''
},
tipsColor: {
type: String,
default: '#999'
},
navigateType: {
type: String,
default: 'right'
},
border: {
type: String,
default: 'b-b'
},
hoverClass: {
type: String,
default: 'cell-hover'
},
iconColor: {
type: String,
default: '#333'
}
},
methods: {
onClick(){
this.$emit('onClick');
}
},
}
</script>
<style scoped lang='scss'>
.mix-list-cell{
display:flex;
align-items: center;
height: 96rpx;
padding: 0 24rpx;
position:relative;
&.cell-hover{
background:#fafafa;
}
&.b-b{
&:after{
left: 30rpx;
border-color: #f0f0f0;
}
}
.cell-icon{
align-self: center;
width: 60rpx;
font-size: 38rpx;
}
.cell-more{
align-self: center;
font-size: 24rpx;
color: #999;
margin-left: 16rpx;
}
.cell-tit{
flex: 1;
font-size: 30rpx;
color: #333;
margin-right:10rpx;
}
.cell-tip{
font-size: 26rpx;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<view>
<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>
<view v-show="status === 2" class="mix-load-more center">
<image class="logo" src="/static/logo-b-w.png"></image>
<text class="text">国云网络提供技术支持</text>
</view>
</view>
</template>
<script>
/**
* 上划加载更多
* @prop {Number} status 0加载前1加载中2没有更多
* @prop {Array} textList ['加载前提示', '加载中提示', '加载完提示']
*/
export default {
name: "mix-load-more",
props: {
status: {
type: Number,
default: 0
},
textList: {
type: Array,
default () {
return [
'上拉显示更多',
'正在加载 ..',
'我也是有底线的~'
];
}
}
}
}
</script>
<style scoped lang="scss">
.mix-load-more{
width: 750rpx;
height: 110rpx;
}
.loading-icon{
width: 64rpx;
height: 68rpx;
margin-right: 20rpx;
}
.text{
font-size: 26rpx;
color: #999;
}
.logo{
width: 34rpx;
height: 34rpx;
margin-right: 12rpx;
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<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>
</view>
<!-- 仓鼠 -->
<image v-else-if="type === 2" class="hamster" src="/static/loading/hamster.gif"></image>
</view>
</view>
</template>
<script>
/**
* loading
* @prop type 1 黑底菊花 2 小胖仓鼠
* @prop mask 遮罩层
* @prop timeout 超时时间(秒) 默认10s
*/
export default {
name: 'MixIconLoading',
data(){
return {
isTimeout: false
}
},
props: {
type: {
type: Number,
default: 1
},
mask: {
type: Boolean,
default: false
},
timeout: {
type: Number,
default: 10
},
title: {
type: String,
default: '请稍候'
}
},
created() {
this._timer = setTimeout(()=>{
if(!this.isLoading){
return;
}
this.isTimeout = true;
uni.showToast({
title: '加载超时,请重试',
icon: 'none'
})
}, this.timeout * 1000)
},
destroyed() {
this._timer && clearTimeout(this._timer);
}
}
</script>
<style scoped lang='scss'>
.mix-loading{
position: fixed;
left: 50vw;
top: 46vh;
width: 0;
height: 0;
z-index: 999;
}
.mask{
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.chry{
min-width: 120rpx;
min-height: 116rpx;
border-radius: 10rpx;
background-color: rgba(17,17,17,.7);
.icon{
width: 64rpx;
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;
}
}
@keyframes mix-loading{
from {
transform:rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
.hamster{
width: 106rpx;
height: 120rpx;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<uni-popup ref="popup">
<view class="pop-content">
<text class="title">{{ title }}</text>
<view class="con center">
<text class="text">{{ text }}</text>
</view>
<view class="btn-group row b-t">
<view class="btn center" @click="close">
<text>{{ cancelText }}</text>
</view>
<view class="btn center b-l" @click="confirm">
<text>{{ confirmText }}</text>
</view>
</view>
</view>
</uni-popup>
</template>
<script>
/**
* 确认对话框
* @prop title 标题
* @prop text 提示内容
* @prop cancelText 取消按钮文字
* @prop confirmText 确认按钮文字
* @event onConfirm 确认按钮点击时触发
*/
export default {
data() {
return {
};
},
props: {
title: String,
text: String,
cancelText: {
type: String,
default: '取消'
},
confirmText: {
type: String,
default: '确定'
}
},
methods: {
confirm(){
this.$emit('onConfirm');
this.close();
},
open(){
this.$refs.popup.open();
},
close(){
this.$refs.popup.close();
}
}
}
</script>
<style scoped lang='scss'>
.pop-content{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 540rpx;
padding-top: 36rpx;
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
.title{
font-size: 32rpx;
color: #333;
line-height: 48rpx;
font-weight: 700;
}
.con{
padding: 36rpx 40rpx 54rpx;
}
.text{
width: 460rpx;
font-size: 26rpx;
color: #333;
line-height: 40rpx;
text-align: center;
}
.btn-group{
width: 100%;
}
.btn{
flex: 1;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
color: #777;
&:last-child{
color: #007aff;
}
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<view class="nav-bar b-b">
<view
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>
<view class="line" :class="{'line--show': current === index}"></view>
</view>
</view>
</template>
<script>
/**
* 顶部tab栏
*/
export default {
data(){
return {
countList: [],
}
},
props: {
navs: {
type: Array,
default(){
return [];
}
},
current: {
type: Number,
default: 0
},
counts: {
type: Array,
default(){
return [];
}
}
},
watch: {
},
methods: {
navChange(index){
this.$emit('onChange', index);
}
}
}
</script>
<style scoped lang='scss'>
/* #ifndef APP-NVUE */
view{
display: flex;
flex-direction: column;
}
/* #endif */
.fill-view{
height: 84rpx;
width: 100%;
}
.nav-bar{
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 750rpx;
height: 84rpx;
background-color: #fff;
z-index: 90;
position: fixed;
left: 0;
top: 0;
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
&:after{
border-color: #f7f7f7;
}
}
.nav-item{
flex: 1;
align-items: center;
justify-content: center;
height: 84rpx;
padding-top: 4rpx;
position: relative;
}
.tit-wrap{
flex: 1;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
}
.number{
position: absolute;
right: -20rpx;
top: 0px;
min-width: 36rpx;
height: 36rpx;
padding: 0 6rpx;
text-align: center;
line-height: 28rpx;
border: 4rpx solid #fff;
background-color: $base-color;
border-radius: 100rpx;
font-size: 20rpx;
color: #fff;
}
.tit{
font-size: 30rpx;
color: #333;
}
.active{
color: #ff4443;
font-weight: 700;
}
.line{
width: 34rpx;
height: 4rpx;
border-radius: 100rpx;
background-color: #ff4443;
opacity: 0;
&--show{
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<view class="uni-numbox">
<view class="uni-numbox-minus"
@click="_calcValue('subtract')"
>
<text class="mix-icon icon--jianhao" :class="minDisabled?'uni-numbox-disabled': ''" ></text>
</view>
<input
class="uni-numbox-value"
type="number"
:disabled="inputDisabled"
:value="inputValue"
@blur="_onBlur"
>
<view
class="uni-numbox-plus"
@click="_calcValue('add')"
>
<text class="mix-icon icon-jia2" :class="maxDisabled?'uni-numbox-disabled': ''" ></text>
</view>
</view>
</template>
<script>
/**
* index 当前行下标
* value 默认值
* min 最小值
* max 最大值
* step 步进值
* disabled 禁用
*/
export default {
name: 'uni-number-box',
props: {
index: {
type: Number,
default: 0
},
value: {
type: Number,
default: 1
},
min: {
type: Number,
default: -Infinity
},
max: {
type: Number,
default: 99
},
step: {
type: Number,
default: 1
},
inputDisabled: {
type: Boolean,
default: false
}
},
data() {
return {
inputValue: this.value,
}
},
created(){
},
computed: {
maxDisabled(){
return this.inputValue >= this.max;
},
minDisabled(){
return this.inputValue <= this.min;
},
},
watch: {
inputValue(number) {
const data = {
number: number,
index: this.index
}
this.$emit('eventChange', data);
},
},
methods: {
_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){
return;
}
this.inputValue = newValue;
},
_onBlur(event) {
let value = event.detail.value;
let constValue = value;
if (!value) {
this.inputValue = 0;
return
}
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
})
}else{
this.inputValue = value
}
}
}
}
</script>
<style>
.uni-numbox {
display: flex;
justify-content: flex-start;
flex-direction: row;
align-items: center;
height: 50rpx;
}
.uni-numbox-minus,
.uni-numbox-plus {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
height: 100%;
line-height: 1;
background-color: #f7f7f7;
}
.uni-numbox-minus .mix-icon,
.uni-numbox-plus .mix-icon{
font-size: 32rpx;
color: #333;
}
.uni-numbox-value {
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
width: 60rpx;
height: 50rpx;
min-height: 50rpx;
text-align: center;
font-size: 28rpx;
color: #333;
}
.uni-numbox-disabled.mix-icon {
color: #C0C4CC;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<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>
</view>
</template>
<script>
/**
* 价格显示组件
*/
export default {
data() {
return {
priceArr: []
};
},
props: {
price: {
type: Number,
default: 0
},
size: {
type: Number,
default: 36
}
},
watch: {
price(){
this.render();
}
},
created() {
this.render();
},
methods: {
render(){
const price = parseFloat(this.price).toFixed(2);
this.priceArr = (''+price).split('.');
}
}
}
</script>
<style scoped lang="scss">
.mix-price-view{
color: $base-color;
}
.price{
font-weight: 700;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<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>
<view class="cen center">
<view class="circle center"></view>
</view>
<view class="right column">
<text class="title">{{ item.title }}</text>
<text v-if="item.tip" class="tip">{{ item.tip }}</text>
</view>
</view>
</view>
</template>
<script>
/**
* 时间轴
* {
* title: 标题
* tip: 小字
* time: 时间戳
* }
*/
export default {
data() {
return {
};
},
props: {
list: {
type: Array,
default(){
return []
}
}
}
}
</script>
<style scoped lang="scss">
.mix-timeline{
}
.cell{
display: flex;
align-items: flex-start;
width: 100%;
padding: 0 30rpx 0;
&:first-child .circle{
&:before{
background-color: $base-color;
}
&:after{
content: '';
position: absolute;
width: 28rpx;
height: 28rpx;
background-color: #f9e0eb;
border-radius: 100rpx;
}
}
&:last-child .right:before{
display: none;
}
}
.left{
.time{
font-size: 26rpx;
color: #333;
line-height: 44rpx;
}
.date{
font-size: 20rpx;
color: #333;
}
}
.cen{
width: 80rpx;
height: 44rpx;
.circle{
width: 16rpx;
height: 16rpx;
position: relative;
z-index: 1;
&:before{
content: '';
width: 16rpx;
height: 16rpx;
background-color: #ddd;
border-radius: 100rpx;
position: relative;
z-index: 5;
}
}
}
.right{
flex: 1;
padding-bottom: 30rpx;
position: relative;
min-height: 96rpx;
&:before{
content: '';
width: 2rpx;
position: absolute;
left: 0;
top: 0;
bottom: 0;
background-color: #ddd;
transform: translate(-42rpx, 22rpx);
}
.title{
font-size: 28rpx;
color: #333;
line-height: 44rpx;
font-weight: 700;
}
.tip{
margin-top: 6rpx;
font-size: 24rpx;
color: #999;
line-height: 36rpx;
}
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<view class="upload-content">
<view
class="upload-item"
v-for="(item, index) in imageList"
:key="index"
>
<image class="upload-img" :src="item.filePath" mode="aspectFill" @click="previewImage(index)"></image>
<!-- 删除按钮 -->
<image class="upload-del-btn"
@click.stop.prevent="showDelModal(index)"
src="data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZGNjhCRTZFMzgwRTExRUFCNkUwOEM2Q0Y3RjUzQTE2IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZGNjhCRTZGMzgwRTExRUFCNkUwOEM2Q0Y3RjUzQTE2Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RkY2OEJFNkMzODBFMTFFQUI2RTA4QzZDRjdGNTNBMTYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RkY2OEJFNkQzODBFMTFFQUI2RTA4QzZDRjdGNTNBMTYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCAAoACgDAREAAhEBAxEB/8QAawABAQEBAQAAAAAAAAAAAAAABggEAQIBAQEAAAAAAAAAAAAAAAAAAAABEAAABQMCAwgBBAMAAAAAAAABEQIDBBMFBgASIRQVMVFhcSIyBwhi8EFCM1M0FhEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AovNs2sWHWJy8XhwUspHYyyghdedEBFLbaREDES8gDiOglbNfsBn2RSHEwpi7JbREaUWEoUObfzfAnFD3kQeGqgH/ANBf6/MdSlV+yrXc39p+7cegeYV9gM+x2Q2mbMXe7aAhVizVCtzb+D4m4ke4zDw0FU4VmtjzGxt3izuCplQih5lYE404HubcTxIQPyENRUrfYDNZGRZ9MhpcEbbZFqhRWj9NRAk+vzU4BH3AGqjH8fYFj2Z2S4W+LPXFzllYvW2I8KUxpUdKAFTaRI6pmPb2FwLcIAP6DeutdE5J7q9bl+R2DVqmWzb36Bp8hfHuPYVj1vhzbguTnUlaXp0FlSVRo0ZSRJCxLdU3bSHdx48CIRDR9f8ANZGO59DhqcELbe1phSmj9NRYkwvzS4JH3COgB5BX69cuY/v5p+qR++ord2+Og82Vu7uXeGizA8N1F5HJBHOrVP0bNvEz0FZpVS2NuLtCvm1VoEEqECMv2H+FXZ+qeoqTr91rrU3rdbq9ZfPcwdWqfq3n++qjuPcx1+2ct/sc2xR7ffUTt7OPboHf2AwqRjufzJiWxC23tapsV0vTUWJvo80uCZdwhoM3x58h2DCrBcZkO3LkZzIWLMCe8CFRo0ZSANSQPdU3GYFx4cSMBAU5e7u5dxvK5jw3UXuYGbvGrVPdv39pnoGGffIVmzOwW6TPtymc3irBmbc2QQmPJipQIApxIDuq7i/iQAfEiSAbvr/hUjIs/hzFNiNtsi0zZTpemogTYR5qcAy7gHQVTmuFWPMbG5Z7w2KmVCC2XkCTjTge1xtXEhA/IQ1FStmv1/z/AB2Q4qHDXe7aAjSlQkitzb+bAG4ke8jDx1UAwx6/i/y4W2VzH+Gg5v7S9u09A8wr6/5/kUhtUyGuyW0RCrKmpFDm38GBJxQ9xkHjoKpwrCrHh1jbs9nbFLKRFbzyxNx1wfc44rgYiXkAaiv/2Q=="
mode="scaleToFill">
</image>
<!-- 进度 -->
<view class="upload-progress" v-if="item.progress < 100">{{item.progress}}%</view>
</view>
<!-- 新增按钮 -->
<view class="upload-add-btn" v-if="rduLength > 0" @click="chooseImage"></view>
<mix-modal ref="mixModal" title="提示" text="确定要放弃这张图片么" @onConfirm="delImage"></mix-modal>
</view>
</template>
<script>
export default {
data() {
return {
imageList: []
};
},
props: {
count: {
type: Number,
default: 5 //单次可选择的图片数量
},
length: {
type: Number,
default: 1 //可上传总数量
},
index: {
type: Number,
default: 0
}
},
computed: {
rduLength(){
return this.length - this.imageList.length;
}
},
methods: {
//选择图片
chooseImage(){
uni.chooseImage({
count: this.rduLength < this.count ? this.rduLength : this.count, //最多可以选择的图片张数默认9
sizeType: ['original', 'compressed'], //original 原图compressed 压缩图,默认二者都有
sourceType: ['album', 'camera'], //album 从相册选图camera 使用相机,默认二者都有
success: res=> {
this.uploadFiles(res.tempFilePaths);
}
});
},
//上传图片
async uploadFiles(files){
const item = {
filePath: files[0],
progress: 0
}
this.imageList.push(item);
const result = await uniCloud.uploadFile({
filePath: item.filePath,
cloudPath: + new Date() + ('000000' + Math.floor(Math.random() * 999999)).slice(-6) + '.jpg',
onUploadProgress: e=> {
item.progress = Math.round(
(e.loaded * 100) / e.total
)
}
});
if(!result.fileID){
this.$util.msg('图片上传失败,上传任务已终止');
this.imageList.pop();
return;
}
const tempFiles = await uniCloud.getTempFileURL({
fileList: [result.fileID]
})
const tempFile = tempFiles.fileList[0];
if(tempFile.download_url || tempFile.fileID){
item.url = tempFile.download_url || tempFile.fileID;
this.$emit('onChange', {
list: this.imageList,
index: this.index
})
}else{
this.$util.msg('图片上传失败,上传任务已终止');
this.imageList.pop();
}
files.shift();
if(files.length > 0){
this.uploadFiles(files);
}
},
//删除图片
showDelModal(index){
this.curIndex = index;
this.$refs.mixModal.open();
},
delImage(index){
this.imageList.splice(this.curIndex, 1);
this.$emit('onChange', {
list: this.imageList,
index: this.index
})
},
//预览图片
previewImage(index){
const urls = this.imageList.map(item=> item.filePath);
uni.previewImage({
current: index,
urls: urls
})
}
}
}
</script>
<style lang="scss">
.upload-content{
display: flex;
flex-wrap: wrap;
background-color: #fff;
}
.upload-item{
position: relative;
width:148rpx;
height:148rpx;
margin-right:28rpx;
margin-bottom:24rpx;
&:nth-child(4n){
margin-right:0;
}
.upload-img{
width:100%;
height:100%;
border-radius:8rpx;
}
.upload-del-btn{
position: absolute;
right:-16rpx;
top:-14rpx;
z-index: 5;
width:36rpx;
height:36rpx;
border: 4rpx solid #fff;
border-radius: 100px;
}
.upload-progress{
position: absolute;
left:0;
top:0;
display:flex;
align-items:center;
justify-content: center;
width:100%;
height:100%;
background-color: rgba(0,0,0,.4);
color:#fff;
font-size:24rpx;
border-radius:8rpx;
}
}
.upload-add-btn {
position: relative;
float:left;
width: 148rpx;
height: 148rpx;
margin-bottom: 24rpx;
z-index: 85;
border-radius:8rpx;
background:#f7f7f7;
&:before,
&:after {
content: " ";
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
width: 4rpx;
height: 60rpx;
background-color: #d6d6d6;
}
&:after {
width: 60rpx;
height: 4rpx;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,97 @@
<template>
<uni-popup ref="uniPopup" type="bottom">
<view class="content">
<text class="mix-icon icon-guanbi" @click="close"></text>
<view class="center title">
<text>输入支付密码</text>
</view>
<view class="input center">
<view class="item center" :class="{has: pwd.length > index}" v-for="(item, index) in 6" :key="index"></view>
</view>
<view class="reset-btn center" @click="navTo('/pages/auth/payPassword')">
<text>重置密码</text>
</view>
<number-keyboard ref="keybord" @onChange="onNumberChange"></number-keyboard>
</view>
</uni-popup>
</template>
<script>
/**
* 支付密码键盘
*/
export default {
data() {
return {
pwd: ''
};
},
watch: {
pwd(pwd){
if(pwd.length === 0){
this.$refs.keybord.val = '';
}
}
},
methods: {
open(){
this.$refs.uniPopup.open();
},
close(){
this.$refs.uniPopup.close();
},
onNumberChange(pwd){
this.pwd = pwd;
if(pwd.length >= 6){
this.$emit('onConfirm', pwd.substring(0,6));
}
}
}
}
</script>
<style scoped lang="scss">
.content{
border-radius: 20rpx 20rpx 0 0;
background-color: #fff;
position: relative;
}
.title{
height: 110rpx;
font-size: 32rpx;
color: #333;
font-weight: 700;
}
.input{
padding: 30rpx 0 60rpx;
.item{
width: 88rpx;
height: 88rpx;
margin: 0 10rpx;
border: 1px solid #ddd;
border-radius: 4rpx;
}
.has:after{
content: '';
width: 16rpx;
height: 16rpx;
border-radius: 100rpx;
background-color: #333;
}
}
.reset-btn{
padding-bottom: 20rpx;
margin-top: -10rpx;
margin-bottom: 30rpx;
font-size: 28rpx;
color: #007aff;
}
.icon-guanbi{
position: absolute;
left: 10rpx;
top: 24rpx;
padding: 20rpx;
font-size: 28rpx;
}
</style>

View File

@@ -0,0 +1,25 @@
import message from './message.js';
// 定义 type 类型:弹出类型top/bottom/center
const config = {
// 顶部弹出
top:'top',
// 底部弹出
bottom:'bottom',
// 居中弹出
center:'center',
// 消息提示
message:'top',
// 对话框
dialog:'center',
// 分享
share:'bottom',
}
export default {
data(){
return {
config:config
}
},
mixins: [message],
}

View File

@@ -0,0 +1,302 @@
<template>
<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 />
</view>
</uni-transition>
</view>
</template>
<script>
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}
*/
export default {
name: 'UniPopup',
components: {
uniTransition
},
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
maskClick: {
type: Boolean,
default: true
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, .5)'
}
},
provide() {
return {
popup: this
}
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(newVal) {
this[this.config[newVal]]()
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick(val) {
this.mkclick = val
}
},
data() {
return {
duration: 300,
ani: [],
showPopupState: false,
showTrans: false,
maskClass: {
'position': 'fixed',
'bottom': 0,
'top': 0,
'left': 0,
'right': 0
},
transClass: {
'position': 'fixed',
'left': 0,
'right': 0,
},
maskShow: true,
mkclick: true,
popupstyle: 'top',
config: {
top:'top',
// 底部弹出
bottom:'bottom',
// 居中弹出
center:'center'
}
}
},
created() {
this.mkclick = this.maskClick
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
},
methods: {
clear(e) {
// TODO nvue 取消冒泡
e.stopPropagation()
},
open() {
this.showPopupState = true
this.$nextTick(() => {
new Promise(resolve => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.showTrans = true
// fixed by mehaotian 兼容 app 端
this.$nextTick(() => {
resolve();
})
}, 50);
}).then(res => {
// 自定义打开事件
clearTimeout(this.msgtimer)
this.msgtimer = setTimeout(() => {
this.customOpen && this.customOpen()
}, 100)
this.$emit('change', {
show: true,
type: this.type
})
})
})
},
close(type) {
this.showTrans = false
this.$nextTick(() => {
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// 自定义关闭事件
this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopupState = false
}, 300)
})
},
onTap() {
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top() {
this.popupstyle = 'top'
this.ani = ['slide-top']
this.transClass = {
'position': 'fixed',
'left': 0,
'right': 0,
}
},
/**
* 底部弹出样式处理
*/
bottom() {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
'position': 'fixed',
'left': 0,
'right': 0,
'bottom': 0
}
},
/**
* 中间弹出样式处理
*/
center() {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
'position': 'fixed',
/* #ifndef APP-NVUE */
'display': 'flex',
'flexDirection': 'column',
/* #endif */
'bottom': 0,
'left': 0,
'right': 0,
'top': 0,
'justifyContent': 'center',
'alignItems': 'center'
}
}
}
}
</script>
<style lang="scss" scoped>
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-popup__mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: $uni-bg-color-mask;
opacity: 0;
}
.mask-ani {
transition-property: opacity;
transition-duration: 0.2s;
}
.uni-top-mask {
opacity: 1;
}
.uni-bottom-mask {
opacity: 1;
}
.uni-center-mask {
opacity: 1;
}
.uni-popup__wrapper {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
}
.top {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.bottom {
bottom: 0;
}
.uni-popup__wrapper-box {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
}
.content-ani {
// transition: transform 0.3s;
transition-property: transform, opacity;
transition-duration: 0.2s;
}
.uni-top-content {
transform: translateY(0);
}
.uni-bottom-content {
transform: translateY(0);
}
.uni-center-content {
transform: scale(1);
opacity: 1;
}
</style>

View File

@@ -0,0 +1,245 @@
const BindingX = uni.requireNativePlugin('bindingx');
const dom = uni.requireNativePlugin('dom');
const animation = uni.requireNativePlugin('animation');
export default {
data() {
return {
right: 0,
button: [],
preventGesture: false
}
},
watch: {
show(newVal) {
if (!this.position || JSON.stringify(this.position) === '{}') return;
if (this.autoClose) return
if (this.isInAnimation) return
if (newVal) {
this.open()
} else {
this.close()
}
},
},
created() {
if (this.swipeaction.children !== undefined) {
this.swipeaction.children.push(this)
}
},
mounted() {
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
setTimeout(() => {
this.getSelectorQuery()
}, 200)
},
beforeDestroy() {
if (this.timing) {
BindingX.unbind({
token: this.timing.token,
eventType: 'timing'
})
}
if (this.eventpan) {
BindingX.unbind({
token: this.eventpan.token,
eventType: 'pan'
})
}
this.swipeaction.children.forEach((item, index) => {
if (item === this) {
this.swipeaction.children.splice(index, 1)
}
})
},
methods: {
onClick(index, item) {
this.$emit('click', {
content: item,
index
})
},
touchstart(e) {
if (this.isInAnimation) return
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,
property: 'transform.translateX',
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.x = 0
}
this.stop = false
this.bindTiming();
}
})
},
touchend(e) {
this.$nextTick(() => {
if (this.isopen && !this.isDrag && !this.isInAnimation) {
this.close()
}
})
},
bindTiming() {
if (this.isopen) {
this.move(this.x, -this.right)
} else {
this.move(this.x, -40)
}
},
move(left, value) {
if (left >= value) {
this.close()
} else {
this.open()
}
},
/**
* 开启swipe
*/
open() {
this.animation(true)
},
/**
* 关闭swipe
*/
close() {
this.animation(false)
},
/**
* 开启关闭动画
* @param {Object} type
*/
animation(type) {
this.isDrag = true
let endWidth = this.right
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})`
let props = [{
element: this.selector,
property: 'transform.translateX',
expression: translate_x_expression
}]
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}/${endWidth}`
let moveX = `(${step}) * ${moveMix}`
let pageButtonX = `easeOutExpo(t,${moveX},${type ? -moveMix + '-' + moveX: 0 + '-' + moveX},${time})`
props.push({
element: buttonSelectors,
property: 'transform.translateX',
expression: pageButtonX
})
}
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
}, (e) => {
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
})
for (let i = 0; i < this.options.length; i++) {
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.autoClose) return
if (this.show) {
this.open()
}
})
}
}
}
}

View File

@@ -0,0 +1,204 @@
/**
* 监听页面内值的变化,主要用于动态开关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)
} else {
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.left = state.left || state.position[0].left;
// 获取最终按钮组的宽度
state.width = pageX - state.left;
ins.callMethod('closeSwipe')
}
/**
* 开始滑动操作
* @param {Object} e
* @param {Object} ownerInstance
*/
function touchmove(e, ownerInstance) {
var instance = e.instance;
var disabled = instance.getDataset().disabled
var state = instance.getState()
// fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复
disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false;
if (disabled) return
var pageX = e.touches[0].pageX;
move(pageX - state.width, instance, ownerInstance)
}
/**
* 结束触摸操作
* @param {Object} e
* @param {Object} ownerInstance
*/
function touchend(e, ownerInstance) {
var instance = e.instance;
var disabled = instance.getDataset().disabled
var state = instance.getState()
// fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复
disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false;
if (disabled) return
// 滑动过程中触摸结束,通过阙值判断是开启还是关闭
// fixed by mehaotian 定时器解决点击按钮touchend 触发比 click 事件时机早的问题 ,主要是 ios13
moveDirection(state.left, -40, instance, ownerInstance)
}
/**
* 设置移动距离
* @param {Object} value
* @param {Object} instance
* @param {Object} ownerInstance
*/
function move(value, instance, ownerInstance) {
var state = instance.getState()
// 获取可滑动范围
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
* @param {Object} value
* @param {Object} ownerInstance
* @param {Object} ins
*/
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)
return
}
// 如果已经是打开状态,进行判断是否关闭,还是保留打开状态
if (isopen) {
if (-left <= position[1].width) {
openState(false, ins, ownerInstance)
} else {
openState(true, ins, ownerInstance)
}
return
}
// 如果是关闭状态,进行判断是否打开,还是保留关闭状态
if (left <= value) {
openState(true, ins, ownerInstance)
} else {
openState(false, ins, ownerInstance)
}
}
/**
* 设置按钮移动距离
* @param {Object} value
* @param {Object} instance
* @param {Object} ownerInstance
*/
function buttonFold(value, instance, ownerInstance) {
var ins = ownerInstance.selectAllComponents('.button-hock');
var state = instance.getState();
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
* @param {Object} ins
* @param {Object} ownerInstance
*/
function openState(type, ins, ownerInstance) {
var state = ins.getState()
var position = state.position
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')
for (var i = 0; i < owner.length; i++) {
owner[i].addClass('ani');
}
// 设置最终移动位置
move(type ? -position[1].width : 0, ins, ownerInstance)
}
module.exports = {
sizeReady: sizeReady,
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend
}

View File

@@ -0,0 +1,160 @@
export default {
data() {
return {
isshow: false,
viewWidth: 0,
buttonWidth: 0,
disabledView: false,
x: 0,
transition: false
}
},
watch: {
show(newVal) {
if (this.autoClose) return
if (newVal) {
this.open()
} else {
this.close()
}
},
},
created() {
if (this.swipeaction.children !== undefined) {
this.swipeaction.children.push(this)
}
},
beforeDestroy() {
this.swipeaction.children.forEach((item, index) => {
if (item === this) {
this.swipeaction.children.splice(index, 1)
}
})
},
mounted() {
this.isopen = false
this.transition = true
setTimeout(() => {
this.getQuerySelect()
}, 50)
},
methods: {
onClick(index, item) {
this.$emit('click', {
content: item,
index
})
},
touchstart(e) {
let {
pageX,
pageY
} = e.changedTouches[0]
this.transition = false
this.startX = pageX
if (this.autoClose) {
this.swipeaction.closeOther(this)
}
},
touchmove(e) {
let {
pageX,
} = e.changedTouches[0]
this.slide = this.getSlide(pageX)
if (this.slide === 0) {
this.disabledView = false
}
},
touchend(e) {
this.stop = false
this.transition = true
if (this.isopen) {
if (this.moveX === -this.buttonWidth) {
this.close()
return
}
this.move()
} else {
if (this.moveX === 0) {
this.close()
return
}
this.move()
}
},
open() {
this.x = this.moveX
this.$nextTick(() => {
this.x = -this.buttonWidth
this.moveX = this.x
if(!this.isopen){
this.isopen = true
this.$emit('change', true)
}
})
},
close() {
this.x = this.moveX
this.$nextTick(() => {
this.x = 0
this.moveX = this.x
if(this.isopen){
this.isopen = false
this.$emit('change', false)
}
})
},
move() {
if (this.slide === 0) {
this.open()
} else {
this.close()
}
},
onChange(e) {
let x = e.detail.x
this.moveX = x
if (x >= this.buttonWidth) {
this.disabledView = true
this.$nextTick(() => {
this.x = this.buttonWidth
})
}
},
getSlide(x) {
if (x >= this.startX) {
this.startX = x
return 1
} else {
this.startX = x
return 0
}
},
getQuerySelect() {
const query = uni.createSelectorQuery().in(this);
query.selectAll('.viewWidth-hook').boundingClientRect(data => {
this.viewWidth = data[0].width
this.buttonWidth = data[1].width
this.transition = false
this.$nextTick(() => {
this.transition = true
})
if (!this.buttonWidth) {
this.disabledView = true
}
if (this.autoClose) return
if (this.show) {
this.open()
}
}).exec();
}
}
}

View File

@@ -0,0 +1,158 @@
// #ifdef APP-NVUE
const dom = weex.requireModule('dom');
// #endif
export default {
data() {
return {
uniShow: false,
left: 0
}
},
computed: {
moveLeft() {
return `translateX(${this.left}px)`
}
},
watch: {
show(newVal) {
if (!this.position || JSON.stringify(this.position) === '{}') return;
if (this.autoClose) return
if (newVal) {
this.$emit('change', true)
this.open()
} else {
this.$emit('change', false)
this.close()
}
}
},
mounted() {
this.position = {}
if (this.swipeaction.children !== undefined) {
this.swipeaction.children.push(this)
}
setTimeout(() => {
this.getSelectorQuery()
}, 100)
},
beforeDestoy() {
this.swipeaction.children.forEach((item, index) => {
if (item === this) {
this.swipeaction.children.splice(index, 1)
}
})
},
methods: {
onClick(index, item) {
this.$emit('click', {
content: item,
index
})
this.close()
},
touchstart(e) {
const {
pageX
} = e.touches[0]
if (this.disabled) return
const left = this.position.content.left
if (this.autoClose) {
this.swipeaction.closeOther(this)
}
this.width = pageX - left
if (this.isopen) return
if (this.uniShow) {
this.uniShow = false
this.isopen = true
this.openleft = this.left + this.position.button.width
}
},
touchmove(e, index) {
if (this.disabled) return
const {
pageX
} = e.touches[0]
this.setPosition(pageX)
},
touchend() {
if (this.disabled) return
if (this.isopen) {
this.move(this.openleft, 0)
return
}
this.move(this.left, -40)
},
setPosition(x, y) {
if (!this.position.button.width) {
return
}
// 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
if (this.isopen) {
this.openleft = this.left + this.position.button.width
}
},
move(left, value) {
if (left >= value) {
this.$emit('change', false)
this.close()
} else {
this.$emit('change', true)
this.open()
}
},
open() {
this.uniShow = true
this.left = -this.position.button.width
this.setValue(-this.position.button.width)
},
close() {
this.uniShow = true
this.setValue(0)
setTimeout(() => {
this.uniShow = false
this.isopen = false
}, 300)
},
getSelectorQuery() {
// #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]
if (this.autoClose) return
if (this.show) {
this.open()
} else {
this.close()
}
})
.exec()
// #endif
// #ifdef APP-NVUE
dom.getComponentRect(this.$refs['selector-content-hock'], (data) => {
if (this.position.content) return
this.position.content = data.size
})
dom.getComponentRect(this.$refs['selector-button-hock'], (data) => {
if (this.position.button) return
this.position.button = data.size
if (this.autoClose) return
if (this.show) {
this.open()
} else {
this.close()
}
})
// #endif
}
}
}

View File

@@ -0,0 +1,97 @@
export default {
data() {
return {
position: [],
button: []
}
},
computed: {
pos() {
return JSON.stringify(this.position)
},
btn() {
return JSON.stringify(this.button)
}
},
watch: {
show(newVal) {
if (this.autoClose) return
let valueObj = this.position[0]
if (!valueObj) {
this.init()
return
}
valueObj.show = newVal
this.$set(this.position, 0, valueObj)
}
},
created() {
if (this.swipeaction.children !== undefined) {
this.swipeaction.children.push(this)
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.swipeaction.children.forEach((item, index) => {
if (item === this) {
this.swipeaction.children.splice(index, 1)
}
})
},
methods: {
init() {
setTimeout(() => {
this.getSize()
this.getButtonSize()
}, 50)
},
closeSwipe(e) {
if (!this.autoClose) return
this.swipeaction.closeOther(this)
},
change(e) {
this.$emit('change', e.open)
let valueObj = this.position[0]
if (valueObj.show !== e.open) {
valueObj.show = e.open
this.$set(this.position, 0, valueObj)
}
},
onClick(index, item) {
this.$emit('click', {
content: item,
index
})
},
appTouchStart(){},
appTouchEnd(){},
getSize() {
const views = uni.createSelectorQuery().in(this)
views
.selectAll('.selector-query-hock')
.boundingClientRect(data => {
if (this.autoClose) {
data[0].show = false
} else {
data[0].show = this.show
}
this.position = data
})
.exec()
},
getButtonSize() {
const views = uni.createSelectorQuery().in(this)
views
.selectAll('.button-hock')
.boundingClientRect(data => {
this.button = data
})
.exec()
}
}
}

View File

@@ -0,0 +1,270 @@
<template>
<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">
<slot />
</view>
<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>
</view>
</view>
</view>
<!-- #endif -->
<!-- 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>
<view ref='selector-content-hock' class="uni-swipe_move-box selector-query-hock">
<view class="uni-swipe_box">
<slot />
</view>
</view>
</view>
<!-- #endif -->
<!-- 在非 app 非微信小程序支付宝小程序h5端使用 js -->
<!-- #ifndef APP-PLUS || MP-WEIXIN || MP-ALIPAY || H5 -->
<view class="uni-swipe_content">
<view ref="selector-button-hock" class="uni-swipe_button-group selector-query-hock move-hock">
<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" @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>
<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" >
<view class="uni-swipe_box">
<slot />
</view>
</view>
</view>
</view>
<!-- #endif -->
<!-- #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">
<slot></slot>
</view>
</movable-view>
</movable-area>
</view>
<view ref="selector-button-hock" class="uni-swipe_button-group viewWidth-hook">
<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" @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>
</view>
<!-- #endif -->
</view>
</template>
<script src="./index.wxs" module="swipe" lang="wxs"></script>
<script>
// #ifdef APP-VUE|| MP-WEIXIN || H5
import mpwxs from './mpwxs'
// #endif
// #ifdef APP-NVUE
import bindingx from './bindingx.js'
// #endif
// #ifndef APP-PLUS|| MP-WEIXIN || MP-ALIPAY || H5
import mixins from './mpother'
// #endif
// #ifdef MP-ALIPAY
import mpalipay from './mpalipay'
// #endif
/**
* 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关闭状态
*/
export default {
// #ifdef APP-VUE|| MP-WEIXIN||H5
mixins: [mpwxs],
// #endif
// #ifdef APP-NVUE
mixins: [bindingx],
// #endif
// #ifndef APP-PLUS|| MP-WEIXIN || MP-ALIPAY || H5
mixins: [mixins],
// #endif
// #ifdef MP-ALIPAY
mixins: [mpalipay],
// #endif
props: {
/**
* 按钮内容
*/
options: {
type: Array,
default () {
return []
}
},
/**
* 禁用
*/
disabled: {
type: Boolean,
default: false
},
/**
* 变量控制开关
*/
show: {
type: Boolean,
default: false
},
/**
* 是否自动关闭
*/
autoClose: {
type: Boolean,
default: true
}
},
inject: ['swipeaction']
}
</script>
<style lang="scss" scoped>
.uni-swipe {
overflow: hidden;
}
.uni-swipe-box {
position: relative;
width: 100%;
}
.uni-swipe_content {
flex: 1;
position: relative;
}
.uni-swipe_move-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
position: relative;
flex-direction: row;
}
.uni-swipe_box {
/* #ifndef APP-NVUE */
display: flex;
flex-direction: row;
width: 100%;
flex-shrink: 0;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
font-size: 14px;
background-color: #fff;
}
.uni-swipe_button-group {
/* #ifndef APP-VUE|| MP-WEIXIN||H5 */
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 0;
/* #endif */
/* #ifndef APP-NVUE */
display: flex;
flex-shrink: 0;
/* #endif */
flex-direction: row;
}
.uni-swipe_button {
/* #ifdef APP-NVUE */
position: absolute;
left: 0;
top: 0;
bottom: 0;
/* #endif */
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0 20px;
}
.uni-swipe_button-text {
/* #ifndef APP-NVUE */
flex-shrink: 0;
/* #endif */
font-size: 14px;
}
.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 {
width: 300px;
height: 100%;
height: 45px;
}
.movable-view {
position: relative;
width: 160%;
height: 45px;
z-index: 2;
}
.transition {
transition: all 0.3s;
}
.movable-view-box {
width: 100%;
height: 100%;
background-color: #fff;
}
/* #endif */
</style>

View File

@@ -0,0 +1,58 @@
<template>
<view>
<slot></slot>
</view>
</template>
<script>
/**
* SwipeAction 滑动操作
* @description 通过滑动触发选项的容器
* @tutorial https://ext.dcloud.net.cn/plugin?id=181
*/
export default {
data() {
return {};
},
provide() {
return {
swipeaction: this
}
},
created() {
this.children = []
},
methods: {
closeOther(vm) {
let children = this.children
children.forEach((item, index) => {
if (vm === item) return
// 支付宝执行以下操作
// #ifdef MP-ALIPAY
if (item.isopen) {
item.close()
}
// #endif
// app vue 端、h5 、微信、支付宝 执行以下操作
// #ifdef APP-VUE || H5 || MP-WEIXIN
let position = item.position[0]
let show = position.show
if (show) {
position.show = false
}
// #endif
// nvue 执行以下操作
// #ifdef APP-NVUE || MP-BAIDU || MP-QQ || MP-TOUTIAO
item.close()
// #endif
})
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,290 @@
<template>
<view
v-if="isShow"
ref="ani"
class="uni-transition"
:class="[ani.in]"
:style="'transform:' +transform+';'+stylesObject"
@click="change"
>
<slot></slot>
</view>
</template>
<script>
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation');
// #endif
/**
* 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`
*/
export default {
name: 'uniTransition',
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: Array,
default () {
return []
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default () {
return {}
}
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)'
}
},
data() {
return {
isShow: false,
transform: '',
ani: { in: '',
active: ''
}
};
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
this.close()
}
},
immediate: true
}
},
computed: {
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
}
},
created() {
// this.timer = null
// this.nextTick = (time = 50) => new Promise(resolve => {
// clearTimeout(this.timer)
// this.timer = setTimeout(resolve, time)
// return this.timer
// });
},
methods: {
change() {
this.$emit('click', {
detail: this.isShow
})
},
open() {
clearTimeout(this.timer)
this.isShow = true
this.transform = ''
this.ani.in = ''
for (let i in this.getTranfrom(false)) {
if (i === 'opacity') {
this.ani.in = 'fade-in'
} else {
this.transform += `${this.getTranfrom(false)[i]} `
}
}
this.$nextTick(() => {
setTimeout(() => {
this._animation(true)
}, 50)
})
},
close(type) {
clearTimeout(this.timer)
this._animation(false)
},
_animation(type) {
let styles = this.getTranfrom(type)
// #ifdef APP-NVUE
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.$emit('change', {
detail: this.isShow
})
})
// #endif
// #ifndef APP-NVUE
this.transform = ''
for (let i in styles) {
if (i === 'opacity') {
this.ani.in = `fade-${type?'out':'in'}`
} else {
this.transform += `${styles[i]} `
}
}
this.timer = setTimeout(() => {
if (!type) {
this.isShow = false
}
this.$emit('change', {
detail: this.isShow
})
}, this.duration)
// #endif
},
getTranfrom(type) {
let styles = {
transform: ''
}
this.modeClass.forEach((mode) => {
switch (mode) {
case 'fade':
styles.opacity = type ? 1 : 0
break;
case 'slide-top':
styles.transform += `translateY(${type?'0':'-100%'}) `
break;
case 'slide-right':
styles.transform += `translateX(${type?'0':'100%'}) `
break;
case 'slide-bottom':
styles.transform += `translateY(${type?'0':'100%'}) `
break;
case 'slide-left':
styles.transform += `translateX(${type?'0':'-100%'}) `
break;
case 'zoom-in':
styles.transform += `scale(${type?1:0.8}) `
break;
case 'zoom-out':
styles.transform += `scale(${type?1:1.2}) `
break;
}
})
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)
} else {
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();
}
}
}
</script>
<style>
.uni-transition {
transition-timing-function: ease;
transition-duration: 0.3s;
transition-property: transform, opacity;
}
.fade-in {
opacity: 0;
}
.fade-active {
opacity: 1;
}
.slide-top-in {
/* transition-property: transform, opacity; */
transform: translateY(-100%);
}
.slide-top-active {
transform: translateY(0);
/* opacity: 1; */
}
.slide-right-in {
transform: translateX(100%);
}
.slide-right-active {
transform: translateX(0);
}
.slide-bottom-in {
transform: translateY(100%);
}
.slide-bottom-active {
transform: translateY(0);
}
.slide-left-in {
transform: translateX(-100%);
}
.slide-left-active {
transform: translateX(0);
opacity: 1;
}
.zoom-in-in {
transform: scale(0.8);
}
.zoom-out-active {
transform: scale(1);
}
.zoom-out-in {
transform: scale(1.2);
}
</style>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
<!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>

50
yudao-ui-app-v1/main.js Normal file
View File

@@ -0,0 +1,50 @@
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 = {
msg,
isLogin,
debounce,
throttle,
prePage,
date
}
// 全局 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()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif

View File

@@ -0,0 +1,72 @@
{
"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" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "2"
}

View File

@@ -0,0 +1,60 @@
{
"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",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
}, {
"path": "pages/auth/login",
"style": {
"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"
}]
}
}

View File

@@ -0,0 +1,326 @@
<template>
<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>
<!-- 登录表单 -->
<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>
<u-form-item prop="password" borderBottom v-else>
<u--input password v-model="form.password" placeholder="请输入密码" border="none"></u--input>
</u-form-item>
</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>
</view>
<!-- 快捷登录 -->
<!-- #ifdef APP-PLUS || MP-WEIXIN -->
<view class="other-wrapper">
<view class="line center">
<text class="title">快捷登录</text>
</view>
<view class="list row">
<!-- #ifdef MP-WEIXIN -->
<view class="item column center" @click="mpWxGetUserInfo">
<image class="icon" src="/static/icon/login-wx.png"></image>
</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view class="item column center" style="width: 180rpx;" @click="loginByWxApp">
<image class="icon" src="/static/icon/login-wx.png"></image>
<text>微信登录</text>
</view>
<!-- #endif -->
</view>
</view>
<!-- #endif -->
</view>
</view>
</template>
<script>
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],
data() {
return {
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,
}
}
},
onLoad() {
this.setLoginType(this.loginType);
},
methods: {
// 手机号登录
mobileLogin() {
if (!this.agreement) {
this.$util.msg('请阅读并同意用户服务及隐私协议');
return;
}
this.$refs.form.validate().then(() => {
this.loading = true;
// 执行登陆
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(() => {
this.loading = false;
})
}).catch(errors => {
});
},
// 登陆成功的处理逻辑
loginSuccessCallBack(data) {
this.$util.msg('登录成功');
this.$store.commit('setToken', data);
// TODO 芋艿:如果当前页是第一页,则无法返回。期望是能够回到首页
setTimeout(()=>{
uni.navigateBack();
}, 1000)
},
navBack() {
uni.navigateBack({
delta: 1
});
},
setLoginType(loginType) {
this.loginType = loginType;
// 修改校验规则
this.rules.code = [];
this.rules.password = [];
if (loginType == 'code') {
this.rules.code = [{
required: true,
message: '请输入验证码'
}, {
min: 4,
max: 6,
message: '验证码不正确'
}];
} else {
this.rules.password = [{
required: true,
message: '请输入密码'
}, {
min: 4,
max: 16,
message: '密码不正确'
}]
}
},
//同意协议
checkAgreement(){
this.agreement = !this.agreement;
},
//打开协议
navToAgreementDetail(type){
this.navTo('/pages/public/article?param=' + JSON.stringify({
module: 'article',
operation: 'getAgreement',
data: {
type
}
}))
},
codeChange(text) {
this.uCode.tips = text;
},
getCode() {
// 处于发送中,则跳过
if (!this.$refs.uCode.canGetCode) {
return;
}
// 校验手机号
this.$refs.form.validateField('mobile', errors => {
if (errors.length > 0) {
uni.$u.toast(errors[0].message);
return;
}
// 发送验证码 TODO 芋艿,枚举类
sendSmsCode(this.form.mobile, 1).then(data => {
uni.$u.toast('验证码已发送');
// 通知验证码组件内部开始倒计时
this.$refs.uCode.start();
}).catch(erros => {
})
})
},
}
}
</script>
<style>
page {
background: #fff;
}
</style>
<style scoped lang='scss'>
.app {
padding-top: 15vh;
position:relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #fff;
}
.wrapper {
position:relative;
z-index: 90;
padding-bottom: 40rpx;
.welcome {
position:relative;
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);
z-index: 90;
padding: 20rpx;
font-size: 32rpx;
color: #606266;
}
.left-top-sign {
font-size: 120rpx;
color: #f8f8f8;
position: relative;
left: -12rpx;
}
/** 手机登录部分 */
.input-content {
padding: 0 60rpx;
.login-button {
margin-top: 30rpx;
}
.login-type {
display: flex;
justify-content: flex-end;
font-size: 13px;
color: #40a2ff;
margin-top: 20rpx;
}
}
/* 其他登录方式 */
.other-wrapper{
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20rpx;
margin-top: 80rpx;
.line{
margin-bottom: 40rpx;
.title {
margin: 0 32rpx;
font-size: 24rpx;
color: #606266;
}
&:before, &:after{
content: '';
width: 160rpx;
height: 0;
border-top: 1px solid #e0e0e0;
}
}
.item{
font-size: 24rpx;
color: #606266;
background-color: #fff;
border: 0;
&:after{
border: 0;
}
}
.icon{
width: 90rpx;
height: 90rpx;
margin: 0 24rpx 16rpx;
}
}
.agreement{
position: absolute;
left: 0;
bottom: 6vh;
z-index: 1;
width: 750rpx;
height: 90rpx;
font-size: 24rpx;
color: #999;
.mix-icon{
font-size: 36rpx;
color: #ccc;
margin-right: 8rpx;
margin-top: 1px;
&.active{
color: $base-color;
}
}
.title {
color: #40a2ff;
}
}
</style>

View File

@@ -0,0 +1,73 @@
export default{
// #ifdef APP-PLUS
methods: {
/**
* 微信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.msg('请阅读并同意用户服务及隐私协议');
return;
}
this.$util.throttle(async ()=>{
let [err, res] = await uni.login({
provider: 'weixin'
})
if(err){
console.log(err);
return;
}
uni.getUserInfo({
provider: 'weixin',
success: async res=>{
const response = await this.$request('user', 'loginByWeixin', {
userInfo: res.userInfo,
}, {
showLoading: true
});
if(response.status === 0){
this.$util.msg(response.msg);
return;
}
if(response.hasBindMobile && response.data.token){
this.loginSuccessCallBack({
token: response.data.token,
tokenExpired: response.data.tokenExpired
});
}else{
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) {
console.log(err);
}
})
})
}
}
// #endif
}

View File

@@ -0,0 +1,81 @@
export default{
// #ifdef MP-WEIXIN
data(){
return {
mpCodeTimer: 0,
mpWxCode: '',
}
},
computed: {
timerIdent(){
return this.$store.state.timerIdent;
}
},
watch: {
timerIdent(){
this.mpCodeTimer ++;
if(this.mpCodeTimer % 30 === 0){
this.getMpWxCode();
}
}
},
onShow(){
this.getMpWxCode();
},
methods: {
//微信小程序登录
mpWxGetUserInfo(){
if(!this.agreement){
this.$util.msg('请阅读并同意用户服务及隐私协议');
return;
}
this.$util.throttle(()=>{
uni.getUserProfile({
desc: '用于展示您的头像及昵称',
success: async profileRes=> {
const res = await this.$request('user', 'loginByWeixin', {
code: this.mpWxCode,
...profileRes.userInfo
}, {
showLoading: true
});
if(res.status === 0){
this.$util.msg(res.msg);
return;
}
if(res.hasBindMobile && res.data.token){
this.loginSuccessCallBack({
token: res.data.token,
tokenExpired: res.data.tokenExpired
});
}else{
this.navTo('/pages/auth/bindMobile?data='+JSON.stringify(res.data))
}
console.log(res)
}
})
})
},
//获取code
getMpWxCode(){
uni.login({
provider: 'weixin',
success: res=> {
this.mpWxCode = res.code;
}
})
},
}
// #endif
}

View File

@@ -0,0 +1,52 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{title}}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello'
}
},
onLoad() {
},
methods: {
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

View File

@@ -0,0 +1,633 @@
(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 {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
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);
} else {
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,
get: function get$$1() {
return tmp.width;
},
set: function set$$1(value) {
tmp.width = value;
}
},
height: {
default: 750,
get: function get$$1() {
return tmp.height;
},
set: function set$$1(value) {
tmp.height = value;
}
},
scale: {
default: 2.5,
get: function get$$1() {
return tmp.scale;
},
set: function set$$1(value) {
tmp.scale = value;
}
},
zoom: {
default: 5,
get: function get$$1() {
return tmp.zoom;
},
set: function set$$1(value) {
tmp.zoom = value;
}
},
src: {
default: 'cropper',
get: function get$$1() {
return tmp.src;
},
set: function set$$1(value) {
tmp.src = value;
}
},
cut: {
default: {},
get: function get$$1() {
return tmp.cut;
},
set: function set$$1(value) {
tmp.cut = value;
}
},
onReady: {
default: null,
get: function get$$1() {
return tmp.ready;
},
set: function set$$1(value) {
tmp.ready = value;
}
},
onBeforeImageLoad: {
default: null,
get: function get$$1() {
return tmp.beforeImageLoad;
},
set: function set$$1(value) {
tmp.beforeImageLoad = value;
}
},
onImageLoad: {
default: null,
get: function get$$1() {
return tmp.imageLoad;
},
set: function set$$1(value) {
tmp.imageLoad = value;
}
},
onBeforeDraw: {
default: null,
get: function get$$1() {
return tmp.beforeDraw;
},
set: function set$$1(value) {
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 self = this;
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 self = this;
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();
return self;
};
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);
} else {
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();
return self;
};
self.getCropperImage = function() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var id = self.id;
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),
success: function success(res) {
typeof args[args.length - 1] === 'function' && args[args.length - 1](res.tempFilePath);
}
});
break;
case '[object Function]':
uni.canvasToTempFilePath({
canvasId: id,
x: x,
y: y,
fileType: "jpg",
width: width,
height: height,
destWidth: width,
destHeight: height,
success: function success(res) {
typeof args[args.length - 1] === 'function' && args[args.length - 1](res.tempFilePath);
}
});
break;
}
return self;
};
}
function update() {
var self = this;
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.updateCanvas();
};
self.__twoTouchStart = function(touch0, touch1) {
var xMove = void 0,
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) {
var xMove = void 0,
yMove = void 0,
newDistance = void 0;
var scale = self.scale,
zoom = self.zoom;
xMove = touch1.x - touch0.x;
yMove = touch1.y - touch0.y;
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.outsideBound(imgLeft, imgTop);
self.updateCanvas();
};
self.__xtouchEnd = function() {
self.oldScale = self.newScale;
self.rectX = self.imgLeft;
self.rectY = self.imgTop;
};
}
var handle = {
touchStart: function touchStart(e) {
var self = this;
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 self = this;
var _e$touches2 = slicedToArray(e.touches, 2),
touch0 = _e$touches2[0],
touch1 = _e$touches2[1];
if (!touch0.x) {
touch0.x = touch0.clientX;
touch0.y = touch0.clientY;
if (touch1) {
touch1.x = touch1.clientX;
touch1.y = touch1.clientY;
}
}
setTouchState(self, null, true);
if (e.touches.length === 1) {
self.__oneTouchMove(touch0);
}
if (e.touches.length >= 2) {
self.__twoTouchMove(touch0, touch1);
}
},
touchEnd: function touchEnd(e) {
var self = this;
setTouchState(self, false, false, true);
self.__xtouchEnd();
}
};
function cut() {
var self = this;
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.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.beginPath();
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.beginPath();
self.ctx.setStrokeStyle(color);
self.ctx.setLineWidth(lineWidth);
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.stroke();
self.ctx.beginPath();
self.ctx.setStrokeStyle(color);
self.ctx.setLineWidth(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.stroke();
self.ctx.beginPath();
self.ctx.setStrokeStyle(color);
self.ctx.setLineWidth(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);
self.ctx.stroke();
};
}
var __version__ = '1.1.4';
var weCropper = function() {
function weCropper(params) {
classCallCheck(this, weCropper);
var self = this;
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();
self.update();
return self;
}
createClass(weCropper, [{
key: 'init',
value: function init() {
var self = this;
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 self;
}
}]);
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;
return weCropper;
})));

View File

@@ -0,0 +1,223 @@
<template>
<view class="content">
<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>
<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>
<view class="cropper-buttons">
<view class="btn upload" @tap="uploadTap">重选</view>
<view class="btn getCropperImage" @tap="getCropperImage">确定</view>
</view>
</view>
</template>
<script>
import weCropper from './cut.js';
const device = uni.getSystemInfoSync();
const width = device.windowWidth;
const height = device.windowHeight;
export default {
data() {
return {
cropperOpt: {
id: 'cropper',
width: width,
height: height,
scale: 2.5,
zoom: 8,
cut: {
x: (width - 200) / 2,
y: (height - this.systemInfo.navigationBarHeight - this.systemInfo.statusBarHeight - 200) / 2,
width: 200,
height: 200
}
},
weCropper: ''
};
},
computed: {
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);
if (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();
});
}
},
methods: {
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);
uni.navigateBack();
} else {
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);
}
});
}
},
};
</script>
<style lang="scss">
page, .content{
width: 100%;
height: 100%;
overflow: hidden;
}
.content {
display: flex;
flex-direction: column;
background-color: #000;
padding-top: var(--status-bar-height);
}
.title-view{
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: transparent;
.back-btn{
display: flex;
justify-content: center;
align-items: center;
width: 42px;
height: 40px;
font-size: 18px;
color: #fff;
}
.title{
font-size: 17px;
color: #fff;
}
.empty{
width: 42px;
}
}
.cropper {
width: 100%;
flex: 1;
}
.cropper-wrapper {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: #000;
}
.cropper-buttons {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 50px;
background-color: rgba(0, 0, 0, 0.4);
.btn{
width: 100px;
height: 50px;
line-height: 50px;
font-size: 15px;
color: #fff;
&.upload{
padding-left: 20px;
}
&.getCropperImage{
padding-right: 20px;
text-align: right;
}
}
}
</style>

View File

@@ -0,0 +1,271 @@
<template>
<view class="app">
<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"
:class="{
'no-transtion': uploadProgress === 0,
show: uploadProgress != 100
}"
:style="{
width: uploadProgress + '%',
height: uploadProgress + '%',
}"></view>
</view>
</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">
</view>
<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-form-item>
</u--form>
</view>
</u-modal>
</view>
</template>
<script>
export default {
data() {
return {
uploadProgress: 100, //头像上传进度
tempAvatar: '',
userInfo: {},
nicknameOpen: false,
nicknameForm: {
nickname: ''
},
nicknameRules: {
nickname: [{
required: true,
message: '请输入昵称'
}]
}
}
},
computed: {
curUserInfo(){
return this.$store.state.userInfo
}
},
watch: {
curUserInfo(curUserInfo){
const {avatar, nickname, gender} = curUserInfo;
this.userInfo = {avatar, nickname, gender,};
}
},
onLoad() {
const {avatar, nickname, gender, anonymous} = this.curUserInfo;
this.userInfo = {avatar, nickname, gender};
},
methods: {
nicknameClick() {
this.nicknameOpen = true;
this.nicknameForm.nickname = this.userInfo.nickname;
},
nicknameCancel() {
this.nicknameOpen = false;
},
nicknameSubmit() {
this.$refs.nicknameForm.validate().then(() => {
this.loading = true;
// 执行登陆
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(() => {
this.loading = false;
})
}).catch(errors => {
});
},
// 提交修改
async confirm() {
// 校验信息是否变化
const {uploadProgress, userInfo, curUserInfo} = this;
let isUpdate = false;
for (let key in userInfo) {
if(userInfo[key] !== curUserInfo[key]){
isUpdate = true;
break;
}
}
if (isUpdate === false) {
this.$util.msg('信息未修改');
this.$refs.confirmBtn.stop();
return;
}
if (!userInfo.avatar) {
this.$util.msg('请上传头像');
this.$refs.confirmBtn.stop();
return;
}
if (uploadProgress !== 100) {
this.$util.msg('请等待头像上传完毕');
this.$refs.confirmBtn.stop();
return;
}
if (!userInfo.nickname) {
this.$util.msg('请输入您的昵称');
this.$refs.confirmBtn.stop();
return;
}
const res = await this.$request('user', 'update', userInfo);
this.$refs.confirmBtn.stop();
this.$util.msg(res.msg);
if(res.status === 1){
this.$store.dispatch('getUserInfo'); //刷新用户信息
setTimeout(()=>{
uni.navigateBack();
}, 1000)
}
},
// 选择头像
chooseImage(){
uni.chooseImage({
count: 1,
success: res=> {
uni.navigateTo({
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('头像上传失败');
return;
}
if(typeof uniCloud.getTempFileURL === 'undefined'){
this.userInfo.avatar = result.fileID;
}else{
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;
}else{
this.$util.msg('头像上传失败');
}
}
}
}
}
</script>
<style scoped lang="scss">
.app{
padding-top: 16rpx;
}
.cell{
display: flex;
align-items: center;
min-height: 110rpx;
padding: 0 40rpx;
&:first-child{
margin-bottom: 10rpx;
}
&:after{
left: 40rpx;
right: 40rpx;
border-color: #d8d8d8;
}
.tit{
font-size: 30rpx;
color: #333;
}
.avatar-wrap{
width: 120rpx;
height: 120rpx;
position: relative;
border-radius: 100rpx;
overflow: hidden;
.avatar{
width: 100%;
height: 100%;
border-radius: 100rpx;
}
.progress{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100rpx;
height: 100rpx;
box-shadow: rgba(0,0,0,.6) 0px 0px 0px 2005px;
border-radius: 100rpx;
transition: .5s;
opacity: 0;
&.no-transtion{
transition: 0s;
}
&.show{
opacity: 1;
}
}
}
.input{
flex: 1;
text-align: right;
font-size: 28rpx;
color: #333;
}
switch{
margin: 0;
transform: scale(0.8) translateX(10rpx);
transform-origin: center right;
}
.tip{
margin-left: 20rpx;
font-size: 28rpx;
color: #999;
}
.checkbox{
padding: 12rpx 0 12rpx 40rpx;
font-size: 28rpx;
color: #333;
.mix-icon{
margin-right: 12rpx;
font-size: 36rpx;
color: #ccc;
}
.icon-xuanzhong{
color: $base-color;
}
}
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<view class="app">
<!-- 个人信息 -->
<view class="user-wrapper">
<image class="user-background" src="/static/backgroud/user.jpg"></image>
<view class="user">
<!-- 头像 -->
<image class="avatar" :src="userInfo.avatar || '/static/icon/default-avatar.png'" @click="navTo('/pages/set/userInfo', {login: true})"></image>
<!-- 已登陆展示昵称 -->
<view class="cen column" v-if="hasLogin">
<text class="username f-m">{{ userInfo.nickname }}</text>
<text class="group">普通会员</text>
</view>
<!-- 未登陆引导登陆 -->
<view class="login-box" v-else @click="navTo('/pages/auth/login')">
<text>点击注册/登录</text>
</view>
</view>
<!-- 下面的圆弧 -->
<image class="user-background-arc-line" src="/static/icon/arc.png" mode="aspectFill"></image>
</view>
<!-- 订单信息 -->
<view class="order-wrap">
<view class="order-header row" @click="navTo('/pages/order/list?current=0', {login: true})">
<text class="title">我的订单</text>
<text class="more">查看全部</text>
<text class="mix-icon icon-you"></text>
</view>
<view class="order-list">
<view class="item center" @click="navTo('/pages/order/list?current=1', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
<text class="mix-icon icon-daifukuan"></text>
<text>待付款</text>
<text v-if="orderCount.c0 > 0" class="number">{{ orderCount.c0 }}</text>
</view>
<view class="item center" @click="navTo('/pages/order/list?current=2', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
<text class="mix-icon icon-daifahuo"></text>
<text>待发货</text>
<text v-if="orderCount.c1 > 0" class="number">{{ orderCount.c1 }}</text>
</view>
<view class="item center" @click="navTo('/pages/order/list?current=3', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
<text class="mix-icon icon-yishouhuo"></text>
<text>待收货</text>
<text v-if="orderCount.c2 > 0" class="number">{{ orderCount.c2 }}</text>
</view>
<view class="item center" @click="navTo('/pages/order/list?current=4', {login: true})" hover-class="hover-gray" :hover-stay-time="50">
<text class="mix-icon icon-daipingjia"></text>
<text>待评价</text>
<text v-if="orderCount.c3 > 0" class="number">{{ orderCount.c3 }}</text>
</view>
</view>
</view>
<!-- 功能入口 -->
<u-cell-group class="option1-wrap">
<u-cell icon="edit-pen" title="个人信息" isLink @click="navTo('/pages/set/userInfo', {login: true})"></u-cell>
<u-cell icon="setting" title="账号安全" isLink @click="navTo('/pages/set/set', {login: true})"></u-cell>
</u-cell-group>
</view>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import { isLogin } from '@/common/js/util.js'
export default {
data() {
return {
orderCount: { // TODO 芋艿:读取
c0: 1,
c1: 2,
c2: 3,
c3: 4
}
};
},
computed: {
...mapState(['userInfo']),
...mapGetters(['hasLogin']),
},
onShow() {
// 获得用户信息 TODO 芋艿:
// if (isLogin()) {
// this.$store.dispatch('obtainUserInfo');
// }
// TODO 芋艿:获得订单数量
}
}
</script>
<style lang="scss">
.app {
padding-bottom: 20rpx;
}
.user-wrapper {
position: relative;
overflow: hidden;
padding-top: calc(var(--status-bar-height) + 52rpx);
padding-bottom: 6rpx;
.user {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
position: relative;
z-index: 5;
padding: 20rpx 30rpx 60rpx;
.avatar {
flex-shrink: 0;
width: 130rpx;
height: 130rpx;
border-radius: 100px;
margin-right: 24rpx;
border: 4rpx solid #fff;
background-color: #fff;
}
.username {
font-size: 34rpx;
color: #fff;
}
.group {
align-self: flex-start;
padding: 10rpx 14rpx;
margin: 16rpx 10rpx; // 10rpx 避免距离昵称太近
font-size: 20rpx;
color: #fff;
background-color: rgba(255, 255, 255,.3);
border-radius: 100rpx;
}
.login-box {
font-size: 36rpx;
color: #fff;
}
}
.user-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 330rpx;
}
.user-background-arc-line {
position: absolute;
left: 0;
bottom: 0;
z-index: 9;
width: 100%;
height: 32rpx;
}
}
.order-wrap {
width: 700rpx;
margin: 20rpx auto 0;
background: #fff;
border-radius: 10rpx;
.order-header {
padding: 28rpx 20rpx 6rpx 26rpx;
.title {
flex: 1;
font-size: 32rpx;
color: #333;
font-weight: 700;
}
.more {
font-size: 24rpx;
color: #999;
}
.icon-you {
margin-left: 4rpx;
font-size: 20rpx;
color: #999;
}
}
.order-list {
display:flex;
justify-content: space-around;
padding: 20rpx 0;
.item{
flex-direction: column;
width: 130rpx;
height: 130rpx;
border-radius: 8rpx;
font-size: 24rpx;
color: #606266;
position: relative;
.mix-icon {
font-size: 50rpx;
margin-bottom: 20rpx;
color: #fa436a;
}
.icon-shouhoutuikuan {
font-size: 44rpx;
}
.number {
position: absolute;
right: 22rpx;
top: 6rpx;
min-width: 34rpx;
height: 34rpx;
line-height: 30rpx;
text-align: center;
padding: 0 8rpx;
font-size: 18rpx;
color: #fff;
border: 2rpx solid #fff;
background-color: $base-color;
border-radius: 100rpx;
}
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,65 @@
import Vue from 'vue'
import Vuex from 'vuex'
// import {request} from '@/common/js/request'
import { getUserInfo } from '@/api/member/userProfile.js'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
openExamine: false, // 是否开启审核状态。用于小程序、App 等审核时关闭部分功能。TODO 芋艿:暂时没找到刷新的地方
token: '', // 用户身份 Token
userInfo: {}, // 用户基本信息
timerIdent: false, // 全局 1s 定时器,只在全局开启一个,所有需要定时执行的任务监听该值即可,无需额外开启 TODO 芋艿:需要看看
},
getters: {
hasLogin(state){
return !!state.token;
}
},
mutations: {
// 更新 state 的通用方法
setStateAttr(state, param) {
if (param instanceof Array) {
for(let item of param){
state[item.key] = item.val;
}
} else {
state[param.key] = param.val;
}
},
// 更新token
setToken(state, data) {
// 设置 Token
const { token } = data;
state.token = token;
uni.setStorageSync('token', token);
// 加载用户信息
this.dispatch('obtainUserInfo');
},
// 退出登录
logout(state) {
// 清空 Token
state.token = '';
uni.removeStorageSync('token');
// 清空用户信息 TODO 芋艿:这里 setTimeout 的原因是,上面可能还有一些 request 请求。后续得优化下
setTimeout(()=>{
state.userInfo = {};
}, 1100)
},
},
actions: {
// 获得用户基本信息
async obtainUserInfo({state, commit}) {
const data = await getUserInfo();
commit('setStateAttr', {
key: 'userInfo',
val: data
});
}
}
})
export default store

13
yudao-ui-app-v1/uni.scss Normal file
View File

@@ -0,0 +1,13 @@
@import '@/uni_modules/uview-ui/theme.scss';
.u-button--success {
background-color: #5ac725 !important; // TODO 芋艿:莫名不行
}
.u-button--primary {
background-color: #3c9cff !important; // TODO 芋艿:莫名不行
}
.u-button--error {
background-color: #f56c6c !important; // TODO 芋艿:莫名不行
}
$base-color: #ff536f;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 www.uviewui.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,105 @@
<p align="center">
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
</p>
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
<h3 align="center">多平台快速开发的UI框架</h3>
## 说明
uView UI是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架全面的组件和便捷的工具会让您信手拈来如鱼得水
## 特性
- 兼容安卓iOS微信小程序H5QQ小程序百度小程序支付宝小程序头条小程序
- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
- 众多贴心的JS利器让您飞镖在手召之即来百步穿杨
- 众多的常用页面和布局,让您专注逻辑,事半功倍
- 详尽的文档支持,现代化的演示效果
- 按需引入,精简打包体积
## 安装
```bash
# npm方式安装
npm i uview-ui
```
## 快速上手
1. `main.js`引入uView库
```js
// main.js
import uView from 'uview-ui';
Vue.use(uView);
```
2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
```css
/* App.vue */
<style lang="scss">
@import "uview-ui/index.scss";
</style>
```
3. `uni.scss`引入全局scss变量文件
```css
/* uni.scss */
@import "uview-ui/theme.scss";
```
4. `pages.json`配置easycom规则(按需引入)
```js
// pages.json
{
"easycom": {
// npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
// npm安装方式
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
// 下载安装方式
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
},
// 此为本身已有的内容
"pages": [
// ......
]
}
```
请通过[快速上手](https://www.uviewui.com/components/quickstart.html)了解更详细的内容
## 使用方法
配置easycom规则后自动按需引入无需`import`组件,直接引用即可。
```html
<template>
<u-button text="按钮"></u-button>
</template>
```
请通过[快速上手](https://www.uviewui.com/components/quickstart.html)了解更详细的内容
## 链接
- [官方文档](https://www.uviewui.com/)
- [更新日志](https://www.www.uviewui.com/components/changelog.html)
- [升级指南](https://www.uviewui.com/components/changelog.html)
- [关于我们](https://www.uviewui.com/cooperation/about.html)
## 预览
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
## 捐赠uView的研发
uView文档和源码全部开源免费如果您认为uView帮到了您的开发工作您可以捐赠uView的研发工作捐赠无门槛哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
<img src="https://uviewui.com/common/alipay.png" width="220" ><img style="margin-left: 100px;" src="https://uviewui.com/common/wechat.png" width="220" >
## 版权信息
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议意味着您无需支付任何费用也无需授权即可将uView应用到您的产品中。

View File

@@ -0,0 +1,55 @@
## 2.0.52021-11-25
## [点击加群交流反馈232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
# uView2.0重磅发布,利剑出鞘,一统江湖
1. calendar在vue下显示异常问题。
2. form组件labelPosition和errorType参数无效的问题
3. input组件inputAlign无效的问题
4. 其他一些修复
## 2.0.42021-11-23
## [点击加群交流反馈232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
# uView2.0重磅发布,利剑出鞘,一统江湖
0. input组件缺失@confirm事件以及subfix和prefix无效问题
1. component.scss文件样式在vue下干扰全局布局问题
2. 修复subsection在vue环境下表现异常的问题
3. tag组件的bgColor等参数无效的问题
4. upload组件不换行的问题
5. 其他的一些修复处理
## 2.0.32021-11-16
## [点击加群交流反馈1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
# uView2.0重磅发布,利剑出鞘,一统江湖
1. uView2.0已实现全面兼容nvue
2. uView2.0对1.x进行了架构重构细节和性能都有极大提升
3. 目前uView2.0为公测阶段,相关细节可能会有变动
4. 我们写了一份与1.x的对比指南详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
5. 处理modal的confirm回调事件拼写错误问题
6. 处理input组件@input事件参数错误问题
7. 其他一些修复
## 2.0.22021-11-16
## [点击加群交流反馈1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
# uView2.0重磅发布,利剑出鞘,一统江湖
1. uView2.0已实现全面兼容nvue
2. uView2.0对1.x进行了架构重构细节和性能都有极大提升
3. 目前uView2.0为公测阶段,相关细节可能会有变动
4. 我们写了一份与1.x的对比指南详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
5. 修复input组件formatter参数缺失问题
6. 优化loading-icon组件的scss写法问题防止不兼容新版本scss
## 2.0.0(2020-11-15)
## [点击加群交流反馈1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
# uView2.0重磅发布,利剑出鞘,一统江湖
1. uView2.0已实现全面兼容nvue
2. uView2.0对1.x进行了架构重构细节和性能都有极大提升
3. 目前uView2.0为公测阶段,相关细节可能会有变动
4. 我们写了一份与1.x的对比指南详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
5. 修复input组件formatter参数缺失问题

Some files were not shown because too many files have changed in this diff Show More