parent
6d951b6824
commit
959778d699
640 changed files with 98802 additions and 0 deletions
@ -0,0 +1,21 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
onLaunch: function() { |
||||||
|
console.log('App Launch') |
||||||
|
}, |
||||||
|
onShow: function() { |
||||||
|
console.log('App Show') |
||||||
|
}, |
||||||
|
onHide: function() { |
||||||
|
console.log('App Hide') |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "colorui/main.css"; |
||||||
|
@import "colorui/icon.css"; |
||||||
|
|
||||||
|
/*每个页面公共css */ |
||||||
|
@import "static/css/main.css"; |
||||||
|
</style> |
@ -0,0 +1,157 @@ |
|||||||
|
export default [{ |
||||||
|
"id": "3", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd3.jpg", |
||||||
|
"goodName": "【3】 美素佳儿Friso婴儿配方奶粉3段 ( 商品【1】【2】 已删除 )", |
||||||
|
"goodPrice": 195.00, |
||||||
|
"goodSold": 968 |
||||||
|
}, { |
||||||
|
"id": "4", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd4.jpg", |
||||||
|
"goodName": "【4】 Fisher goodPrice费雪 费雪三轮儿童滑行车", |
||||||
|
"goodPrice": 298.00, |
||||||
|
"goodSold": 65 |
||||||
|
}, { |
||||||
|
"id": "5", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd5.jpg", |
||||||
|
"goodName": "【5】 Babylee巴布力 实木婴儿床 雷卡拉130*70cm", |
||||||
|
"goodPrice": 1789.00, |
||||||
|
"goodSold": 20 |
||||||
|
}, { |
||||||
|
"id": "6", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd6.jpg", |
||||||
|
"goodName": "【6】 Pigeon贝亲 独立三层奶粉盒 送小罐奶粉1段200g", |
||||||
|
"goodPrice": 70.00, |
||||||
|
"goodSold": 658 |
||||||
|
}, { |
||||||
|
"id": "7", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd7.jpg", |
||||||
|
"goodName": "【7】 TTBOO兔兔小布 肩纽扣套装", |
||||||
|
"goodPrice": 268.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "8", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd8.jpg", |
||||||
|
"goodName": "【8】 Nuna璐拉 婴儿布里奇果精纯嫩肤沐浴露婴儿精纯芦荟胶", |
||||||
|
"goodPrice": 140.00, |
||||||
|
"goodSold": 366 |
||||||
|
}, { |
||||||
|
"id": "9", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd9.jpg", |
||||||
|
"goodName": "【9】 illuma启赋 奶粉3段900g", |
||||||
|
"goodPrice": 252.00, |
||||||
|
"goodSold": 98 |
||||||
|
}, { |
||||||
|
"id": "10", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd10.jpg", |
||||||
|
"goodName": "【10】 Abbott雅培乳蛋白部分水解婴儿配方奶粉3段820g", |
||||||
|
"goodPrice": 89.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "11", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd11.jpg", |
||||||
|
"goodName": "【11】 韩蜜 酷炫唇蜜(礼盒套装)2.8g*4", |
||||||
|
"goodPrice": 179.00, |
||||||
|
"goodSold": 35 |
||||||
|
}, { |
||||||
|
"id": "12", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd12.jpg", |
||||||
|
"goodName": "【12】 保税区直发【3包装】日本Merries花王纸尿裤NB90", |
||||||
|
"goodPrice": 289.00, |
||||||
|
"goodSold": 1928 |
||||||
|
}, { |
||||||
|
"id": "13", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd13.jpg", |
||||||
|
"goodName": "【13】 Comotomo可么多么 硅胶奶瓶(0-3月奶嘴)150ml绿色", |
||||||
|
"goodPrice": 203.00, |
||||||
|
"goodSold": 87 |
||||||
|
}, { |
||||||
|
"id": "14", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd14.jpg", |
||||||
|
"goodName": "【14】 香港直邮德国瑞德露Rival de Loop芦荟精华安瓶", |
||||||
|
"goodPrice": 152.00, |
||||||
|
"goodSold": 61 |
||||||
|
}, { |
||||||
|
"id": "15", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd15.jpg", |
||||||
|
"goodName": "【15】 保税区直发药师堂尊马油香草味温和保湿无刺激面霜", |
||||||
|
"goodPrice": 269.00, |
||||||
|
"goodSold": 73 |
||||||
|
}, { |
||||||
|
"id": "16", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd16.jpg", |
||||||
|
"goodName": "【16】 香港直邮日本Spatreatment眼膜保湿去细纹法令纹", |
||||||
|
"goodPrice": 219.00, |
||||||
|
"goodSold": 13 |
||||||
|
}, { |
||||||
|
"id": "17", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd17.jpg", |
||||||
|
"goodName": "【17】 韩国MEDIHEALNMF可莱丝针剂睡眠面膜", |
||||||
|
"goodPrice": 81.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "18", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd18.jpg", |
||||||
|
"goodName": "【18】 DHC蝶翠诗橄榄蜂蜜滋养洗脸手工皂90g", |
||||||
|
"goodPrice": 123.00, |
||||||
|
"goodSold": 77 |
||||||
|
}, { |
||||||
|
"id": "19", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd19.jpg", |
||||||
|
"goodName": "【19】 日本资生堂CPB肌肤之钥新版隔离霜 清爽型 30ml", |
||||||
|
"goodPrice": 429.00, |
||||||
|
"goodSold": 36 |
||||||
|
}, { |
||||||
|
"id": "20", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd20.jpg", |
||||||
|
"goodName": "【20】 Heinz亨氏 婴儿面条优加面条全素套餐组合3口味3盒", |
||||||
|
"goodPrice": 39.00, |
||||||
|
"goodSold": 61 |
||||||
|
}, { |
||||||
|
"id": "21", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd21.jpg", |
||||||
|
"goodName": "【21】 Heinz亨氏 乐维滋果汁泥组合5口味15袋", |
||||||
|
"goodPrice": 69.00, |
||||||
|
"goodSold": 55 |
||||||
|
}, { |
||||||
|
"id": "22", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd22.jpg", |
||||||
|
"goodName": "【22】 保税区直发澳大利亚Swisse高浓度蔓越莓胶囊30粒", |
||||||
|
"goodPrice": 271.00, |
||||||
|
"goodSold": 19 |
||||||
|
}, { |
||||||
|
"id": "23", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd23.jpg", |
||||||
|
"goodName": "【23】 挪威Nordic Naturals小鱼婴幼儿鱼油DHA滴剂", |
||||||
|
"goodPrice": 102.00, |
||||||
|
"goodSold": 125 |
||||||
|
}, { |
||||||
|
"id": "24", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd24.jpg", |
||||||
|
"goodName": "【24】 澳大利亚Bio island DHA for Pregnancy海藻油DHA", |
||||||
|
"goodPrice": 289.00, |
||||||
|
"goodSold": 28 |
||||||
|
}, { |
||||||
|
"id": "25", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd25.jpg", |
||||||
|
"goodName": "【25】 澳大利亚Fatblaster Coconut Detox椰子水", |
||||||
|
"goodPrice": 152.00, |
||||||
|
"goodSold": 17 |
||||||
|
}, { |
||||||
|
"id": "26", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd26.jpg", |
||||||
|
"goodName": "【26】 Suitsky舒比奇 高护极薄舒爽纸尿片尿不湿XL60", |
||||||
|
"goodPrice": 99.00, |
||||||
|
"goodSold": 181 |
||||||
|
}, { |
||||||
|
"id": "27", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd27.jpg", |
||||||
|
"goodName": "【27】 英国JUST SOAP手工皂 玫瑰天竺葵蛋糕皂", |
||||||
|
"goodPrice": 72.00, |
||||||
|
"goodSold": 66 |
||||||
|
}, { |
||||||
|
"id": "28", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd28.jpg", |
||||||
|
"goodName": "【28】 德国NUK 多色婴幼儿带盖学饮杯", |
||||||
|
"goodPrice": 92.00, |
||||||
|
"goodSold": 138 |
||||||
|
}] |
@ -0,0 +1,169 @@ |
|||||||
|
export default [{ |
||||||
|
"id": "1", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd1.jpg", |
||||||
|
"goodName": "【1】 六罐装荷兰美素佳儿金装2段900g", |
||||||
|
"goodPrice": 1149.00, |
||||||
|
"goodSold": 648 |
||||||
|
}, { |
||||||
|
"id": "2", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd2.jpg", |
||||||
|
"goodName": "【2】 韩国Amore爱茉莉红吕洗发水套装修复受损发质", |
||||||
|
"goodPrice": 89.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "3", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd3.jpg", |
||||||
|
"goodName": "【3】 Friso美素佳儿 金装婴儿配方奶粉3段900g", |
||||||
|
"goodPrice": 195.00, |
||||||
|
"goodSold": 968 |
||||||
|
}, { |
||||||
|
"id": "4", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd4.jpg", |
||||||
|
"goodName": "【4】 Fisher goodPrice费雪 费雪三轮儿童滑行车", |
||||||
|
"goodPrice": 299.00, |
||||||
|
"goodSold": 85 |
||||||
|
}, { |
||||||
|
"id": "5", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd5.jpg", |
||||||
|
"goodName": "【5】 Babylee巴布力 实木婴儿床 雷卡拉130*70cm", |
||||||
|
"goodPrice": 1889.00, |
||||||
|
"goodSold": 18 |
||||||
|
}, { |
||||||
|
"id": "6", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd6.jpg", |
||||||
|
"goodName": "【6】 Pigeon贝亲 独立三层奶粉盒 送小罐奶粉1段200g", |
||||||
|
"goodPrice": 70.00, |
||||||
|
"goodSold": 658 |
||||||
|
}, { |
||||||
|
"id": "7", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd7.jpg", |
||||||
|
"goodName": "【7】 TTBOO兔兔小布 肩纽扣套装", |
||||||
|
"goodPrice": 268.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "8", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd8.jpg", |
||||||
|
"goodName": "【8】 Nuna璐拉 婴儿布里奇果精纯嫩肤沐浴露婴儿精纯芦荟胶", |
||||||
|
"goodPrice": 140.00, |
||||||
|
"goodSold": 366 |
||||||
|
}, { |
||||||
|
"id": "9", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd9.jpg", |
||||||
|
"goodName": "【9】 illuma启赋 奶粉3段900g", |
||||||
|
"goodPrice": 252.00, |
||||||
|
"goodSold": 98 |
||||||
|
}, { |
||||||
|
"id": "10", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd10.jpg", |
||||||
|
"goodName": "【10】 Abbott雅培乳蛋白部分水解婴儿配方奶粉3段820g", |
||||||
|
"goodPrice": 89.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "11", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd11.jpg", |
||||||
|
"goodName": "【11】 韩蜜 酷炫唇蜜(礼盒套装)2.8g*4", |
||||||
|
"goodPrice": 179.00, |
||||||
|
"goodSold": 35 |
||||||
|
}, { |
||||||
|
"id": "12", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd12.jpg", |
||||||
|
"goodName": "【12】 保税区直发【3包装】日本Merries花王纸尿裤NB90", |
||||||
|
"goodPrice": 289.00, |
||||||
|
"goodSold": 1928 |
||||||
|
}, { |
||||||
|
"id": "13", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd13.jpg", |
||||||
|
"goodName": "【13】 Comotomo可么多么 硅胶奶瓶(0-3月奶嘴)150ml绿色", |
||||||
|
"goodPrice": 203.00, |
||||||
|
"goodSold": 87 |
||||||
|
}, { |
||||||
|
"id": "14", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd14.jpg", |
||||||
|
"goodName": "【14】 香港直邮德国瑞德露Rival de Loop芦荟精华安瓶", |
||||||
|
"goodPrice": 152.00, |
||||||
|
"goodSold": 61 |
||||||
|
}, { |
||||||
|
"id": "15", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd15.jpg", |
||||||
|
"goodName": "【15】 保税区直发药师堂尊马油香草味温和保湿无刺激面霜", |
||||||
|
"goodPrice": 269.00, |
||||||
|
"goodSold": 73 |
||||||
|
}, { |
||||||
|
"id": "16", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd16.jpg", |
||||||
|
"goodName": "【16】 香港直邮日本Spatreatment眼膜保湿去细纹法令纹", |
||||||
|
"goodPrice": 219.00, |
||||||
|
"goodSold": 13 |
||||||
|
}, { |
||||||
|
"id": "17", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd17.jpg", |
||||||
|
"goodName": "【17】 韩国MEDIHEALNMF可莱丝针剂睡眠面膜", |
||||||
|
"goodPrice": 81.00, |
||||||
|
"goodSold": 128 |
||||||
|
}, { |
||||||
|
"id": "18", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd18.jpg", |
||||||
|
"goodName": "【18】 DHC蝶翠诗橄榄蜂蜜滋养洗脸手工皂90g", |
||||||
|
"goodPrice": 123.00, |
||||||
|
"goodSold": 77 |
||||||
|
}, { |
||||||
|
"id": "19", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd19.jpg", |
||||||
|
"goodName": "【19】 日本资生堂CPB肌肤之钥新版隔离霜 清爽型 30ml", |
||||||
|
"goodPrice": 429.00, |
||||||
|
"goodSold": 36 |
||||||
|
}, { |
||||||
|
"id": "20", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd20.jpg", |
||||||
|
"goodName": "【20】 Heinz亨氏 婴儿面条优加面条全素套餐组合3口味3盒", |
||||||
|
"goodPrice": 39.00, |
||||||
|
"goodSold": 61 |
||||||
|
}, { |
||||||
|
"id": "21", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd21.jpg", |
||||||
|
"goodName": "【21】 Heinz亨氏 乐维滋果汁泥组合5口味15袋", |
||||||
|
"goodPrice": 69.00, |
||||||
|
"goodSold": 55 |
||||||
|
}, { |
||||||
|
"id": "22", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd22.jpg", |
||||||
|
"goodName": "【22】 保税区直发澳大利亚Swisse高浓度蔓越莓胶囊30粒", |
||||||
|
"goodPrice": 271.00, |
||||||
|
"goodSold": 19 |
||||||
|
}, { |
||||||
|
"id": "23", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd23.jpg", |
||||||
|
"goodName": "【23】 挪威Nordic Naturals小鱼婴幼儿鱼油DHA滴剂", |
||||||
|
"goodPrice": 102.00, |
||||||
|
"goodSold": 125 |
||||||
|
}, { |
||||||
|
"id": "24", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd24.jpg", |
||||||
|
"goodName": "【24】 澳大利亚Bio island DHA for Pregnancy海藻油DHA", |
||||||
|
"goodPrice": 289.00, |
||||||
|
"goodSold": 28 |
||||||
|
}, { |
||||||
|
"id": "25", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd25.jpg", |
||||||
|
"goodName": "【25】 澳大利亚Fatblaster Coconut Detox椰子水", |
||||||
|
"goodPrice": 152.00, |
||||||
|
"goodSold": 17 |
||||||
|
}, { |
||||||
|
"id": "26", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd26.jpg", |
||||||
|
"goodName": "【26】 Suitsky舒比奇 高护极薄舒爽纸尿片尿不湿XL60", |
||||||
|
"goodPrice": 99.00, |
||||||
|
"goodSold": 181 |
||||||
|
}, { |
||||||
|
"id": "27", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd27.jpg", |
||||||
|
"goodName": "【27】 英国JUST SOAP手工皂 玫瑰天竺葵蛋糕皂", |
||||||
|
"goodPrice": 72.00, |
||||||
|
"goodSold": 66 |
||||||
|
}, { |
||||||
|
"id": "28", |
||||||
|
"goodImg": "https://www.mescroll.com/demo/res/img/pd28.jpg", |
||||||
|
"goodName": "【28】 德国NUK 多色婴幼儿带盖学饮杯", |
||||||
|
"goodPrice": 92.00, |
||||||
|
"goodSold": 138 |
||||||
|
}] |
@ -0,0 +1,169 @@ |
|||||||
|
/* |
||||||
|
本地模拟接口请求, 仅demo演示用. |
||||||
|
实际项目以您服务器接口返回的数据为准,无需本地处理分页. |
||||||
|
请参考官方写法: https://www.mescroll.com/uni.html?v=20200210#tagUpCallback
|
||||||
|
* */ |
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
import goods from "./goods.js"; |
||||||
|
import goodsEdit from "./goods-edit.js"; |
||||||
|
|
||||||
|
// 获取新闻列表
|
||||||
|
export function apiNewList(pageNum, pageSize) { |
||||||
|
return new Promise((resolute, reject)=>{ |
||||||
|
//延时一秒,模拟联网
|
||||||
|
setTimeout(function() { |
||||||
|
try { |
||||||
|
let list = []; |
||||||
|
if (!pageNum) { |
||||||
|
//模拟下拉刷新返回的数据
|
||||||
|
let id=new Date().getTime(); |
||||||
|
let newObj = { |
||||||
|
id:id, |
||||||
|
title: "【新增新闻" + id + "】 标题", |
||||||
|
content: "新增新闻的内容" |
||||||
|
}; |
||||||
|
list.push(newObj); |
||||||
|
} else { |
||||||
|
//模拟上拉加载返回的数据
|
||||||
|
for (let i = 0; i < pageSize; i++) { |
||||||
|
let upIndex = (pageNum - 1) * pageSize + i + 1; |
||||||
|
let newObj = { |
||||||
|
id:upIndex, |
||||||
|
title: "【新闻" + upIndex + "】 标题标题标题标题标题", |
||||||
|
content: "内容内容内容内容内容内容内容内容内容" |
||||||
|
}; |
||||||
|
list.push(newObj); |
||||||
|
} |
||||||
|
console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length); |
||||||
|
} |
||||||
|
//模拟接口请求成功
|
||||||
|
resolute(list); |
||||||
|
} catch (e) { |
||||||
|
//模拟接口请求失败
|
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}, 1000) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 获取商品列表数据
|
||||||
|
export function apiGoods(pageNum, pageSize, isGoodsEdit) { |
||||||
|
return new Promise((resolute, reject)=>{ |
||||||
|
//延时一秒,模拟联网
|
||||||
|
setTimeout(()=> { |
||||||
|
try{ |
||||||
|
let data = isGoodsEdit ? goodsEdit : goods; |
||||||
|
//模拟分页数据
|
||||||
|
let list=[]; |
||||||
|
for (let i = (pageNum-1)*pageSize; i < pageNum*pageSize; i++) { |
||||||
|
if(i==data.length) break; |
||||||
|
list.push(data[i]); |
||||||
|
} |
||||||
|
//模拟接口请求成功
|
||||||
|
console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length); |
||||||
|
resolute(list); |
||||||
|
} catch (e) { |
||||||
|
//模拟接口请求失败
|
||||||
|
reject(e); |
||||||
|
} |
||||||
|
},1000) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 搜索商品
|
||||||
|
export function apiSearch(pageNum, pageSize, keyword) { |
||||||
|
return new Promise((resolute, reject)=>{ |
||||||
|
//延时一秒,模拟联网
|
||||||
|
setTimeout(()=> { |
||||||
|
try{ |
||||||
|
// 模拟搜索
|
||||||
|
let list = [] |
||||||
|
if (!keyword || keyword == "全部") { |
||||||
|
// 模拟搜索全部商品
|
||||||
|
for (let i = (pageNum - 1) * pageSize; i < pageNum * pageSize; i++) { |
||||||
|
if (i === goods.length) break |
||||||
|
list.push(goods[i]) |
||||||
|
} |
||||||
|
} else{ |
||||||
|
// 模拟关键词搜索
|
||||||
|
if(keyword=="母婴") keyword="婴"; // 为这个关键词展示多几条数据
|
||||||
|
for (let i = 0; i < goods.length; i++) { |
||||||
|
if (goods[i].goodName.indexOf(keyword) !== -1) { |
||||||
|
list.push(goods[i]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//模拟接口请求成功
|
||||||
|
console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length+", keyword="+keyword); |
||||||
|
resolute(list); |
||||||
|
} catch (e) { |
||||||
|
//模拟接口请求失败
|
||||||
|
reject(e); |
||||||
|
} |
||||||
|
},1000) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 获取微博列表
|
||||||
|
export function apiWeiboList(pageNum, pageSize) { |
||||||
|
return new Promise((resolute, reject)=>{ |
||||||
|
//延时2秒,模拟联网
|
||||||
|
setTimeout(function() { |
||||||
|
try { |
||||||
|
let list = []; |
||||||
|
if(!pageNum){ |
||||||
|
//此处模拟下拉刷新返回的数据
|
||||||
|
let id=new Date().getTime(); |
||||||
|
let newObj={id:id, title:"【新增微博"+id+"】 新增微博", content:"新增微博的内容,新增微博的内容"}; |
||||||
|
list.push(newObj); |
||||||
|
}else{ |
||||||
|
//此处模拟上拉加载返回的数据
|
||||||
|
for (let i = 0; i < pageSize; i++) { |
||||||
|
let upIndex=(pageNum-1)*pageSize+i+1; |
||||||
|
let newObj={id:upIndex, title:"【微博"+upIndex+"】 标题标题标题标题标题标题", content:"内容内容内容内容内容内容内容内容内容内容"}; |
||||||
|
list.push(newObj); |
||||||
|
} |
||||||
|
console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length); |
||||||
|
} |
||||||
|
//模拟接口请求成功
|
||||||
|
resolute(list); |
||||||
|
} catch (e) { |
||||||
|
//模拟接口请求失败
|
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}, 2000) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// 获取消息列表(共5页消息)
|
||||||
|
export function apiMsgList(pageNum, pageSize) { |
||||||
|
return new Promise((resolute, reject)=>{ |
||||||
|
//延时一秒,模拟联网
|
||||||
|
setTimeout(function() { |
||||||
|
try { |
||||||
|
let list = []; |
||||||
|
//模拟下拉加载更多记录
|
||||||
|
for (let i = 0; i < pageSize; i++) { |
||||||
|
let msgId = (pageNum - 1) * pageSize + i + 1; |
||||||
|
let newObj = { |
||||||
|
id: msgId, |
||||||
|
title: "【消息" + msgId + "】", |
||||||
|
content: "内容: 下拉获取聊天记录" |
||||||
|
}; |
||||||
|
// 此处模拟只有5页的消息 (第5页只有3条)
|
||||||
|
if(pageNum>=5 && i>=3){}else{ |
||||||
|
list.unshift(newObj); |
||||||
|
} |
||||||
|
} |
||||||
|
console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length); |
||||||
|
//模拟接口请求成功
|
||||||
|
resolute(list); |
||||||
|
} catch (e) { |
||||||
|
//模拟接口请求失败
|
||||||
|
reject(e); |
||||||
|
} |
||||||
|
}, 1000) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,184 @@ |
|||||||
|
/* |
||||||
|
Animation 微动画 |
||||||
|
基于ColorUI组建库的动画模块 by 文晓港 2019年3月26日19:52:28 |
||||||
|
*/ |
||||||
|
|
||||||
|
/* css 滤镜 控制黑白底色gif的 */ |
||||||
|
.gif-black{ |
||||||
|
mix-blend-mode: screen; |
||||||
|
} |
||||||
|
.gif-white{ |
||||||
|
mix-blend-mode: multiply; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* Animation css */ |
||||||
|
[class*=animation-] { |
||||||
|
animation-duration: .5s; |
||||||
|
animation-timing-function: ease-out; |
||||||
|
animation-fill-mode: both |
||||||
|
} |
||||||
|
|
||||||
|
.animation-fade { |
||||||
|
animation-name: fade; |
||||||
|
animation-duration: .8s; |
||||||
|
animation-timing-function: linear |
||||||
|
} |
||||||
|
|
||||||
|
.animation-scale-up { |
||||||
|
animation-name: scale-up |
||||||
|
} |
||||||
|
|
||||||
|
.animation-scale-down { |
||||||
|
animation-name: scale-down |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-top { |
||||||
|
animation-name: slide-top |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-bottom { |
||||||
|
animation-name: slide-bottom |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-left { |
||||||
|
animation-name: slide-left |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-right { |
||||||
|
animation-name: slide-right |
||||||
|
} |
||||||
|
|
||||||
|
.animation-shake { |
||||||
|
animation-name: shake |
||||||
|
} |
||||||
|
|
||||||
|
.animation-reverse { |
||||||
|
animation-direction: reverse |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes fade { |
||||||
|
0% { |
||||||
|
opacity: 0 |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes scale-up { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: scale(.2) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: scale(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes scale-down { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: scale(1.8) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: scale(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-top { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateY(-100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateY(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-bottom { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateY(100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateY(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes shake { |
||||||
|
|
||||||
|
0%, |
||||||
|
100% { |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
|
||||||
|
10% { |
||||||
|
transform: translateX(-9px) |
||||||
|
} |
||||||
|
|
||||||
|
20% { |
||||||
|
transform: translateX(8px) |
||||||
|
} |
||||||
|
|
||||||
|
30% { |
||||||
|
transform: translateX(-7px) |
||||||
|
} |
||||||
|
|
||||||
|
40% { |
||||||
|
transform: translateX(6px) |
||||||
|
} |
||||||
|
|
||||||
|
50% { |
||||||
|
transform: translateX(-5px) |
||||||
|
} |
||||||
|
|
||||||
|
60% { |
||||||
|
transform: translateX(4px) |
||||||
|
} |
||||||
|
|
||||||
|
70% { |
||||||
|
transform: translateX(-3px) |
||||||
|
} |
||||||
|
|
||||||
|
80% { |
||||||
|
transform: translateX(2px) |
||||||
|
} |
||||||
|
|
||||||
|
90% { |
||||||
|
transform: translateX(-1px) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-left { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(-100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-right { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<view class="cu-custom" :style="[{height:CustomBar + 'px'}]"> |
||||||
|
<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]"> |
||||||
|
<view class="action" @tap="BackPage" v-if="isBack"> |
||||||
|
<text class="cuIcon-back"></text> |
||||||
|
<slot name="backText"></slot> |
||||||
|
</view> |
||||||
|
<view class="content" :style="[{top:StatusBar + 'px'}]"> |
||||||
|
<slot name="content"></slot> |
||||||
|
</view> |
||||||
|
<slot name="right"></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
StatusBar: this.StatusBar, |
||||||
|
CustomBar: this.CustomBar |
||||||
|
}; |
||||||
|
}, |
||||||
|
name: 'cu-custom', |
||||||
|
computed: { |
||||||
|
style() { |
||||||
|
var StatusBar= this.StatusBar; |
||||||
|
var CustomBar= this.CustomBar; |
||||||
|
var bgImage = this.bgImage; |
||||||
|
var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`; |
||||||
|
if (this.bgImage) { |
||||||
|
style = `${style}background-image:url(${bgImage});`; |
||||||
|
} |
||||||
|
return style |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
bgColor: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
isBack: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
bgImage: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
BackPage() { |
||||||
|
if (getCurrentPages().length < 2 && 'undefined' !== typeof __wxConfig) { |
||||||
|
let url = '/' + __wxConfig.pages[0] |
||||||
|
return uni.redirectTo({url}) |
||||||
|
} |
||||||
|
uni.navigateBack({ |
||||||
|
delta: 1 |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
|
||||||
|
</style> |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,354 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-combox"> |
||||||
|
<view v-if="label" class="uni-combox__label" :style="labelStyle"> |
||||||
|
<text>{{label}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__input-box"> |
||||||
|
<input class="uni-combox__input" type="text" :placeholder="placeholder" :disabled="isDisabled" v-model="inputVal" |
||||||
|
@input="onInput" @focus="onFocus" @blur="onBlur" /> |
||||||
|
<icon type="clear" size="16" @tap="clearInputValue" v-if="!isDisabled" /> |
||||||
|
<image class="uni-combox__input-arrow" src="../../static/cuihai-combox/arrow_down.png" style="width: 30rpx;height: 30rpx;" @click="toggleSelector"></image> |
||||||
|
<!-- <uni-icons class="uni-combox__input-arrow" type="arrowdown" size="14" @click="toggleSelector"></uni-icons> --> |
||||||
|
<view class="uni-combox__selector" v-if="showSelector"> |
||||||
|
<scroll-view scroll-y="true" class="uni-combox__selector-scroll"> |
||||||
|
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0"> |
||||||
|
<text>{{emptyTips}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__selector-item" v-for="(item,index) in filterCandidates" :key="index" @click="onSelectorClick(index)" |
||||||
|
:style="isCheck(index)?'color:'+selectColor+';background-color:'+selectBgColor+';':'color:'+color+';'"> |
||||||
|
<text>{{isJSON?item[keyName]:item}}</text> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Combox 组合输入框 |
||||||
|
* @description 组合输入框一般用于既可以输入也可以选择的场景 |
||||||
|
* @property {String} label 左侧文字 |
||||||
|
* @property {String} labelWidth 左侧内容宽度 |
||||||
|
* @property {String} placeholder 输入框占位符 |
||||||
|
* @property {Array} candidates 候选项列表 |
||||||
|
* @property {String} emptyTips 筛选结果为空时显示的文字 |
||||||
|
* @property {String} value 单选时组合框的初始值 |
||||||
|
* @property {Array} initValue 多选时组合框的初始值(下标集合) |
||||||
|
* @property {String} keyName json数组显示的字段值 |
||||||
|
* @property {Boolean} isJSON 是否是json数组,默认不是 |
||||||
|
* @property {Boolean} isDisabled 是否是禁用输入,默认不禁用 |
||||||
|
* @property {Boolean} isCheckBox 是否是多选,默认不是,多选时不能输入查询 |
||||||
|
* @property {String} color 默认字体颜色,默认#000000 |
||||||
|
* @property {String} selectColor 选中字体颜色,默认#0081ff |
||||||
|
* @property {String} selectBgColor 选中背景颜色,默认#e8e8e8 |
||||||
|
*/ |
||||||
|
export default { |
||||||
|
name: 'comboxSearch', |
||||||
|
props: { |
||||||
|
label: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
labelWidth: { |
||||||
|
type: String, |
||||||
|
default: 'auto' |
||||||
|
}, |
||||||
|
placeholder: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
candidates: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
emptyTips: { |
||||||
|
type: String, |
||||||
|
default: '无匹配项' |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
initValue: { |
||||||
|
type: Array, |
||||||
|
default: null |
||||||
|
}, |
||||||
|
keyName: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
isJSON: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
isDisabled: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
isCheckBox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
color: { |
||||||
|
default: "#000000", |
||||||
|
type: String |
||||||
|
}, |
||||||
|
selectColor: { |
||||||
|
default: "#0081ff", |
||||||
|
type: String |
||||||
|
}, |
||||||
|
selectBgColor: { |
||||||
|
default: "#e8e8e8", |
||||||
|
type: String |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
showSelector: false, |
||||||
|
inputVal: '', |
||||||
|
arrays: [], |
||||||
|
gid: `sm-org-dropDown-${(new Date()).getTime()}${Math.random()}` |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
labelStyle() { |
||||||
|
if (this.labelWidth === 'auto') { |
||||||
|
return {} |
||||||
|
} |
||||||
|
return { |
||||||
|
width: this.labelWidth |
||||||
|
} |
||||||
|
}, |
||||||
|
filterCandidates() { |
||||||
|
if (!this.isDisabled) { |
||||||
|
if (this.isJSON) { |
||||||
|
let index = 0; |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
item.index = index; |
||||||
|
index++; |
||||||
|
return item[this.keyName].indexOf(this.inputVal) > -1 |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
return item.indexOf(this.inputVal) > -1 |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (this.isJSON) { |
||||||
|
let index = 0; |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
item.index = index; |
||||||
|
index++; |
||||||
|
return item[this.keyName] != undefined; |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return this.candidates |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
filterCandidatesLength() { |
||||||
|
return this.filterCandidates.length |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
if (this.initValue != null) { |
||||||
|
this.arrays = this.initValue; |
||||||
|
this.inputVal = this.getInpuevals(); |
||||||
|
} |
||||||
|
//在created的时候,给子组件放置一个监听,这个时候只是监听器被建立,此时这段代码不会生效 |
||||||
|
//uni.$on接收一个sm-org-dropDown-show的广播 |
||||||
|
uni.$on('sm-org-dropDown-show', (targetId) => { |
||||||
|
//接收广播,当该组件处于下拉框被打开的状态,并且跟下一个被点击的下拉框的gid不同时,将该组件的下拉框关闭,产生一个互斥效果。 |
||||||
|
if (this.showSelector && this.gid != targetId) { |
||||||
|
this.showSelector = false |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
//当子组件销毁的时候,将建立的监听销毁,这里不会影响代码效果,但是在多次反复引用组件时,会在根节点定义越来越多的监听,长此以往影响性能,所以在不用的时候及时销毁,养成好习惯 |
||||||
|
beforeDestroy() { |
||||||
|
uni.$off('sm-org-dropDown-show') |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value: { |
||||||
|
handler(newVal) { |
||||||
|
this.inputVal = newVal |
||||||
|
}, |
||||||
|
immediate: true |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleSelector() { |
||||||
|
this.showSelector = !this.showSelector |
||||||
|
if (this.showSelector) { |
||||||
|
uni.$emit('sm-org-dropDown-show', this.gid) |
||||||
|
} |
||||||
|
}, |
||||||
|
onFocus() { |
||||||
|
this.showSelector = true; |
||||||
|
uni.$emit('sm-org-dropDown-show', this.gid) |
||||||
|
}, |
||||||
|
onBlur() { |
||||||
|
/* setTimeout(() => { |
||||||
|
this.showSelector = false; |
||||||
|
}, 50) */ |
||||||
|
}, |
||||||
|
onSelectorClick(index) { |
||||||
|
if (this.isCheckBox) { |
||||||
|
let flag = this.arrays.indexOf(index); |
||||||
|
if (flag != -1) { |
||||||
|
let x = (this.arrays || []).findIndex((item) => item === index) |
||||||
|
this.arrays.splice(x, 1); |
||||||
|
} else { |
||||||
|
this.arrays.push(index); |
||||||
|
} |
||||||
|
this.inputVal = this.getInpuevals(); |
||||||
|
if (this.isJSON) { |
||||||
|
this.$emit('getValue', this.arrays); |
||||||
|
} else { |
||||||
|
this.$emit('getValue', this.trimSpace(this.inputVal.split(" "))); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.showSelector = false |
||||||
|
if (this.isJSON) { |
||||||
|
this.$emit('getValue', this.filterCandidates[index].index); |
||||||
|
this.inputVal = this.filterCandidates[index][this.keyName]; |
||||||
|
} else { |
||||||
|
this.$emit('getValue', this.filterCandidates[index]); |
||||||
|
this.inputVal = this.filterCandidates[index] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
trimSpace(array) { |
||||||
|
for (var i = 0; i < array.length; i++) { |
||||||
|
if (array[i] == "") { |
||||||
|
array.splice(i, 1); |
||||||
|
i = i - 1; |
||||||
|
} |
||||||
|
} |
||||||
|
return array; |
||||||
|
}, |
||||||
|
onInput() { |
||||||
|
setTimeout(() => { |
||||||
|
this.$emit('input', this.inputVal) |
||||||
|
}) |
||||||
|
}, |
||||||
|
clearInputValue() { |
||||||
|
this.inputVal = ""; |
||||||
|
this.showSelector = false; |
||||||
|
}, |
||||||
|
getInpuevals() { |
||||||
|
if (this.arrays.length == 0) { |
||||||
|
this.inputVal = ""; |
||||||
|
} else { |
||||||
|
this.arrays.sort(function(a, b) { |
||||||
|
return a - b |
||||||
|
}) |
||||||
|
this.inputVal = ""; |
||||||
|
if (this.isJSON) { |
||||||
|
this.arrays.forEach(v => { |
||||||
|
this.inputVal += this.candidates[v][this.keyName] + " "; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.arrays.forEach(v => { |
||||||
|
this.inputVal += this.candidates[v] + " "; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
return this.inputVal; |
||||||
|
}, |
||||||
|
isCheck(index) { |
||||||
|
return this.arrays.indexOf(index) != -1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.uni-combox { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
height: 40px; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
/* border-bottom: solid 1px #DDDDDD; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__label { |
||||||
|
font-size: 16px; |
||||||
|
line-height: 22px; |
||||||
|
padding-right: 10px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-box { |
||||||
|
position: relative; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input { |
||||||
|
flex: 1; |
||||||
|
font-size: 16px; |
||||||
|
height: 22px; |
||||||
|
line-height: 22px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-arrow { |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector { |
||||||
|
box-sizing: border-box; |
||||||
|
position: absolute; |
||||||
|
top: 42px; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
background-color: #FFFFFF; |
||||||
|
border-radius: 6px; |
||||||
|
box-shadow: #DDDDDD 4px 4px 8px, #DDDDDD -4px -4px 8px; |
||||||
|
z-index: 2; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-scroll { |
||||||
|
max-height: 200px; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-bottom: solid 6px #FFFFFF; |
||||||
|
border-right: solid 6px transparent; |
||||||
|
border-left: solid 6px transparent; |
||||||
|
left: 50%; |
||||||
|
top: -6px; |
||||||
|
margin-left: -6px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty, |
||||||
|
.uni-combox__selector-item { |
||||||
|
/* #ifdef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
line-height: 36px; |
||||||
|
font-size: 14px; |
||||||
|
text-align: center; |
||||||
|
border-bottom: solid 1px #DDDDDD; |
||||||
|
/* margin: 0px 10px; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty:last-child, |
||||||
|
.uni-combox__selector-item:last-child { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,87 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" v-for="(item,index) in list" :key="index" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
<text class="charge-status">{{item.status}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<text>{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,166 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<slot name="formItem" v-if="$slots.formItem"></slot> |
||||||
|
<view v-else class="evan-form-item-container" :class="'evan-form-item-container--'+mLabelPosition" :style="{borderWidth:border?'1rpx':0}"> |
||||||
|
<view v-if="label" class="evan-form-item-container__label" :class="{showAsteriskRect:hasRequiredAsterisk,isRequired:showRequiredAsterisk}" |
||||||
|
:style="mLabelStyle">{{label}}</view> |
||||||
|
<view class="evan-form-item-container__main" :style="mContentStyle"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: 'EvanFormItem', |
||||||
|
props: { |
||||||
|
labelStyle: Object, |
||||||
|
label: String, |
||||||
|
contentStyle: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
prop: String, |
||||||
|
border: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
labelPosition: { |
||||||
|
validator: function(value) { |
||||||
|
if (!value) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return ['top', 'left'].indexOf(value) !== -1 |
||||||
|
}, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
required: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
message: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
type: [Object, Array], |
||||||
|
default: null |
||||||
|
} |
||||||
|
}, |
||||||
|
inject: ['evanForm'], |
||||||
|
computed: { |
||||||
|
mLabelStyle() { |
||||||
|
const parent = this.getParent() |
||||||
|
let labelStyle = Object.assign({}, (parent.labelStyle || {}), (this.labelStyle || {})) |
||||||
|
let arr = Object.keys(labelStyle).map((key) => `${key}:${labelStyle[key]}`) |
||||||
|
return arr.join(';') |
||||||
|
}, |
||||||
|
mContentStyle() { |
||||||
|
let contentStyle = Object.assign({}, this.contentStyle || {}) |
||||||
|
let arr = Object.keys(contentStyle).map((key) => `${key}:${contentStyle[key]}`) |
||||||
|
return arr.join(';') |
||||||
|
}, |
||||||
|
mLabelPosition() { |
||||||
|
if (this.labelPosition) { |
||||||
|
return this.labelPosition |
||||||
|
} |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent) { |
||||||
|
return parent.labelPosition |
||||||
|
} |
||||||
|
return 'left' |
||||||
|
}, |
||||||
|
// 整个表单是否有*号 |
||||||
|
hasRequiredAsterisk() { |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent) { |
||||||
|
return parent.hasRequiredAsterisk |
||||||
|
} |
||||||
|
return false |
||||||
|
}, |
||||||
|
// 当前formItem是否显示*号 |
||||||
|
showRequiredAsterisk() { |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent && parent.hideRequiredAsterisk) { |
||||||
|
return false |
||||||
|
} |
||||||
|
const rules = this.getRules() |
||||||
|
if (rules && rules.length > 0) { |
||||||
|
if (rules.find((rule) => rule.required === true)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return this.required |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 获取EvanForm组件 |
||||||
|
getParent() { |
||||||
|
return this.evanForm |
||||||
|
}, |
||||||
|
getRules() { |
||||||
|
let form = this.getParent() |
||||||
|
const formRules = form.mRules && form.mRules[this.prop] ? form.mRules[this.prop] : []; |
||||||
|
const selfRules = this.rules |
||||||
|
const requiredRules = this.required ? { |
||||||
|
required: true, |
||||||
|
message: this.message || `${this.label}必填` |
||||||
|
} : [] |
||||||
|
return [].concat(selfRules || formRules || []).concat(requiredRules) |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.evanForm.$emit('evan.form.addField', this) |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.evanForm.$emit('evan.form.removeField', this) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.evan-form-item-container { |
||||||
|
|
||||||
|
&__label { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #666; |
||||||
|
line-height: 40rpx; |
||||||
|
padding: 25rpx 0; |
||||||
|
display: inline-block; |
||||||
|
|
||||||
|
&.showAsteriskRect::before { |
||||||
|
content: ''; |
||||||
|
color: #F56C6C; |
||||||
|
width: 20rpx; |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
&.isRequired::before { |
||||||
|
content: '*'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__main { |
||||||
|
flex: 1; |
||||||
|
min-height: 90rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
&--left { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
&--top { |
||||||
|
.evan-form-item-container__label { |
||||||
|
padding-bottom: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,168 @@ |
|||||||
|
<template> |
||||||
|
<view class="evan-form-container"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import utils from './utils.js' |
||||||
|
export default { |
||||||
|
name: 'EvanForm', |
||||||
|
props: { |
||||||
|
labelStyle: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
model: Object, |
||||||
|
hideRequiredAsterisk: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showMessage: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
labelPosition: { |
||||||
|
validator: function(value) { |
||||||
|
return ['top', 'left'].indexOf(value) !== -1 |
||||||
|
}, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
provide() { |
||||||
|
return { |
||||||
|
evanForm: this |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 整个form是否有*号,为了保证label对齐,而不是和*号对齐 |
||||||
|
hasRequiredAsterisk() { |
||||||
|
if (this.hideRequiredAsterisk) { |
||||||
|
return false |
||||||
|
} |
||||||
|
if (this.mRules) { |
||||||
|
const values = Object.values(this.mRules) |
||||||
|
if (values && values.length > 0) { |
||||||
|
for (let i = 0; i < values.length; i++) { |
||||||
|
const value = values[i] |
||||||
|
if (Array.isArray(value) && value.length > 0) { |
||||||
|
const requiredItem = value.find((v) => v.required === true) |
||||||
|
if (requiredItem) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (value && value.required) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return this.childHasRequired |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
rules: { |
||||||
|
immediate: true, |
||||||
|
deep: true, |
||||||
|
handler(value) { |
||||||
|
this.mRules = value || {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mRules: {}, |
||||||
|
fields: [], |
||||||
|
childHasRequired: false |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
setRules(rules) { |
||||||
|
this.mRules = rules || {} |
||||||
|
}, |
||||||
|
async validate(callback) { |
||||||
|
const rules = this.getRules() |
||||||
|
if (typeof callback === 'function') { |
||||||
|
utils.validate(this.model, rules, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return await utils.validate(this.model, rules, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
async validateField(props, callback) { |
||||||
|
const rules = this.getRules() |
||||||
|
if (typeof callback === 'function') { |
||||||
|
utils.validateField(this.model, rules, props, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return await utils.validateField(this.model, rules, props, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
getRules() { |
||||||
|
const rules = {} |
||||||
|
this.fields.forEach((field) => { |
||||||
|
if (field.prop) { |
||||||
|
const requiredRules = field.required ? { |
||||||
|
required: true, |
||||||
|
message: field.message || `${field.label}必填` |
||||||
|
} : [] |
||||||
|
const formRules = this.mRules && this.mRules[field.prop] ? this.mRules[field.prop] : [] |
||||||
|
rules[field.prop] = [].concat(field.rules || formRules || []).concat(requiredRules) |
||||||
|
} |
||||||
|
}) |
||||||
|
return rules |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.$on('evan.form.addField', (field) => { |
||||||
|
// 小程序中直接push field报错 |
||||||
|
if (field.prop) { |
||||||
|
this.fields.push({ |
||||||
|
rules: field.rules, |
||||||
|
prop: field.prop, |
||||||
|
required: field.required, |
||||||
|
label: field.label, |
||||||
|
message: field.message, |
||||||
|
_uid: field._uid |
||||||
|
}) |
||||||
|
if (!this.childHasRequired) { |
||||||
|
if (field.required) { |
||||||
|
this.childHasRequired = field.required |
||||||
|
return |
||||||
|
} |
||||||
|
if (field.rules) { |
||||||
|
const fieldRules = [].concat(field.rules) |
||||||
|
fieldRules.forEach((item) => { |
||||||
|
if (item.required) { |
||||||
|
this.childHasRequired = true |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
this.$on('evan.form.removeField', (field) => { |
||||||
|
this.fields.splice(this.fields.findIndex((item) => item._uid === field._uid), 1) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.evan-form-container {} |
||||||
|
</style> |
@ -0,0 +1,148 @@ |
|||||||
|
import AsyncValidator from 'async-validator' |
||||||
|
const utils = { |
||||||
|
validate: (model, rules, callback, options) => { |
||||||
|
const initOptions = { |
||||||
|
showMessage: true |
||||||
|
} |
||||||
|
options = Object.assign({}, initOptions, options || {}) |
||||||
|
let promise = null; |
||||||
|
if (typeof callback !== 'function') { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
valid ? resolve(valid) : reject(valid) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
// 如果需要验证的fields为空,调用验证时立刻返回callback
|
||||||
|
if (!rules || (Array.isArray(rules) && rules.length === 0) || (typeof rules === 'object' && Object.keys(rules).length === |
||||||
|
0)) { |
||||||
|
callback(true, null); |
||||||
|
if(promise){ |
||||||
|
return promise |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
let errors = [] |
||||||
|
const props = Object.keys(rules) |
||||||
|
let count = 0 |
||||||
|
for (let i in props) { |
||||||
|
const prop = props[i] |
||||||
|
const value = utils.getValueByProp(model, prop) |
||||||
|
utils.validateItem(rules, prop, value, (err) => { |
||||||
|
if (err && err.length > 0) { |
||||||
|
errors = errors.concat(err) |
||||||
|
} |
||||||
|
// 处理异步校验,等所有校验都结束时再callback
|
||||||
|
count++ |
||||||
|
if (count === props.length) { |
||||||
|
if (errors.length > 0) { |
||||||
|
if (options.showMessage) { |
||||||
|
utils.showToast(errors[0].message) |
||||||
|
} |
||||||
|
callback(false, errors) |
||||||
|
} else { |
||||||
|
callback(true, null) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
if (promise) { |
||||||
|
return promise |
||||||
|
} |
||||||
|
}, |
||||||
|
validateField: (model, rules, props, callback, options) => { |
||||||
|
const initOptions = { |
||||||
|
showMessage: true |
||||||
|
} |
||||||
|
options = Object.assign({}, initOptions, options || {}) |
||||||
|
let promise = null; |
||||||
|
if (typeof callback !== 'function') { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
valid ? resolve(valid) : reject(valid) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
props = [].concat(props) |
||||||
|
if (props.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
let errors = [] |
||||||
|
let count = 0 |
||||||
|
for (let i in props) { |
||||||
|
const prop = props[i] |
||||||
|
const value = utils.getValueByProp(model, prop) |
||||||
|
utils.validateItem(rules, prop, value, (err) => { |
||||||
|
if (err && err.length > 0) { |
||||||
|
errors = errors.concat(err) |
||||||
|
} |
||||||
|
// 处理异步校验,等所有校验都结束时再callback
|
||||||
|
count++ |
||||||
|
if (count === props.length) { |
||||||
|
if (errors.length > 0) { |
||||||
|
if (options.showMessage) { |
||||||
|
utils.showToast(errors[0].message) |
||||||
|
} |
||||||
|
callback(false, errors) |
||||||
|
} else { |
||||||
|
callback(true, null) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
if (promise) { |
||||||
|
return promise |
||||||
|
} |
||||||
|
}, |
||||||
|
validateItem(rules, prop, value, callback) { |
||||||
|
if (!rules || JSON.stringify(rules) === '{}') { |
||||||
|
if (callback instanceof Function) { |
||||||
|
callback(); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
const propRules = [].concat(rules[prop] || []); |
||||||
|
propRules.forEach((rule) => { |
||||||
|
if (rule.pattern) { |
||||||
|
rule.pattern = new RegExp(rule.pattern) |
||||||
|
} |
||||||
|
}) |
||||||
|
const descriptor = { |
||||||
|
[prop]: propRules |
||||||
|
}; |
||||||
|
const validator = new AsyncValidator(descriptor); |
||||||
|
const model = { |
||||||
|
[prop]: value |
||||||
|
}; |
||||||
|
validator.validate(model, { |
||||||
|
firstFields: true |
||||||
|
}, (errors) => { |
||||||
|
callback(errors); |
||||||
|
}); |
||||||
|
}, |
||||||
|
getValueByProp: (obj, prop) => { |
||||||
|
let tempObj = obj; |
||||||
|
prop = prop.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, ''); |
||||||
|
let keyArr = prop.split('.'); |
||||||
|
let i = 0; |
||||||
|
for (let len = keyArr.length; i < len - 1; ++i) { |
||||||
|
if (!tempObj) break; |
||||||
|
let key = keyArr[i]; |
||||||
|
if (key in tempObj) { |
||||||
|
tempObj = tempObj[key]; |
||||||
|
} else { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return tempObj ? (typeof tempObj[keyArr[i]] === 'string' ? tempObj[keyArr[i]].trim() : tempObj[keyArr[i]]) : |
||||||
|
null |
||||||
|
}, |
||||||
|
showToast: (message) => { |
||||||
|
uni.showToast({ |
||||||
|
title: message, |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default utils |
@ -0,0 +1,110 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" v-for="(item,index) in list" :key="index" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
font-weight: bold; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
.time-text{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
.status-text{ |
||||||
|
font-size: 28rpx; |
||||||
|
padding: 8rpx 12rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,200 @@ |
|||||||
|
<template> |
||||||
|
<text :class="classObj.wrapper" @click.stop="handleClick"> |
||||||
|
<text :class="[classObj.input, {'is-indeterminate': indeterminate, 'is-checked': checked, 'is-disabled': disabled}]"> |
||||||
|
<text :class="classObj.inner"></text> |
||||||
|
</text> |
||||||
|
</text> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
classObj: {} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
validator(t) { |
||||||
|
return t === 'radio' || t === 'checkbox' |
||||||
|
} |
||||||
|
}, |
||||||
|
checked: Boolean, |
||||||
|
disabled: Boolean, |
||||||
|
indeterminate: Boolean |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
this.classObj = { |
||||||
|
wrapper: `ly-${this.type}`, |
||||||
|
input: `ly-${this.type}__input`, |
||||||
|
inner: `ly-${this.type}__inner` |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
handleClick() { |
||||||
|
this.$emit('check', this.checked); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
/* lyRadio/lyCheckbox-start */ |
||||||
|
.ly-checkbox, |
||||||
|
.ly-radio { |
||||||
|
color: #606266; |
||||||
|
font-weight: 500; |
||||||
|
font-size: 28rpx; |
||||||
|
cursor: pointer; |
||||||
|
user-select: none; |
||||||
|
padding-right: 16rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input, |
||||||
|
.ly-radio__input { |
||||||
|
cursor: pointer; |
||||||
|
outline: 0; |
||||||
|
line-height: 1; |
||||||
|
vertical-align: middle |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner { |
||||||
|
background-color: #edf2fc; |
||||||
|
border-color: #DCDFE6; |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner::after { |
||||||
|
cursor: not-allowed; |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner+.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner+.ly-radio__label { |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-checked .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-disabled.is-checked .ly-radio__inner { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-checked .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-disabled.is-checked .ly-radio__inner::after { |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-indeterminate .ly-checkbox__inner { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-indeterminate .ly-checkbox__inner::before { |
||||||
|
background-color: #C0C4CC; |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-checked .ly-radio__inner, |
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner { |
||||||
|
background-color: #409EFF; |
||||||
|
border-color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled+text.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-disabled+text.ly-radio__label { |
||||||
|
color: #C0C4CC; |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-checked .ly-radio__inner::after { |
||||||
|
-webkit-transform: rotate(45deg) scaleY(1); |
||||||
|
transform: rotate(45deg) scaleY(1) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked+.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-checked+.ly-radio__label { |
||||||
|
color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-focus .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-focus .ly-radio__inner { |
||||||
|
border-color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
display: block; |
||||||
|
background-color: #FFF; |
||||||
|
height: 6rpx; |
||||||
|
-webkit-transform: scale(.5); |
||||||
|
transform: scale(.5); |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
top: 10rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner::after { |
||||||
|
display: none |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__inner, |
||||||
|
.ly-radio__inner { |
||||||
|
display: inline-block; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
-webkit-box-sizing: border-box; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 28rpx; |
||||||
|
height: 28rpx; |
||||||
|
background-color: #FFF; |
||||||
|
z-index: 1; |
||||||
|
-webkit-transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46); |
||||||
|
transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-radio__inner { |
||||||
|
border-radius: 50%; |
||||||
|
width: 34rpx !important; |
||||||
|
height: 34rpx !important; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__inner::after, |
||||||
|
.ly-radio__inner::after { |
||||||
|
-webkit-box-sizing: content-box; |
||||||
|
box-sizing: content-box; |
||||||
|
content: ""; |
||||||
|
border: 2rpx solid #FFF; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 14rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
-webkit-transform: rotate(45deg) scaleY(0); |
||||||
|
transform: rotate(45deg) scaleY(0); |
||||||
|
width: 6rpx; |
||||||
|
-webkit-transition: -webkit-transform .15s ease-in .05s; |
||||||
|
transition: -webkit-transform .15s ease-in .05s; |
||||||
|
transition: transform .15s ease-in .05s; |
||||||
|
transition: transform .15s ease-in .05s, -webkit-transform .15s ease-in .05s; |
||||||
|
-webkit-transform-origin: center; |
||||||
|
transform-origin: center |
||||||
|
} |
||||||
|
|
||||||
|
.ly-radio__inner::after { |
||||||
|
left: 12rpx !important; |
||||||
|
top: 6rpx !important; |
||||||
|
} |
||||||
|
/* lyRadio/lyCheckbox-end */ |
||||||
|
</style> |
@ -0,0 +1,424 @@ |
|||||||
|
<template> |
||||||
|
<view ref="node" |
||||||
|
name="LyTreeNode" |
||||||
|
v-show="node.visible" |
||||||
|
class="ly-tree-node" |
||||||
|
:class="{ |
||||||
|
'is-expanded': expanded, |
||||||
|
'is-hidden': !node.visible, |
||||||
|
'is-checked': !node.disabled && node.checked |
||||||
|
}" |
||||||
|
role="treeitem" |
||||||
|
@tap.stop="handleClick" > |
||||||
|
<view class="ly-tree-node__content" |
||||||
|
:class="{ |
||||||
|
'is-current': node.isCurrent && highlightCurrent |
||||||
|
}" |
||||||
|
:style="{ |
||||||
|
'padding-left': (node.level - 1) * indent + 'px' |
||||||
|
}"> |
||||||
|
<text |
||||||
|
@tap.stop="handleExpandIconClick" |
||||||
|
:class="[ |
||||||
|
{ |
||||||
|
'is-leaf': node.isLeaf, |
||||||
|
expanded: !node.isLeaf && node.expanded |
||||||
|
}, |
||||||
|
'ly-tree-node__expand-icon', |
||||||
|
iconClass ? iconClass : 'ly-iconfont ly-icon-caret-right' |
||||||
|
]"> |
||||||
|
</text> |
||||||
|
|
||||||
|
<ly-checkbox v-if="checkboxVisible || radioVisible" |
||||||
|
:type="checkboxVisible ? 'checkbox' : 'radio'" |
||||||
|
:checked="node.checked" |
||||||
|
:indeterminate="node.indeterminate" |
||||||
|
:disabled="!!node.disabled" |
||||||
|
@check="handleCheckChange(!node.checked)"/> |
||||||
|
|
||||||
|
<text v-if="node.loading" |
||||||
|
class="ly-tree-node__loading-icon ly-iconfont ly-icon-loading"> |
||||||
|
</text> |
||||||
|
|
||||||
|
<template v-if="node.icon && node.icon.length > 0"> |
||||||
|
<image |
||||||
|
v-if="node.icon.indexOf('/') !== -1" |
||||||
|
class="ly-tree-node__icon" |
||||||
|
mode="widthFix" |
||||||
|
:src="node.icon" |
||||||
|
@error="handleImageError" |
||||||
|
> |
||||||
|
</image> |
||||||
|
<text v-else |
||||||
|
class="ly-tree-node__icon" |
||||||
|
:class="node.icon"> |
||||||
|
</text> |
||||||
|
</template> |
||||||
|
|
||||||
|
<text class="ly-tree-node__label">{{node.label}}</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view v-if="!renderAfterExpand || childNodeRendered" |
||||||
|
v-show="expanded" |
||||||
|
class="ly-tree-node__children" |
||||||
|
role="group"> |
||||||
|
<ly-tree-node v-for="cNodeId in node.childNodesId" |
||||||
|
:nodeId="cNodeId" |
||||||
|
:render-after-expand="renderAfterExpand" |
||||||
|
:show-checkbox="showCheckbox" |
||||||
|
:show-radio="showRadio" |
||||||
|
:check-only-leaf="checkOnlyLeaf" |
||||||
|
:key="getNodeKey(cNodeId)" |
||||||
|
:indent="indent" |
||||||
|
:icon-class="iconClass"> |
||||||
|
</ly-tree-node> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {getNodeKey} from './tool/util.js'; |
||||||
|
import lyCheckbox from './components/ly-checkbox.vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'LyTreeNode', |
||||||
|
|
||||||
|
componentName: 'LyTreeNode', |
||||||
|
|
||||||
|
components: { |
||||||
|
lyCheckbox |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
nodeId: [Number, String], |
||||||
|
renderAfterExpand: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
checkOnlyLeaf: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showCheckbox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showRadio: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
indent: Number, |
||||||
|
iconClass: String |
||||||
|
}, |
||||||
|
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
node: { |
||||||
|
indeterminate: false, |
||||||
|
checked: false, |
||||||
|
expanded: false |
||||||
|
}, |
||||||
|
expanded: false, |
||||||
|
childNodeRendered: false, |
||||||
|
oldChecked: null, |
||||||
|
oldIndeterminate: null, |
||||||
|
highlightCurrent: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
inject: ['tree'], |
||||||
|
|
||||||
|
computed: { |
||||||
|
checkboxVisible() { |
||||||
|
if (this.checkOnlyLeaf) { |
||||||
|
return this.showCheckbox && this.node.isLeaf; |
||||||
|
} |
||||||
|
|
||||||
|
return this.showCheckbox; |
||||||
|
}, |
||||||
|
radioVisible() { |
||||||
|
if (this.checkOnlyLeaf) { |
||||||
|
return this.showRadio && this.node.isLeaf; |
||||||
|
} |
||||||
|
|
||||||
|
return this.showRadio; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
watch: { |
||||||
|
'node.indeterminate'(val) { |
||||||
|
this.handleSelectChange(this.node.checked, val); |
||||||
|
}, |
||||||
|
'node.checked'(val) { |
||||||
|
this.handleSelectChange(val, this.node.indeterminate); |
||||||
|
}, |
||||||
|
'node.expanded'(val) { |
||||||
|
this.$nextTick(() => this.expanded = val); |
||||||
|
if (val) { |
||||||
|
this.childNodeRendered = true; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
getNodeKey(nodeId) { |
||||||
|
let node = this.tree.store.root.getChildNodes([nodeId])[0]; |
||||||
|
return getNodeKey(this.tree.nodeKey, node.data); |
||||||
|
}, |
||||||
|
|
||||||
|
handleSelectChange(checked, indeterminate) { |
||||||
|
if (this.oldChecked !== checked && this.oldIndeterminate !== indeterminate) { |
||||||
|
|
||||||
|
if (this.checkOnlyLeaf && !this.node.isLeaf) return; |
||||||
|
|
||||||
|
if (this.checkboxVisible) { |
||||||
|
const allNodes = this.tree.store._getAllNodes(); |
||||||
|
this.tree.$emit('check-change', { |
||||||
|
checked, |
||||||
|
indeterminate, |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedall: allNodes.every(item => item.checked) |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.tree.$emit('radio-change', { |
||||||
|
checked, |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedall: false |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.expanded && this.tree.expandOnCheckNode && checked) { |
||||||
|
this.handleExpandIconClick(); |
||||||
|
} |
||||||
|
|
||||||
|
this.oldChecked = checked; |
||||||
|
this.indeterminate = indeterminate; |
||||||
|
}, |
||||||
|
|
||||||
|
handleClick() { |
||||||
|
this.tree.store.setCurrentNode(this.node); |
||||||
|
this.tree.$emit('current-change', { |
||||||
|
node: this.node, |
||||||
|
data: this.tree.store.currentNode ? this.tree.store.currentNode.data : null, |
||||||
|
currentNode: this.tree.store.currentNode |
||||||
|
}); |
||||||
|
this.tree.currentNode = this.node; |
||||||
|
|
||||||
|
if (this.tree.expandOnClickNode) { |
||||||
|
this.handleExpandIconClick(); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.tree.checkOnClickNode && !this.node.disabled) { |
||||||
|
(this.checkboxVisible || this.radioVisible) && this.handleCheckChange(!this.node.checked); |
||||||
|
} |
||||||
|
|
||||||
|
this.tree.$emit('node-click', this.node); |
||||||
|
}, |
||||||
|
|
||||||
|
handleExpandIconClick() { |
||||||
|
if (this.node.isLeaf) return; |
||||||
|
|
||||||
|
if (this.expanded) { |
||||||
|
this.tree.$emit('node-collapse', this.node); |
||||||
|
this.node.collapse(); |
||||||
|
} else { |
||||||
|
this.node.expand(); |
||||||
|
this.tree.$emit('node-expand', this.node); |
||||||
|
|
||||||
|
if (this.tree.accordion) { |
||||||
|
uni.$emit(`${this.tree.elId}-tree-node-expand`, this.node); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
handleCheckChange(checked) { |
||||||
|
if (this.node.disabled) return; |
||||||
|
|
||||||
|
if (this.checkboxVisible) { |
||||||
|
this.node.setChecked(checked, !(this.tree.checkStrictly || this.checkOnlyLeaf)); |
||||||
|
} else { |
||||||
|
this.node.setRadioChecked(checked); |
||||||
|
} |
||||||
|
|
||||||
|
this.$nextTick(() => { |
||||||
|
this.tree.$emit('check', { |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedNodes: this.tree.store.getCheckedNodes(), |
||||||
|
checkedKeys: this.tree.store.getCheckedKeys(), |
||||||
|
halfCheckedNodes: this.tree.store.getHalfCheckedNodes(), |
||||||
|
halfCheckedKeys: this.tree.store.getHalfCheckedKeys() |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
handleImageError() { |
||||||
|
this.node.icon = this.tree.defaultNodeIcon; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
if (!this.tree) { |
||||||
|
throw new Error('Can not find node\'s tree.'); |
||||||
|
} |
||||||
|
|
||||||
|
this.node = this.tree.store.nodesMap[this.nodeId]; |
||||||
|
this.highlightCurrent = this.tree.highlightCurrent; |
||||||
|
|
||||||
|
if (this.node.expanded) { |
||||||
|
this.expanded = true; |
||||||
|
this.childNodeRendered = true; |
||||||
|
} |
||||||
|
|
||||||
|
const props = this.tree.props || {}; |
||||||
|
const childrenKey = props['children'] || 'children'; |
||||||
|
this.$watch(`node.data.${childrenKey}`, () => { |
||||||
|
this.node.updateChildren(); |
||||||
|
}); |
||||||
|
|
||||||
|
if (this.tree.accordion) { |
||||||
|
uni.$on(`${this.tree.elId}-tree-node-expand`, node => { |
||||||
|
if (this.node.id !== node.id && this.node.level === node.level) { |
||||||
|
this.node.collapse(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
beforeDestroy() { |
||||||
|
this.$parent = null; |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.ly-tree-node { |
||||||
|
white-space: nowrap; |
||||||
|
outline: 0 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
height: 70rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content.is-current { |
||||||
|
background-color: #F5F7FA; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content>.ly-tree-node__expand-icon { |
||||||
|
padding: 12rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__checkbox { |
||||||
|
display: flex; |
||||||
|
margin-right: 16rpx; |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__checkbox>image { |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon { |
||||||
|
color: #C0C4CC; |
||||||
|
font-size: 28rpx; |
||||||
|
-webkit-transform: rotate(0); |
||||||
|
transform: rotate(0); |
||||||
|
-webkit-transition: -webkit-transform .3s ease-in-out; |
||||||
|
transition: -webkit-transform .3s ease-in-out; |
||||||
|
transition: transform .3s ease-in-out; |
||||||
|
transition: transform .3s ease-in-out, -webkit-transform .3s ease-in-out |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon.expanded { |
||||||
|
-webkit-transform: rotate(90deg); |
||||||
|
transform: rotate(90deg) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon.is-leaf { |
||||||
|
color: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__icon { |
||||||
|
width: 34rpx; |
||||||
|
height: 34rpx; |
||||||
|
overflow: hidden; |
||||||
|
margin-right: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__label { |
||||||
|
font-size: 28rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__loading-icon { |
||||||
|
margin-right: 16rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #C0C4CC; |
||||||
|
-webkit-animation: rotating 2s linear infinite; |
||||||
|
animation: rotating 2s linear infinite |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node>.ly-tree-node__children { |
||||||
|
overflow: hidden; |
||||||
|
background-color: transparent |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node>.ly-tree-node__children.collapse-transition { |
||||||
|
transition: height .3s ease-in-out; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node.is-expanded>.ly-tree-node__children { |
||||||
|
display: block |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node_collapse { |
||||||
|
overflow: hidden; |
||||||
|
padding-top: 0; |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
/* lyTree-end */ |
||||||
|
|
||||||
|
/* iconfont-start */ |
||||||
|
@font-face { |
||||||
|
font-family: "ly-iconfont"; |
||||||
|
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPsAAsAAAAACKwAAAOeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDBgqFDIQPATYCJAMMCwgABCAFhG0HQBtfB8gekiSCdAwUAKgCFMA5Hj7H0PeTlABUr57PVyGqugqzSWJnNwWoWJjx/9rUr4TPL1ZSQpU2mycqwoRwIN3p+MkqMqyEW+OtMBLPSUBb8v//XtWMKTavxYIUsT/Wy1qbQzkBDOYEKGB7dVpPyVqgCnJNwvMvhZl10nMCtQbFoPVhY8ZDncJfF4grbqpQ13AqE52hWqgcOFrEQ6hWnW5VfMCD7Pfjn4WoI6nI/K0bl0MNGPBz0qcflVqYnvCA4vNDPUXGPFCIw8HgtsqiOK9SrW2smm6sVITElWlpISMdVBn8wyMJopLfXg+myZ48KCrSkvj9g37U1ItbXYke4APwXxK3N4TuehyBfmM0I3zbNdt7uk3VnjPtzX0rnIl7z7bZvb/thHohsu9QuykKo+Cws4nL7LsPmI3n2qN9B9upZEIKd4hu0NCKi0rt7fNtdl+I1N25hOJMDQK6odS123tROR7Pg8toEhDaF+kR0TYjxW6M58F5+ZNQOxmZHtE2g+IYjxjlNy/yIRQpCmrgq5R4/3jx8PvT8Ha8d3/xiLnt4EGyaDnznzRv8vpyZ+9TFHf/ntX9e59A+b6+fPHd5+dy0wYHVvHOroWbnWe879O9DnL53bN/gUHuwm28b/n8i/V3ry4E3IoXNqS6Rvs0LhJxeNVjoUkM3LKosU+0a6rh45FVvLt+2oz7Zd53b4QOy7/9snDXHbqVu+A+f8r7PnM2H8kXrWm5c8/vLu7LqRee7HW637mz3kHc5U/RCXf25d7G8tkdgEfwIpzpkknGpaMw3ww55q9Mn9OQNyua/wB/49OOWydn4eL/6roCfjx6FMmcxfJStYRKfd3UwoHiML4rF4uMSK+SvYTuNxMHrpl8yd3Q6v32cAeo/KFaowBJlQHIqo3zi3geKtRZhErVlqDWnOGn67QRKkWpwaw1AkKza5A0egFZszf8In4HFTp9h0rNUQm1NqP1lXUmgyuDBVUlNYi2gHA98FnokUreOZaac1xV1JlMMZGKEs+QdCLVrgynPhUcO0pzzYyUjDAReGSYeBl13YCEIrCpLhOWlGE+mWRD35TQAw8UawRKJVEGQrMAwekCPpaMlpTOz49FmeZwqcREX1t3Ikoo4dMTaQmpBfzhRn9R30uZXTKXKUOSmLSKEQIeYhjqKZcrcIzhMLLRrJMSrA35UF4yGMaWGhPHm733dwJq+Z/NkSJHUXemCirjgpuWrHMD1eC+mQUAAAA=') format('woff2'); |
||||||
|
} |
||||||
|
|
||||||
|
.ly-iconfont { |
||||||
|
font-family: "ly-iconfont" !important; |
||||||
|
font-size: 30rpx; |
||||||
|
font-style: normal; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-icon-caret-right:before { |
||||||
|
content: "\e8ee"; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-icon-loading:before { |
||||||
|
content: "\e657"; |
||||||
|
} |
||||||
|
/* iconfont-end */ |
||||||
|
|
||||||
|
/* animate-start */ |
||||||
|
@keyframes rotating { |
||||||
|
0% { |
||||||
|
-webkit-transform: rotateZ(0); |
||||||
|
transform: rotateZ(0) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
-webkit-transform: rotateZ(360deg); |
||||||
|
transform: rotateZ(360deg) |
||||||
|
} |
||||||
|
} |
||||||
|
/* animate-end */ |
||||||
|
</style> |
@ -0,0 +1,605 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<template v-if="showLoading"> |
||||||
|
<view class="ly-loader ly-flex-center"> |
||||||
|
<view class="ly-loader-inner">加载中...</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<template v-else> |
||||||
|
<view v-if="isEmpty || !visible" |
||||||
|
class="ly-empty"> |
||||||
|
{{emptyText}} |
||||||
|
</view> |
||||||
|
<view :key="updateKey" |
||||||
|
class="ly-tree" |
||||||
|
:class="{'is-empty': isEmpty || !visible}" |
||||||
|
role="tree" |
||||||
|
name="LyTreeExpand"> |
||||||
|
<ly-tree-node v-for="nodeId in childNodesId" |
||||||
|
:nodeId="nodeId" |
||||||
|
:render-after-expand="renderAfterExpand" |
||||||
|
:show-checkbox="showCheckbox" |
||||||
|
:show-radio="showRadio" |
||||||
|
:check-only-leaf="checkOnlyLeaf" |
||||||
|
:key="getNodeKey(nodeId)" |
||||||
|
:indent="indent" |
||||||
|
:icon-class="iconClass"> |
||||||
|
</ly-tree-node> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import Vue from 'vue' |
||||||
|
import TreeStore from './model/tree-store.js'; |
||||||
|
import {getNodeKey} from './tool/util.js'; |
||||||
|
import LyTreeNode from './ly-tree-node.vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'LyTree', |
||||||
|
|
||||||
|
componentName: 'LyTree', |
||||||
|
|
||||||
|
components: { |
||||||
|
LyTreeNode |
||||||
|
}, |
||||||
|
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
updateKey: new Date().getTime(), // 数据更新的时候,重新渲染树 |
||||||
|
elId: `ly_${Math.ceil(Math.random() * 10e5).toString(36)}`, |
||||||
|
visible: true, |
||||||
|
store: { |
||||||
|
ready: false |
||||||
|
}, |
||||||
|
currentNode: null, |
||||||
|
childNodesId: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
provide() { |
||||||
|
return { |
||||||
|
tree: this |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
// 展示数据 |
||||||
|
treeData: Array, |
||||||
|
|
||||||
|
// 自主控制loading加载,避免数据还没获取到的空档出现“暂无数据”字样 |
||||||
|
ready: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 内容为空的时候展示的文本 |
||||||
|
emptyText: { |
||||||
|
type: String, |
||||||
|
default: '暂无数据' |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否在第一次展开某个树节点后才渲染其子节点 |
||||||
|
renderAfterExpand: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 |
||||||
|
nodeKey: String, |
||||||
|
|
||||||
|
// 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false |
||||||
|
checkStrictly: Boolean, |
||||||
|
|
||||||
|
// 是否默认展开所有节点 |
||||||
|
defaultExpandAll: Boolean, |
||||||
|
|
||||||
|
// 切换全部展开、全部折叠 |
||||||
|
toggleExpendAll: Boolean, |
||||||
|
|
||||||
|
// 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点 |
||||||
|
expandOnClickNode: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 选中的时候展开节点 |
||||||
|
expandOnCheckNode: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点 |
||||||
|
checkOnClickNode: Boolean, |
||||||
|
checkDescendants: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 展开子节点的时候是否自动展开父节点 |
||||||
|
autoExpandParent: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 默认勾选的节点的 key 的数组 |
||||||
|
defaultCheckedKeys: Array, |
||||||
|
|
||||||
|
// 默认展开的节点的 key 的数组 |
||||||
|
defaultExpandedKeys: Array, |
||||||
|
|
||||||
|
// 是否展开当前节点的父节点 |
||||||
|
expandCurrentNodeParent: Boolean, |
||||||
|
|
||||||
|
// 当前选中的节点 |
||||||
|
currentNodeKey: [String, Number], |
||||||
|
|
||||||
|
// 是否最后一层叶子节点才显示单选/多选框 |
||||||
|
checkOnlyLeaf: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 节点是否可被选择 |
||||||
|
showCheckbox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 节点单选 |
||||||
|
showRadio: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 配置选项 |
||||||
|
props: { |
||||||
|
type: [Object, Function], |
||||||
|
default () { |
||||||
|
return { |
||||||
|
children: 'children', // 指定子树为节点对象的某个属性值 |
||||||
|
label: 'label', // 指定节点标签为节点对象的某个属性值 |
||||||
|
disabled: 'disabled' // 指定节点选择框是否禁用为节点对象的某个属性值 |
||||||
|
}; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否懒加载子节点,需与 load 方法结合使用 |
||||||
|
lazy: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否高亮当前选中节点,默认值是 false |
||||||
|
highlightCurrent: Boolean, |
||||||
|
|
||||||
|
// 加载子树数据的方法,仅当 lazy 属性为true 时生效 |
||||||
|
load: Function, |
||||||
|
|
||||||
|
// 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏 |
||||||
|
filterNodeMethod: Function, |
||||||
|
|
||||||
|
// 搜索时是否展示匹配项的所有子节点 |
||||||
|
childVisibleForFilterNode: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否每次只打开一个同级树节点展开 |
||||||
|
accordion: Boolean, |
||||||
|
|
||||||
|
// 相邻级节点间的水平缩进,单位为像素 |
||||||
|
indent: { |
||||||
|
type: Number, |
||||||
|
default: 18 |
||||||
|
}, |
||||||
|
|
||||||
|
// 自定义树节点的展开图标 |
||||||
|
iconClass: String, |
||||||
|
|
||||||
|
// 是否显示节点图标,如果配置为true,需要配置props中对应的图标属性名称 |
||||||
|
showNodeIcon: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 当节点图标显示出错时,显示的默认图标 |
||||||
|
defaultNodeIcon: { |
||||||
|
type: String, |
||||||
|
default: 'https://img-cdn-qiniu.dcloud.net.cn/uniapp/doc/github.svg' |
||||||
|
}, |
||||||
|
|
||||||
|
// 如果数据量较大,建议不要在node节点中添加parent属性,会造成性能损耗 |
||||||
|
isInjectParentInNode: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
computed: { |
||||||
|
isEmpty() { |
||||||
|
if (this.store.root) { |
||||||
|
const childNodes = this.store.root.getChildNodes(this.childNodesId); |
||||||
|
|
||||||
|
return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}, |
||||||
|
showLoading() { |
||||||
|
return !(this.store.ready && this.ready); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
watch: { |
||||||
|
toggleExpendAll(newVal) { |
||||||
|
this.store.toggleExpendAll(newVal); |
||||||
|
}, |
||||||
|
defaultCheckedKeys(newVal) { |
||||||
|
this.store.setDefaultCheckedKey(newVal); |
||||||
|
}, |
||||||
|
defaultExpandedKeys(newVal) { |
||||||
|
this.store.defaultExpandedKeys = newVal; |
||||||
|
this.store.setDefaultExpandedKeys(newVal); |
||||||
|
}, |
||||||
|
checkStrictly(newVal) { |
||||||
|
this.store.checkStrictly = newVal || this.checkOnlyLeaf; |
||||||
|
}, |
||||||
|
'store.root.childNodesId'(newVal) { |
||||||
|
this.childNodesId = newVal; |
||||||
|
}, |
||||||
|
'store.root.visible'(newVal) { |
||||||
|
this.visible = newVal; |
||||||
|
}, |
||||||
|
childNodesId(){ |
||||||
|
this.$nextTick(() => { |
||||||
|
this.$emit('ly-tree-render-completed'); |
||||||
|
}); |
||||||
|
}, |
||||||
|
treeData: { |
||||||
|
handler(newVal) { |
||||||
|
this.updateKey = new Date().getTime(); |
||||||
|
this.store.setData(newVal); |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
/* |
||||||
|
* @description 对树节点进行筛选操作 |
||||||
|
* @method filter |
||||||
|
* @param {all} value 在 filter-node-method 中作为第一个参数 |
||||||
|
* @param {Object} data 搜索指定节点的节点数据,不传代表搜索所有节点,假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
*/ |
||||||
|
filter(value, data) { |
||||||
|
if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter'); |
||||||
|
this.store.filter(value, data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取节点的唯一标识符 |
||||||
|
* @method getNodeKey |
||||||
|
* @param {String, Number} nodeId |
||||||
|
* @return {String, Number} 匹配到的数据中的某一项数据 |
||||||
|
*/ |
||||||
|
getNodeKey(nodeId) { |
||||||
|
let node = this.store.root.getChildNodes([nodeId])[0]; |
||||||
|
return getNodeKey(this.nodeKey, node.data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取节点路径 |
||||||
|
* @method getNodePath |
||||||
|
* @param {Object} data 节点数据 |
||||||
|
* @return {Array} 路径数组 |
||||||
|
*/ |
||||||
|
getNodePath(data) { |
||||||
|
return this.store.getNodePath(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组 |
||||||
|
* @method getCheckedNodes |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点,默认false |
||||||
|
* @param {Boolean} includeHalfChecked 是否包含半选节点,默认false |
||||||
|
* @return {Array} 目前被选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getCheckedNodes(leafOnly, includeHalfChecked) { |
||||||
|
return this.store.getCheckedNodes(leafOnly, includeHalfChecked); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组 |
||||||
|
* @method getCheckedKeys |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点,默认false,若为 true 则仅返回被选中的叶子节点的 keys |
||||||
|
* @param {Boolean} includeHalfChecked 是否返回indeterminate为true的节点,默认false |
||||||
|
* @return {Array} 目前被选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getCheckedKeys(leafOnly, includeHalfChecked) { |
||||||
|
return this.store.getCheckedKeys(leafOnly, includeHalfChecked); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取当前被选中节点的 data,若没有节点被选中则返回 null |
||||||
|
* @method getCurrentNode |
||||||
|
* @return {Object} 当前被选中节点的 data,若没有节点被选中则返回 null |
||||||
|
*/ |
||||||
|
getCurrentNode() { |
||||||
|
const currentNode = this.store.getCurrentNode(); |
||||||
|
return currentNode ? currentNode.data : null; |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取当前被选中节点的 key,若没有节点被选中则返回 null |
||||||
|
* @method getCurrentKey |
||||||
|
* @return {all} 当前被选中节点的 key, 若没有节点被选中则返回 null |
||||||
|
*/ |
||||||
|
getCurrentKey() { |
||||||
|
const currentNode = this.getCurrentNode(); |
||||||
|
return currentNode ? currentNode[this.nodeKey] : null; |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 设置全选/取消全选 |
||||||
|
* @method setCheckAll |
||||||
|
* @param {Boolean} isCheckAll 选中状态,默认为true |
||||||
|
*/ |
||||||
|
setCheckAll(isCheckAll = true) { |
||||||
|
if (this.showRadio) throw new Error('You set the "show-radio" property, so you cannot select all nodes'); |
||||||
|
|
||||||
|
if (!this.showCheckbox) console.warn('You have not set the property "show-checkbox". Please check your settings'); |
||||||
|
|
||||||
|
this.store.setCheckAll(isCheckAll); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 设置目前勾选的节点 |
||||||
|
* @method setCheckedNodes |
||||||
|
* @param {Array} nodes 接收勾选节点数据的数组 |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false |
||||||
|
*/ |
||||||
|
setCheckedNodes(nodes, leafOnly) { |
||||||
|
this.store.setCheckedNodes(nodes, leafOnly); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 keys 设置目前勾选的节点 |
||||||
|
* @method setCheckedKeys |
||||||
|
* @param {Array} keys 勾选节点的 key 的数组 |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false |
||||||
|
*/ |
||||||
|
setCheckedKeys(keys, leafOnly) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys'); |
||||||
|
this.store.setCheckedKeys(keys, leafOnly); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 key / data 设置某个节点的勾选状态 |
||||||
|
* @method setChecked |
||||||
|
* @param {all} data 勾选节点的 key 或者 data |
||||||
|
* @param {Boolean} checked 节点是否选中 |
||||||
|
* @param {Boolean} deep 是否设置子节点 ,默认为 false |
||||||
|
*/ |
||||||
|
setChecked(data, checked, deep) { |
||||||
|
this.store.setChecked(data, checked, deep); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点所组成的数组 |
||||||
|
* @method getHalfCheckedNodes |
||||||
|
* @return {Array} 目前半选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getHalfCheckedNodes() { |
||||||
|
return this.store.getHalfCheckedNodes(); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点的 key 所组成的数组 |
||||||
|
* @method getHalfCheckedKeys |
||||||
|
* @return {Array} 目前半选中的节点的 key 所组成的数组 |
||||||
|
*/ |
||||||
|
getHalfCheckedKeys() { |
||||||
|
return this.store.getHalfCheckedKeys(); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 node 设置某个节点的当前选中状态 |
||||||
|
* @method setCurrentNode |
||||||
|
* @param {Object} node 待被选节点的 node |
||||||
|
*/ |
||||||
|
setCurrentNode(node) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode'); |
||||||
|
this.store.setUserCurrentNode(node); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 key 设置某个节点的当前选中状态 |
||||||
|
* @method setCurrentKey |
||||||
|
* @param {all} key 待被选节点的 key,若为 null 则取消当前高亮的节点 |
||||||
|
*/ |
||||||
|
setCurrentKey(key) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey'); |
||||||
|
this.store.setCurrentNodeKey(key); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 根据 data 或者 key 拿到 Tree 组件中的 node |
||||||
|
* @method getNode |
||||||
|
* @param {all} data 要获得 node 的 key 或者 data |
||||||
|
*/ |
||||||
|
getNode(data) { |
||||||
|
return this.store.getNode(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 删除 Tree 中的一个节点 |
||||||
|
* @method remove |
||||||
|
* @param {all} data 要删除的节点的 data 或者 node |
||||||
|
*/ |
||||||
|
remove(data) { |
||||||
|
this.store.remove(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 中的一个节点追加一个子节点 |
||||||
|
* @method append |
||||||
|
* @param {Object} data 要追加的子节点的 data |
||||||
|
* @param {Object} parentNode 子节点的 parent 的 data、key 或者 node |
||||||
|
*/ |
||||||
|
append(data, parentNode) { |
||||||
|
this.store.append(data, parentNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 的一个节点的前面增加一个节点 |
||||||
|
* @method insertBefore |
||||||
|
* @param {Object} data 要增加的节点的 data |
||||||
|
* @param {all} refNode 要增加的节点的后一个节点的 data、key 或者 node |
||||||
|
*/ |
||||||
|
insertBefore(data, refNode) { |
||||||
|
this.store.insertBefore(data, refNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 的一个节点的后面增加一个节点 |
||||||
|
* @method insertAfter |
||||||
|
* @param {Object} data 要增加的节点的 data |
||||||
|
* @param {all} refNode 要增加的节点的前一个节点的 data、key 或者 node |
||||||
|
*/ |
||||||
|
insertAfter(data, refNode) { |
||||||
|
this.store.insertAfter(data, refNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 keys 设置节点子元素 |
||||||
|
* @method updateKeyChildren |
||||||
|
* @param {String, Number} key 节点 key |
||||||
|
* @param {Object} data 节点数据的数组 |
||||||
|
*/ |
||||||
|
updateKeyChildren(key, data) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild'); |
||||||
|
this.store.updateChildren(key, data); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
this.isTree = true; |
||||||
|
|
||||||
|
let props = this.props; |
||||||
|
if (typeof this.props === 'function') props = this.props(); |
||||||
|
if (typeof props !== 'object') throw new Error('props must be of object type.'); |
||||||
|
|
||||||
|
this.store = new TreeStore({ |
||||||
|
key: this.nodeKey, |
||||||
|
data: this.treeData, |
||||||
|
lazy: this.lazy, |
||||||
|
props: props, |
||||||
|
load: this.load, |
||||||
|
showCheckbox: this.showCheckbox, |
||||||
|
showRadio: this.showRadio, |
||||||
|
currentNodeKey: this.currentNodeKey, |
||||||
|
checkStrictly: this.checkStrictly || this.checkOnlyLeaf, |
||||||
|
checkDescendants: this.checkDescendants, |
||||||
|
expandOnCheckNode: this.expandOnCheckNode, |
||||||
|
defaultCheckedKeys: this.defaultCheckedKeys, |
||||||
|
defaultExpandedKeys: this.defaultExpandedKeys, |
||||||
|
expandCurrentNodeParent: this.expandCurrentNodeParent, |
||||||
|
autoExpandParent: this.autoExpandParent, |
||||||
|
defaultExpandAll: this.defaultExpandAll, |
||||||
|
filterNodeMethod: this.filterNodeMethod, |
||||||
|
childVisibleForFilterNode: this.childVisibleForFilterNode, |
||||||
|
showNodeIcon: this.showNodeIcon, |
||||||
|
isInjectParentInNode: this.isInjectParentInNode |
||||||
|
}); |
||||||
|
|
||||||
|
this.childNodesId = this.store.root.childNodesId; |
||||||
|
}, |
||||||
|
|
||||||
|
beforeDestroy() { |
||||||
|
if (this.accordion) { |
||||||
|
uni.$off(`${this.elId}-tree-node-expand`) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.ly-tree { |
||||||
|
position: relative; |
||||||
|
cursor: default; |
||||||
|
background: #FFF; |
||||||
|
color: #606266; |
||||||
|
padding: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree.is-empty { |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
/* lyEmpty-start */ |
||||||
|
.ly-empty { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
margin-top: 100rpx; |
||||||
|
} |
||||||
|
/* lyEmpty-end */ |
||||||
|
|
||||||
|
/* lyLoader-start */ |
||||||
|
.ly-loader { |
||||||
|
margin-top: 100rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner, |
||||||
|
.ly-loader-inner:before, |
||||||
|
.ly-loader-inner:after { |
||||||
|
background: #efefef; |
||||||
|
animation: load 1s infinite ease-in-out; |
||||||
|
width: .5em; |
||||||
|
height: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:before, |
||||||
|
.ly-loader-inner:after { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
content: ''; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:before { |
||||||
|
left: -1em; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner { |
||||||
|
text-indent: -9999em; |
||||||
|
position: relative; |
||||||
|
font-size: 22rpx; |
||||||
|
animation-delay: 0.16s; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:after { |
||||||
|
left: 1em; |
||||||
|
animation-delay: 0.32s; |
||||||
|
} |
||||||
|
/* lyLoader-end */ |
||||||
|
|
||||||
|
@keyframes load { |
||||||
|
0%, |
||||||
|
80%, |
||||||
|
100% { |
||||||
|
box-shadow: 0 0 #efefef; |
||||||
|
height: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
40% { |
||||||
|
box-shadow: 0 -1.5em #efefef; |
||||||
|
height: 1.5em; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,538 @@ |
|||||||
|
import { |
||||||
|
markNodeData, |
||||||
|
objectAssign, |
||||||
|
arrayFindIndex, |
||||||
|
getChildState, |
||||||
|
reInitChecked, |
||||||
|
getPropertyFromData, |
||||||
|
isNull, |
||||||
|
NODE_KEY |
||||||
|
} from '../tool/util'; |
||||||
|
|
||||||
|
const getStore = function(store) { |
||||||
|
let thisStore = store; |
||||||
|
|
||||||
|
return function() { |
||||||
|
return thisStore; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let nodeIdSeed = 0; |
||||||
|
|
||||||
|
export default class Node { |
||||||
|
constructor(options) { |
||||||
|
this.time = new Date().getTime(); |
||||||
|
this.id = nodeIdSeed++; |
||||||
|
this.text = null; |
||||||
|
this.checked = false; |
||||||
|
this.indeterminate = false; |
||||||
|
this.data = null; |
||||||
|
this.expanded = false; |
||||||
|
this.parentId = null; |
||||||
|
this.visible = true; |
||||||
|
this.isCurrent = false; |
||||||
|
|
||||||
|
for (let name in options) { |
||||||
|
if (options.hasOwnProperty(name)) { |
||||||
|
if (name === 'store') { |
||||||
|
this.store = getStore(options[name]); |
||||||
|
} else { |
||||||
|
this[name] = options[name]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.store()) { |
||||||
|
throw new Error('[Node]store is required!'); |
||||||
|
} |
||||||
|
|
||||||
|
// internal
|
||||||
|
this.level = 0; |
||||||
|
this.loaded = false; |
||||||
|
this.childNodesId = []; |
||||||
|
this.loading = false; |
||||||
|
this.label = getPropertyFromData(this, 'label'); |
||||||
|
this.key = this._getKey(); |
||||||
|
this.disabled = getPropertyFromData(this, 'disabled'); |
||||||
|
this.nextSibling = null; |
||||||
|
this.previousSibling = null; |
||||||
|
this.icon = ''; |
||||||
|
|
||||||
|
this._handleParentAndLevel(); |
||||||
|
this._handleProps(); |
||||||
|
this._handleExpand(); |
||||||
|
this._handleCurrent(); |
||||||
|
|
||||||
|
if (this.store().lazy) { |
||||||
|
this.store()._initDefaultCheckedNode(this); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
_getKey() { |
||||||
|
if (!this.data || Array.isArray(this.data)) return null; |
||||||
|
|
||||||
|
if (typeof this.data === 'object') { |
||||||
|
const nodeKey = this.store().key; |
||||||
|
const key = this.data[nodeKey]; |
||||||
|
|
||||||
|
if (typeof key === 'undefined') { |
||||||
|
throw new Error(`您配置的node-key为"${nodeKey}",但数据中并未找到对应"${nodeKey}"属性的值,请检查node-key的配置是否合理`) |
||||||
|
} |
||||||
|
|
||||||
|
return key; |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error('不合法的data数据'); |
||||||
|
} |
||||||
|
|
||||||
|
_handleParentAndLevel() { |
||||||
|
if (this.parentId !== null) { |
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
|
||||||
|
if (this.store().isInjectParentInNode) { |
||||||
|
this.parent = parent; |
||||||
|
} |
||||||
|
|
||||||
|
// 由于这里做了修改,默认第一个对象不会被注册到nodesMap中,所以找不到parent会报错,所以默认parent的level是0
|
||||||
|
if (!parent) { |
||||||
|
parent = { |
||||||
|
level: 0 |
||||||
|
} |
||||||
|
} else { |
||||||
|
const parentChildNodes = parent.getChildNodes(parent.childNodesId); |
||||||
|
const index = parent.childNodesId.indexOf(this.key); |
||||||
|
this.nextSibling = index > -1 ? parentChildNodes[index + 1] : null; |
||||||
|
this.previousSibling = index > 0 ? parentChildNodes[index - 1] : null; |
||||||
|
} |
||||||
|
this.level = parent.level + 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleProps() { |
||||||
|
const props = this.store().props; |
||||||
|
|
||||||
|
if (this.store().showNodeIcon) { |
||||||
|
if (props && typeof props.icon !== 'undefined') { |
||||||
|
this.icon = getPropertyFromData(this, 'icon'); |
||||||
|
} else { |
||||||
|
console.warn('请配置props属性中的"icon"字段') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.store().registerNode(this); |
||||||
|
|
||||||
|
if (props && typeof props.isLeaf !== 'undefined') { |
||||||
|
const isLeaf = getPropertyFromData(this, 'isLeaf'); |
||||||
|
if (typeof isLeaf === 'boolean') { |
||||||
|
this.isLeafByUser = isLeaf; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleExpand() { |
||||||
|
if (this.store().lazy !== true && this.data) { |
||||||
|
this.setData(this.data); |
||||||
|
|
||||||
|
if (this.store().defaultExpandAll) { |
||||||
|
this.expanded = true; |
||||||
|
} |
||||||
|
} else if (this.level > 0 && this.store().lazy && this.store().defaultExpandAll) { |
||||||
|
this.expand(); |
||||||
|
} |
||||||
|
|
||||||
|
if (!Array.isArray(this.data)) { |
||||||
|
markNodeData(this, this.data); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.data) return; |
||||||
|
|
||||||
|
const defaultExpandedKeys = this.store().defaultExpandedKeys; |
||||||
|
const key = this.store().key; |
||||||
|
if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) { |
||||||
|
this.expand(null, this.store().autoExpandparent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleCurrent() { |
||||||
|
const key = this.store().key; |
||||||
|
|
||||||
|
if (key && this.store().currentNodeKey !== undefined && this.key === this.store().currentNodeKey) { |
||||||
|
this.store().currentNode = this; |
||||||
|
this.store().currentNode.isCurrent = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
destroyStore() { |
||||||
|
getStore(null) |
||||||
|
} |
||||||
|
|
||||||
|
setData(data) { |
||||||
|
if (!Array.isArray(data)) { |
||||||
|
markNodeData(this, data); |
||||||
|
} |
||||||
|
|
||||||
|
this.data = data; |
||||||
|
this.childNodesId = []; |
||||||
|
|
||||||
|
let children; |
||||||
|
if (this.level === 0 && Array.isArray(this.data)) { |
||||||
|
children = this.data; |
||||||
|
} else { |
||||||
|
children = getPropertyFromData(this, 'children') || []; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0, j = children.length; i < j; i++) { |
||||||
|
this.insertChild({ |
||||||
|
data: children[i] |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contains(target, deep = true) { |
||||||
|
const walk = function(parent) { |
||||||
|
const children = parent.getChildNodes(parent.childNodesId) || []; |
||||||
|
let result = false; |
||||||
|
for (let i = 0, j = children.length; i < j; i++) { |
||||||
|
const child = children[i]; |
||||||
|
if (child === target || (deep && walk(child))) { |
||||||
|
result = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
}; |
||||||
|
|
||||||
|
return walk(this); |
||||||
|
} |
||||||
|
|
||||||
|
remove() { |
||||||
|
if (this.parentId !== null) { |
||||||
|
const parent = this.getParent(this.parentId); |
||||||
|
parent.removeChild(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
insertChild(child, index, batch) { |
||||||
|
if (!child) throw new Error('insertChild error: child is required.'); |
||||||
|
|
||||||
|
if (!(child instanceof Node)) { |
||||||
|
if (!batch) { |
||||||
|
const children = this.getChildren(true); |
||||||
|
if (children.indexOf(child.data) === -1) { |
||||||
|
if (typeof index === 'undefined' || index < 0) { |
||||||
|
children.push(child.data); |
||||||
|
} else { |
||||||
|
children.splice(index, 0, child.data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
objectAssign(child, { |
||||||
|
parentId: isNull(this.key) ? '' : this.key, |
||||||
|
store: this.store() |
||||||
|
}); |
||||||
|
child = new Node(child); |
||||||
|
} |
||||||
|
|
||||||
|
child.level = this.level + 1; |
||||||
|
|
||||||
|
if (typeof index === 'undefined' || index < 0) { |
||||||
|
this.childNodesId.push(child.key); |
||||||
|
} else { |
||||||
|
this.childNodesId.splice(index, 0, child.key); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
insertBefore(child, ref) { |
||||||
|
let index; |
||||||
|
if (ref) { |
||||||
|
index = this.childNodesId.indexOf(ref.id); |
||||||
|
} |
||||||
|
this.insertChild(child, index); |
||||||
|
} |
||||||
|
|
||||||
|
insertAfter(child, ref) { |
||||||
|
let index; |
||||||
|
if (ref) { |
||||||
|
index = this.childNodesId.indexOf(ref.id); |
||||||
|
if (index !== -1) index += 1; |
||||||
|
} |
||||||
|
this.insertChild(child, index); |
||||||
|
} |
||||||
|
|
||||||
|
removeChild(child) { |
||||||
|
const children = this.getChildren() || []; |
||||||
|
const dataIndex = children.indexOf(child.data); |
||||||
|
if (dataIndex > -1) { |
||||||
|
children.splice(dataIndex, 1); |
||||||
|
} |
||||||
|
|
||||||
|
const index = this.childNodesId.indexOf(child.key); |
||||||
|
|
||||||
|
if (index > -1) { |
||||||
|
this.store() && this.store().deregisterNode(child); |
||||||
|
child.parentId = null; |
||||||
|
this.childNodesId.splice(index, 1); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
removeChildByData(data) { |
||||||
|
let targetNode = null; |
||||||
|
|
||||||
|
for (let i = 0; i < this.childNodesId.length; i++) { |
||||||
|
let node = this.getChildNodes(this.childNodesId); |
||||||
|
if (node[i].data === data) { |
||||||
|
targetNode = node[i]; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (targetNode) { |
||||||
|
this.removeChild(targetNode); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 为了避免APP端parent嵌套结构导致报错,这里parent需要从nodesMap中获取
|
||||||
|
getParent(parentId) { |
||||||
|
try { |
||||||
|
if (!parentId.toString()) return null; |
||||||
|
return this.store().nodesMap[parentId]; |
||||||
|
} catch (error) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 为了避免APP端childNodes嵌套结构导致报错,这里childNodes需要从nodesMap中获取
|
||||||
|
getChildNodes(childNodesId) { |
||||||
|
let childNodes = []; |
||||||
|
if (childNodesId.length === 0) return childNodes; |
||||||
|
childNodesId.forEach((key) => { |
||||||
|
childNodes.push(this.store().nodesMap[key]); |
||||||
|
}) |
||||||
|
return childNodes; |
||||||
|
} |
||||||
|
|
||||||
|
expand(callback, expandparent) { |
||||||
|
const done = () => { |
||||||
|
if (expandparent) { |
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
while (parent && parent.level > 0) { |
||||||
|
parent.expanded = true; |
||||||
|
parent = this.getParent(parent.parentId); |
||||||
|
} |
||||||
|
} |
||||||
|
this.expanded = true; |
||||||
|
if (callback) callback(); |
||||||
|
}; |
||||||
|
|
||||||
|
if (this.shouldLoadData()) { |
||||||
|
this.loadData(function(data) { |
||||||
|
if (Array.isArray(data)) { |
||||||
|
if (this.checked) { |
||||||
|
this.setChecked(true, true); |
||||||
|
} else if (!this.store().checkStrictly) { |
||||||
|
reInitChecked(this); |
||||||
|
} |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
done(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
doCreateChildren(array, defaultProps = {}) { |
||||||
|
array.forEach((item) => { |
||||||
|
this.insertChild(objectAssign({ |
||||||
|
data: item |
||||||
|
}, defaultProps), undefined, true); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
collapse() { |
||||||
|
this.expanded = false; |
||||||
|
} |
||||||
|
|
||||||
|
shouldLoadData() { |
||||||
|
return this.store().lazy === true && this.store().load && !this.loaded; |
||||||
|
} |
||||||
|
|
||||||
|
updateLeafState() { |
||||||
|
if (this.store().lazy === true && this.loaded !== true && typeof this.isLeafByUser !== 'undefined') { |
||||||
|
this.isLeaf = this.isLeafByUser; |
||||||
|
return; |
||||||
|
} |
||||||
|
const childNodesId = this.childNodesId; |
||||||
|
if (!this.store().lazy || (this.store().lazy === true && this.loaded === true)) { |
||||||
|
this.isLeaf = !childNodesId || childNodesId.length === 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
this.isLeaf = false; |
||||||
|
} |
||||||
|
|
||||||
|
setChecked(value, deep, recursion, passValue) { |
||||||
|
this.indeterminate = value === 'half'; |
||||||
|
this.checked = value === true; |
||||||
|
|
||||||
|
if (this.checked && this.store().expandOnCheckNode) { |
||||||
|
this.expand(null, true) |
||||||
|
} |
||||||
|
|
||||||
|
if (this.store().checkStrictly) return; |
||||||
|
if (this.store().showRadio) return; |
||||||
|
|
||||||
|
if (!(this.shouldLoadData() && !this.store().checkDescendants)) { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId); |
||||||
|
let { |
||||||
|
all, |
||||||
|
allWithoutDisable |
||||||
|
} = getChildState(childNodes); |
||||||
|
|
||||||
|
if (!this.isLeaf && (!all && allWithoutDisable)) { |
||||||
|
this.checked = false; |
||||||
|
value = false; |
||||||
|
} |
||||||
|
|
||||||
|
const handleDescendants = () => { |
||||||
|
if (deep) { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId) |
||||||
|
for (let i = 0, j = childNodes.length; i < j; i++) { |
||||||
|
const child = childNodes[i]; |
||||||
|
passValue = passValue || value !== false; |
||||||
|
const isCheck = child.disabled ? child.checked : passValue; |
||||||
|
child.setChecked(isCheck, deep, true, passValue); |
||||||
|
} |
||||||
|
const { |
||||||
|
half, |
||||||
|
all |
||||||
|
} = getChildState(childNodes); |
||||||
|
|
||||||
|
if (!all) { |
||||||
|
this.checked = all; |
||||||
|
this.indeterminate = half; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (this.shouldLoadData()) { |
||||||
|
this.loadData(() => { |
||||||
|
handleDescendants(); |
||||||
|
reInitChecked(this); |
||||||
|
}, { |
||||||
|
checked: value !== false |
||||||
|
}); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
handleDescendants(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.parentId) return; |
||||||
|
|
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
if (parent && parent.level === 0) return; |
||||||
|
|
||||||
|
if (!recursion) { |
||||||
|
reInitChecked(parent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setRadioChecked(value) { |
||||||
|
const allNodes = this.store()._getAllNodes().sort((a, b) => b.level - a.level); |
||||||
|
allNodes.forEach(node => node.setChecked(false, false)); |
||||||
|
this.checked = value === true; |
||||||
|
} |
||||||
|
|
||||||
|
getChildren(forceInit = false) { |
||||||
|
if (this.level === 0) return this.data; |
||||||
|
const data = this.data; |
||||||
|
if (!data) return null; |
||||||
|
|
||||||
|
const props = this.store().props; |
||||||
|
let children = 'children'; |
||||||
|
if (props) { |
||||||
|
children = props.children || 'children'; |
||||||
|
} |
||||||
|
|
||||||
|
if (data[children] === undefined) { |
||||||
|
data[children] = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (forceInit && !data[children]) { |
||||||
|
data[children] = []; |
||||||
|
} |
||||||
|
|
||||||
|
return data[children]; |
||||||
|
} |
||||||
|
|
||||||
|
updateChildren() { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId); |
||||||
|
const newData = this.getChildren() || []; |
||||||
|
const oldData = childNodes.map((node) => node.data); |
||||||
|
|
||||||
|
const newDataMap = {}; |
||||||
|
const newNodes = []; |
||||||
|
|
||||||
|
newData.forEach((item, index) => { |
||||||
|
const key = item[NODE_KEY]; |
||||||
|
const isNodeExists = !!key && arrayFindIndex(oldData, data => data[NODE_KEY] === key) >= 0; |
||||||
|
if (isNodeExists) { |
||||||
|
newDataMap[key] = { |
||||||
|
index, |
||||||
|
data: item |
||||||
|
}; |
||||||
|
} else { |
||||||
|
newNodes.push({ |
||||||
|
index, |
||||||
|
data: item |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
if (!this.store().lazy) { |
||||||
|
oldData.forEach((item) => { |
||||||
|
if (!newDataMap[item[NODE_KEY]]) this.removeChildByData(item); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
newNodes.forEach(({ |
||||||
|
index, |
||||||
|
data |
||||||
|
}) => { |
||||||
|
this.insertChild({ |
||||||
|
data |
||||||
|
}, index); |
||||||
|
}); |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
loadData(callback, defaultProps = {}) { |
||||||
|
if (this.store().lazy === true &&
|
||||||
|
this.store().load && !this.loaded &&
|
||||||
|
(!this.loading || Object.keys(defaultProps).length) |
||||||
|
) { |
||||||
|
this.loading = true; |
||||||
|
|
||||||
|
const resolve = (children) => { |
||||||
|
this.loaded = true; |
||||||
|
this.loading = false; |
||||||
|
this.childNodesId = []; |
||||||
|
this.doCreateChildren(children, defaultProps); |
||||||
|
this.updateLeafState(); |
||||||
|
|
||||||
|
callback && callback.call(this,children); |
||||||
|
}; |
||||||
|
|
||||||
|
this.store().load(this, resolve); |
||||||
|
} else { |
||||||
|
callback && callback.call(this); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,419 @@ |
|||||||
|
import Node from './node'; |
||||||
|
import { |
||||||
|
getNodeKey, |
||||||
|
getPropertyFromData |
||||||
|
} from '../tool/util'; |
||||||
|
|
||||||
|
export default class TreeStore { |
||||||
|
constructor(options) { |
||||||
|
this.ready = false; |
||||||
|
this.currentNode = null; |
||||||
|
this.currentNodeKey = null; |
||||||
|
|
||||||
|
Object.assign(this, options); |
||||||
|
|
||||||
|
if (!this.key) { |
||||||
|
throw new Error('[Tree] nodeKey is required'); |
||||||
|
} |
||||||
|
|
||||||
|
this.nodesMap = {}; |
||||||
|
this.root = new Node({ |
||||||
|
data: this.data, |
||||||
|
store: this |
||||||
|
}); |
||||||
|
|
||||||
|
if (this.lazy && this.load) { |
||||||
|
const loadFn = this.load; |
||||||
|
loadFn(this.root, (data) => { |
||||||
|
this.root.doCreateChildren(data); |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
this.ready = true; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
this.ready = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
filter(value, data) { |
||||||
|
const filterNodeMethod = this.filterNodeMethod; |
||||||
|
const lazy = this.lazy; |
||||||
|
const _self = this; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (data && typeof data === 'object') { |
||||||
|
let nodePath = _self.getNodePath(child.data); |
||||||
|
if (!nodePath.some(pathItem => pathItem[_self.key] === data[_self.key])) { |
||||||
|
child.visible = false; |
||||||
|
traverse(child); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (_self.childVisibleForFilterNode) { |
||||||
|
let parent = child.getParent(child.parentId); |
||||||
|
child.visible = filterNodeMethod.call(child, value, child.data, child) || (parent && parent.visible); |
||||||
|
} else { |
||||||
|
child.visible = filterNodeMethod.call(child, value, child.data, child); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
|
||||||
|
if (!node.visible && childNodes.length) { |
||||||
|
let allHidden = true; |
||||||
|
allHidden = !childNodes.some(child => child.visible); |
||||||
|
|
||||||
|
if (node.root) { |
||||||
|
node.root.visible = allHidden === false; |
||||||
|
} else { |
||||||
|
node.visible = allHidden === false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!value) return; |
||||||
|
|
||||||
|
if (node.visible && !node.isLeaf && !lazy) node.expand(); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
} |
||||||
|
|
||||||
|
setData(newVal) { |
||||||
|
const instanceChanged = newVal !== this.root.data; |
||||||
|
if (instanceChanged) { |
||||||
|
this.root.setData(newVal); |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
} else { |
||||||
|
this.root.updateChildren(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getNode(data) { |
||||||
|
if (data instanceof Node) return data; |
||||||
|
const key = typeof data !== 'object' ? data : getNodeKey(this.key, data); |
||||||
|
if (!key) return null; |
||||||
|
return this.nodesMap[key] || null; |
||||||
|
} |
||||||
|
|
||||||
|
insertBefore(data, refData) { |
||||||
|
const refNode = this.getNode(refData); |
||||||
|
let parent = refNode.getParent(refNode.parentId); |
||||||
|
parent.insertBefore({ |
||||||
|
data |
||||||
|
}, refNode); |
||||||
|
} |
||||||
|
|
||||||
|
insertAfter(data, refData) { |
||||||
|
const refNode = this.getNode(refData); |
||||||
|
let parent = refNode.getParent(refNode.parentId); |
||||||
|
parent.insertAfter({ |
||||||
|
data |
||||||
|
}, refNode); |
||||||
|
} |
||||||
|
|
||||||
|
remove(data) { |
||||||
|
const node = this.getNode(data); |
||||||
|
|
||||||
|
if (node && node.parentId !== null) { |
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
if (node === this.currentNode) { |
||||||
|
this.currentNode = null; |
||||||
|
} |
||||||
|
parent.removeChild(node); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
append(data, parentData) { |
||||||
|
const parentNode = parentData ? this.getNode(parentData) : this.root; |
||||||
|
|
||||||
|
if (parentNode) { |
||||||
|
parentNode.insertChild({ |
||||||
|
data |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_initDefaultCheckedNodes() { |
||||||
|
const defaultCheckedKeys = this.defaultCheckedKeys || []; |
||||||
|
const nodesMap = this.nodesMap; |
||||||
|
let checkedKeyfromData = []; |
||||||
|
let totalCheckedKeys = [] |
||||||
|
|
||||||
|
for (let key in nodesMap) { |
||||||
|
let checked = getPropertyFromData(nodesMap[key], 'checked') || false; |
||||||
|
checked && checkedKeyfromData.push(key); |
||||||
|
} |
||||||
|
|
||||||
|
totalCheckedKeys = Array.from(new Set([...defaultCheckedKeys, ...checkedKeyfromData])); |
||||||
|
totalCheckedKeys.forEach((checkedKey) => { |
||||||
|
const node = nodesMap[checkedKey]; |
||||||
|
|
||||||
|
if (node) { |
||||||
|
node.setChecked(true, !this.checkStrictly); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
_initDefaultCheckedNode(node) { |
||||||
|
const defaultCheckedKeys = this.defaultCheckedKeys || []; |
||||||
|
|
||||||
|
if (defaultCheckedKeys.indexOf(node.key) !== -1) { |
||||||
|
node.setChecked(true, !this.checkStrictly); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
toggleExpendAll(isExpandAll) { |
||||||
|
const allNodes = this._getAllNodes(); |
||||||
|
|
||||||
|
allNodes.forEach(item => { |
||||||
|
const node = this.getNode(item.key);
|
||||||
|
|
||||||
|
if (node) isExpandAll ? node.expand() : node.collapse(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
setCheckAll(isCkeckAll) { |
||||||
|
const allNodes = this._getAllNodes(); |
||||||
|
|
||||||
|
allNodes.forEach(item => { |
||||||
|
item.setChecked(isCkeckAll, false); |
||||||
|
});
|
||||||
|
} |
||||||
|
|
||||||
|
setDefaultCheckedKey(newVal) { |
||||||
|
if (newVal !== this.defaultCheckedKeys) { |
||||||
|
this.defaultCheckedKeys = newVal; |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
registerNode(node) { |
||||||
|
|
||||||
|
const key = this.key; |
||||||
|
if (!key || !node || !node.data) return; |
||||||
|
|
||||||
|
const nodeKey = node.key; |
||||||
|
if (nodeKey !== undefined) this.nodesMap[node.key] = node; |
||||||
|
} |
||||||
|
|
||||||
|
deregisterNode(node) { |
||||||
|
const key = this.key; |
||||||
|
if (!key || !node || !node.data) return; |
||||||
|
|
||||||
|
let childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
childNodes.forEach(child => { |
||||||
|
this.deregisterNode(child); |
||||||
|
}); |
||||||
|
|
||||||
|
delete this.nodesMap[node.key]; |
||||||
|
} |
||||||
|
|
||||||
|
getNodePath(data) { |
||||||
|
if (!this.key) throw new Error('[Tree] nodeKey is required in getNodePath'); |
||||||
|
const node = this.getNode(data); |
||||||
|
if (!node) return []; |
||||||
|
|
||||||
|
const path = [node.data]; |
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
while (parent && parent !== this.root) { |
||||||
|
path.push(parent.data); |
||||||
|
parent = parent.getParent(parent.parentId); |
||||||
|
} |
||||||
|
return path.reverse(); |
||||||
|
} |
||||||
|
|
||||||
|
getCheckedNodes(leafOnly = false, includeHalfChecked = false) { |
||||||
|
const checkedNodes = []; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) { |
||||||
|
checkedNodes.push(child.data); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
|
||||||
|
return checkedNodes; |
||||||
|
} |
||||||
|
|
||||||
|
getCheckedKeys(leafOnly = false, includeHalfChecked = false) { |
||||||
|
return this.getCheckedNodes(leafOnly, includeHalfChecked).map((data) => (data || {})[this.key]); |
||||||
|
} |
||||||
|
|
||||||
|
getHalfCheckedNodes() { |
||||||
|
const nodes = []; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (child.indeterminate) { |
||||||
|
nodes.push(child.data); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
|
||||||
|
return nodes; |
||||||
|
} |
||||||
|
|
||||||
|
getHalfCheckedKeys() { |
||||||
|
return this.getHalfCheckedNodes().map((data) => (data || {})[this.key]); |
||||||
|
} |
||||||
|
|
||||||
|
_getAllNodes() { |
||||||
|
const allNodes = []; |
||||||
|
const nodesMap = this.nodesMap; |
||||||
|
for (let nodeKey in nodesMap) { |
||||||
|
if (nodesMap.hasOwnProperty(nodeKey)) { |
||||||
|
allNodes.push(nodesMap[nodeKey]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return allNodes; |
||||||
|
} |
||||||
|
|
||||||
|
updateChildren(key, data) { |
||||||
|
const node = this.nodesMap[key]; |
||||||
|
if (!node) return; |
||||||
|
const childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
for (let i = childNodes.length - 1; i >= 0; i--) { |
||||||
|
const child = childNodes[i]; |
||||||
|
this.remove(child.data); |
||||||
|
} |
||||||
|
for (let i = 0, j = data.length; i < j; i++) { |
||||||
|
const child = data[i]; |
||||||
|
this.append(child, node.data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_setCheckedKeys(key, leafOnly = false, checkedKeys) { |
||||||
|
const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level); |
||||||
|
const cache = Object.create(null); |
||||||
|
const keys = Object.keys(checkedKeys); |
||||||
|
allNodes.forEach(node => node.setChecked(false, false)); |
||||||
|
for (let i = 0, j = allNodes.length; i < j; i++) { |
||||||
|
const node = allNodes[i]; |
||||||
|
let nodeKey = node.data[key]; |
||||||
|
|
||||||
|
if (typeof nodeKey === 'undefined') continue; |
||||||
|
|
||||||
|
nodeKey = nodeKey.toString(); |
||||||
|
let checked = keys.indexOf(nodeKey) > -1; |
||||||
|
if (!checked) { |
||||||
|
if (node.checked && !cache[nodeKey]) { |
||||||
|
node.setChecked(false, false); |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
while (parent && parent.level > 0) { |
||||||
|
cache[parent.data[key]] = true; |
||||||
|
parent = parent.getParent(parent.parentId); |
||||||
|
} |
||||||
|
|
||||||
|
if (node.isLeaf || this.checkStrictly) { |
||||||
|
node.setChecked(true, false); |
||||||
|
continue; |
||||||
|
} |
||||||
|
node.setChecked(true, true); |
||||||
|
|
||||||
|
if (leafOnly) { |
||||||
|
node.setChecked(false, false); |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (!child.isLeaf) { |
||||||
|
child.setChecked(false, false); |
||||||
|
} |
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
traverse(node); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setCheckedNodes(array, leafOnly = false) { |
||||||
|
const key = this.key; |
||||||
|
const checkedKeys = {}; |
||||||
|
array.forEach((item) => { |
||||||
|
checkedKeys[(item || {})[key]] = true; |
||||||
|
}); |
||||||
|
|
||||||
|
this._setCheckedKeys(key, leafOnly, checkedKeys); |
||||||
|
} |
||||||
|
|
||||||
|
setCheckedKeys(keys, leafOnly = false) { |
||||||
|
this.defaultCheckedKeys = keys; |
||||||
|
const key = this.key; |
||||||
|
const checkedKeys = {}; |
||||||
|
keys.forEach((key) => { |
||||||
|
checkedKeys[key] = true; |
||||||
|
}); |
||||||
|
|
||||||
|
this._setCheckedKeys(key, leafOnly, checkedKeys); |
||||||
|
} |
||||||
|
|
||||||
|
setDefaultExpandedKeys(keys) { |
||||||
|
keys = keys || []; |
||||||
|
this.defaultExpandedKeys = keys; |
||||||
|
|
||||||
|
keys.forEach((key) => { |
||||||
|
const node = this.getNode(key); |
||||||
|
if (node) node.expand(null, this.autoExpandParent); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
setChecked(data, checked, deep) { |
||||||
|
const node = this.getNode(data); |
||||||
|
|
||||||
|
if (node) { |
||||||
|
node.setChecked(!!checked, deep); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getCurrentNode() { |
||||||
|
return this.currentNode; |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentNode(currentNode) { |
||||||
|
const prevCurrentNode = this.currentNode; |
||||||
|
if (prevCurrentNode) { |
||||||
|
prevCurrentNode.isCurrent = false; |
||||||
|
} |
||||||
|
this.currentNode = currentNode; |
||||||
|
this.currentNode.isCurrent = true; |
||||||
|
|
||||||
|
this.expandCurrentNodeParent && this.currentNode.expand(null, true) |
||||||
|
} |
||||||
|
|
||||||
|
setUserCurrentNode(node) { |
||||||
|
const key = node[this.key]; |
||||||
|
const currNode = this.nodesMap[key]; |
||||||
|
this.setCurrentNode(currNode); |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentNodeKey(key) { |
||||||
|
if (key === null || key === undefined) { |
||||||
|
this.currentNode && (this.currentNode.isCurrent = false); |
||||||
|
this.currentNode = null; |
||||||
|
return; |
||||||
|
} |
||||||
|
const node = this.getNode(key); |
||||||
|
if (node) { |
||||||
|
this.setCurrentNode(node); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,115 @@ |
|||||||
|
export const NODE_KEY = '$treeNodeId'; |
||||||
|
|
||||||
|
export const markNodeData = function(node, data) { |
||||||
|
if (!data || data[NODE_KEY]) return; |
||||||
|
Object.defineProperty(data, NODE_KEY, { |
||||||
|
value: node.id, |
||||||
|
enumerable: false, |
||||||
|
configurable: false, |
||||||
|
writable: false |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export const getNodeKey = function(key, data) { |
||||||
|
if (!data) return null; |
||||||
|
if (!key) return data[NODE_KEY]; |
||||||
|
return data[key]; |
||||||
|
}; |
||||||
|
|
||||||
|
export const objectAssign = function(target) { |
||||||
|
for (let i = 1, j = arguments.length; i < j; i++) { |
||||||
|
let source = arguments[i] || {}; |
||||||
|
for (let prop in source) { |
||||||
|
if (source.hasOwnProperty(prop)) { |
||||||
|
let value = source[prop]; |
||||||
|
if (value !== undefined) { |
||||||
|
target[prop] = value; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return target; |
||||||
|
}; |
||||||
|
|
||||||
|
// TODO: use native Array.find, Array.findIndex when IE support is dropped
|
||||||
|
export const arrayFindIndex = function(arr, pred) { |
||||||
|
for (let i = 0; i !== arr.length; ++i) { |
||||||
|
if (pred(arr[i])) { |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getChildState = function(node) { |
||||||
|
let all = true; |
||||||
|
let none = true; |
||||||
|
let allWithoutDisable = true; |
||||||
|
for (let i = 0, j = node.length; i < j; i++) { |
||||||
|
const n = node[i]; |
||||||
|
if (n.checked !== true || n.indeterminate) { |
||||||
|
all = false; |
||||||
|
if (!n.disabled) { |
||||||
|
allWithoutDisable = false; |
||||||
|
} |
||||||
|
} |
||||||
|
if (n.checked !== false || n.indeterminate) { |
||||||
|
none = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
all, |
||||||
|
none, |
||||||
|
allWithoutDisable, |
||||||
|
half: !all && !none |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const reInitChecked = function(node) { |
||||||
|
if (!node || node.childNodesId.length === 0) return; |
||||||
|
|
||||||
|
let childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
const { |
||||||
|
all, |
||||||
|
none, |
||||||
|
half |
||||||
|
} = getChildState(childNodes); |
||||||
|
if (all) { |
||||||
|
node.checked = true; |
||||||
|
node.indeterminate = false; |
||||||
|
} else if (half) { |
||||||
|
node.checked = false; |
||||||
|
node.indeterminate = true; |
||||||
|
} else if (none) { |
||||||
|
node.checked = false; |
||||||
|
node.indeterminate = false; |
||||||
|
} |
||||||
|
|
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
if (!parent || parent.level === 0) return; |
||||||
|
|
||||||
|
if (!node.store().checkStrictly) { |
||||||
|
reInitChecked(parent); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export const getPropertyFromData = function(node, prop) { |
||||||
|
const props = node.store().props; |
||||||
|
const data = node.data || {}; |
||||||
|
const config = props[prop]; |
||||||
|
|
||||||
|
if (typeof config === 'function') { |
||||||
|
return config(data, node); |
||||||
|
} else if (typeof config === 'string') { |
||||||
|
return data[config]; |
||||||
|
} else if (typeof config === 'undefined') { |
||||||
|
const dataProp = data[prop]; |
||||||
|
return dataProp === undefined ? '' : dataProp; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export const isNull = function(v) { |
||||||
|
return v === undefined || v === null || v === ''; |
||||||
|
} |
@ -0,0 +1,184 @@ |
|||||||
|
<!-- tab组件: <me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> --> |
||||||
|
<template> |
||||||
|
<view class="me-tabs" :class="{'tabs-fixed': fixed}" :style="{height: tabHeightVal}"> |
||||||
|
<scroll-view v-if="tabs.length" :id="viewId" :scroll-left="scrollLeft" scroll-x scroll-with-animation :scroll-animation-duration="300"> |
||||||
|
<view class="tabs-item" :class="{'tabs-flex':!isScroll, 'tabs-scroll':isScroll}"> |
||||||
|
<!-- tab --> |
||||||
|
<view class="tab-item" :style="{width: tabWidthVal, height: tabHeightVal, 'line-height':tabHeightVal}" v-for="(tab, i) in tabs" :class="{'active': value===i}" :key="i" @click="tabClick(i)"> |
||||||
|
{{getTabName(tab)}} |
||||||
|
</view> |
||||||
|
<!-- 下划线 --> |
||||||
|
<view class="tabs-line" :style="{left:lineLeft}"></view> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
tabs: { // 支持格式: ['全部', '待付款'] 或 [{name:'全部'}, {name:'待付款'}] |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
nameKey: { // 取name的字段 |
||||||
|
type: String, |
||||||
|
default: 'name' |
||||||
|
}, |
||||||
|
value: { // 当前显示的下标 (使用v-model语法糖: 1.props需为value; 2.需回调input事件) |
||||||
|
type: [String, Number], |
||||||
|
default: 0 |
||||||
|
}, |
||||||
|
fixed: Boolean, // 是否悬浮,默认false |
||||||
|
tabWidth: Number, // 每个tab的宽度,默认不设置值,为flex平均分配; 如果指定宽度,则不使用flex,每个tab居左,超过则水平滑动(单位默认rpx) |
||||||
|
height: { // 高度,单位rpx |
||||||
|
type: Number, |
||||||
|
default: 90 |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), |
||||||
|
scrollLeft: 0 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
isScroll(){ |
||||||
|
return this.tabWidth && this.tabs.length // 指定了tabWidth的宽度,则支持水平滑动 |
||||||
|
}, |
||||||
|
tabHeightPx(){ |
||||||
|
return uni.upx2px(this.height) |
||||||
|
}, |
||||||
|
tabHeightVal(){ |
||||||
|
return this.tabHeightPx+'px' |
||||||
|
}, |
||||||
|
tabWidthPx(){ |
||||||
|
return uni.upx2px(this.tabWidth) |
||||||
|
}, |
||||||
|
tabWidthVal(){ |
||||||
|
return this.isScroll ? this.tabWidthPx+'px' : '' |
||||||
|
}, |
||||||
|
lineLeft() { |
||||||
|
if (this.isScroll) { |
||||||
|
return this.tabWidthPx * this.value + this.tabWidthPx/2 + 'px' // 需转为px (用rpx的话iOS真机显示有误差) |
||||||
|
} else{ |
||||||
|
return 100/this.tabs.length*(this.value + 1) - 100/(this.tabs.length*2) + '%' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
tabs() { |
||||||
|
this.warpWidth = null; // 重新计算容器宽度 |
||||||
|
this.scrollCenter(); // 水平滚动到中间 |
||||||
|
}, |
||||||
|
value() { |
||||||
|
this.scrollCenter(); // 水平滚动到中间 |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getTabName(tab){ |
||||||
|
return typeof tab === "object" ? tab[this.nameKey] : tab |
||||||
|
}, |
||||||
|
tabClick(i){ |
||||||
|
if(this.value!=i){ |
||||||
|
this.$emit("input",i); |
||||||
|
this.$emit("change",i); |
||||||
|
} |
||||||
|
}, |
||||||
|
async scrollCenter(){ |
||||||
|
if(!this.isScroll) return; |
||||||
|
if(!this.warpWidth){ // tabs容器的宽度 |
||||||
|
let rect = await this.initWarpRect() |
||||||
|
this.warpWidth = rect ? rect.width : uni.getSystemInfoSync().windowWidth; // 某些情况下取不到宽度,暂时取屏幕宽度 |
||||||
|
} |
||||||
|
let tabLeft = this.tabWidthPx * this.value + this.tabWidthPx/2; // 当前tab中心点到左边的距离 |
||||||
|
let diff = tabLeft - this.warpWidth/2 // 如果超过tabs容器的一半,则滚动差值 |
||||||
|
this.scrollLeft = diff; |
||||||
|
// #ifdef MP-TOUTIAO |
||||||
|
this.scrollTimer && clearTimeout(this.scrollTimer) |
||||||
|
this.scrollTimer = setTimeout(()=>{ // 字节跳动小程序,需延时再次设置scrollLeft,否则tab切换跨度较大时不生效 |
||||||
|
this.scrollLeft = Math.ceil(diff) |
||||||
|
},400) |
||||||
|
// #endif |
||||||
|
}, |
||||||
|
initWarpRect(){ |
||||||
|
return new Promise(resolve=>{ |
||||||
|
setTimeout(()=>{ // 延时确保dom已渲染, 不使用$nextclick |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
query.select('#'+this.viewId).boundingClientRect(data => { |
||||||
|
resolve(data) |
||||||
|
}).exec(); |
||||||
|
},20) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.scrollCenter() // 滚动到当前下标 |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.me-tabs{ |
||||||
|
position: relative; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #707070; |
||||||
|
box-sizing: border-box; |
||||||
|
overflow-y: hidden; |
||||||
|
background-color: #fff; |
||||||
|
&.tabs-fixed{ |
||||||
|
z-index: 990; |
||||||
|
position: fixed; |
||||||
|
top: var(--window-top); |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.tabs-item{ |
||||||
|
position: relative; |
||||||
|
white-space: nowrap; |
||||||
|
padding-bottom: 30rpx; // 撑开高度,再配合me-tabs的overflow-y: hidden,以达到隐藏滚动条的目的 |
||||||
|
box-sizing: border-box; |
||||||
|
.tab-item{ |
||||||
|
position: relative; |
||||||
|
text-align: center; |
||||||
|
box-sizing: border-box; |
||||||
|
&.active{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 平分的方式显示item |
||||||
|
.tabs-flex{ |
||||||
|
display: flex; |
||||||
|
.tab-item{ |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
// 居左显示item,支持水平滑动 |
||||||
|
.tabs-scroll{ |
||||||
|
.tab-item{ |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 选中tab的线 |
||||||
|
.tabs-line{ |
||||||
|
z-index: 1; |
||||||
|
position: absolute; |
||||||
|
bottom: 30rpx; // 至少与.tabs-item的padding-bottom一致,才能保证在底部边缘 |
||||||
|
width: 132rpx; |
||||||
|
height: 8rpx; |
||||||
|
transform: translateX(-50%); |
||||||
|
border-radius: 4rpx; |
||||||
|
transition: left .3s; |
||||||
|
background: #00B9FF; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,179 @@ |
|||||||
|
<!-- 视频组件: <me-video src="视频地址" poster="封面图"></me-video> |
||||||
|
video标签在APP端是原生组件, 真机APP端下拉时会渲染不及时, 出现悬浮错位现象; |
||||||
|
me-video组件, 未播放时自动展示image封面, 播放时才显示video, 提高性能; 如果播放中执行下拉,会自动显示封面, 避免视频下拉悬浮错位; |
||||||
|
--> |
||||||
|
<template> |
||||||
|
<view class="me-video" :style="{width:width, height:height}"> |
||||||
|
<!-- 播放的时候才渲染video标签 --> |
||||||
|
<video v-if="showVideo" ref="videoRef" class="video" :class="{'full-play': fullplay&&!autoplay, 'mescroll-dowload': mescrollDownLoad}" :src="src" autoplay :loop="loop" @click="videoClick" x5-playsinline="true" x5-video-player-type="h5" playsinline="true" webkit-playsinline="true" x5-video-player-fullscreen="false"></video> |
||||||
|
<!-- 播放按钮 --> |
||||||
|
<view v-else class="btn-play"> <view class="triangle"></view> </view> |
||||||
|
<!-- 封面 --> |
||||||
|
<image v-if="(!showVideo || mescrollDownLoad) && poster" class="poster" :src="poster" @click="play()" mode="aspectFit"></image> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
src: String, // 视频地址 |
||||||
|
poster: String, // 封面图 |
||||||
|
autoplay: { // 是否自动播放 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
fullplay: { // 是否全屏播放,默认不全屏 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
loop: { // 是否循环播放 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return true // 循环播放可避免Android微信播放完毕显示广告 |
||||||
|
} |
||||||
|
}, |
||||||
|
width: { // 宽度 (需带单位,支持格式: '100%', '300px', '300rpx') |
||||||
|
type: String, |
||||||
|
default: "100%" |
||||||
|
}, |
||||||
|
height: { // 高度 (需带单位,支持格式: '100%', '300px', '300rpx') |
||||||
|
type: String, |
||||||
|
default: "225px" |
||||||
|
}, |
||||||
|
mescroll: { // mescroll对象,APP端下拉刷新时显示封面,隐藏视频.缓解APP端视频下拉悬浮错位问题 |
||||||
|
type: Object, |
||||||
|
default(){ |
||||||
|
return {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
showVideo: this.autoplay // 是否播放视频 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 是否下拉中 (下拉隐藏视频,显示封面, 仅APP端生效) |
||||||
|
mescrollDownLoad() { |
||||||
|
// #ifdef APP-PLUS |
||||||
|
return this.mescroll.downLoadType |
||||||
|
// #endif |
||||||
|
// #ifndef APP-PLUS |
||||||
|
return false |
||||||
|
// #endif |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
autoplay(val) { |
||||||
|
if(val) this.play() |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 播放 |
||||||
|
play(){ |
||||||
|
this.showVideo = true |
||||||
|
this.wxAutoPlay() |
||||||
|
}, |
||||||
|
// 视频点击事件 |
||||||
|
videoClick(){ |
||||||
|
// 全屏播放时,点击视频退出 |
||||||
|
if(this.fullplay) this.showVideo = false |
||||||
|
}, |
||||||
|
// 解决微信端视频无法自动播放的问题 |
||||||
|
wxAutoPlay(){ |
||||||
|
// #ifdef H5 |
||||||
|
// 微信端 |
||||||
|
if(navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ |
||||||
|
// iOS |
||||||
|
let head = document.getElementsByTagName("head")[0] |
||||||
|
let wxscript = document.createElement("script"); |
||||||
|
wxscript.type = "text/javascript" |
||||||
|
wxscript.src = "https://res.wx.qq.com/open/js/jweixin-1.6.0.js" |
||||||
|
head.appendChild(wxscript) |
||||||
|
let vm = this |
||||||
|
let doPlay = function(){ |
||||||
|
vm.$refs.videoRef && vm.$refs.videoRef.play() |
||||||
|
} |
||||||
|
wxscript.onload = function(){ |
||||||
|
window.wx.config({ |
||||||
|
debug: !1, |
||||||
|
appId: "", |
||||||
|
timestamp: 1, |
||||||
|
nonceStr: "", |
||||||
|
signature: "", |
||||||
|
jsApiList: [] |
||||||
|
}) |
||||||
|
window.wx.ready(doPlay) |
||||||
|
} |
||||||
|
// Android |
||||||
|
document.addEventListener("WeixinJSBridgeReady", doPlay, false); |
||||||
|
// 先尝试播放 |
||||||
|
setTimeout(()=>{ |
||||||
|
doPlay() |
||||||
|
},20) |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.me-video{ |
||||||
|
position: relative; |
||||||
|
background-color: #000; |
||||||
|
overflow: hidden; |
||||||
|
// 播放按钮 |
||||||
|
.btn-play{ |
||||||
|
z-index: 9; |
||||||
|
position: absolute; |
||||||
|
left: 50%; |
||||||
|
top: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
width: 100rpx; |
||||||
|
height: 100rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: rgba(0,0,0,.75); |
||||||
|
pointer-events: none; |
||||||
|
.triangle{ |
||||||
|
position: absolute; |
||||||
|
left: 50%; |
||||||
|
top: 50%; |
||||||
|
transform: translate(-25%, -50%); |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-top: 16rpx solid transparent; |
||||||
|
border-left: 24rpx solid #fff; |
||||||
|
border-bottom: 16rpx solid transparent; |
||||||
|
} |
||||||
|
} |
||||||
|
// 封面图 |
||||||
|
.poster{ |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
vertical-align: bottom; |
||||||
|
} |
||||||
|
// 视频 (默认非全屏播放) |
||||||
|
.video{ |
||||||
|
z-index: 8; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
// 全屏播放 |
||||||
|
&.full-play{ |
||||||
|
z-index: 999; |
||||||
|
position: fixed; |
||||||
|
} |
||||||
|
// 下拉时隐藏视频 |
||||||
|
&.mescroll-dowload{ |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,47 @@ |
|||||||
|
/*下拉刷新--标语*/ |
||||||
|
.mescroll-downwarp .downwarp-slogan{ |
||||||
|
display: block; |
||||||
|
width: 420rpx; |
||||||
|
height: 168rpx; |
||||||
|
margin: auto; |
||||||
|
} |
||||||
|
/*下拉刷新--向下进度动画*/ |
||||||
|
.mescroll-downwarp .downwarp-progress{ |
||||||
|
display: inline-block; |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-position: center; |
||||||
|
background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png); |
||||||
|
transition: all 300ms; |
||||||
|
} |
||||||
|
/*下拉刷新--进度条*/ |
||||||
|
.mescroll-downwarp .downwarp-loading{ |
||||||
|
display: inline-block; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
border: 2rpx solid #FF8095; |
||||||
|
border-bottom-color: transparent; |
||||||
|
} |
||||||
|
/*下拉刷新--吉祥物*/ |
||||||
|
.mescroll-downwarp .downwarp-mascot{ |
||||||
|
position: absolute; |
||||||
|
right: 16rpx; |
||||||
|
bottom: 0; |
||||||
|
width: 100rpx; |
||||||
|
height: 100rpx; |
||||||
|
background-size: contain; |
||||||
|
background-repeat: no-repeat; |
||||||
|
animation: animMascot .6s steps(1,end) infinite; |
||||||
|
} |
||||||
|
@keyframes animMascot { |
||||||
|
0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)} |
||||||
|
25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)} |
||||||
|
50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)} |
||||||
|
75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)} |
||||||
|
100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
<!-- 下拉刷新区域 --> |
||||||
|
<template> |
||||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object , // down的配置项 |
||||||
|
type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption(){ |
||||||
|
return this.option || {} |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.type === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "./mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,330 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
@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"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</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端生效) --> |
||||||
|
<!-- #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> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
}; |
||||||
|
}, |
||||||
|
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 |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
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 this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
}, |
||||||
|
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: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
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为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} 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> |
||||||
|
@import "../../mescroll-uni/mescroll-body.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,29 @@ |
|||||||
|
// mescroll-uni和mescroll-body 的全局配置
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 暂无相关数据 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -0,0 +1,406 @@ |
|||||||
|
<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-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<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"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</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端生效) --> |
||||||
|
<!-- #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> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
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 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
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 this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
}, |
||||||
|
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已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).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组件时,此行不可删) |
||||||
|
}, |
||||||
|
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'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (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 |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
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/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,44 @@ |
|||||||
|
/*下拉刷新--上下箭头*/ |
||||||
|
.mescroll-downwarp .downwarp-arrow { |
||||||
|
display: inline-block; |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
margin: 10px; |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png); |
||||||
|
background-size: contain; |
||||||
|
vertical-align: middle; |
||||||
|
transition: all 300ms; |
||||||
|
} |
||||||
|
|
||||||
|
/*下拉刷新--旋转进度条*/ |
||||||
|
.mescroll-downwarp .downwarp-progress{ |
||||||
|
width: 36px; |
||||||
|
height: 36px; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
animation: progressRotate 0.6s steps(6, start) infinite; |
||||||
|
} |
||||||
|
@keyframes progressRotate { |
||||||
|
0% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
16% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png); |
||||||
|
} |
||||||
|
32% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png); |
||||||
|
} |
||||||
|
48% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png); |
||||||
|
} |
||||||
|
64% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png); |
||||||
|
} |
||||||
|
80% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png); |
||||||
|
} |
||||||
|
100% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
<!-- 下拉刷新区域 --> |
||||||
|
<template> |
||||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view> |
||||||
|
<view class="downwarp-tip">{{ downText }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object, // down的配置项 |
||||||
|
type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption() { |
||||||
|
return this.option || {}; |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading() { |
||||||
|
return this.type === 3; |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate() { |
||||||
|
return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
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-uni/components/mescroll-down.css'; |
||||||
|
@import './mescroll-down.css'; |
||||||
|
</style> |
@ -0,0 +1,32 @@ |
|||||||
|
/*上拉加载--旋转进度条*/ |
||||||
|
.mescroll-upwarp .upwarp-progress { |
||||||
|
width: 36px; |
||||||
|
height: 36px; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
animation: progressRotate 0.6s steps(6, start) infinite; |
||||||
|
} |
||||||
|
@keyframes progressRotate { |
||||||
|
0% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
16% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png); |
||||||
|
} |
||||||
|
32% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png); |
||||||
|
} |
||||||
|
48% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png); |
||||||
|
} |
||||||
|
64% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png); |
||||||
|
} |
||||||
|
80% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png); |
||||||
|
} |
||||||
|
100% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
<!-- 上拉加载区域 --> |
||||||
|
<template> |
||||||
|
<view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="isUpLoading"> |
||||||
|
<view class="upwarp-progress mescroll-rotate"></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 // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption() { |
||||||
|
return this.option || {}; |
||||||
|
}, |
||||||
|
// 加载中 |
||||||
|
isUpLoading() { |
||||||
|
return this.type === 1; |
||||||
|
}, |
||||||
|
// 没有更多了 |
||||||
|
isUpNoMore() { |
||||||
|
return this.type === 2; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import '../../../mescroll-uni/components/mescroll-up.css'; |
||||||
|
@import './mescroll-up.css'; |
||||||
|
</style> |
@ -0,0 +1,350 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
@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"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ 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 && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view 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"></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端生效) --> |
||||||
|
<!-- #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> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
}; |
||||||
|
}, |
||||||
|
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 |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
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 this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText() { |
||||||
|
if(!this.mescroll) return ""; |
||||||
|
switch (this.downLoadType) { |
||||||
|
case 1: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: |
||||||
|
return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: |
||||||
|
return this.mescroll.optDown.textLoading; |
||||||
|
case 4: |
||||||
|
return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
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: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
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为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} 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> |
||||||
|
@import "../../mescroll-uni/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import "./components/mescroll-up.css"; |
||||||
|
</style> |
@ -0,0 +1,36 @@ |
|||||||
|
// 全局配置
|
||||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 0, // 延时结束的时长 (此处设置为0)
|
||||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 空空如也 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -0,0 +1,430 @@ |
|||||||
|
<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-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<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"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ 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 && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view 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"></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端生效) --> |
||||||
|
<!-- #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> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
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 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
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 this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText() { |
||||||
|
if(!this.mescroll) return ""; |
||||||
|
switch (this.downLoadType) { |
||||||
|
case 1: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: |
||||||
|
return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: |
||||||
|
return this.mescroll.optDown.textLoading; |
||||||
|
case 4: |
||||||
|
return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
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已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).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组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
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'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (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 |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
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/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import "./components/mescroll-up.css"; |
||||||
|
</style> |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption(){ |
||||||
|
return this.option || {} |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.type === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return 'rotate(' + 360 * this.rate + 'deg)' |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText(){ |
||||||
|
switch (this.type){ |
||||||
|
case 1: return this.mOption.textInOffset; |
||||||
|
case 2: return this.mOption.textOutOffset; |
||||||
|
case 3: return this.mOption.textLoading; |
||||||
|
case 4: return this.mOption.textLoading; |
||||||
|
default: return this.mOption.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "./mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,90 @@ |
|||||||
|
<!--空布局 |
||||||
|
|
||||||
|
可作为独立的组件, 不使用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; |
||||||
|
width: 100%; |
||||||
|
padding: 100rpx 50rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty.empty-fixed { |
||||||
|
z-index: 99; |
||||||
|
position: absolute; /*transform会使fixed失效,最终会降级为absolute */ |
||||||
|
top: 100rpx; |
||||||
|
left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-icon { |
||||||
|
width: 280rpx; |
||||||
|
height: 280rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-tip { |
||||||
|
margin-top: 20rpx; |
||||||
|
font-size: 24rpx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
.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> |
@ -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> |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 // 上拉加载的状态:0(loading前),1(loading中),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> |
@ -0,0 +1,19 @@ |
|||||||
|
.mescroll-body { |
||||||
|
position: relative; /* 下拉刷新区域相对自身定位 */ |
||||||
|
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/ |
||||||
|
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */ |
||||||
|
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */ |
||||||
|
.mescroll-body.mescorll-sticky{ |
||||||
|
overflow: unset !important |
||||||
|
} |
||||||
|
|
||||||
|
/* 适配 iPhoneX */ |
||||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||||
|
.mescroll-safearea { |
||||||
|
padding-bottom: constant(safe-area-inset-bottom); |
||||||
|
padding-bottom: env(safe-area-inset-bottom); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,348 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp" |
||||||
|
> |
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> |
||||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||||
|
</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> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || 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, // 上拉加载状态:0(loading前),1(loading中),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 |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
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.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
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: { |
||||||
|
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组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
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为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} 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> |
||||||
|
@import "./mescroll-body.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import './components/mescroll-up.css'; |
||||||
|
</style> |
@ -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对象 (兼容字节跳动小程序)
|
||||||
|
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; |
@ -0,0 +1,36 @@ |
|||||||
|
// 全局配置
|
||||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 空空如也 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,799 @@ |
|||||||
|
/* mescroll |
||||||
|
* version 1.3.3 |
||||||
|
* 2020-09-15 wenju |
||||||
|
* https://www.mescroll.com
|
||||||
|
*/ |
||||||
|
|
||||||
|
export default function MeScroll(options, isScrollBody) { |
||||||
|
let me = this; |
||||||
|
me.version = '1.3.3'; // 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: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||||
|
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: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
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.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
|
||||||
|
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(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
|
||||||
|
} |
||||||
|
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.isDownEndSuccess = true |
||||||
|
me.endDownScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
// 结束上拉加载
|
||||||
|
if (me.optUp.use) { |
||||||
|
let isShowNoMore; // 是否已无更多数据
|
||||||
|
if (dataSize != null) { |
||||||
|
let pageNum = me.optUp.page.num; // 当前页码
|
||||||
|
let pageSize = me.optUp.page.size; // 每页长度
|
||||||
|
// 如果是第一页
|
||||||
|
if (pageNum === 1) { |
||||||
|
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
|
||||||
|
} |
||||||
|
if (dataSize < pageSize || hasNext === false) { |
||||||
|
// 返回的数据不满一页时,则说明已无更多数据
|
||||||
|
me.optUp.hasNext = false; |
||||||
|
if (dataSize === 0 && pageNum === 1) { |
||||||
|
// 如果第一页无任何数据且配置了空布局
|
||||||
|
isShowNoMore = false; |
||||||
|
me.showEmpty(); |
||||||
|
} 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) { |
||||||
|
this.isDownEndSuccess = false |
||||||
|
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() |
||||||
|
} |
@ -0,0 +1,424 @@ |
|||||||
|
<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-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<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"> |
||||||
|
<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> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || 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 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
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.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
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已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).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组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
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'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (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 |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
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> |
@ -0,0 +1,48 @@ |
|||||||
|
/** |
||||||
|
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期 |
||||||
|
*/ |
||||||
|
const MescrollCompMixin = { |
||||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
|
||||||
|
onPageScroll(e) { |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom() { |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||||
|
onPullDownRefresh(){ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
}, |
||||||
|
// mescroll-body写在子子子...组件的情况 (多级)
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: { |
||||||
|
onPageScroll: e=>{ |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom: ()=>{ |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
onPullDownRefresh: ()=>{ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
handlePageScroll(e){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onPageScroll(e); |
||||||
|
}, |
||||||
|
handleReachBottom(){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onReachBottom(); |
||||||
|
}, |
||||||
|
handlePullDownRefresh(){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onPullDownRefresh(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default MescrollCompMixin; |
@ -0,0 +1,59 @@ |
|||||||
|
/** |
||||||
|
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例) |
||||||
|
*/ |
||||||
|
const MescrollMoreItemMixin = { |
||||||
|
// 支付宝小程序不支持props的mixin,需写在具体的页面中
|
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||||
|
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: { |
||||||
|
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||||
|
mescrollInitByRef() { |
||||||
|
if(!this.mescroll || !this.mescroll.resetUpScroll){ |
||||||
|
// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
|
||||||
|
let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i]; |
||||||
|
if(mescrollRef) this.mescroll = mescrollRef.mescroll |
||||||
|
} |
||||||
|
}, |
||||||
|
// 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; |
@ -0,0 +1,74 @@ |
|||||||
|
/** |
||||||
|
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期 |
||||||
|
*/ |
||||||
|
const MescrollMoreMixin = { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
tabIndex: 0, // 当前tab下标
|
||||||
|
mescroll: { |
||||||
|
onPageScroll: e=>{ |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom: ()=>{ |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
onPullDownRefresh: ()=>{ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
|
||||||
|
onPageScroll(e) { |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom() { |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||||
|
onPullDownRefresh(){ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
handlePageScroll(e){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onPageScroll(e); |
||||||
|
}, |
||||||
|
handleReachBottom(){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onReachBottom(); |
||||||
|
}, |
||||||
|
handlePullDownRefresh(){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onPullDownRefresh(); |
||||||
|
}, |
||||||
|
// 根据下标获取对应子组件的mescroll
|
||||||
|
getMescroll(i){ |
||||||
|
if(!this.mescrollItems) this.mescrollItems = []; |
||||||
|
if(!this.mescrollItems[i]) { |
||||||
|
// v-for中的refs
|
||||||
|
let vForItem = this.$refs["mescrollItem"]; |
||||||
|
if(vForItem){ |
||||||
|
this.mescrollItems[i] = vForItem[i] |
||||||
|
}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; |
@ -0,0 +1,109 @@ |
|||||||
|
// 定义在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 || MP-QQ || APP-PLUS || H5
|
||||||
|
wxsBiz: { |
||||||
|
//注册列表touchstart事件,用于下拉刷新
|
||||||
|
touchstartEvent: e=> { |
||||||
|
this.mescroll.touchstartEvent(e); |
||||||
|
}, |
||||||
|
//注册列表touchmove事件,用于下拉刷新
|
||||||
|
touchmoveEvent: e=> { |
||||||
|
this.mescroll.touchmoveEvent(e); |
||||||
|
}, |
||||||
|
//注册列表touchend事件,用于下拉刷新
|
||||||
|
touchendEvent: e=> { |
||||||
|
this.mescroll.touchendEvent(e); |
||||||
|
}, |
||||||
|
propObserver(){}, // 抹平wxs的写法
|
||||||
|
callObserver(){} // 抹平wxs的写法
|
||||||
|
}, |
||||||
|
// #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 |
||||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||||
|
// 重置是否加载成功的状态
|
||||||
|
this.$set(this.mescroll, 'isDownEndSuccess', null) |
||||||
|
}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 || MP-QQ || APP-PLUS || H5
|
||||||
|
// 配置主动触发wxs显示加载进度的回调
|
||||||
|
this.mescroll.optDown.afterLoading = ()=>{ |
||||||
|
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
} |
||||||
|
// 配置主动触发wxs隐藏加载进度的回调
|
||||||
|
this.mescroll.optDown.afterEndDownScroll = ()=>{ |
||||||
|
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0) |
||||||
|
setTimeout(()=>{ |
||||||
|
if(this.downLoadType === 4 || this.downLoadType === 0){ |
||||||
|
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
} |
||||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||||
|
}, delay) |
||||||
|
} |
||||||
|
// 初始化wxs的数据
|
||||||
|
this.wxsCall({type: 'setWxsProp'}) |
||||||
|
// #endif
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default WxsMixin; |
@ -0,0 +1,92 @@ |
|||||||
|
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
|
||||||
|
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
|
||||||
|
// https://uniapp.dcloud.io/frame?id=renderjs
|
||||||
|
|
||||||
|
// 与wxs的me实例一致
|
||||||
|
var me = {} |
||||||
|
|
||||||
|
// 初始化window对象的touch事件 (仅初始化一次)
|
||||||
|
if(window && !window.$mescrollRenderInit){ |
||||||
|
window.$mescrollRenderInit = true |
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', function(e){ |
||||||
|
if (me.disabled()) return; |
||||||
|
me.startPoint = me.getPoint(e); // 记录起点
|
||||||
|
}, {passive: true}) |
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', function(e){ |
||||||
|
if (me.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; |
@ -0,0 +1,268 @@ |
|||||||
|
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响 |
||||||
|
// https://uniapp.dcloud.io/frame?id=wxs |
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html |
||||||
|
|
||||||
|
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致 |
||||||
|
var me = {} |
||||||
|
|
||||||
|
// ------ 自定义下拉刷新动画 start ------ |
||||||
|
|
||||||
|
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */ |
||||||
|
me.onMoving = function (ins, rate, downHight){ |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题 |
||||||
|
'transform': 'translateY(' + downHight + 'px)', |
||||||
|
'transition': '' |
||||||
|
}) |
||||||
|
// 环形进度条 |
||||||
|
var progress = ins.selectComponent('.mescroll-wxs-progress') |
||||||
|
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示下拉刷新进度 */ |
||||||
|
me.showLoading = function (ins){ |
||||||
|
me.downHight = me.optDown.offset |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': 'auto', |
||||||
|
'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({ |
||||||
|
'will-change': 'auto', |
||||||
|
'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({ |
||||||
|
'will-change': '', |
||||||
|
'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) { |
||||||
|
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.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 |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<view class="cu-timeline"> |
||||||
|
<view class="cu-time">审核流程进程</view> |
||||||
|
<view class="cu-item cur cuIcon-noticefill"> |
||||||
|
<view class="content bg-green shadow-blur"> |
||||||
|
<text>22:22</text> 【广州市】快件已到达地球 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-red cuIcon-attentionforbidfill"> |
||||||
|
<view class="content bg-red shadow-blur"> |
||||||
|
这是第一次,我家的铲屎官走了这么久。久到足足有三天!! |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-grey cuIcon-evaluate_fill"> |
||||||
|
<view class="content bg-grey shadow-blur"> |
||||||
|
这是第一次,我家的铲屎官走了这么久。 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-blue"> |
||||||
|
<view class="bg-blue content"> |
||||||
|
<text>20:00</text> 【月球】快件已到达月球,准备发往地球 |
||||||
|
</view> |
||||||
|
<view class="bg-cyan content"> |
||||||
|
<text>10:00</text> 【银河系】快件已到达银河系,准备发往月球 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-timeline mat15"> |
||||||
|
<view class="cu-time">业务流程进程</view> |
||||||
|
<view class="cu-item cur cuIcon-noticefill"> |
||||||
|
<view class="content bg-green shadow-blur"> |
||||||
|
<text>22:22</text> 【广州市】快件已到达地球 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-red cuIcon-attentionforbidfill"> |
||||||
|
<view class="content bg-red shadow-blur"> |
||||||
|
这是第一次,我家的铲屎官走了这么久。久到足足有三天!! |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-grey cuIcon-evaluate_fill"> |
||||||
|
<view class="content bg-grey shadow-blur"> |
||||||
|
这是第一次,我家的铲屎官走了这么久。 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-item text-blue"> |
||||||
|
<view class="bg-blue content"> |
||||||
|
<text>20:00</text> 【月球】快件已到达月球,准备发往地球 |
||||||
|
</view> |
||||||
|
<view class="bg-cyan content"> |
||||||
|
<text>10:00</text> 【银河系】快件已到达银河系,准备发往月球 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
</style> |
@ -0,0 +1,161 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in list" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/pages/investigationSee/investigationSee')">查看</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/pages/assignAB/assignAB')">指派</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
listTouchStart: { |
||||||
|
type: Number, |
||||||
|
default(){ |
||||||
|
return 0 |
||||||
|
} |
||||||
|
}, |
||||||
|
listTouchDirection: { |
||||||
|
type: String, |
||||||
|
default(){ |
||||||
|
return '' |
||||||
|
} |
||||||
|
}, |
||||||
|
modalName: { |
||||||
|
type: String, |
||||||
|
default(){ |
||||||
|
return '' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
// this.listTouchStart = e.touches[0].pageX |
||||||
|
this.$emit('TouchStart', e); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
// this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
this.$emit('TouchMove', e); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
this.$emit('TouchEnd', e); |
||||||
|
// if (this.listTouchDirection == 'left') { |
||||||
|
// this.modalName = e.currentTarget.dataset.target |
||||||
|
// } else { |
||||||
|
// this.modalName = null |
||||||
|
// } |
||||||
|
// this.listTouchDirection = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
font-weight: bold; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
.time-text{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
.status-text{ |
||||||
|
font-size: 28rpx; |
||||||
|
padding: 8rpx 12rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,316 @@ |
|||||||
|
|
||||||
|
const events = { |
||||||
|
load: 'load', |
||||||
|
error: 'error' |
||||||
|
} |
||||||
|
const pageMode = { |
||||||
|
add: 'add', |
||||||
|
replace: 'replace' |
||||||
|
} |
||||||
|
|
||||||
|
const attrs = [ |
||||||
|
'pageCurrent', |
||||||
|
'pageSize', |
||||||
|
'collection', |
||||||
|
'action', |
||||||
|
'field', |
||||||
|
'getcount', |
||||||
|
'orderby', |
||||||
|
'where' |
||||||
|
] |
||||||
|
|
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
loading: false, |
||||||
|
listData: this.getone ? {} : [], |
||||||
|
paginationInternal: { |
||||||
|
current: this.pageCurrent, |
||||||
|
size: this.pageSize, |
||||||
|
count: 0 |
||||||
|
}, |
||||||
|
errorMessage: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
let db = null; |
||||||
|
let dbCmd = null; |
||||||
|
|
||||||
|
if(this.collection){ |
||||||
|
this.db = uniCloud.database(); |
||||||
|
this.dbCmd = this.db.command; |
||||||
|
} |
||||||
|
|
||||||
|
this._isEnded = false |
||||||
|
|
||||||
|
this.$watch(() => { |
||||||
|
var al = [] |
||||||
|
attrs.forEach(key => { |
||||||
|
al.push(this[key]) |
||||||
|
}) |
||||||
|
return al |
||||||
|
}, (newValue, oldValue) => { |
||||||
|
this.paginationInternal.pageSize = this.pageSize |
||||||
|
|
||||||
|
let needReset = false |
||||||
|
for (let i = 2; i < newValue.length; i++) { |
||||||
|
if (newValue[i] != oldValue[i]) { |
||||||
|
needReset = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if (needReset) { |
||||||
|
this.clear() |
||||||
|
this.reset() |
||||||
|
} |
||||||
|
if (newValue[0] != oldValue[0]) { |
||||||
|
this.paginationInternal.current = this.pageCurrent |
||||||
|
} |
||||||
|
|
||||||
|
this._execLoadData() |
||||||
|
}) |
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (process.env.NODE_ENV === 'development') { |
||||||
|
this._debugDataList = [] |
||||||
|
if (!window.unidev) { |
||||||
|
window.unidev = { |
||||||
|
clientDB: { |
||||||
|
data: [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
unidev.clientDB.data.push(this._debugDataList) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-TOUTIAO
|
||||||
|
let changeName |
||||||
|
let events = this.$scope.dataset.eventOpts |
||||||
|
for (var i = 0; i < events.length; i++) { |
||||||
|
let event = events[i] |
||||||
|
if (event[0].includes('^load')) { |
||||||
|
changeName = event[1][0][0] |
||||||
|
} |
||||||
|
} |
||||||
|
if (changeName) { |
||||||
|
let parent = this.$parent |
||||||
|
let maxDepth = 16 |
||||||
|
this._changeDataFunction = null |
||||||
|
while (parent && maxDepth > 0) { |
||||||
|
let fun = parent[changeName] |
||||||
|
if (fun && typeof fun === 'function') { |
||||||
|
this._changeDataFunction = fun |
||||||
|
maxDepth = 0 |
||||||
|
break |
||||||
|
} |
||||||
|
parent = parent.$parent |
||||||
|
maxDepth--; |
||||||
|
} |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
|
||||||
|
// if (!this.manual) {
|
||||||
|
// this.loadData()
|
||||||
|
// }
|
||||||
|
}, |
||||||
|
// #ifdef H5
|
||||||
|
beforeDestroy() { |
||||||
|
if (process.env.NODE_ENV === 'development' && window.unidev) { |
||||||
|
var cd = this._debugDataList |
||||||
|
var dl = unidev.clientDB.data |
||||||
|
for (var i = dl.length - 1; i >= 0; i--) { |
||||||
|
if (dl[i] === cd) { |
||||||
|
dl.splice(i, 1) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// #endif
|
||||||
|
methods: { |
||||||
|
loadData(args1, args2) { |
||||||
|
let callback = null |
||||||
|
if (typeof args1 === 'object') { |
||||||
|
if (args1.clear) { |
||||||
|
this.clear() |
||||||
|
this.reset() |
||||||
|
} |
||||||
|
if (args1.current !== undefined) { |
||||||
|
this.paginationInternal.current = args1.current |
||||||
|
} |
||||||
|
if (typeof args2 === 'function') { |
||||||
|
callback = args2 |
||||||
|
} |
||||||
|
} else if (typeof args1 === 'function') { |
||||||
|
callback = args1 |
||||||
|
} |
||||||
|
|
||||||
|
this._execLoadData(callback) |
||||||
|
}, |
||||||
|
loadMore() { |
||||||
|
if (this._isEnded) { |
||||||
|
return |
||||||
|
} |
||||||
|
this._execLoadData() |
||||||
|
}, |
||||||
|
refresh() { |
||||||
|
this.clear() |
||||||
|
this._execLoadData() |
||||||
|
}, |
||||||
|
clear() { |
||||||
|
this._isEnded = false |
||||||
|
this.listData = [] |
||||||
|
}, |
||||||
|
reset() { |
||||||
|
this.paginationInternal.current = 1 |
||||||
|
}, |
||||||
|
remove(id, { |
||||||
|
action, |
||||||
|
callback, |
||||||
|
confirmTitle, |
||||||
|
confirmContent |
||||||
|
} = {}) { |
||||||
|
if (!id || !id.length) { |
||||||
|
return |
||||||
|
} |
||||||
|
uni.showModal({ |
||||||
|
title: confirmTitle || '提示', |
||||||
|
content: confirmContent || '是否删除该数据', |
||||||
|
showCancel: true, |
||||||
|
success: (res) => { |
||||||
|
if (!res.confirm) { |
||||||
|
return |
||||||
|
} |
||||||
|
this._execRemove(id, action, callback) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
_execLoadData(callback) { |
||||||
|
if (this.loading) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.loading = true |
||||||
|
this.errorMessage = '' |
||||||
|
|
||||||
|
this._getExec().then((res) => { |
||||||
|
this.loading = false |
||||||
|
const { |
||||||
|
data, |
||||||
|
count |
||||||
|
} = res.result |
||||||
|
this._isEnded = data.length < this.pageSize |
||||||
|
|
||||||
|
callback && callback(data, this._isEnded) |
||||||
|
this._dispatchEvent(events.load, data) |
||||||
|
|
||||||
|
if (this.getone) { |
||||||
|
this.listData = data.length ? data[0] : undefined |
||||||
|
} else if (this.pageData === pageMode.add) { |
||||||
|
this.listData.push(...data) |
||||||
|
if (this.listData.length) { |
||||||
|
this.paginationInternal.current++ |
||||||
|
} |
||||||
|
} else if (this.pageData === pageMode.replace) { |
||||||
|
this.listData = data |
||||||
|
this.paginationInternal.count = count |
||||||
|
} |
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (process.env.NODE_ENV === 'development') { |
||||||
|
this._debugDataList.length = 0 |
||||||
|
this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData))) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
}).catch((err) => { |
||||||
|
this.loading = false |
||||||
|
this.errorMessage = err |
||||||
|
callback && callback() |
||||||
|
this.$emit(events.error, err) |
||||||
|
}) |
||||||
|
}, |
||||||
|
_getExec() { |
||||||
|
let exec = this.db |
||||||
|
if (this.action) { |
||||||
|
exec = exec.action(this.action) |
||||||
|
} |
||||||
|
|
||||||
|
exec = exec.collection(this.collection) |
||||||
|
|
||||||
|
if (!(!this.where || !Object.keys(this.where).length)) { |
||||||
|
exec = exec.where(this.where) |
||||||
|
} |
||||||
|
if (this.field) { |
||||||
|
exec = exec.field(this.field) |
||||||
|
} |
||||||
|
if (this.orderby) { |
||||||
|
exec = exec.orderBy(this.orderby) |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
current, |
||||||
|
size |
||||||
|
} = this.paginationInternal |
||||||
|
exec = exec.skip(size * (current - 1)).limit(size).get({ |
||||||
|
getCount: this.getcount |
||||||
|
}) |
||||||
|
|
||||||
|
return exec |
||||||
|
}, |
||||||
|
_execRemove(id, action, callback) { |
||||||
|
if (!this.collection || !id) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const ids = Array.isArray(id) ? id : [id] |
||||||
|
if (!ids.length) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
uni.showLoading({ |
||||||
|
mask: true |
||||||
|
}) |
||||||
|
|
||||||
|
let exec = this.db |
||||||
|
if (action) { |
||||||
|
exec = exec.action(action) |
||||||
|
} |
||||||
|
|
||||||
|
exec.collection(this.collection).where({ |
||||||
|
_id: dbCmd.in(ids) |
||||||
|
}).remove().then((res) => { |
||||||
|
callback && callback(res.result) |
||||||
|
if (this.pageData === pageMode.replace) { |
||||||
|
this.refresh() |
||||||
|
} else { |
||||||
|
this.removeData(ids) |
||||||
|
} |
||||||
|
}).catch((err) => { |
||||||
|
uni.showModal({ |
||||||
|
content: err.message, |
||||||
|
showCancel: false |
||||||
|
}) |
||||||
|
}).finally(() => { |
||||||
|
uni.hideLoading() |
||||||
|
}) |
||||||
|
}, |
||||||
|
removeData(ids) { |
||||||
|
let il = ids.slice(0) |
||||||
|
let dl = this.listData |
||||||
|
for (let i = dl.length - 1; i >= 0; i--) { |
||||||
|
let index = il.indexOf(dl[i]._id) |
||||||
|
if (index >= 0) { |
||||||
|
dl.splice(i, 1) |
||||||
|
il.splice(index, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
_dispatchEvent(type, data) { |
||||||
|
if (this._changeDataFunction) { |
||||||
|
this._changeDataFunction(data, this._isEnded) |
||||||
|
} else { |
||||||
|
this.$emit(type, data, this._isEnded) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,833 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-data-checklist"> |
||||||
|
<template v-if="loading"> |
||||||
|
<view class="uni-data-loading"> |
||||||
|
<uni-load-more status="loading" iconType="snow" :iconSize="18" :content-text="contentText"></uni-load-more> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<checkbox-group v-if="multiple" class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="chagne"> |
||||||
|
<label class="checklist-box" :class="item.labelClass" :style="[item.styleBackgroud]" v-for="(item,index) in dataList" |
||||||
|
:key="index"> |
||||||
|
<checkbox class="hidden" hidden :disabled="!!item.disabled" :value="item.value+''" :checked="item.selected" /> |
||||||
|
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="checkbox__inner" |
||||||
|
:class="item.checkboxBgClass" :style="[item.styleIcon]"> |
||||||
|
<view class="checkbox__inner-icon" :class="item.checkboxClass"></view> |
||||||
|
</view> |
||||||
|
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}"> |
||||||
|
<text class="checklist-text" :class="item.textClass" :style="[item.styleIconText]">{{item.text}}</text> |
||||||
|
<uni-easyinput v-if="inputVisble" disabled type="text" :inputBorder="true" v-model="item.InputValue" placeholder="请输入"></uni-easyinput> |
||||||
|
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :class="item.listClass" :style="[item.styleBackgroud]"></view> |
||||||
|
</view> |
||||||
|
</label> |
||||||
|
</checkbox-group> |
||||||
|
<radio-group v-else class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="chagne"> |
||||||
|
<label class="checklist-box" :class="item.labelClass" :style="[item.styleBackgroud]" v-for="(item,index) in dataList" :key="index"> |
||||||
|
<radio hidden :disabled="item.disabled" :value="item.value+''" :checked="item.selected" /> |
||||||
|
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="radio__inner" |
||||||
|
:class="item.checkboxBgClass" :style="[item.styleBackgroud]"> |
||||||
|
<view class="radio__inner-icon" :class="item.checkboxClass" :style="[item.styleIcon]"></view> |
||||||
|
</view> |
||||||
|
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}"> |
||||||
|
<text class="checklist-text" :class="item.textClass" :style="[item.styleIconText]">{{item.text}}</text> |
||||||
|
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :class="item.listClass" :style="[item.styleRightIcon]"></view> |
||||||
|
</view> |
||||||
|
</label> |
||||||
|
</radio-group> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* DataChecklist 数据选择器 |
||||||
|
* @description 通过数据渲染 checkbox 和 radio |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx |
||||||
|
* @property {String} mode = [default| list | button | tag] 显示模式 |
||||||
|
* @value default 默认横排模式 |
||||||
|
* @value list 列表模式 |
||||||
|
* @value button 按钮模式 |
||||||
|
* @value tag 标签模式 |
||||||
|
* @property {Boolean} multiple = [true|false] 是否多选 |
||||||
|
* @property {Array|String|Number} value 默认值 |
||||||
|
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}] |
||||||
|
* @property {Number|String} min 最小选择个数 ,multiple为true时生效 |
||||||
|
* @property {Number|String} max 最大选择个数 ,multiple为true时生效 |
||||||
|
* @property {Boolean} wrap 是否换行显示 |
||||||
|
* @property {String} icon = [left|right] list 列表模式下icon显示位置 |
||||||
|
* @property {Boolean} selectedColor 选中颜色 |
||||||
|
* @property {Boolean} selectedTextColor 选中文本颜色,如不填写则自动显示 |
||||||
|
* @value left 左侧显示 |
||||||
|
* @value right 右侧显示 |
||||||
|
* @event {Function} change 选中发生变化触发 |
||||||
|
*/ |
||||||
|
|
||||||
|
import clientdb from './clientdb.js' |
||||||
|
export default { |
||||||
|
name: 'uniDataChecklist', |
||||||
|
mixins: [clientdb], |
||||||
|
props: { |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'default' |
||||||
|
}, |
||||||
|
multiple: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [Array, String, Number], |
||||||
|
default () { |
||||||
|
return '' |
||||||
|
} |
||||||
|
}, |
||||||
|
localdata: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
min: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
max: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
wrap: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
icon: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
selectedColor:{ |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
selectedTextColor:{ |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// clientDB 相关 |
||||||
|
options: { |
||||||
|
type: [Object, Array], |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
collection: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
action: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
field: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
pageData: { |
||||||
|
type: String, |
||||||
|
default: 'add' |
||||||
|
}, |
||||||
|
pageCurrent: { |
||||||
|
type: Number, |
||||||
|
default: 1 |
||||||
|
}, |
||||||
|
pageSize: { |
||||||
|
type: Number, |
||||||
|
default: 20 |
||||||
|
}, |
||||||
|
getcount: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
orderby: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
where: { |
||||||
|
type: [String, Object], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
getone: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
manual: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
inputVisble: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
localdata: { |
||||||
|
handler(newVal) { |
||||||
|
this.range = newVal |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(newVal)) |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
}, |
||||||
|
|
||||||
|
listData(newVal) { |
||||||
|
this.range = newVal |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(newVal)) |
||||||
|
}, |
||||||
|
value(newVal) { |
||||||
|
this.dataList = this.getDataList(newVal) |
||||||
|
this.formItem && this.formItem.setValue(newVal) |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
dataList: [], |
||||||
|
range: [], |
||||||
|
contentText: { |
||||||
|
contentdown: '查看更多', |
||||||
|
contentrefresh: '加载中', |
||||||
|
contentnomore: '没有更多' |
||||||
|
}, |
||||||
|
styles: { |
||||||
|
selectedColor: '#007aff', |
||||||
|
selectedTextColor: '#333', |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
this.formItem && this.formItem.setValue(this.value) |
||||||
|
this.styles = { |
||||||
|
selectedColor: this.selectedColor, |
||||||
|
selectedTextColor: this.selectedTextColor |
||||||
|
} |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.localdata && this.localdata.length !== 0) { |
||||||
|
this.range = this.localdata |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(this.range)) |
||||||
|
} else { |
||||||
|
if (this.collection) { |
||||||
|
this.loadData() |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init(range) {}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
chagne(e) { |
||||||
|
const values = e.detail.value |
||||||
|
|
||||||
|
let detail = { |
||||||
|
value: [], |
||||||
|
data: [] |
||||||
|
} |
||||||
|
|
||||||
|
if (this.multiple) { |
||||||
|
this.range.forEach(item => { |
||||||
|
if (values.includes(item.value + '')) { |
||||||
|
detail.value.push(item.value) |
||||||
|
detail.data.push(item) |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
const range = this.range.find(item => (item.value + '') === values) |
||||||
|
if (range) { |
||||||
|
detail = { |
||||||
|
value: range.value, |
||||||
|
data: range |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
this.formItem && this.formItem.setValue(detail.value) |
||||||
|
this.$emit('input', detail.value) |
||||||
|
this.$emit('change', { |
||||||
|
detail |
||||||
|
}) |
||||||
|
if (this.multiple) { |
||||||
|
// 如果 v-model 没有绑定 ,则走内部逻辑 |
||||||
|
// if (this.value.length === 0) { |
||||||
|
this.dataList = this.getDataList(detail.value, true) |
||||||
|
// } |
||||||
|
} else { |
||||||
|
this.dataList = this.getDataList(detail.value) |
||||||
|
} |
||||||
|
}, |
||||||
|
getLabelClass(item, index) { |
||||||
|
let classes = [] |
||||||
|
switch (this.mode) { |
||||||
|
case 'default': |
||||||
|
item.disabled && classes.push('disabled-cursor') |
||||||
|
break |
||||||
|
case 'button': |
||||||
|
classes.push(...['is-button', ...this.getClasses(item, 'button')]) |
||||||
|
break |
||||||
|
case 'list': |
||||||
|
if (this.multiple) { |
||||||
|
classes.push('is-list-multiple-box') |
||||||
|
} else { |
||||||
|
classes.push('is-list-box') |
||||||
|
} |
||||||
|
item.disabled && classes.push('is-list-disabled') |
||||||
|
index !== 0 && classes.push('is-list-border') |
||||||
|
break |
||||||
|
case 'tag': |
||||||
|
classes.push(...['is-tag', ...this.getClasses(item, 'tag')]) |
||||||
|
break |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
getCheckboxClass(item, type = '') { |
||||||
|
let classes = [] |
||||||
|
if (this.multiple) { |
||||||
|
classes.push(...this.getClasses(item, 'default-multiple', type)) |
||||||
|
} else { |
||||||
|
classes.push(...this.getClasses(item, 'default', type)) |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
getTextClass(item) { |
||||||
|
let classes = [] |
||||||
|
switch (this.mode) { |
||||||
|
case 'default': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'button': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'list': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'tag': |
||||||
|
classes.push(...['is-tag-text', ...this.getClasses(item, 'tag-text')]) |
||||||
|
break |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取渲染的新数组 |
||||||
|
* @param {Object} value 选中内容 |
||||||
|
*/ |
||||||
|
getDataList(value) { |
||||||
|
// 解除引用关系,破坏原引用关系,避免污染源数据 |
||||||
|
let dataList = JSON.parse(JSON.stringify(this.range)) |
||||||
|
let list = [] |
||||||
|
if (this.multiple) { |
||||||
|
if (!Array.isArray(value)) { |
||||||
|
value = [] |
||||||
|
// console.error('props 类型错误'); |
||||||
|
} |
||||||
|
} |
||||||
|
dataList.forEach((item, index) => { |
||||||
|
item.disabled = item.disable || item.disabled || false |
||||||
|
if (this.multiple) { |
||||||
|
if (value.length > 0) { |
||||||
|
let have = value.find(val => val === item.value) |
||||||
|
item.selected = have !== undefined |
||||||
|
} else { |
||||||
|
item.selected = false |
||||||
|
} |
||||||
|
} else { |
||||||
|
item.selected = value === item.value |
||||||
|
} |
||||||
|
|
||||||
|
list.push(item) |
||||||
|
}) |
||||||
|
return this.setRange(list) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 处理最大最小值 |
||||||
|
* @param {Object} list |
||||||
|
*/ |
||||||
|
setRange(list) { |
||||||
|
let selectList = list.filter(item => item.selected) |
||||||
|
let min = Number(this.min) || 0 |
||||||
|
let max = Number(this.max) || '' |
||||||
|
list.forEach((item, index) => { |
||||||
|
if (this.multiple) { |
||||||
|
if (selectList.length <= min) { |
||||||
|
let have = selectList.find(val => val.value === item.value) |
||||||
|
if (have !== undefined) { |
||||||
|
item.disabled = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (selectList.length >= max && max !== '') { |
||||||
|
let have = selectList.find(val => val.value === item.value) |
||||||
|
if (have === undefined) { |
||||||
|
item.disabled = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
this.setClass(item, index) |
||||||
|
list[index] = item |
||||||
|
}) |
||||||
|
return list |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 设置 class |
||||||
|
* @param {Object} item |
||||||
|
* @param {Object} index |
||||||
|
*/ |
||||||
|
setClass(item, index) { |
||||||
|
// 设置 label 的 class |
||||||
|
item.labelClass = this.getLabelClass(item, index) |
||||||
|
// 设置 checkbox外层样式 |
||||||
|
item.checkboxBgClass = this.getCheckboxClass(item, '-bg') |
||||||
|
// 设置 checkbox 内层样式 |
||||||
|
item.checkboxClass = this.getCheckboxClass(item) |
||||||
|
// 设置文本样式 |
||||||
|
item.textClass = this.getTextClass(item) |
||||||
|
// 设置 list 对勾右侧样式 |
||||||
|
item.listClass = this.getCheckboxClass(item, '-list') |
||||||
|
|
||||||
|
// 设置自定义样式 |
||||||
|
item.styleBackgroud = this.setStyleBackgroud(item) |
||||||
|
item.styleIcon = this.setStyleIcon(item) |
||||||
|
item.styleIconText = this.setStyleIconText(item) |
||||||
|
item.styleRightIcon = this.setStyleRightIcon(item) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取 class |
||||||
|
*/ |
||||||
|
getClasses(item, name, type = '') { |
||||||
|
let classes = [] |
||||||
|
item.disabled && classes.push('is-' + name + '-disabled' + type) |
||||||
|
item.selected && classes.push('is-' + name + '-checked' + type) |
||||||
|
|
||||||
|
if (this.mode !== 'button' || name === 'button') { |
||||||
|
item.selected && item.disabled && classes.push('is-' + name + '-disabled-checked' + type) |
||||||
|
} |
||||||
|
|
||||||
|
return classes |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取选中值 |
||||||
|
* @param {Object} range |
||||||
|
*/ |
||||||
|
getSelectedValue(range) { |
||||||
|
if (!this.multiple) return this.value |
||||||
|
let selectedArr = [] |
||||||
|
range.forEach((item) => { |
||||||
|
if (item.selected) { |
||||||
|
selectedArr.push(item.value) |
||||||
|
} |
||||||
|
}) |
||||||
|
return this.value.length > 0 ? this.value : selectedArr |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置背景样式 |
||||||
|
*/ |
||||||
|
setStyleBackgroud(item) { |
||||||
|
let styles = {} |
||||||
|
|
||||||
|
if (item.selected) { |
||||||
|
if (this.mode !== 'list') { |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
if (this.mode === 'tag') { |
||||||
|
styles.backgroundColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleIcon(item) { |
||||||
|
let styles = {} |
||||||
|
|
||||||
|
if (item.selected) { |
||||||
|
styles.backgroundColor = this.styles.selectedColor |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleIconText(item) { |
||||||
|
let styles = {} |
||||||
|
if (item.selected) { |
||||||
|
if (this.styles.selectedTextColor) { |
||||||
|
styles.color = this.styles.selectedTextColor |
||||||
|
} else { |
||||||
|
if(this.mode === 'tag'){ |
||||||
|
styles.color = '#fff' |
||||||
|
}else{ |
||||||
|
styles.color = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleRightIcon(item){ |
||||||
|
let styles = {} |
||||||
|
if (item.selected) { |
||||||
|
if(this.mode === 'list'){ |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return styles |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-data-checklist { |
||||||
|
position: relative; |
||||||
|
z-index: 0; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
/* min-height: 36px; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-data-loading { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
/* justify-content: center; */ |
||||||
|
height: 72rpx; |
||||||
|
padding-left: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-group { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
margin: 10rpx 0; |
||||||
|
margin-right: 50rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-left: 10rpx; |
||||||
|
transition: color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button { |
||||||
|
margin-right: 20rpx; |
||||||
|
padding: 6rpx 30rpx; |
||||||
|
border: 2rpx #DCDFE6 solid; |
||||||
|
border-radius: 6rpx; |
||||||
|
transition: border-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
padding-left: 0; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
.list-content { |
||||||
|
margin-left: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-multiple-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
padding-left: 0; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-border { |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag { |
||||||
|
margin-right: 20rpx; |
||||||
|
padding: 6rpx 20rpx; |
||||||
|
border: 2rpx #eee solid; |
||||||
|
border-radius: 6rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
/* transition: border-color 0.1s; */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text { |
||||||
|
margin: 0; |
||||||
|
color: #666; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color 0.1s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner-icon { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 16rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
width: 6rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .2s; |
||||||
|
transform-origin: center; |
||||||
|
transform: rotate(40deg) scaleY(0.4); |
||||||
|
} |
||||||
|
|
||||||
|
.radio__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color .3s; |
||||||
|
} |
||||||
|
|
||||||
|
.radio__inner-icon { |
||||||
|
width: 16rpx; |
||||||
|
height: 16rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .3s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkobx__list { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 24rpx; |
||||||
|
width: 12rpx; |
||||||
|
transform-origin: center; |
||||||
|
opacity: 0; |
||||||
|
transition: all 0.3s; |
||||||
|
transform: rotate(45deg); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* 禁用样式 */ |
||||||
|
.is-default-disabled-bg { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6; |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-bg { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6; |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled { |
||||||
|
border-color: #F2F6FC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled { |
||||||
|
border-color: #F2F6FC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
color: #999; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled-checked { |
||||||
|
color: #a1dcc1; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.is-button-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
border-color: #EBEEF5; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-text-disabled { |
||||||
|
color: #C0C4CC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-disabled-checked { |
||||||
|
border-color: #a1dcc1; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
border-color: #e9e9eb; |
||||||
|
background-color: #f4f4f5; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text-disabled { |
||||||
|
color: #bcbec2; |
||||||
|
} |
||||||
|
|
||||||
|
/* 选中样式 */ |
||||||
|
.is-default-checked-bg { |
||||||
|
border-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked-bg { |
||||||
|
border-color: #007aff; |
||||||
|
background-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-checked { |
||||||
|
opacity: 1; |
||||||
|
background-color: #007aff; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked { |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled-checked-bg { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-checked-bg { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.is-default-checked-list { |
||||||
|
border-color: #007aff; |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked-list { |
||||||
|
border-color: #007aff; |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled-checked-list { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-checked-list { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-checked { |
||||||
|
border-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-checked { |
||||||
|
color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-checked { |
||||||
|
border-color: #007aff; |
||||||
|
background-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text-checked { |
||||||
|
color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.disabled-cursor { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-wrap { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.hidden { |
||||||
|
/* #ifdef MP-ALIPAY */ |
||||||
|
display: none; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,364 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-datetime-picker"> |
||||||
|
<view @click="tiggerTimePicker"> |
||||||
|
<slot> |
||||||
|
<view class="uni-datetime-picker-timebox uni-datetime-picker-flex"> |
||||||
|
{{time}}<view v-if="!time" class="uni-datetime-picker-time">选择日期<!-- 时间 --></view> |
||||||
|
<view class="uni-datetime-picker-down-arrow"></view> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-mask" @click="initTimePicker"></view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-popup"> |
||||||
|
<view class="uni-title"> |
||||||
|
设置日期<!-- 和时间 --> |
||||||
|
</view> |
||||||
|
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd" @change="bindDateChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-hyphen"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">{{item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column class="uni-datetime-picker-hyphen"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> |
||||||
|
<!-- <picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> --> |
||||||
|
<view class="uni-datetime-picker-btn"> |
||||||
|
<!-- <view class="" @click="clearTime">重置</view> --> |
||||||
|
<view class="uni-datetime-picker-btn-group"> |
||||||
|
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">取消</view> |
||||||
|
<view class="" @click="setTime">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
visible: false, |
||||||
|
time: '', |
||||||
|
years: [], |
||||||
|
months: [], |
||||||
|
days: [], |
||||||
|
hours: [], |
||||||
|
minutes: [], |
||||||
|
seconds: [], |
||||||
|
year: 1900, |
||||||
|
month: 0, |
||||||
|
day: 0, |
||||||
|
hour: 0, |
||||||
|
minute: 0, |
||||||
|
second: 0, |
||||||
|
indicatorStyle: `height: 50px;`, |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'datetime-local' |
||||||
|
}, |
||||||
|
timestamp: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
maxYear: { |
||||||
|
type: [Number, String], |
||||||
|
default: 2100 |
||||||
|
}, |
||||||
|
minYear: { |
||||||
|
type: [Number, String], |
||||||
|
default: 1900 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
ymd() { |
||||||
|
return [this.year - this.minYear, this.month - 1, this.day - 1] |
||||||
|
}, |
||||||
|
hms() { |
||||||
|
return [this.hour, this.minute, this.second] |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newValue) { |
||||||
|
this.parseValue(this.value) |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
const date = new Date() |
||||||
|
for (let i = this.minYear; i <= this.maxYear; i++) { |
||||||
|
this.years.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 1; i <= 12; i++) { |
||||||
|
this.months.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 1; i <= 31; i++) { |
||||||
|
this.days.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 23; i++) { |
||||||
|
this.hours.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 59; i++) { |
||||||
|
this.minutes.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 59; i++) { |
||||||
|
this.seconds.push(i) |
||||||
|
} |
||||||
|
this.parseValue(this.value) |
||||||
|
if (this.value) { |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
parseDateTime(datetime) { |
||||||
|
let defaultDate = null |
||||||
|
if (!datetime) { |
||||||
|
defaultDate = new Date() |
||||||
|
} else { |
||||||
|
defaultDate = new Date(datetime) |
||||||
|
} |
||||||
|
this.year = defaultDate.getFullYear() |
||||||
|
if (this.year < this.minYear || this.year > this.maxYear) { |
||||||
|
const now = Date.now() |
||||||
|
this.parseDateTime(now) |
||||||
|
return |
||||||
|
} |
||||||
|
this.month = defaultDate.getMonth() + 1 |
||||||
|
this.day = defaultDate.getDate() |
||||||
|
this.hour = defaultDate.getHours() |
||||||
|
this.minute = defaultDate.getMinutes() |
||||||
|
this.second = defaultDate.getSeconds() |
||||||
|
}, |
||||||
|
parseValue(defaultTime) { |
||||||
|
if (Number(defaultTime)) { |
||||||
|
defaultTime = parseInt(defaultTime) |
||||||
|
} |
||||||
|
this.parseDateTime(defaultTime) |
||||||
|
}, |
||||||
|
bindDateChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.year = this.years[val[0]] |
||||||
|
this.month = this.months[val[1]] |
||||||
|
this.day = this.days[val[2]] |
||||||
|
console.log(this.year+'-'+this.month+'-'+this.day) |
||||||
|
}, |
||||||
|
bindTimeChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.hour = this.hours[val[0]] |
||||||
|
this.minute = this.minutes[val[1]] |
||||||
|
this.second = this.seconds[val[2]] |
||||||
|
}, |
||||||
|
initTimePicker() { |
||||||
|
// if (!this.time) { |
||||||
|
// this.parseValue() |
||||||
|
// } |
||||||
|
this.parseValue(this.value) |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
tiggerTimePicker() { |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
clearTime() { |
||||||
|
this.time = '' |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
initTime() { |
||||||
|
this.time = this.createDomSting() |
||||||
|
if (!this.timestamp) { |
||||||
|
this.formItem && this.formItem.setValue(this.time) |
||||||
|
this.$emit('change', this.time) |
||||||
|
} else { |
||||||
|
this.formItem && this.formItem.setValue(this.createTimeStamp(this.time)) |
||||||
|
this.$emit('change', this.createTimeStamp(this.time)) |
||||||
|
} |
||||||
|
}, |
||||||
|
setTime() { |
||||||
|
this.initTime() |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
createTimeStamp(time) { |
||||||
|
return Date.parse(new Date(time)) |
||||||
|
}, |
||||||
|
createDomSting() { |
||||||
|
const yymmdd = this.year + |
||||||
|
'-' + |
||||||
|
(this.month < 10 ? '0' + this.month : this.month) + |
||||||
|
'-' + |
||||||
|
(this.day < 10 ? '0' + this.day : this.day) |
||||||
|
// + |
||||||
|
// ' ' + |
||||||
|
// (this.hour < 10 ? '0' + this.hour : this.hour) + |
||||||
|
// ':' + |
||||||
|
// (this.minute < 10 ? '0' + this.minute : this.minute) + |
||||||
|
// ':' + |
||||||
|
// (this.second < 10 ? '0' + this.second : this.second) |
||||||
|
|
||||||
|
return yymmdd |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-datetime-picker-view { |
||||||
|
width: 100%; |
||||||
|
height: 130px; |
||||||
|
margin-top: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-item { |
||||||
|
line-height: 100rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-title{ |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn { |
||||||
|
margin-top: 120rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
color: #00B9FF; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn-group { |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-cancel { |
||||||
|
margin-right: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-mask { |
||||||
|
position: fixed; |
||||||
|
bottom: 0px; |
||||||
|
top: 0px; |
||||||
|
left: 0px; |
||||||
|
right: 0px; |
||||||
|
background-color: rgba(0, 0, 0, 0.4); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 998; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-popup { |
||||||
|
border-radius: 16rpx; |
||||||
|
padding: 60rpx; |
||||||
|
width: 540rpx; |
||||||
|
background-color: #fff; |
||||||
|
position: fixed; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-time { |
||||||
|
color: grey; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-colon::after { |
||||||
|
content: ':'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-hyphen::after { |
||||||
|
content: '-'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: -4rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-timebox { |
||||||
|
border: 2rpx solid #E5E5E5; |
||||||
|
border-radius: 40rpx; |
||||||
|
padding: 14rpx 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
cursor: pointer; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
// 下箭头 |
||||||
|
.uni-datetime-picker-down-arrow { |
||||||
|
display :inline-block; |
||||||
|
position: relative; |
||||||
|
width: 40rpx; |
||||||
|
height: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-down-arrow::after { |
||||||
|
display: inline-block; |
||||||
|
content: " "; |
||||||
|
height: 18rpx; |
||||||
|
width: 18rpx; |
||||||
|
border-width: 0 2rpx 2rpx 0; |
||||||
|
border-color: #E5E5E5; |
||||||
|
border-style: solid; |
||||||
|
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0); |
||||||
|
transform-origin: center; |
||||||
|
transition: transform .3s; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
right: 10rpx; |
||||||
|
margin-top: -10rpx; |
||||||
|
} |
||||||
|
.uni-datetime-picker-flex { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,56 @@ |
|||||||
|
/** |
||||||
|
* @desc 函数防抖 |
||||||
|
* @param func 目标函数 |
||||||
|
* @param wait 延迟执行毫秒数 |
||||||
|
* @param immediate true - 立即执行, false - 延迟执行 |
||||||
|
*/ |
||||||
|
export const debounce = function(func, wait = 1000, immediate = true) { |
||||||
|
let timer; |
||||||
|
console.log(1); |
||||||
|
return function() { |
||||||
|
console.log(123); |
||||||
|
let context = this, |
||||||
|
args = arguments; |
||||||
|
if (timer) clearTimeout(timer); |
||||||
|
if (immediate) { |
||||||
|
let callNow = !timer; |
||||||
|
timer = setTimeout(() => { |
||||||
|
timer = null; |
||||||
|
}, wait); |
||||||
|
if (callNow) func.apply(context, args); |
||||||
|
} else { |
||||||
|
timer = setTimeout(() => { |
||||||
|
func.apply(context, args); |
||||||
|
}, wait) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
/** |
||||||
|
* @desc 函数节流 |
||||||
|
* @param func 函数 |
||||||
|
* @param wait 延迟执行毫秒数 |
||||||
|
* @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发 |
||||||
|
*/ |
||||||
|
export const throttle = (func, wait = 1000, type = 1) => { |
||||||
|
let previous = 0; |
||||||
|
let timeout; |
||||||
|
return function() { |
||||||
|
let context = this; |
||||||
|
let args = arguments; |
||||||
|
if (type === 1) { |
||||||
|
let now = Date.now(); |
||||||
|
|
||||||
|
if (now - previous > wait) { |
||||||
|
func.apply(context, args); |
||||||
|
previous = now; |
||||||
|
} |
||||||
|
} else if (type === 2) { |
||||||
|
if (!timeout) { |
||||||
|
timeout = setTimeout(() => { |
||||||
|
timeout = null; |
||||||
|
func.apply(context, args) |
||||||
|
}, wait) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,402 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-easyinput" :class="{'uni-easyinput-error':msg}" :style="{color:inputBorder && msg?'#dd524d':styles.color}"> |
||||||
|
<view class="uni-easyinput__content" :class="{'is-input-border':inputBorder ,'is-input-error-border':inputBorder && msg,'is-textarea':type==='textarea','is-disabled':disabled}" |
||||||
|
:style="{'border-color':inputBorder && msg?'#dd524d':styles.borderColor,'background-color':disabled?styles.disableColor:'#fff'}"> |
||||||
|
<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')"></uni-icons> |
||||||
|
<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{'input-padding':inputBorder}" |
||||||
|
:name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" |
||||||
|
:maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" @input="onInput" @blur="onBlur" @focus="onFocus" |
||||||
|
@confirm="onConfirm"></textarea> |
||||||
|
<input v-else :type="type === 'password'?'text':type" class="uni-easyinput__content-input" :style="{ |
||||||
|
'padding-right':type === 'password' ||clearable || prefixIcon?'':'40rpx', |
||||||
|
'padding-left':prefixIcon?'':'40rpx' |
||||||
|
}" |
||||||
|
:name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" |
||||||
|
:placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" @focus="onFocus" |
||||||
|
@blur="onBlur" @input="onInput" @confirm="onConfirm" /> |
||||||
|
<template v-if="type === 'password'"> |
||||||
|
<uni-icons v-if="val != '' " class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" :type="showPassword?'eye-slash-filled':'eye-filled'" |
||||||
|
:size="18" color="#c0c4cc" @click="onEyes"></uni-icons> |
||||||
|
</template> |
||||||
|
<template v-else-if="suffixIcon"> |
||||||
|
<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')"></uni-icons> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<uni-icons class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" type="clear" :size="clearSize" |
||||||
|
v-if="clearable && focused && val " color="#c0c4cc" @click="onClear"></uni-icons> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Field 输入框 |
||||||
|
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21001 |
||||||
|
* @property {String| Number} value 输入内容 |
||||||
|
* @property {String } type 输入框的类型(默认text) password/text/textarea/.. |
||||||
|
* @value text 文本输入键盘 |
||||||
|
* @value textarea 多行文本输入键盘 |
||||||
|
* @value password 密码输入键盘 |
||||||
|
* @value number 数字输入键盘,注意iOS上app-vue弹出的数字键盘并非9宫格方式 |
||||||
|
* @value idcard 身份证输入键盘,信、支付宝、百度、QQ小程序 |
||||||
|
* @value digit 带小数点的数字键盘 ,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持 |
||||||
|
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true) |
||||||
|
* @property {Boolean} autoHeight 是否自动增高输入区域,type为textarea时有效(默认true) |
||||||
|
* @property {String } placeholder 输入框的提示文字 |
||||||
|
* @property {String } placeholderStyle placeholder的样式(内联样式,字符串),如"color: #ddd" |
||||||
|
* @property {Boolean} focus 是否自动获得焦点(默认false) |
||||||
|
* @property {Boolean} disabled 是否不可输入(默认false) |
||||||
|
* @property {Number } maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140) |
||||||
|
* @property {String } confirmType 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done) |
||||||
|
* @property {Number } clearSize 清除图标的大小,单位px(默认15) |
||||||
|
* @property {String} prefixIcon 输入框头部图标 |
||||||
|
* @property {String} suffixIcon 输入框尾部图标 |
||||||
|
* @property {Boolean} trim 是否自动去除两端的空格 |
||||||
|
* @property {Boolean} inputBorder 是否显示input输入框的边框(默认false) |
||||||
|
* @property {Object} styles 自定义颜色 |
||||||
|
* @event {Function} input 输入框内容发生变化时触发 |
||||||
|
* @event {Function} focus 输入框获得焦点时触发 |
||||||
|
* @event {Function} blur 输入框失去焦点时触发 |
||||||
|
* @event {Function} confirm 点击完成按钮时触发 |
||||||
|
* @event {Function} iconClick 点击图标时触发 |
||||||
|
* @example <uni-easyinput v-model="mobile"></uni-easyinput> |
||||||
|
*/ |
||||||
|
|
||||||
|
import { |
||||||
|
debounce, |
||||||
|
throttle |
||||||
|
} from './common.js' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'uni-easyinput', |
||||||
|
props: { |
||||||
|
name: String, |
||||||
|
value: [Number, String], |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'text' |
||||||
|
}, |
||||||
|
clearable: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
autoHeight: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
placeholder: String, |
||||||
|
placeholderStyle: String, |
||||||
|
focus: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
disabled: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
maxlength: { |
||||||
|
type: [Number, String], |
||||||
|
default: 140 |
||||||
|
}, |
||||||
|
confirmType: { |
||||||
|
type: String, |
||||||
|
default: 'done' |
||||||
|
}, |
||||||
|
// 清除按钮的大小 |
||||||
|
clearSize: { |
||||||
|
type: [Number, String], |
||||||
|
default: 15 |
||||||
|
}, |
||||||
|
// 是否显示 input 边框 |
||||||
|
inputBorder: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
prefixIcon: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
suffixIcon: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// 是否自动去除两端的空格 |
||||||
|
trim: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 自定义样式 |
||||||
|
styles: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return { |
||||||
|
color: '#333', |
||||||
|
disableColor: '#eee', |
||||||
|
borderColor: '#e5e5e5' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
focused: false, |
||||||
|
errMsg: '', |
||||||
|
val: '', |
||||||
|
showMsg: '', |
||||||
|
border: false, |
||||||
|
isFirstBorder: false, |
||||||
|
showClearIcon: false, |
||||||
|
showPassword: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
msg() { |
||||||
|
return this.errorMessage || this.errMsg; |
||||||
|
}, |
||||||
|
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值 |
||||||
|
inputMaxlength() { |
||||||
|
return Number(this.maxlength); |
||||||
|
}, |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newVal) { |
||||||
|
if (this.errMsg) this.errMsg = '' |
||||||
|
this.val = newVal |
||||||
|
if (this.formItem) { |
||||||
|
this.formItem.setValue(newVal) |
||||||
|
} |
||||||
|
}, |
||||||
|
focus(newVal) { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.focused = this.focus |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.val = this.value |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.onInput = throttle(this.input, 500) |
||||||
|
this.$nextTick(() => { |
||||||
|
this.focused = this.focus |
||||||
|
}) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 初始化变量值 |
||||||
|
*/ |
||||||
|
init() { |
||||||
|
|
||||||
|
}, |
||||||
|
onClickIcon(type) { |
||||||
|
this.$emit('iconClick', type) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false; |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
|
||||||
|
onEyes() { |
||||||
|
this.showPassword = !this.showPassword |
||||||
|
}, |
||||||
|
onInput(event) { |
||||||
|
let value = event.detail.value; |
||||||
|
// 判断是否去除空格 |
||||||
|
if (this.trim) value = this.trimStr(value); |
||||||
|
if (this.errMsg) this.errMsg = '' |
||||||
|
this.val = value |
||||||
|
this.$emit('input', value); |
||||||
|
}, |
||||||
|
|
||||||
|
onFocus(event) { |
||||||
|
this.focused = true; |
||||||
|
this.$emit('focus', event); |
||||||
|
}, |
||||||
|
onBlur(event) { |
||||||
|
let value = event.detail.value; |
||||||
|
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错 |
||||||
|
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时 |
||||||
|
setTimeout(() => { |
||||||
|
this.focused = false; |
||||||
|
}, 100); |
||||||
|
this.$emit('blur', event); |
||||||
|
}, |
||||||
|
onConfirm(e) { |
||||||
|
this.$emit('confirm', e.detail.value); |
||||||
|
}, |
||||||
|
onClear(event) { |
||||||
|
this.val = ''; |
||||||
|
this.$emit('input', ''); |
||||||
|
}, |
||||||
|
fieldClick() { |
||||||
|
this.$emit('click'); |
||||||
|
}, |
||||||
|
trimStr(str, pos = 'both') { |
||||||
|
if (pos == 'both') { |
||||||
|
return str.replace(/^\s+|\s+$/g, ''); |
||||||
|
} else if (pos == 'left') { |
||||||
|
return str.replace(/^\s*/, ''); |
||||||
|
} else if (pos == 'right') { |
||||||
|
return str.replace(/(\s*$)/g, ''); |
||||||
|
} else if (pos == 'all') { |
||||||
|
return str.replace(/\s+/g, ''); |
||||||
|
} else { |
||||||
|
return str; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-easyinput { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
position: relative; |
||||||
|
// padding: 16px 14px; |
||||||
|
text-align: left; |
||||||
|
color: #333; |
||||||
|
font-size: 28rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content { |
||||||
|
flex: 1; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
box-sizing: border-box; |
||||||
|
min-height: 72rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content-input { |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex: 1; |
||||||
|
width: auto; |
||||||
|
line-height: 2; |
||||||
|
font-size: 28rpx; |
||||||
|
// padding-right: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.is-textarea { |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.is-textarea-icon { |
||||||
|
margin-top: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content-textarea { |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex: 1; |
||||||
|
width: auto; |
||||||
|
line-height: 1.5; |
||||||
|
font-size: 28rpx; |
||||||
|
// padding-right: 10px; |
||||||
|
padding-top: 20rpx; |
||||||
|
padding-bottom: 20rpx; |
||||||
|
// box-sizing: border-box; |
||||||
|
min-height: 160rpx; |
||||||
|
height: 160rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.input-padding { |
||||||
|
padding-left: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.content-clear-icon { |
||||||
|
padding: 0 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.label-icon { |
||||||
|
margin-right: 10rpx; |
||||||
|
margin-top: -2rpx; |
||||||
|
} |
||||||
|
|
||||||
|
// 显示边框 |
||||||
|
.is-input-border { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
border: 2rpx solid $uni-border-color; |
||||||
|
border-radius: 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__right { |
||||||
|
// margin-left: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-message { |
||||||
|
position: absolute; |
||||||
|
bottom: -34rpx; |
||||||
|
left: 0; |
||||||
|
line-height: 24rpx; |
||||||
|
color: $uni-color-error; |
||||||
|
font-size: 24rpx; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-msg--boeder { |
||||||
|
position: relative; |
||||||
|
bottom: 0; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-input-error-border { |
||||||
|
border-color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput--border { |
||||||
|
margin-bottom: 0; |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-bottom: 0; |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput-error { |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-first-border { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
.is-disabled { |
||||||
|
background-color: #eee; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,441 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-forms-item" :class="{'uni-forms-item--border':border,'is-first-border':border&&isFirstBorder,'uni-forms-item-error':msg}"> |
||||||
|
<view class="uni-forms-item__inner" :class="['is-direction-'+labelPos,]"> |
||||||
|
<view v-if="label" class="uni-forms-item__label" :style="{width:labelWid+'px',justifyContent: justifyContent}"> |
||||||
|
<slot name="left"> |
||||||
|
<uni-icons v-if="leftIcon" class="label-icon" size="16" :type="leftIcon" :color="iconColor" /> |
||||||
|
<text v-if="required" class="is-required">*</text> |
||||||
|
<text class="label-color">{{label}}</text> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view class="uni-forms-item__content" :class="{'is-input-error-border': msg}"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="uni-error-message" :class="{'uni-error-msg--boeder':border}" :style="{ |
||||||
|
paddingLeft: (labelPos === 'left'? Number(labelWid)+5:5) + 'px' |
||||||
|
}">{{ showMsg === 'undertext' ? msg:'' }}</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Field 输入框 |
||||||
|
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21001 |
||||||
|
* @property {Boolean} required 是否必填,左边显示红色"*"号(默认false) |
||||||
|
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit 可选 |
||||||
|
* @value bind 发生变化时触发 |
||||||
|
* @value submit 提交时触发 |
||||||
|
* @property {String } leftIcon label左边的图标,限 uni-ui 的图标名称 |
||||||
|
* @property {String } iconColor 左边通过icon配置的图标的颜色(默认#606266) |
||||||
|
* @property {String } label 输入框左边的文字提示 |
||||||
|
* @property {Number } labelWidth label的宽度,单位px(默认65) |
||||||
|
* @property {String } labelAlign = [left|center|right] label的文字对齐方式(默认left) |
||||||
|
* @value left label 左侧显示 |
||||||
|
* @value center label 居中 |
||||||
|
* @value right label 右侧对齐 |
||||||
|
* @property {String } labelPosition = [top|left] label的文字的位置(默认left) |
||||||
|
* @value top 顶部显示 label |
||||||
|
* @value left 左侧显示 label |
||||||
|
* @property {String } errorMessage 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息 |
||||||
|
* @property {String } name 表单域的属性名,在使用校验规则时必填 |
||||||
|
*/ |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default { |
||||||
|
name: "uniFormsItem", |
||||||
|
props: { |
||||||
|
// 自定义内容 |
||||||
|
custom: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
// 是否显示报错信息 |
||||||
|
showMessage: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
name: String, |
||||||
|
required: Boolean, |
||||||
|
validateTrigger: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
leftIcon: String, |
||||||
|
iconColor: { |
||||||
|
type: String, |
||||||
|
default: '#606266' |
||||||
|
}, |
||||||
|
label: String, |
||||||
|
// 左边标题的宽度单位px |
||||||
|
labelWidth: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// 对齐方式,left|center|right |
||||||
|
labelAlign: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// lable的位置,可选为 left-左边,top-上边 |
||||||
|
labelPosition: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
errorMessage: { |
||||||
|
type: [String, Boolean], |
||||||
|
default: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
errorTop: false, |
||||||
|
errorBottom: false, |
||||||
|
labelMarginBottom: '', |
||||||
|
errorWidth: '', |
||||||
|
errMsg: '', |
||||||
|
val: '', |
||||||
|
labelPos: '', |
||||||
|
labelWid: '', |
||||||
|
labelAli: '', |
||||||
|
showMsg: 'undertext', |
||||||
|
border: false, |
||||||
|
isFirstBorder: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
msg() { |
||||||
|
return this.errorMessage || this.errMsg; |
||||||
|
}, |
||||||
|
fieldStyle() { |
||||||
|
let style = {} |
||||||
|
let _this = this |
||||||
|
if (_this.labelPos == 'top') { |
||||||
|
style.padding = '0 0' |
||||||
|
_this.labelMarginBottom = '6px' |
||||||
|
} |
||||||
|
if (_this.labelPos == 'left' && _this.msg !== false && _this.msg != '') { |
||||||
|
style.paddingBottom = '0px' |
||||||
|
_this.errorBottom = true |
||||||
|
_this.errorTop = false |
||||||
|
} else if (_this.labelPos == 'top' && _this.msg !== false && _this.msg != '') { |
||||||
|
_this.errorBottom = false |
||||||
|
_this.errorTop = true |
||||||
|
} else { |
||||||
|
// style.paddingBottom = '' |
||||||
|
_this.errorTop = false |
||||||
|
_this.errorBottom = false |
||||||
|
} |
||||||
|
return style |
||||||
|
}, |
||||||
|
|
||||||
|
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法 |
||||||
|
justifyContent() { |
||||||
|
if (this.labelAli === 'left') return 'flex-start'; |
||||||
|
if (this.labelAli === 'center') return 'center'; |
||||||
|
if (this.labelAli === 'right') return 'flex-end'; |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
watch: { |
||||||
|
validateTrigger(trigger) { |
||||||
|
this.formTrigger = trigger |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm() |
||||||
|
this.group = this.getForm('uniGroup') |
||||||
|
this.formRules = [] |
||||||
|
this.formTrigger = this.validateTrigger |
||||||
|
// if (this.form) { |
||||||
|
this.form.childrens.push(this) |
||||||
|
// } |
||||||
|
this.init() |
||||||
|
}, |
||||||
|
destroyed() { |
||||||
|
if (this.form) { |
||||||
|
this.form.childrens.forEach((item, index) => { |
||||||
|
if (item === this) { |
||||||
|
this.form.childrens.splice(index, 1) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init() { |
||||||
|
if (this.form) { |
||||||
|
let { |
||||||
|
formRules, |
||||||
|
validator, |
||||||
|
formData, |
||||||
|
value, |
||||||
|
labelPosition, |
||||||
|
labelWidth, |
||||||
|
labelAlign, |
||||||
|
errShowType |
||||||
|
} = this.form |
||||||
|
|
||||||
|
this.labelPos = this.labelPosition ? this.labelPosition : labelPosition |
||||||
|
this.labelWid = this.label ? (this.labelWidth ? this.labelWidth : labelWidth):0 |
||||||
|
this.labelAli = this.labelAlign ? this.labelAlign : labelAlign |
||||||
|
// 判断第一个 item |
||||||
|
if (!this.form.isFirstBorder) { |
||||||
|
this.form.isFirstBorder = true |
||||||
|
this.isFirstBorder = true |
||||||
|
} |
||||||
|
// 判断 group 里的第一个 item |
||||||
|
if (this.group) { |
||||||
|
if (!this.group.isFirstBorder) { |
||||||
|
this.group.isFirstBorder = true |
||||||
|
this.isFirstBorder = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.border = this.form.border |
||||||
|
this.showMsg = errShowType |
||||||
|
|
||||||
|
if (formRules) { |
||||||
|
this.formRules = formRules[this.name] || {} |
||||||
|
} |
||||||
|
|
||||||
|
this.validator = validator |
||||||
|
|
||||||
|
// if (this.name) { |
||||||
|
// formData[this.name] = value.hasOwnProperty(this.name) ? value[this.name] : this.form._getValue(this, '') |
||||||
|
// } |
||||||
|
} else { |
||||||
|
this.labelPos = this.labelPosition || 'left' |
||||||
|
this.labelWid = this.labelWidth || 65 |
||||||
|
this.labelAli = this.labelAlign || 'left' |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 移除该表单项的校验结果 |
||||||
|
*/ |
||||||
|
clearValidate() { |
||||||
|
this.errMsg = '' |
||||||
|
}, |
||||||
|
setValue(value){ |
||||||
|
if (this.name) { |
||||||
|
if(this.errMsg) this.errMsg = '' |
||||||
|
this.form.formData[this.name] = this.form._getValue(this, value) |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 校验规则 |
||||||
|
* @param {Object} value |
||||||
|
*/ |
||||||
|
async triggerCheck(value, callback) { |
||||||
|
let promise = null; |
||||||
|
this.errMsg = '' |
||||||
|
// if no callback, return promise |
||||||
|
if (callback && typeof callback !== 'function' && Promise) { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
!valid ? resolve(valid) : reject(valid) |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.validator) { |
||||||
|
typeof callback === 'function' && callback(null); |
||||||
|
if (promise) return promise |
||||||
|
} |
||||||
|
|
||||||
|
const isNoField = this.isRequired(this.formRules.rules || []) |
||||||
|
|
||||||
|
|
||||||
|
let isTrigger = this.isTrigger(this.formRules.validateTrigger, this.validateTrigger, this.form.validateTrigger) |
||||||
|
|
||||||
|
let result = null |
||||||
|
|
||||||
|
if (!(!isTrigger)) { |
||||||
|
result = this.validator && (await this.validator.validateUpdate({ |
||||||
|
[this.name]: value |
||||||
|
}, this.form.formData)) |
||||||
|
} |
||||||
|
// 判断是否必填 |
||||||
|
if (!isNoField && !value) { |
||||||
|
result = null |
||||||
|
} |
||||||
|
|
||||||
|
if (isTrigger && result && result.errorMessage) { |
||||||
|
if (this.form.errShowType === 'toast') { |
||||||
|
uni.showToast({ |
||||||
|
title: result.errorMessage || '校验错误', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
if (this.form.errShowType === 'modal') { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: result.errorMessage || '校验错误' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.errMsg = !result ? '' : result.errorMessage |
||||||
|
this.form.validateCheck(result ? result : null) |
||||||
|
typeof callback === 'function' && callback(result ? result : null); |
||||||
|
if (promise) return promise |
||||||
|
|
||||||
|
}, |
||||||
|
/** |
||||||
|
* 触发时机 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
isTrigger(rule, itemRlue, parentRule) { |
||||||
|
let rl = true; |
||||||
|
// bind submit |
||||||
|
if (rule === 'submit' || !rule) { |
||||||
|
if (rule === undefined) { |
||||||
|
if (itemRlue !== 'bind') { |
||||||
|
if (!itemRlue) { |
||||||
|
return parentRule === 'bind' ? true : false |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
return true; |
||||||
|
}, |
||||||
|
// 是否有必填字段 |
||||||
|
isRequired(rules) { |
||||||
|
let isNoField = false |
||||||
|
for (let i = 0; i < rules.length; i++) { |
||||||
|
const ruleData = rules[i] |
||||||
|
if (ruleData.required) { |
||||||
|
isNoField = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return isNoField |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-forms-item { |
||||||
|
position: relative; |
||||||
|
// padding: 16px 14px; |
||||||
|
text-align: left; |
||||||
|
color: #333; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-bottom: 44rpx; |
||||||
|
background-color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__inner { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
// flex-direction: row; |
||||||
|
// align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.is-direction-left { |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.is-direction-top { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__label { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
flex-shrink: 0; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
width: 130rpx; |
||||||
|
// line-height: 2; |
||||||
|
// margin-top: 3px; |
||||||
|
padding: 10rpx 0; |
||||||
|
box-sizing: border-box; |
||||||
|
height: 72rpx; |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 100%; |
||||||
|
// display: flex; |
||||||
|
/* #endif */ |
||||||
|
// flex: 1; |
||||||
|
// flex-direction: row; |
||||||
|
// align-items: center; |
||||||
|
box-sizing: border-box; |
||||||
|
min-height: 72rpx; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.label-icon { |
||||||
|
margin-right: 10rpx; |
||||||
|
margin-top: -2px; |
||||||
|
} |
||||||
|
|
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.label-color{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-message { |
||||||
|
position: absolute; |
||||||
|
bottom: -34rpx; |
||||||
|
left: 0; |
||||||
|
line-height: 24rpx; |
||||||
|
color: $uni-color-error; |
||||||
|
font-size: 24rpx; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-msg--boeder { |
||||||
|
position: relative; |
||||||
|
bottom: 0; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-input-error-border { |
||||||
|
border-color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item--border { |
||||||
|
margin-bottom: 0; |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-bottom: 0; |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item-error { |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-first-border { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,420 @@ |
|||||||
|
<template> |
||||||
|
<!-- --> |
||||||
|
<view class="uni-forms" :class="{'uni-forms--top':!border}"> |
||||||
|
<form @submit.stop="submitForm" @reset="resetForm"> |
||||||
|
<slot></slot> |
||||||
|
</form> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Forms 表单 |
||||||
|
* @description 由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773 |
||||||
|
* @property {Object} rules 表单校验规则 |
||||||
|
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit 可选 |
||||||
|
* @value bind 发生变化时触发 |
||||||
|
* @value submit 提交时触发 |
||||||
|
* @property {String} labelPosition = [top|left] label 位置 默认 left 可选 |
||||||
|
* @value top 顶部显示 label |
||||||
|
* @value left 左侧显示 label |
||||||
|
* @property {String} labelWidth label 宽度,默认 65px |
||||||
|
* @property {String} labelAlign = [left|center|right] label 居中方式 默认 left 可选 |
||||||
|
* @value left label 左侧显示 |
||||||
|
* @value center label 居中 |
||||||
|
* @value right label 右侧对齐 |
||||||
|
* @property {String} errShowType = [undertext|toast|modal] 校验错误信息提示方式 |
||||||
|
* @value undertext 错误信息在底部显示 |
||||||
|
* @value toast 错误信息toast显示 |
||||||
|
* @value modal 错误信息modal显示 |
||||||
|
* @event {Function} submit 提交时触发 |
||||||
|
*/ |
||||||
|
import Vue from 'vue' |
||||||
|
Vue.prototype.binddata = function(name, value, formName) { |
||||||
|
if (formName) { |
||||||
|
this.$refs[formName].setValue(name, value) |
||||||
|
} else { |
||||||
|
let formVm |
||||||
|
for (let i in this.$refs) { |
||||||
|
const vm = this.$refs[i] |
||||||
|
if (vm && vm.$options && vm.$options.name === 'uniForms') { |
||||||
|
formVm = vm |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性') |
||||||
|
formVm.setValue(name, value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
import Validator from './validate.js' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'uniForms', |
||||||
|
props: { |
||||||
|
value: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 表单校验规则 |
||||||
|
rules: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 校验触发器方式,默认 关闭 |
||||||
|
validateTrigger: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// label 位置,可选值 top/left |
||||||
|
labelPosition: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
// label 宽度,单位 px |
||||||
|
labelWidth: { |
||||||
|
type: [String, Number], |
||||||
|
default: 65 |
||||||
|
}, |
||||||
|
// label 居中方式,可选值 left/center/right |
||||||
|
labelAlign: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
errShowType: { |
||||||
|
type: String, |
||||||
|
default: 'undertext' |
||||||
|
}, |
||||||
|
border: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
formData: {} |
||||||
|
}; |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
rules(newVal) { |
||||||
|
this.init(newVal) |
||||||
|
}, |
||||||
|
trigger(trigger) { |
||||||
|
this.formTrigger = trigger |
||||||
|
}, |
||||||
|
value: { |
||||||
|
handler(newVal) { |
||||||
|
if (this.isChildEdit) { |
||||||
|
this.isChildEdit = false |
||||||
|
return |
||||||
|
} |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
if (item.name) { |
||||||
|
const formDataValue = newVal.hasOwnProperty(item.name) ? newVal[item.name] : null |
||||||
|
this.formData[item.name] = this._getValue(item, formDataValue) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
let _this = this |
||||||
|
this.childrens = [] |
||||||
|
this.inputChildrens = [] |
||||||
|
this.checkboxChildrens = [] |
||||||
|
this.formRules = [] |
||||||
|
this.init(this.rules) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init(formRules) { |
||||||
|
if (Object.keys(formRules).length > 0) { |
||||||
|
this.formTrigger = this.trigger |
||||||
|
this.formRules = formRules |
||||||
|
if (!this.validator) { |
||||||
|
this.validator = new Validator(formRules) |
||||||
|
} |
||||||
|
} |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
item.init() |
||||||
|
}) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 设置校验规则 |
||||||
|
* @param {Object} formRules |
||||||
|
*/ |
||||||
|
setRules(formRules) { |
||||||
|
this.init(formRules) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 公开给用户使用 |
||||||
|
* 设置自定义表单组件 value 值 |
||||||
|
* @param {String} name 字段名称 |
||||||
|
* @param {String} value 字段值 |
||||||
|
*/ |
||||||
|
setValue(name, value, callback) { |
||||||
|
let example = this.childrens.find(child => child.name === name) |
||||||
|
if (!example) return null |
||||||
|
this.isChildEdit = true |
||||||
|
value = this._getValue(example, value) |
||||||
|
this.formData[name] = value |
||||||
|
example.val = value |
||||||
|
this.$emit('input', Object.assign({}, this.value, this.formData)) |
||||||
|
return example.triggerCheck(value, callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO 表单提交, 小程序暂不支持这种用法 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
submitForm(event) { |
||||||
|
const value = event.detail.value |
||||||
|
return this.validateAll(value || this.formData, 'submit') |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 表单重置 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
resetForm(event) { |
||||||
|
this.childrens.forEach(item => { |
||||||
|
item.errMsg = '' |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === item.name) |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
inputComp.$emit('input', inputComp.multiple?[]:'') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
this.isChildEdit = true |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
if (item.name) { |
||||||
|
this.formData[item.name] = this._getValue(item, '') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
this.$emit('input', this.formData) |
||||||
|
this.$emit('reset', event) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 触发表单校验,通过 @validate 获取 |
||||||
|
* @param {Object} validate |
||||||
|
*/ |
||||||
|
validateCheck(validate) { |
||||||
|
if (validate === null) validate = null |
||||||
|
this.$emit('validate', validate) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 校验所有或者部分表单 |
||||||
|
*/ |
||||||
|
async validateAll(invalidFields, type, callback) { |
||||||
|
|
||||||
|
this.childrens.forEach(item => { |
||||||
|
item.errMsg = '' |
||||||
|
}) |
||||||
|
|
||||||
|
let promise; |
||||||
|
if (!callback && typeof callback !== 'function' && Promise) { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid, invalidFields) { |
||||||
|
!valid ? resolve(invalidFields) : reject(valid); |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
let fieldsValue = {} |
||||||
|
let tempInvalidFields = Object.assign({}, invalidFields) |
||||||
|
|
||||||
|
Object.keys(this.formRules).forEach(item => { |
||||||
|
const values = this.formRules[item] |
||||||
|
const rules = (values && values.rules) || [] |
||||||
|
let isNoField = false |
||||||
|
for (let i = 0; i < rules.length; i++) { |
||||||
|
const rule = rules[i] |
||||||
|
if (rule.required) { |
||||||
|
isNoField = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 如果存在 required 才会将内容插入校验对象 |
||||||
|
if (!isNoField && (!tempInvalidFields[item] && tempInvalidFields[item] !== false)) { |
||||||
|
delete tempInvalidFields[item] |
||||||
|
} |
||||||
|
|
||||||
|
}) |
||||||
|
// 循环字段是否存在于校验规则中 |
||||||
|
for (let i in this.formRules) { |
||||||
|
for (let j in tempInvalidFields) { |
||||||
|
if (i === j) { |
||||||
|
fieldsValue[i] = tempInvalidFields[i] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let result = [] |
||||||
|
let example = null |
||||||
|
if (this.validator) { |
||||||
|
for (let i in fieldsValue) { |
||||||
|
const resultData = await this.validator.validateUpdate({ |
||||||
|
[i]: fieldsValue[i] |
||||||
|
}, this.formData) |
||||||
|
if (resultData) { |
||||||
|
example = this.childrens.find(child => child.name === resultData.key) |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === example.name) |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = resultData.errorMessage |
||||||
|
} |
||||||
|
result.push(resultData) |
||||||
|
if (this.errShowType === 'undertext') { |
||||||
|
if (example) example.errMsg = resultData.errorMessage |
||||||
|
} else { |
||||||
|
if (this.errShowType === 'toast') { |
||||||
|
uni.showToast({ |
||||||
|
title: resultData.errorMessage || '校验错误', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
break |
||||||
|
} else if (this.errShowType === 'modal') { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: resultData.errorMessage || '校验错误' |
||||||
|
}) |
||||||
|
break |
||||||
|
} else { |
||||||
|
if (example) example.errMsg = resultData.errorMessage |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (Array.isArray(result)) { |
||||||
|
if (result.length === 0) result = null |
||||||
|
} |
||||||
|
if (type === 'submit') { |
||||||
|
this.$emit('submit', { |
||||||
|
detail: { |
||||||
|
value: invalidFields, |
||||||
|
errors: result |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
this.$emit('validate', result) |
||||||
|
} |
||||||
|
callback && typeof callback === 'function' && callback(result, invalidFields) |
||||||
|
if (promise && callback) { |
||||||
|
return promise |
||||||
|
} else { |
||||||
|
return null |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 外部调用方法 |
||||||
|
* 手动提交校验表单 |
||||||
|
* 对整个表单进行校验的方法,参数为一个回调函数。 |
||||||
|
*/ |
||||||
|
submit(callback) { |
||||||
|
// Object.assign(this.formData,formData) |
||||||
|
return this.validateAll(this.formData, 'submit', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 外部调用方法 |
||||||
|
* 校验表单 |
||||||
|
* 对整个表单进行校验的方法,参数为一个回调函数。 |
||||||
|
*/ |
||||||
|
validate(callback) { |
||||||
|
return this.validateAll(this.formData, '', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 部分表单校验 |
||||||
|
* @param {Object} props |
||||||
|
* @param {Object} cb |
||||||
|
*/ |
||||||
|
validateField(props, callback) { |
||||||
|
props = [].concat(props); |
||||||
|
let invalidFields = {} |
||||||
|
this.childrens.forEach(item => { |
||||||
|
// item.parentVal((val, name) => { |
||||||
|
if (props.indexOf(item.name) !== -1) { |
||||||
|
invalidFields = Object.assign({}, invalidFields, { |
||||||
|
[item.name]: this.formData[item.name] |
||||||
|
}) |
||||||
|
} |
||||||
|
// }) |
||||||
|
|
||||||
|
}) |
||||||
|
return this.validateAll(invalidFields, '', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果 |
||||||
|
*/ |
||||||
|
resetFields() { |
||||||
|
this.resetForm() |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果 |
||||||
|
*/ |
||||||
|
clearValidate(props) { |
||||||
|
props = [].concat(props); |
||||||
|
this.childrens.forEach(item => { |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === item.name) |
||||||
|
if (props.length === 0) { |
||||||
|
item.errMsg = '' |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (props.indexOf(item.name) !== -1) { |
||||||
|
item.errMsg = '' |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 把 value 转换成指定的类型 |
||||||
|
_getValue(item, value) { |
||||||
|
const rules = item.formRules.rules || [] |
||||||
|
const isRuleNum = rules.find(val => val.format && this.type_filter(val.format)) |
||||||
|
const isRuleBool = rules.find(val => val.format && val.format === 'boolean' || val.format === 'bool') |
||||||
|
// 输入值为 number |
||||||
|
if (isRuleNum) { |
||||||
|
value = value === '' || value === null ? null : Number(value) |
||||||
|
} |
||||||
|
// 简单判断真假值 |
||||||
|
if (isRuleBool) { |
||||||
|
value = !value ? false : true |
||||||
|
} |
||||||
|
return value |
||||||
|
}, |
||||||
|
// 过滤数字类型 |
||||||
|
type_filter(format) { |
||||||
|
return format === 'int' || format === 'double' || format === 'number' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-forms { |
||||||
|
overflow: hidden; |
||||||
|
// padding: 10px 15px; |
||||||
|
// background-color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms--top { |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-top: 22px; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,442 @@ |
|||||||
|
|
||||||
|
var pattern = { |
||||||
|
email: /^\S+?@\S+?\.\S+?$/, |
||||||
|
url: new RegExp("^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", 'i') |
||||||
|
}; |
||||||
|
|
||||||
|
const FORMAT_MAPPING = { |
||||||
|
"int": 'number', |
||||||
|
"bool": 'boolean', |
||||||
|
"double": 'number', |
||||||
|
"long": 'number', |
||||||
|
"password": 'string' |
||||||
|
} |
||||||
|
|
||||||
|
function formatMessage(args, resources) { |
||||||
|
var defaultMessage = ['label'] |
||||||
|
defaultMessage.forEach((item) => { |
||||||
|
if (args[item] === undefined) { |
||||||
|
args[item] = '' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let str = resources |
||||||
|
for (let key in args) { |
||||||
|
let reg = new RegExp('{' + key + '}') |
||||||
|
str = str.replace(reg, args[key]) |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
function isEmptyValue(value, type) { |
||||||
|
if (value === undefined || value === null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof value === 'string' && !value) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (Array.isArray(value) && !value.length) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (type === 'object' && !Object.keys(value).length) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
const types = { |
||||||
|
integer(value) { |
||||||
|
return types.number(value) && parseInt(value, 10) === value; |
||||||
|
}, |
||||||
|
string(value) { |
||||||
|
return typeof value === 'string'; |
||||||
|
}, |
||||||
|
number(value) { |
||||||
|
if (isNaN(value)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return typeof value === 'number'; |
||||||
|
}, |
||||||
|
"boolean": function (value) { |
||||||
|
return typeof value === 'boolean'; |
||||||
|
}, |
||||||
|
"float": function (value) { |
||||||
|
return types.number(value) && !types.integer(value); |
||||||
|
}, |
||||||
|
array(value) { |
||||||
|
return Array.isArray(value); |
||||||
|
}, |
||||||
|
object(value) { |
||||||
|
return typeof value === 'object' && !types.array(value); |
||||||
|
}, |
||||||
|
date(value) { |
||||||
|
var v |
||||||
|
if (value instanceof Date) { |
||||||
|
v = value; |
||||||
|
} else { |
||||||
|
v = new Date(value); |
||||||
|
} |
||||||
|
return typeof v.getTime === 'function' && typeof v.getMonth === 'function' && typeof v.getYear === 'function' && !isNaN(v.getTime()); |
||||||
|
}, |
||||||
|
timestamp(value) { |
||||||
|
if (!this.integer(value) || Math.abs(value).toString().length > 16) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return this.date(value); |
||||||
|
}, |
||||||
|
email(value) { |
||||||
|
return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255; |
||||||
|
}, |
||||||
|
url(value) { |
||||||
|
return typeof value === 'string' && !!value.match(pattern.url); |
||||||
|
}, |
||||||
|
pattern(reg, value) { |
||||||
|
try { |
||||||
|
return new RegExp(reg).test(value); |
||||||
|
} catch (e) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
}, |
||||||
|
method(value) { |
||||||
|
return typeof value === 'function'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RuleValidator { |
||||||
|
|
||||||
|
constructor(message) { |
||||||
|
this._message = message |
||||||
|
} |
||||||
|
|
||||||
|
async validateRule(key, value, data, allData) { |
||||||
|
var result = null |
||||||
|
|
||||||
|
let rules = key.rules |
||||||
|
|
||||||
|
let hasRequired = rules.findIndex((item) => { |
||||||
|
return item.required |
||||||
|
}) |
||||||
|
if (hasRequired < 0) { |
||||||
|
if (value === null || value === undefined) { |
||||||
|
return result |
||||||
|
} |
||||||
|
if (typeof value === 'string' && !value.length) { |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var message = this._message |
||||||
|
|
||||||
|
if (rules === undefined) { |
||||||
|
return message['default'] |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < rules.length; i++) { |
||||||
|
let rule = rules[i] |
||||||
|
let vt = this._getValidateType(rule) |
||||||
|
|
||||||
|
if (key.label !== undefined) { |
||||||
|
Object.assign(rule, { |
||||||
|
label: key.label |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if (RuleValidatorHelper[vt]) { |
||||||
|
result = RuleValidatorHelper[vt](rule, value, message) |
||||||
|
if (result != null) { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (rule.validateExpr) { |
||||||
|
let now = Date.now() |
||||||
|
let resultExpr = rule.validateExpr(value, allData, now) |
||||||
|
if (resultExpr === false) { |
||||||
|
result = this._getMessage(rule, rule.errorMessage || this._message['default']) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (rule.validateFunction) { |
||||||
|
result = await this.validateFunction(rule, value, data, allData, vt) |
||||||
|
if (result !== null) { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async validateFunction(rule, value, data, allData, vt) { |
||||||
|
let result = null |
||||||
|
try { |
||||||
|
let callbackMessage = null |
||||||
|
const res = await rule.validateFunction(rule, value, allData || data, (message) => { |
||||||
|
callbackMessage = message |
||||||
|
}) |
||||||
|
if (callbackMessage || (typeof res === 'string' && res) || res === false) { |
||||||
|
result = this._getMessage(rule, callbackMessage || res, vt) |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
result = this._getMessage(rule, e.message, vt) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
_getMessage(rule, message, vt) { |
||||||
|
return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default']) |
||||||
|
} |
||||||
|
|
||||||
|
_getValidateType(rule) { |
||||||
|
// TODO
|
||||||
|
var result = '' |
||||||
|
if (rule.required) { |
||||||
|
result = 'required' |
||||||
|
} else if (rule.format) { |
||||||
|
result = 'format' |
||||||
|
} else if (rule.range) { |
||||||
|
result = 'range' |
||||||
|
} else if (rule.maximum || rule.minimum) { |
||||||
|
result = 'rangeNumber' |
||||||
|
} else if (rule.maxLength || rule.minLength) { |
||||||
|
result = 'rangeLength' |
||||||
|
} else if (rule.pattern) { |
||||||
|
result = 'pattern' |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const RuleValidatorHelper = { |
||||||
|
required(rule, value, message) { |
||||||
|
if (rule.required && isEmptyValue(value, rule.format || typeof value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.required); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
range(rule, value, message) { |
||||||
|
const { range, errorMessage } = rule; |
||||||
|
|
||||||
|
let list = new Array(range.length); |
||||||
|
for (let i = 0; i < range.length; i++) { |
||||||
|
const item = range[i]; |
||||||
|
if (types.object(item) && item.value !== undefined) { |
||||||
|
list[i] = item.value; |
||||||
|
} else { |
||||||
|
list[i] = item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let result = false |
||||||
|
if (Array.isArray(value)) { |
||||||
|
result = (new Set(value.concat(list)).size === list.length); |
||||||
|
} else { |
||||||
|
if (list.indexOf(value) > -1) { |
||||||
|
result = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!result) { |
||||||
|
return formatMessage(rule, errorMessage || message['enum']); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
rangeNumber(rule, value, message) { |
||||||
|
if (!types.number(value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
let { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = rule; |
||||||
|
let min = exclusiveMinimum ? value <= minimum : value < minimum; |
||||||
|
let max = exclusiveMaximum ? value >= maximum : value > maximum; |
||||||
|
|
||||||
|
if (minimum !== undefined && min) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].min) |
||||||
|
} else if (maximum !== undefined && max) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].max) |
||||||
|
} else if (minimum !== undefined && maximum !== undefined && (min || max)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].range) |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
rangeLength(rule, value, message) { |
||||||
|
if (!types.string(value) && !types.array(value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
let min = rule.minLength; |
||||||
|
let max = rule.maxLength; |
||||||
|
let val = value.length; |
||||||
|
|
||||||
|
if (min !== undefined && val < min) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].min) |
||||||
|
} else if (max !== undefined && val > max) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].max) |
||||||
|
} else if (min !== undefined && max !== undefined && (val < min || val > max)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].range) |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
pattern(rule, value, message) { |
||||||
|
if (!types['pattern'](rule.pattern, value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
format(rule, value, message) { |
||||||
|
var customTypes = Object.keys(types); |
||||||
|
var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : rule.format; |
||||||
|
|
||||||
|
if (customTypes.indexOf(format) > -1) { |
||||||
|
if (!types[format](value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.types[format]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SchemaValidator extends RuleValidator { |
||||||
|
|
||||||
|
constructor(schema, options) { |
||||||
|
super(SchemaValidator.message); |
||||||
|
|
||||||
|
this._schema = schema |
||||||
|
this._options = options || null |
||||||
|
} |
||||||
|
|
||||||
|
updateSchema(schema) { |
||||||
|
this._schema = schema |
||||||
|
} |
||||||
|
|
||||||
|
async validate(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidate(data, false, allData) |
||||||
|
} |
||||||
|
return result.length ? result[0] : null |
||||||
|
} |
||||||
|
|
||||||
|
async validateAll(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidate(data, true, allData) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async validateUpdate(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidateUpdate(data, false, allData) |
||||||
|
} |
||||||
|
return result.length ? result[0] : null |
||||||
|
} |
||||||
|
|
||||||
|
async invokeValidate(data, all, allData) { |
||||||
|
let result = [] |
||||||
|
let schema = this._schema |
||||||
|
for (let key in schema) { |
||||||
|
let value = schema[key] |
||||||
|
let errorMessage = await this.validateRule(value, data[key], data, allData) |
||||||
|
if (errorMessage != null) { |
||||||
|
result.push({ |
||||||
|
key, |
||||||
|
errorMessage |
||||||
|
}) |
||||||
|
if (!all) break |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async invokeValidateUpdate(data, all, allData) { |
||||||
|
let result = [] |
||||||
|
for (let key in data) { |
||||||
|
let errorMessage = await this.validateRule(this._schema[key], data[key], data, allData) |
||||||
|
if (errorMessage != null) { |
||||||
|
result.push({ |
||||||
|
key, |
||||||
|
errorMessage |
||||||
|
}) |
||||||
|
if (!all) break |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
_checkFieldInSchema(data) { |
||||||
|
var keys = Object.keys(data) |
||||||
|
var keys2 = Object.keys(this._schema) |
||||||
|
if (new Set(keys.concat(keys2)).size === keys2.length) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
return [{ |
||||||
|
key: 'invalid', |
||||||
|
errorMessage: SchemaValidator.message['defaultInvalid'] |
||||||
|
}] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function Message() { |
||||||
|
return { |
||||||
|
default: '验证错误', |
||||||
|
defaultInvalid: '字段超出范围', |
||||||
|
required: '{label}必填', |
||||||
|
'enum': '{label}超出范围', |
||||||
|
whitespace: '{label}不能为空', |
||||||
|
date: { |
||||||
|
format: '{label}日期{value}格式无效', |
||||||
|
parse: '{label}日期无法解析,{value}无效', |
||||||
|
invalid: '{label}日期{value}无效' |
||||||
|
}, |
||||||
|
types: { |
||||||
|
string: '{label}类型无效', |
||||||
|
array: '{label}类型无效', |
||||||
|
object: '{label}类型无效', |
||||||
|
number: '{label}类型无效', |
||||||
|
date: '{label}类型无效', |
||||||
|
boolean: '{label}类型无效', |
||||||
|
integer: '{label}类型无效', |
||||||
|
float: '{label}类型无效', |
||||||
|
regexp: '{label}无效', |
||||||
|
email: '{label}类型无效', |
||||||
|
url: '{label}类型无效' |
||||||
|
}, |
||||||
|
length: { |
||||||
|
min: '{label}长度不能少于{minLength}', |
||||||
|
max: '{label}长度不能超过{maxLength}', |
||||||
|
range: '{label}必须介于{minLength}和{maxLength}之间' |
||||||
|
}, |
||||||
|
number: { |
||||||
|
min: '{label}不能小于{minimum}', |
||||||
|
max: '{label}不能大于{maximum}', |
||||||
|
range: '{label}必须介于{minimum}and{maximum}之间' |
||||||
|
}, |
||||||
|
pattern: { |
||||||
|
mismatch: '{label}格式不匹配' |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
SchemaValidator.message = new Message(); |
||||||
|
|
||||||
|
export default SchemaValidator |
@ -0,0 +1,127 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-group" :class="['uni-group--'+mode ,margin?'group-margin':'']" :style="{marginTop: `${top}px` }"> |
||||||
|
<slot name="title"> |
||||||
|
<view v-if="title" class="uni-group__title" :style="{'padding-left':border?'30px':'15px'}"> |
||||||
|
<text class="uni-group__title-text">{{ title }}</text> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
<view class="uni-group__content" :class="{'group-conent-padding':border}"> |
||||||
|
<slot /> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Group 分组 |
||||||
|
* @description 表单字段分组 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21002 |
||||||
|
* @property {String} title 主标题 |
||||||
|
* @property {Number} top 分组间隔 |
||||||
|
*/ |
||||||
|
export default { |
||||||
|
name: 'uniGroup', |
||||||
|
props: { |
||||||
|
title: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
top: { |
||||||
|
type: [Number, String], |
||||||
|
default: 10 |
||||||
|
}, |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'default' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
margin: false, |
||||||
|
border: false |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
title(newVal) { |
||||||
|
if (uni.report && newVal !== '') { |
||||||
|
uni.report('title', newVal) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm() |
||||||
|
if (this.form) { |
||||||
|
this.margin = true |
||||||
|
this.border = this.form.border |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm() { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== 'uniForms') { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
onClick() { |
||||||
|
this.$emit('click') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-group { |
||||||
|
background: #fff; |
||||||
|
margin-top: 20rpx; |
||||||
|
// border: 1px red solid; |
||||||
|
} |
||||||
|
|
||||||
|
.group-margin { |
||||||
|
margin: 0 -30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__title { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
align-items: center; |
||||||
|
padding-left: 30rpx; |
||||||
|
height: 80rpx; |
||||||
|
background-color: $uni-bg-color-grey; |
||||||
|
font-weight: normal; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__content { |
||||||
|
padding: 30rpx 0; |
||||||
|
// padding-bottom: 5px; |
||||||
|
background-color: #FFF; |
||||||
|
} |
||||||
|
|
||||||
|
.group-conent-padding { |
||||||
|
padding: 0 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__title-text { |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.distraction { |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group--card { |
||||||
|
margin: 20rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
overflow: hidden; |
||||||
|
box-shadow: 0 0 10rpx 2rpx rgba($color: #000000, $alpha: 0.08); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||||
|
export default { |
||||||
|
'contact': '\ue100', |
||||||
|
'person': '\ue101', |
||||||
|
'personadd': '\ue102', |
||||||
|
'contact-filled': '\ue130', |
||||||
|
'person-filled': '\ue131', |
||||||
|
'personadd-filled': '\ue132', |
||||||
|
'phone': '\ue200', |
||||||
|
'email': '\ue201', |
||||||
|
'chatbubble': '\ue202', |
||||||
|
'chatboxes': '\ue203', |
||||||
|
'phone-filled': '\ue230', |
||||||
|
'email-filled': '\ue231', |
||||||
|
'chatbubble-filled': '\ue232', |
||||||
|
'chatboxes-filled': '\ue233', |
||||||
|
'weibo': '\ue260', |
||||||
|
'weixin': '\ue261', |
||||||
|
'pengyouquan': '\ue262', |
||||||
|
'chat': '\ue263', |
||||||
|
'qq': '\ue264', |
||||||
|
'videocam': '\ue300', |
||||||
|
'camera': '\ue301', |
||||||
|
'mic': '\ue302', |
||||||
|
'location': '\ue303', |
||||||
|
'mic-filled': '\ue332', |
||||||
|
'speech': '\ue332', |
||||||
|
'location-filled': '\ue333', |
||||||
|
'micoff': '\ue360', |
||||||
|
'image': '\ue363', |
||||||
|
'map': '\ue364', |
||||||
|
'compose': '\ue400', |
||||||
|
'trash': '\ue401', |
||||||
|
'upload': '\ue402', |
||||||
|
'download': '\ue403', |
||||||
|
'close': '\ue404', |
||||||
|
'redo': '\ue405', |
||||||
|
'undo': '\ue406', |
||||||
|
'refresh': '\ue407', |
||||||
|
'star': '\ue408', |
||||||
|
'plus': '\ue409', |
||||||
|
'minus': '\ue410', |
||||||
|
'circle': '\ue411', |
||||||
|
'checkbox': '\ue411', |
||||||
|
'close-filled': '\ue434', |
||||||
|
'clear': '\ue434', |
||||||
|
'refresh-filled': '\ue437', |
||||||
|
'star-filled': '\ue438', |
||||||
|
'plus-filled': '\ue439', |
||||||
|
'minus-filled': '\ue440', |
||||||
|
'circle-filled': '\ue441', |
||||||
|
'checkbox-filled': '\ue442', |
||||||
|
'closeempty': '\ue460', |
||||||
|
'refreshempty': '\ue461', |
||||||
|
'reload': '\ue462', |
||||||
|
'starhalf': '\ue463', |
||||||
|
'spinner': '\ue464', |
||||||
|
'spinner-cycle': '\ue465', |
||||||
|
'search': '\ue466', |
||||||
|
'plusempty': '\ue468', |
||||||
|
'forward': '\ue470', |
||||||
|
'back': '\ue471', |
||||||
|
'left-nav': '\ue471', |
||||||
|
'checkmarkempty': '\ue472', |
||||||
|
'home': '\ue500', |
||||||
|
'navigate': '\ue501', |
||||||
|
'gear': '\ue502', |
||||||
|
'paperplane': '\ue503', |
||||||
|
'info': '\ue504', |
||||||
|
'help': '\ue505', |
||||||
|
'locked': '\ue506', |
||||||
|
'more': '\ue507', |
||||||
|
'flag': '\ue508', |
||||||
|
'home-filled': '\ue530', |
||||||
|
'gear-filled': '\ue532', |
||||||
|
'info-filled': '\ue534', |
||||||
|
'help-filled': '\ue535', |
||||||
|
'more-filled': '\ue537', |
||||||
|
'settings': '\ue560', |
||||||
|
'list': '\ue562', |
||||||
|
'bars': '\ue563', |
||||||
|
'loop': '\ue565', |
||||||
|
'paperclip': '\ue567', |
||||||
|
'eye': '\ue568', |
||||||
|
'arrowup': '\ue580', |
||||||
|
'arrowdown': '\ue581', |
||||||
|
'arrowleft': '\ue582', |
||||||
|
'arrowright': '\ue583', |
||||||
|
'arrowthinup': '\ue584', |
||||||
|
'arrowthindown': '\ue585', |
||||||
|
'arrowthinleft': '\ue586', |
||||||
|
'arrowthinright': '\ue587', |
||||||
|
'pulldown': '\ue588', |
||||||
|
'closefill': '\ue589', |
||||||
|
'sound': '\ue590', |
||||||
|
'scan': '\ue612' |
||||||
|
} |
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -0,0 +1,210 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-pagination"> |
||||||
|
<view class="uni-pagination__btns"> |
||||||
|
<view class="uni-pagination__btn" :class="currentIndex === 1 ? 'uni-pagination--disabled' : 'uni-pagination--enabled'" |
||||||
|
:hover-class="currentIndex === 1 ? '' : 'uni-pagination--hover'" :hover-start-time="20" :hover-stay-time="70" |
||||||
|
@click="clickLeft"> |
||||||
|
<template v-if="showIcon===true || showIcon === 'true'"> |
||||||
|
<uni-icons color="#000" size="20" type="arrowleft" /> |
||||||
|
</template> |
||||||
|
<template v-else><text class="uni-pagination__child-btn">{{ prevText }}</text></template> |
||||||
|
</view> |
||||||
|
<view class="uni-pagination__btn" :class="currentIndex === maxPage ? 'uni-pagination--disabled' : 'uni-pagination--enabled'" |
||||||
|
:hover-class="currentIndex === maxPage ? '' : 'uni-pagination--hover'" :hover-start-time="20" :hover-stay-time="70" |
||||||
|
@click="clickRight"> |
||||||
|
<template v-if="showIcon===true || showIcon === 'true'"> |
||||||
|
<uni-icons color="#000" size="20" type="arrowright" /> |
||||||
|
</template> |
||||||
|
<template v-else><text class="uni-pagination__child-btn">{{ nextText }}</text></template> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="uni-pagination__num"> |
||||||
|
<view class="uni-pagination__num-current"> |
||||||
|
<text class="uni-pagination__num-current-text" style="color:#007aff">{{ currentIndex }}</text><text class="uni-pagination__num-current-text">/{{ maxPage || 0 }}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import uniIcons from '../uni-icons/uni-icons.vue' |
||||||
|
export default { |
||||||
|
name: 'UniPagination', |
||||||
|
components: { |
||||||
|
uniIcons |
||||||
|
}, |
||||||
|
props: { |
||||||
|
prevText: { |
||||||
|
type: String, |
||||||
|
default: '上一页' |
||||||
|
}, |
||||||
|
nextText: { |
||||||
|
type: String, |
||||||
|
default: '下一页' |
||||||
|
}, |
||||||
|
current: { |
||||||
|
type: [Number, String], |
||||||
|
default: 1 |
||||||
|
}, |
||||||
|
total: { // 数据总量 |
||||||
|
type: [Number, String], |
||||||
|
default: 0 |
||||||
|
}, |
||||||
|
pageSize: { // 每页数据量 |
||||||
|
type: [Number, String], |
||||||
|
default: 10 |
||||||
|
}, |
||||||
|
showIcon: { // 是否以 icon 形式展示按钮 |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
currentIndex: 1 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
maxPage() { |
||||||
|
let maxPage = 1 |
||||||
|
let total = Number(this.total) |
||||||
|
let pageSize = Number(this.pageSize) |
||||||
|
if (total && pageSize) { |
||||||
|
maxPage = Math.ceil(total / pageSize) |
||||||
|
} |
||||||
|
return maxPage |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
current(val) { |
||||||
|
this.currentIndex = +val |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.currentIndex = +this.current |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
clickLeft() { |
||||||
|
if (Number(this.currentIndex) === 1) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.currentIndex -= 1 |
||||||
|
this.change('prev') |
||||||
|
}, |
||||||
|
clickRight() { |
||||||
|
if (Number(this.currentIndex) === this.maxPage) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.currentIndex += 1 |
||||||
|
this.change('next') |
||||||
|
}, |
||||||
|
change(e) { |
||||||
|
this.$emit('change', { |
||||||
|
type: e, |
||||||
|
current: this.currentIndex |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-pagination { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
/* #ifdef APP-NVUE */ |
||||||
|
padding: 0 40rpx; |
||||||
|
/* #endif */ |
||||||
|
width: 700rpx; |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__btns { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__btn { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
width: 60rpx; |
||||||
|
height: 60rpx; |
||||||
|
line-height: 60rpx; |
||||||
|
border-radius: 50%; |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
position: relative; |
||||||
|
background-color: $uni-bg-color-grey; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
border-width: 2rpx; |
||||||
|
border-style: solid; |
||||||
|
border-color: $uni-border-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__child-btn { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
position: relative; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
position: absolute; |
||||||
|
left: 300rpx; |
||||||
|
top: 0; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
width: 100rpx; |
||||||
|
height: 60rpx; |
||||||
|
line-height: 60rpx; |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num-current { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num-current-text { |
||||||
|
font-size: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--enabled { |
||||||
|
color: #333333; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--disabled { |
||||||
|
opacity: 0.3; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--hover { |
||||||
|
color: rgba(0, 0, 0, .6); |
||||||
|
background-color: $uni-bg-color-hover; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,324 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-datetime-picker"> |
||||||
|
<view @click="tiggerTimePicker"> |
||||||
|
<slot> |
||||||
|
<view class="uni-datetime-picker-timebox uni-datetime-picker-flex"> |
||||||
|
{{pickValue}}<view v-if="!pickValue" class="uni-datetime-picker-time">请选择<!-- 时间 --></view> |
||||||
|
<template> |
||||||
|
<uni-icons class="content-clear-icon is-textarea-icon" type="clear" :size="clearSize" |
||||||
|
color="#c0c4cc" @click="onClear"></uni-icons> |
||||||
|
</template> |
||||||
|
<view class="uni-datetime-picker-down-arrow"></view> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-mask" @click="initTimePicker"></view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-popup"> |
||||||
|
<view class="uni-title"> |
||||||
|
请选择<!-- 和时间 --> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- <picker @change="bindPickerChange" :value="index" :range="array" :range-key="'name'" class="uni-datetime-picker-hyphen"> |
||||||
|
|
||||||
|
</picker> --> |
||||||
|
|
||||||
|
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="pickValue" @change="bindPickerChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in pickData" :key="index">{{item.name}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> |
||||||
|
<view class="uni-datetime-picker-btn"> |
||||||
|
<!-- <view class="" @click="clearTime">重置</view> --> |
||||||
|
<view class="uni-datetime-picker-btn-group"> |
||||||
|
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">取消</view> |
||||||
|
<view class="" @click="setTime">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
visible: false, |
||||||
|
pickName: '', |
||||||
|
years: [], |
||||||
|
year: 1900, |
||||||
|
indicatorStyle: `height: 50px;`, |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
pickData: { |
||||||
|
type: Object, |
||||||
|
default: [] |
||||||
|
}, |
||||||
|
pickValue: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'datetime-local' |
||||||
|
}, |
||||||
|
timestamp: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newValue) { |
||||||
|
this.parseValue(this.value) |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.parseValue(this.value) |
||||||
|
if (this.value) { |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//切换遮罩层 |
||||||
|
tiggerTimePicker() { |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
bindPickerChange(e){ |
||||||
|
const val = e.detail.value |
||||||
|
this.pickValue = this.pickData[val[0]].value |
||||||
|
this.pickName = this.pickData[val[0]].name |
||||||
|
console.log(this.pickValue) |
||||||
|
}, |
||||||
|
onClear(){ |
||||||
|
this.pickValue = '', |
||||||
|
this.pickName = '' |
||||||
|
}, |
||||||
|
setTime() { |
||||||
|
this.$emit('change', this.pickName) |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
parseDateTime(datetime) { |
||||||
|
let defaultDate = null |
||||||
|
if (!datetime) { |
||||||
|
defaultDate = new Date() |
||||||
|
} else { |
||||||
|
defaultDate = new Date(datetime) |
||||||
|
} |
||||||
|
this.year = defaultDate.getFullYear() |
||||||
|
if (this.year < this.minYear || this.year > this.maxYear) { |
||||||
|
const now = Date.now() |
||||||
|
this.parseDateTime(now) |
||||||
|
return |
||||||
|
} |
||||||
|
this.month = defaultDate.getMonth() + 1 |
||||||
|
this.day = defaultDate.getDate() |
||||||
|
this.hour = defaultDate.getHours() |
||||||
|
this.minute = defaultDate.getMinutes() |
||||||
|
this.second = defaultDate.getSeconds() |
||||||
|
}, |
||||||
|
parseValue(defaultTime) { |
||||||
|
if (Number(defaultTime)) { |
||||||
|
defaultTime = parseInt(defaultTime) |
||||||
|
} |
||||||
|
this.parseDateTime(defaultTime) |
||||||
|
}, |
||||||
|
bindDateChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.year = this.years[val[0]] |
||||||
|
this.month = this.months[val[1]] |
||||||
|
this.day = this.days[val[2]] |
||||||
|
}, |
||||||
|
bindTimeChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.hour = this.hours[val[0]] |
||||||
|
this.minute = this.minutes[val[1]] |
||||||
|
this.second = this.seconds[val[2]] |
||||||
|
}, |
||||||
|
initTimePicker() { |
||||||
|
// if (!this.time) { |
||||||
|
// this.parseValue() |
||||||
|
// } |
||||||
|
this.parseValue(this.value) |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
clearTime() { |
||||||
|
this.time = '' |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
initTime() { |
||||||
|
this.time = this.createDomSting() |
||||||
|
if (!this.timestamp) { |
||||||
|
this.formItem && this.formItem.setValue(this.time) |
||||||
|
this.$emit('change', this.time) |
||||||
|
} else { |
||||||
|
this.formItem && this.formItem.setValue(this.createTimeStamp(this.time)) |
||||||
|
this.$emit('change', this.createTimeStamp(this.time)) |
||||||
|
} |
||||||
|
}, |
||||||
|
createTimeStamp(time) { |
||||||
|
return Date.parse(new Date(time)) |
||||||
|
}, |
||||||
|
createDomSting() { |
||||||
|
const yymmdd = this.year + |
||||||
|
'-' + |
||||||
|
(this.month < 10 ? '0' + this.month : this.month) + |
||||||
|
'-' + |
||||||
|
(this.day < 10 ? '0' + this.day : this.day) |
||||||
|
// + |
||||||
|
// ' ' + |
||||||
|
// (this.hour < 10 ? '0' + this.hour : this.hour) + |
||||||
|
// ':' + |
||||||
|
// (this.minute < 10 ? '0' + this.minute : this.minute) + |
||||||
|
// ':' + |
||||||
|
// (this.second < 10 ? '0' + this.second : this.second) |
||||||
|
|
||||||
|
return yymmdd |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-datetime-picker-view { |
||||||
|
width: 100%; |
||||||
|
height: 130px; |
||||||
|
margin-top: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-item { |
||||||
|
line-height: 100rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-title{ |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn { |
||||||
|
margin-top: 120rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
color: #00B9FF; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn-group { |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-cancel { |
||||||
|
margin-right: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-mask { |
||||||
|
position: fixed; |
||||||
|
bottom: 0px; |
||||||
|
top: 0px; |
||||||
|
left: 0px; |
||||||
|
right: 0px; |
||||||
|
background-color: rgba(0, 0, 0, 0.4); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 998; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-popup { |
||||||
|
border-radius: 16rpx; |
||||||
|
padding: 60rpx; |
||||||
|
width: 540rpx; |
||||||
|
background-color: #fff; |
||||||
|
position: fixed; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-time { |
||||||
|
color: grey; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-colon::after { |
||||||
|
content: ':'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-hyphen::after { |
||||||
|
content: '-'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: -4rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-timebox { |
||||||
|
border: 2rpx solid #E5E5E5; |
||||||
|
border-radius: 40rpx; |
||||||
|
padding: 14rpx 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
cursor: pointer; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
// 下箭头 |
||||||
|
.uni-datetime-picker-down-arrow { |
||||||
|
display :inline-block; |
||||||
|
position: relative; |
||||||
|
width: 40rpx; |
||||||
|
height: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-down-arrow::after { |
||||||
|
display: inline-block; |
||||||
|
content: " "; |
||||||
|
height: 18rpx; |
||||||
|
width: 18rpx; |
||||||
|
border-width: 0 2rpx 2rpx 0; |
||||||
|
border-color: #E5E5E5; |
||||||
|
border-style: solid; |
||||||
|
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0); |
||||||
|
transform-origin: center; |
||||||
|
transition: transform .3s; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
right: 10rpx; |
||||||
|
margin-top: -10rpx; |
||||||
|
} |
||||||
|
.uni-datetime-picker-flex { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,297 @@ |
|||||||
|
// import $http from './requestConfig';
|
||||||
|
// // let host = 'http://192.168.31.140:8080';
|
||||||
|
// // let host = 'http://192.168.31.136:8080';
|
||||||
|
|
||||||
|
// let host = 'http://dq.huorantech.cn'
|
||||||
|
|
||||||
|
// export const login = p => post('/apiHrmsAuth/hrms/auth/userlogin/login', p); //登录
|
||||||
|
// export const phoneCode = p => get('/apiHrmsAuth/hrms/auth/userlogin/sendCode', p); //找回密码验证码
|
||||||
|
|
||||||
|
// export const verifyMobile = p => post('/apiHrmsAuth/hrms/auth/userlogin/verifyMobile', p); //忘记密码
|
||||||
|
// export const getBackPwd = p => post('/apiHrmsAuth/hrms/auth/userlogin/getBackPwd', p); //设置新密码
|
||||||
|
// export const querycustomerList = p => $http.get('/api-crms/crms/customer/queryList', p); //查询客户
|
||||||
|
// export const deleteCustomer = p => del('/api-crms/crms/customer/deleteCustomer', p); //删除客户
|
||||||
|
// export const queryDetails = p => get('/api-crms/crms/customer/queryDetails', p); //查询客户详情
|
||||||
|
// export const savePersonal = p => post('/api-crms/crms/customer/savePersonal', p); //添加个人客户
|
||||||
|
// export const saveCompany = p => post('/api-crms/crms/customer/saveCompany', p); //添加企业客户
|
||||||
|
// export const updateCompany = p => post('/api-crms/crms/customer/updateCompany', p); //编辑企业客户
|
||||||
|
// export const updatePersonal = p => post('/api-crms/crms/customer/updatePersonal', p); //编辑个人客户
|
||||||
|
// export const listEmployeeName = p => get('/api-hrms/hrms/employee/listEmployeeName', p); //查询所有的客户经理
|
||||||
|
// export const updatePwd = p => post('/apiHrmsAuth/hrms/auth/userlogin/updatePwd', p); //修改平台超管员密码
|
||||||
|
// export const loginLogList = p => get('/apiHrmsAuth/hrms/auth/systemLog/loginLogList', p); //平台日志列表
|
||||||
|
// export const rolePermissionList = p => get('/apiHrmsAuth/hrms/auth/permission/rolePermissionList', p); //角色权限列表
|
||||||
|
// export const queryPermission = p => get('/apiHrmsAuth/hrms/auth/permission/queryPermissionByRoleId', p); //查看角色权限
|
||||||
|
// export const saveRolePermission = p => post('/apiHrmsAuth/hrms/auth/permission/saveRolePermission', p); //新增角色权限
|
||||||
|
// export const updateRolePermission = p => post('/apiHrmsAuth/hrms/auth/permission/updateRolePermission', p); //编辑角色权限
|
||||||
|
// export const delRolePermission = p => post('/apiHrmsAuth/hrms/auth/permission/delRolePermission', p); //删除角色权限
|
||||||
|
// export const jurTree = p => get('/apiHrmsAuth/hrms/auth/permission/tree', p); //查询所有权限树形结构
|
||||||
|
|
||||||
|
// // 后台-人资-部门
|
||||||
|
// export const departmentTree = p => get('/api-hrms/hrms/dept/tree',p); //部门树形结构数据(显示部门)
|
||||||
|
// export const addDepartment = p => post('/api-hrms/hrms/dept/save', p); //新增一条部门信息
|
||||||
|
// export const deleteDepartment = p => del('/api-hrms/hrms/dept/delete', p); //删除部门信息(树结构)
|
||||||
|
// export const editDepartment = p => put('/api-hrms/hrms/dept/update', p); //根据id修改部门信息(编辑)
|
||||||
|
// export const dragDepartment = p => put('/api-hrms/hrms/dept/update/sort', p); //修改部门的拖拽排序(传id和排序)
|
||||||
|
// export const rulesAll = p => get('/api-hrms/hrms/position/listRoleIdAndName', p); //所有角色id+名称
|
||||||
|
// export const postAll = p => post('/api-hrms/hrms/position/listPositionIdAndName', p); //所有职位id+名称
|
||||||
|
// export const ruleJurisdiction = p => post('/api-hrms/hrms/employee/tree', p); //新增员工-角色对应的权限
|
||||||
|
// export const clickDepartment = p => get('/api-hrms/hrms/dept/children', p); //点击部门,搜索该部门名下的员工
|
||||||
|
// // 后台-人资-员工列表
|
||||||
|
// export const deleteStaff = p => del('/api-hrms/hrms/employee/delete', p); //根据id删除员工
|
||||||
|
// export const forbiddenStaff = p => get('/api-hrms/hrms/employee/forbidInfo', p); //禁用详情(为啥禁用)
|
||||||
|
// export const queryStaff = p => get('/api-hrms/hrms/employee/getEmployeeAndDeptById', p); //根据id查询员工的基本信息
|
||||||
|
// export const queryStaffName = p => get('/api-hrms/hrms/employee/getEmployeeById', p); //根据id查询员工姓名
|
||||||
|
// export const staffMessage = p => get('/api-hrms/hrms/employee/info', p); //根据id查询员工详细信息
|
||||||
|
// export const staffList = p => get('/api-hrms/hrms/employee/list', p); //查询列表
|
||||||
|
// export const staffResetPwd = p => get('/api-hrms/hrms/employee/reset', p); //员工重置密码
|
||||||
|
// export const addStaff = p => post('/api-hrms/hrms/employee/save', p); //新增员工
|
||||||
|
// export const startStaff = p => get('/api-hrms/hrms/employee/start', p); //启用员工
|
||||||
|
// export const stopStaff = p => post('/api-hrms/hrms/employee/stop', p); //禁用员工
|
||||||
|
// export const roleStaff = p => post('/api-hrms/hrms/employee/tree', p); //角色对应权限
|
||||||
|
// export const editStaff = p => post('/api-hrms/hrms/employee/update', p); //修改员工
|
||||||
|
// // 后台-员工-导入
|
||||||
|
// export const importStaff = p => post('/api-hrms/hrms/employee/excelImport',p); //导入员工(上传)
|
||||||
|
// // 后台-员工-导出
|
||||||
|
// export const excelStaffFile = `${host}/api-hrms/hrms/employee/excelExport`; //导出员工文件
|
||||||
|
// export const excelTemplateStaff = `${host}/api-hrms/hrms/employee/excelTemplate`; //导出员工模板
|
||||||
|
// // 后台-人资-职位
|
||||||
|
// export const deletePost = p => del('/api-hrms/hrms/position/delete', p); //删除职位信息
|
||||||
|
// export const searchPostId = p => get('/api-hrms/hrms/position/info', p); //职位id查询职位信息详情
|
||||||
|
// export const postList = p => get('/api-hrms/hrms/position/list', p); //职位列表+查询
|
||||||
|
// export const addPostMessage = p => post('/api-hrms/hrms/position/save', p); //新增职位信息
|
||||||
|
// export const editPostMessage = p => put('/api-hrms/hrms/position/update', p); //修改职位信息
|
||||||
|
|
||||||
|
|
||||||
|
// // 系统-新增角色权限
|
||||||
|
// export const newAddRolePermission = p => post('/apiHrmsAuth/hrms/auth/permission/saveRolePermission', p); //新增角色权限
|
||||||
|
// export const editRolePermission = p => post('/apiHrmsAuth/hrms/auth/permission/updateRolePermission', p); //修改角色权限
|
||||||
|
// export const getPermissionIds = p => get('/apiHrmsAuth/hrms/auth/permission/queryPermissionArrById', p); //已有权限的ids
|
||||||
|
|
||||||
|
// // 工作台--人力资源
|
||||||
|
// // 绑定手机号
|
||||||
|
// export const workBindPhone = p => get('/api-hrms/hrms/user/binding', p);
|
||||||
|
// // 微信解绑
|
||||||
|
// export const wxUnbundle = p => get('/api-hrms/hrms/user/cancel', p);
|
||||||
|
// // 验证码
|
||||||
|
// export const workPhoneAuthCord = p => get('/api-hrms/hrms/user/code', p);
|
||||||
|
// // 用户信息
|
||||||
|
// export const workUserMsg = p => get('/api-hrms/hrms/user/info', p);
|
||||||
|
// // 修改用户信息9
|
||||||
|
// export const woreEditUserMsg = p => post('/api-hrms/hrms/user/update', p);
|
||||||
|
// // 修改密码
|
||||||
|
// export const workEditPwd = p => get('/api-hrms/hrms/user/updatePassword', p);
|
||||||
|
// // 头像上传
|
||||||
|
// export const headImgUp = p => post('/api-hrms/hrms/user/uploadFile', p);
|
||||||
|
|
||||||
|
|
||||||
|
// // 工作台--客户资源管理
|
||||||
|
// // 删除
|
||||||
|
// export const workClientDel = p => post('/api-crms/crms/workbench/delete', p);
|
||||||
|
// // 详情
|
||||||
|
// export const workClientParticulars = p => get('/api-crms/crms/workbench/info', p);
|
||||||
|
// // 客户信息列表展示
|
||||||
|
// export const workClientMsgList = p => get('/api-crms/crms/workbench/list', p);
|
||||||
|
// // 获取当前客户部门经理--下属客户经理
|
||||||
|
// export const workClientManager= p => get('/api-crms/crms/workbench/manager', p);
|
||||||
|
// // 不通过
|
||||||
|
// export const workClientNoPass = p => get('/api-crms/crms/workbench/noPass', p);
|
||||||
|
// // 通过审核
|
||||||
|
// export const workClientPass = p => get('/api-crms/crms/workbench/pass', p);
|
||||||
|
// // 新增企业类型客户
|
||||||
|
// export const workClientAddEnterprise = p => post('/api-crms/crms/workbench/saveCompany', p);
|
||||||
|
// // 新增企业类型客户新版接口
|
||||||
|
// export const insertCompany = p => post('/api-crms/crms/workbench/insertCompany', p);
|
||||||
|
// // 新增个人类型客户
|
||||||
|
// export const workClientMsg = p => post('/api-crms/crms/workbench/savePersonal', p);
|
||||||
|
// // 修改企业类型客户
|
||||||
|
// export const workEditClientEnterpriseMsg = p => post('/api-crms/crms/workbench/updateCompany', p);
|
||||||
|
// // 修改企业类型客户新版接口
|
||||||
|
// export const updateCompanyNew = p => post('/api-crms/crms/workbench/updateCompanyNew', p);
|
||||||
|
// // 修改个人类型客户
|
||||||
|
// export const workEditClientMsg = p => post('/api-crms/crms/workbench/updatePersonal', p);
|
||||||
|
|
||||||
|
|
||||||
|
// // 登录微信扫码(图片)
|
||||||
|
// // export const WXCordImg = p => get('/apiHrmsAuth/hrms/auth/userlogin/wxLoginUrl', p);
|
||||||
|
// // 微信回调授权码(不用管)
|
||||||
|
// export const WXAuthCord = p => get('/apiHrmsAuth/hrms/auth/userlogin/user/callback', p);
|
||||||
|
// // 登录绑定手机号
|
||||||
|
// export const bindPhone = p => post('/apiHrmsAuth/hrms/auth/userlogin/bindPhoneAndOpenId', p);
|
||||||
|
// // 登录手机验证码
|
||||||
|
// export const phoneAuthCord = p => get('/apiHrmsAuth/hrms/auth/userlogin/sendCode', p);
|
||||||
|
// // 判断手机是否存在
|
||||||
|
// export const phoneExist = p => get('/apiHrmsAuth/hrms/auth/userlogin/isPhoneExist', p);
|
||||||
|
|
||||||
|
// // 工作台--担保业务
|
||||||
|
// //业务申请列表
|
||||||
|
// export const businessApplyList = p => get('/api-guarantee/dg-apply-amount-info/businessApplicationList', p);
|
||||||
|
// //新建业务
|
||||||
|
// export const businessApplication = p => post('/api-guarantee/dg-apply-amount-info/businessApplication', p);
|
||||||
|
// // 获取所有客户的信息
|
||||||
|
// export const getAllClient = p => get('/api-crms/crms/customer/queryCompanyCodeAndName', p);
|
||||||
|
// //客户编号/取得信息
|
||||||
|
// export const companyInfoBySth = p => post('/api-guarantee/dg-apply-amount-info/companyInfoBySth', p);
|
||||||
|
// // 业务申请-查看
|
||||||
|
// export const businessApplyWatch = p => get('/api-guarantee/dg-apply-amount-info/businessApplicationDetail', p);
|
||||||
|
// // 业务-修改
|
||||||
|
// export const businessApplyEdit = p => post('/api-guarantee/dg-apply-amount-info/updateBusinessApplication', p);
|
||||||
|
// // 业务申请-审核
|
||||||
|
// export const businessApplyAudit = p => post('/api-guarantee/dg-apply-amount-info/approvalBusinessApplication', p);
|
||||||
|
// // 业务-撤销
|
||||||
|
// export const businessApplyRepeal = p => get('/api-guarantee/dg-apply-amount-info/revokeBusinessApplication', p);
|
||||||
|
// // 所有银行
|
||||||
|
// export const allBankName = p => get('/api-guarantee/dg-bank/bankList', p);
|
||||||
|
|
||||||
|
// // 担保调查列表
|
||||||
|
// export const guaranteeList = p => get('/api-guarantee/dg-guarantee-assign-user/guaranteeList', p);
|
||||||
|
// // 查看详情功能--领导/调查员
|
||||||
|
// export const guaranteeDetails = p => get('/api-guarantee/dg-guarantee-assign-user/guaranteeDetail', p);
|
||||||
|
// // 调查功能
|
||||||
|
// export const guaranteeSurvey = p => post('/api-guarantee/dg-guarantee-assign-user/investigateGuarantee', p);
|
||||||
|
// // 领导审核
|
||||||
|
// export const guaranteeleader = p => post('/api-guarantee/dg-guarantee-assign-user/approvalGuarantee', p);
|
||||||
|
// // 指派A/B角
|
||||||
|
// export const guaranteeDesignateAB = p => post('/api-guarantee/dg-guarantee-assign-user/assignCorners', p);
|
||||||
|
|
||||||
|
// // 指派--查询所有部门底下的员工---限制版
|
||||||
|
// // export const designateTrees = p => get('/api-hrms/hrms/dept/trees', p);
|
||||||
|
|
||||||
|
// // 指派树---不做限制版-仅工作会
|
||||||
|
// export const designateEmpTrees = p => get('/api-hrms//hrms/dept/empTrees', p);
|
||||||
|
|
||||||
|
|
||||||
|
// // 关联人列表
|
||||||
|
// export const getLinkmanList = p => get('/api-crms/crms-company-personal/companyPersonalList', p);
|
||||||
|
// // 查询关联人
|
||||||
|
// export const searchLinkmanList = p => get('/api-crms/crms-company-personal/selectCompanyPersonal', p);
|
||||||
|
// // 查询关联人新版
|
||||||
|
// export const NewsearchLinkmanList = p => get('/api-crms/crms-company-personal/selectCompanyPersonal', p);
|
||||||
|
// // 删除关联人
|
||||||
|
// export const delLinkmanList = p => post('/api-crms/crms-company-personal/deleteCompanyPersonal', p);
|
||||||
|
// // 删除关联人新版
|
||||||
|
// export const NewdelLinkmanList = p => post('/api-crms/crms-company-personal/deleteCompanyPersonal', p);
|
||||||
|
// // 新增关联人
|
||||||
|
// export const newLinkmanList = p => post('/api-crms/crms-company-personal/insertCompanyPersonal', p);
|
||||||
|
// // 新增关联人新版
|
||||||
|
// export const newLinkList = p => post('/api-crms/crms-company-personal/insertCompanyPersonal', p);
|
||||||
|
// // 编辑关联人
|
||||||
|
// export const editLinkmanList = p => post('/api-crms/crms-company-personal/updateCompanyPersonal', p);
|
||||||
|
// // 编辑关联人新版
|
||||||
|
// export const NeweditLinkmanList = p => post('/api-crms/crms-company-personal/updateCompanyPersonal', p);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // 资产部--审核
|
||||||
|
// export const assetAudit = p => post('/api-guarantee/dg-assets-investigation/approvalAssets', p);
|
||||||
|
// // 资产部--查看
|
||||||
|
// export const assetWatch = p => get('/api-guarantee/dg-assets-investigation/assetsDetail', p);
|
||||||
|
// // 资产部--列表
|
||||||
|
// export const assetList = p => post('/api-guarantee/dg-assets-investigation/assetsList', p);
|
||||||
|
// // 资产部--指派
|
||||||
|
// export const assetAssign = p => post('/api-guarantee/dg-assets-investigation/assignCorners', p);
|
||||||
|
// // 资产部--调查
|
||||||
|
// export const assetSurvey = p => post('/api-guarantee/dg-assets-investigation/investigateAssets', p);
|
||||||
|
|
||||||
|
// // 信息部--列表
|
||||||
|
// export const messageList = p => get('/api-guarantee/dg-message-investigation/messageList', p);
|
||||||
|
// // 信息部详情
|
||||||
|
// export const messageDetail = p => get('/api-guarantee/dg-message-investigation/messageDetail', p);
|
||||||
|
// // 信息部--指派
|
||||||
|
// export const messageAssign = p => post('/api-guarantee/dg-message-investigation/assignCorners', p);
|
||||||
|
// // 信息部--调查
|
||||||
|
// export const messageSurvey = p => post('/api-guarantee/dg-message-investigation/investigateMessage', p);
|
||||||
|
// // 信息部审核
|
||||||
|
// export const messageAudit = p => post('/api-guarantee/dg-message-investigation/approvalMessage', p);
|
||||||
|
|
||||||
|
// // 合规调查-指派
|
||||||
|
// export const complianceAssign = p => get('/api-guarantee/compliance/investigation/assign', p);
|
||||||
|
// // 合规-审核
|
||||||
|
// export const complianceAudit = p => post('/api-guarantee/compliance/investigation/check', p);
|
||||||
|
// // 合规-列表
|
||||||
|
// export const complianceList = p => get('/api-guarantee/compliance/investigation/list', p);
|
||||||
|
// // 合规-查看详情
|
||||||
|
// export const complianceDetail = p => get('/api-guarantee/compliance/investigation/query', p);
|
||||||
|
// // 合规-调查
|
||||||
|
// export const complianceSurvey = p => post('/api-guarantee/compliance/investigation/survey', p);
|
||||||
|
|
||||||
|
// // 工作会-审议
|
||||||
|
// export const workAudit = p => post('/api-guarantee/work/conference/check', p);
|
||||||
|
// //工作会-所有审核意见
|
||||||
|
// export const workOpinion = p => get('/api-guarantee/work/conference/getAuditOpinion', p);
|
||||||
|
// // 工作会列表
|
||||||
|
// export const workList = p => get('/api-guarantee/work/conference/list', p);
|
||||||
|
// // 工作会-查看
|
||||||
|
// export const workDetail = p => get('/api-guarantee/work/conference/query', p);
|
||||||
|
// // 工作会-评委抽取
|
||||||
|
// export const workExtract = p => post('/api-guarantee/work/conference/theJudgesDrawn', p);
|
||||||
|
|
||||||
|
// // 贷审会-审议
|
||||||
|
// export const loansAudit = p => post('/api-guarantee/committee/consider/check', p);
|
||||||
|
// // 贷审会-列表
|
||||||
|
// export const loansList = p => get('/api-guarantee/committee/consider/list', p);
|
||||||
|
// // 贷审会-查看
|
||||||
|
// export const loansDetail = p => get('/api-guarantee/committee/consider/query', p);
|
||||||
|
// // 贷审会--获取抽取的评委
|
||||||
|
// export const loansJudge = p => get('/api-guarantee/committee/consider/theJudgesDrawn', p);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// /* 业务+担保 上传文件 */
|
||||||
|
// export const guaranteeUploadFile = p => post('/api-guarantee/dg-apply-amount-info/uploadFile', p);
|
||||||
|
// /* 业务+担保 删除文件 */
|
||||||
|
// export const guaranteeDeleteFile = p => get('/api-guarantee/dg-apply-amount-info/deleteServerFile', p);
|
||||||
|
// //担保函的接口
|
||||||
|
// export const guaranteeLetterList = p => post('/api-guarantee/dg-guarantee-letter-assign-user/guaranteeLetterList', p); //担保函列表
|
||||||
|
// export const updateStatus = p => post('/api-guarantee/dg-guarantee-letter-assign-user/updateStatus', p); //用户提交审核意见
|
||||||
|
|
||||||
|
// //回款确认
|
||||||
|
// export const paymentList = p => post('/api-guarantee/dg-payment-confirmation-consider/paymentConfirmationList', p); //回款确认列表
|
||||||
|
// export const updatePayment = p => post('/api-guarantee/dg-payment-confirmation-consider/updatePaymentConfirmation', p); //更新回款确认
|
||||||
|
|
||||||
|
// //放款通知
|
||||||
|
// export const loanNoticeList = p => post('/api-guarantee/dg-loan-notice/loanNoticeList', p); //放款通知列表
|
||||||
|
// export const updateLoanNotice = p => post('/api-guarantee/dg-loan-notice/updateLoanNotice', p); //放款通知确认
|
||||||
|
|
||||||
|
|
||||||
|
// export const auditProcessList = p => get('/api-guarantee/dg-audit-process/auditProcessList', p);//审核流程进程列表
|
||||||
|
// export const auditProcessPop = p => get('/api-guarantee/dg-audit-process/getProcessId', p);//审核流程进程--弹框
|
||||||
|
|
||||||
|
// //获取当前用户角色
|
||||||
|
// export const getNowRole = p => get('/api-guarantee/dg-apply-amount-info/getRoles', p);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // 业务-导出
|
||||||
|
// export const businessExportList = `${host}/api-guarantee/dg-apply-amount-info/excelExport`;
|
||||||
|
// // 担保-导出
|
||||||
|
// export const guaranteeExportList = `${host}/api-guarantee/dg-guarantee-assign-user/excelExport`;
|
||||||
|
// // 资产-导出
|
||||||
|
// export const assetExportList = `${host}/api-guarantee/dg-assets-investigation/excelExport`;
|
||||||
|
// // 信息-导出
|
||||||
|
// export const messageExportList = `${host}/api-guarantee/dg-message-investigation/excelExport`;
|
||||||
|
// // 合规-导出
|
||||||
|
// export const complianceExportList = `${host}/api-guarantee/compliance/investigation/excelExport`;
|
||||||
|
// // 工作会-导出
|
||||||
|
// export const workExportList = `${host}/api-guarantee/work/conference/excelExport`;
|
||||||
|
// // 贷审会-导出
|
||||||
|
// export const loansExportList = `${host}/api-guarantee/committee/consider/excelExport`;
|
||||||
|
|
||||||
|
|
||||||
|
// export const excelTemplate = `${host}/api-crms/crms/customer/excelTemplate`; //导出客户模板
|
||||||
|
// export const excelExport = `${host}/api-crms/crms/customer/excelExport`; //导出客户
|
||||||
|
// export const excelImport = `${host}/api-crms/crms/customer/excelImport`; //导入excal
|
||||||
|
|
||||||
|
// export const excelExportStaff = `${host}/api-crms/crms/customer/excelExport`; //导出客户
|
||||||
|
// export const excelImportStaff = `${host}/api-crms/crms/customer/excelImport`; //导入员工excal
|
||||||
|
|
||||||
|
|
||||||
|
// export const excelExportLetter = `${host}/api-guarantee/dg-guarantee-letter-assign-user/guaranteeLetterListExport`; //导出担保函列表
|
||||||
|
// export const exportGuaranteeLetter = `${host}/api-guarantee/dg-guarantee-letter-assign-user/exportGuaranteeLetter`; //导出担保函
|
||||||
|
|
||||||
|
// export const excelExportPayment = `${host}/api-guarantee/dg-payment-confirmation-consider/paymentConfirmationListExport`; //导出回款确认列表数据
|
||||||
|
|
||||||
|
// export const loanNoticeListExport = `${host}/api-guarantee/dg-loan-notice/loanNoticeListExport`; //导出放款通知列表数据
|
||||||
|
// export const exportLoanNotice = `${host}/api-guarantee/dg-loan-notice/exportLoanNotice`; //导出放款通知
|
@ -0,0 +1,130 @@ |
|||||||
|
import { mergeConfig, dispatchRequest, jsonpRequest} from "./utils.js"; |
||||||
|
export default class request { |
||||||
|
constructor(options) { |
||||||
|
//请求公共地址
|
||||||
|
this.baseUrl = options.baseUrl || ""; |
||||||
|
//公共文件上传请求地址
|
||||||
|
this.fileUrl = options.fileUrl || ""; |
||||||
|
// 超时时间
|
||||||
|
this.timeout = options.timeout || 6000; |
||||||
|
// 服务器上传图片默认url
|
||||||
|
this.defaultUploadUrl = options.defaultUploadUrl || ""; |
||||||
|
//默认请求头
|
||||||
|
this.header = options.header || {}; |
||||||
|
//默认配置
|
||||||
|
this.config = options.config || { |
||||||
|
isPrompt: true, |
||||||
|
load: true, |
||||||
|
isFactory: true, |
||||||
|
resend: 0 |
||||||
|
}; |
||||||
|
} |
||||||
|
//post请求
|
||||||
|
post(url = '', data = {}, options = {}) { |
||||||
|
return this.request({ |
||||||
|
method: "POST", |
||||||
|
data: data, |
||||||
|
url: url, |
||||||
|
...options |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
//get请求
|
||||||
|
get(url = '', data = {}, options = {}) { |
||||||
|
return this.request({ |
||||||
|
method: "GET", |
||||||
|
data: data, |
||||||
|
url: url, |
||||||
|
...options |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
//put请求
|
||||||
|
put(url = '', data = {}, options = {}) { |
||||||
|
return this.request({ |
||||||
|
method: "PUT", |
||||||
|
data: data, |
||||||
|
url: url, |
||||||
|
...options |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
//delete请求
|
||||||
|
delete(url = '', data = {}, options = {}) { |
||||||
|
return this.request({ |
||||||
|
method: "DELETE", |
||||||
|
data: data, |
||||||
|
url: url, |
||||||
|
...options |
||||||
|
}); |
||||||
|
} |
||||||
|
//jsonp请求(只限于H5使用)
|
||||||
|
jsonp(url = '', data = {}, options = {}) { |
||||||
|
return this.request({ |
||||||
|
method: "JSONP", |
||||||
|
data: data, |
||||||
|
url: url, |
||||||
|
...options |
||||||
|
}); |
||||||
|
} |
||||||
|
//接口请求方法
|
||||||
|
async request(data) { |
||||||
|
// 请求数据
|
||||||
|
let requestInfo, |
||||||
|
// 是否运行过请求开始钩子
|
||||||
|
runRequestStart = false; |
||||||
|
try { |
||||||
|
if (!data.url) { |
||||||
|
throw { errMsg: "【request】缺失数据url", statusCode: 0} |
||||||
|
} |
||||||
|
// 数据合并
|
||||||
|
requestInfo = mergeConfig(this, data); |
||||||
|
// 代表之前运行到这里
|
||||||
|
runRequestStart = true; |
||||||
|
//请求前回调
|
||||||
|
if (this.requestStart) { |
||||||
|
let requestStart = this.requestStart(requestInfo); |
||||||
|
if (typeof requestStart == "object") { |
||||||
|
let changekeys = ["data", "header", "isPrompt", "load", "isFactory"]; |
||||||
|
changekeys.forEach(key => { |
||||||
|
requestInfo[key] = requestStart[key]; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
throw { |
||||||
|
errMsg: "【request】请求开始拦截器未通过", |
||||||
|
statusCode: 0, |
||||||
|
data: requestInfo.data, |
||||||
|
method: requestInfo.method, |
||||||
|
header: requestInfo.header, |
||||||
|
url: requestInfo.url, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let requestResult = {}; |
||||||
|
if(requestInfo.method == "JSONP"){ |
||||||
|
requestResult = await jsonpRequest(requestInfo); |
||||||
|
} else { |
||||||
|
requestResult = await dispatchRequest(requestInfo); |
||||||
|
} |
||||||
|
//是否用外部的数据处理方法
|
||||||
|
if (requestInfo.isFactory && this.dataFactory) { |
||||||
|
//数据处理
|
||||||
|
let result = await this.dataFactory({ |
||||||
|
...requestInfo, |
||||||
|
response: requestResult |
||||||
|
}); |
||||||
|
return Promise.resolve(result); |
||||||
|
} else { |
||||||
|
return Promise.resolve(requestResult); |
||||||
|
} |
||||||
|
} catch (err){ |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} finally { |
||||||
|
// 如果请求开始未运行到,请求结束也不运行
|
||||||
|
if(runRequestStart){ |
||||||
|
this.requestEnd && this.requestEnd(requestInfo); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
// 获取合并的数据
|
||||||
|
export const mergeConfig = function(_this, options) { |
||||||
|
//判断url是不是链接
|
||||||
|
let urlType = /^(http|https):\/\//.test(options.url); |
||||||
|
let config = Object.assign({ |
||||||
|
timeout: _this.timeout |
||||||
|
}, _this.config, options); |
||||||
|
if (options.method == "FILE") { |
||||||
|
config.url = urlType ? options.url : _this.fileUrl + options.url; |
||||||
|
} else { |
||||||
|
config.url = urlType ? options.url : _this.baseUrl + options.url; |
||||||
|
} |
||||||
|
//请求头
|
||||||
|
if (options.header) { |
||||||
|
config.header = Object.assign({}, _this.header, options.header); |
||||||
|
} else { |
||||||
|
config.header = Object.assign({}, _this.header); |
||||||
|
} |
||||||
|
return config; |
||||||
|
} |
||||||
|
// 请求
|
||||||
|
export const dispatchRequest = function(requestInfo) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
let requestAbort = true; |
||||||
|
let requestData = { |
||||||
|
url: requestInfo.url, |
||||||
|
header: requestInfo.header, //加入请求头
|
||||||
|
success: (res) => { |
||||||
|
requestAbort = false; |
||||||
|
resolve(res); |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
requestAbort = false; |
||||||
|
if(err.errMsg == "request:fail abort"){ |
||||||
|
reject({ |
||||||
|
errMsg: "请求超时,请重新尝试", |
||||||
|
statusCode: 0, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
reject(err); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
//请求类型
|
||||||
|
if (requestInfo.method) { |
||||||
|
requestData.method = requestInfo.method; |
||||||
|
} |
||||||
|
if (requestInfo.data) { |
||||||
|
requestData.data = requestInfo.data; |
||||||
|
} |
||||||
|
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||||||
|
if (requestInfo.timeout) { |
||||||
|
requestData.timeout = requestInfo.timeout; |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
if (requestInfo.dataType) { |
||||||
|
requestData.dataType = requestInfo.dataType; |
||||||
|
} |
||||||
|
// #ifndef APP-PLUS || MP-ALIPAY
|
||||||
|
if (requestInfo.responseType) { |
||||||
|
requestData.responseType = requestInfo.responseType; |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
if (requestInfo.withCredentials) { |
||||||
|
requestData.withCredentials = requestInfo.withCredentials; |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
let requestTask = uni.request(requestData); |
||||||
|
setTimeout(() => { |
||||||
|
if(requestAbort){ |
||||||
|
requestTask.abort(); |
||||||
|
} |
||||||
|
}, requestInfo.timeout) |
||||||
|
}) |
||||||
|
} |
||||||
|
// jsonp请求
|
||||||
|
export const jsonpRequest = function(requestInfo) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
let dataStr = ''; |
||||||
|
Object.keys(requestInfo.data).forEach(key => { |
||||||
|
dataStr += key + '=' + requestInfo.data[key] + '&'; |
||||||
|
}); |
||||||
|
//匹配最后一个&并去除
|
||||||
|
if (dataStr !== '') { |
||||||
|
dataStr = dataStr.substr(0, dataStr.lastIndexOf('&')); |
||||||
|
} |
||||||
|
requestInfo.url = requestInfo.url + '?' + dataStr; |
||||||
|
let callbackName = "callback" + Math.ceil(Math.random() * 1000000); |
||||||
|
// #ifdef H5
|
||||||
|
window[callbackName] = function(data) { |
||||||
|
resolve(data); |
||||||
|
} |
||||||
|
let script = document.createElement("script"); |
||||||
|
script.src = requestInfo.url + "&callback=" + callbackName; |
||||||
|
document.head.appendChild(script); |
||||||
|
// 及时删除,防止加载过多的JS
|
||||||
|
document.head.removeChild(script); |
||||||
|
// #endif
|
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
/***************纯粹的数据请求(如果使用这种可以删除掉fileUpload.js)******************/ |
||||||
|
// import request from "./core/request.js";
|
||||||
|
// export default request;
|
||||||
|
|
||||||
|
/********数据请求同时继承了文件上传(包括七牛云上传)************/ |
||||||
|
import upload from "./upload/upload.js"; |
||||||
|
export default upload; |
@ -0,0 +1,471 @@ |
|||||||
|
# request请求、配置简单、批量上传图片、视频、超强适应性(支持多域名请求) |
||||||
|
1. 配置简单、源码清晰注释多、适用于一项目多域名请求、第三方请求、七牛云图片上传、本地服务器图片上传等等 |
||||||
|
2. 支持请求`get`、`post`、`put`、`delete` |
||||||
|
3. 自动显示请求加载动画(可单个接口关闭) |
||||||
|
4. 全局`api`数据处理函数,只回调请求正确的数据(可单个接口关闭) |
||||||
|
5. 未登录或登录失效自动拦截并调用登录方法(可单个接口关闭) |
||||||
|
6. 全局自动提示接口抛出的错误信息(可单个接口关闭) |
||||||
|
7. 支持 Promise |
||||||
|
8. 支持拦截器 |
||||||
|
9. 支持七牛云文件(图片、视频)批量上传 |
||||||
|
10. 支持本地服务器文件(图片、视频)批量上传 |
||||||
|
11. 支持上传文件拦截过滤 |
||||||
|
12. 支持上传文件进度监听 |
||||||
|
13. 支持上传文件单张成功回调 |
||||||
|
|
||||||
|
| `QQ交流群(607391225)` | `微信交流群(加我好友备注"进群")` | |
||||||
|
| ----------------------------|--------------------------- | |
||||||
|
|![QQ交流群](http://qn.kemean.cn//upload/202004/14/15868301778472k7oubi6.png)|![微信交流群](https://qn.kemean.cn/upload/202010/13/weiXin_group_code.jpg)| |
||||||
|
| QQ群号:607391225 |微信号:zhou0612wei| |
||||||
|
|
||||||
|
### [点击跳转-插件示例](https://ext.dcloud.net.cn/plugin?id=2009) |
||||||
|
### [点击跳转-5年的web前端开源的uni-app快速开发模板-下载看文档](https://ext.dcloud.net.cn/plugin?id=2009) |
||||||
|
|
||||||
|
### 常见问题 |
||||||
|
1.接口请求成功了,没有返回数据或者数据是走的catch回调 |
||||||
|
|
||||||
|
答:`requestConfig.js` 请求配置文件里面,有一个`$http.dataFactory`方法,里面写的只是参考示例,`此方法需要开发者根据各自的接口返回类型修改` |
||||||
|
|
||||||
|
2.官方的方法有数据,本插件方法请求报错跨域问题 |
||||||
|
|
||||||
|
答:`requestConfig.js` 请求配置文件里面,`header`请求头设置的`content-type`请求类型需求和后台保持一致 |
||||||
|
|
||||||
|
3.登录后用户`token`怎么设置? |
||||||
|
|
||||||
|
答:`requestConfig.js` 请求配置文件里面,`$http.requestStart`请求开始拦截器里面设置 |
||||||
|
|
||||||
|
4.怎么判断上传的文件(图片)太大?怎么过滤掉太大的文件(图片)? |
||||||
|
|
||||||
|
答:`requestConfig.js` 请求配置文件里面,`$http.requestStart`请求开始拦截器里面设置 |
||||||
|
|
||||||
|
5.接口请求成功了,一直提示“网络错误,请检查一下网络”? |
||||||
|
|
||||||
|
答:`requestConfig.js` 请求配置文件里面,有一个`$http.dataFactory`方法,里面写的只是参考示例,`此方法需要开发者根据各自的接口返回类型修改` |
||||||
|
|
||||||
|
### 本次更新注意事项 |
||||||
|
1. 所有的headers都改成了header(和官方统一) |
||||||
|
2. 七牛云的获取token等信息提取到了`requestConfig.js`文件,参考如下 |
||||||
|
|
||||||
|
``` |
||||||
|
// 添加获取七牛云token的方法 |
||||||
|
$http.getQnToken = function(callback){ |
||||||
|
//该地址需要开发者自行配置(每个后台的接口风格都不一样) |
||||||
|
$http.get("api/kemean/aid/qn_upload").then(data => { |
||||||
|
/* |
||||||
|
*接口返回参数: |
||||||
|
*visitPrefix:访问文件的域名 |
||||||
|
*token:七牛云上传token |
||||||
|
*folderPath:上传的文件夹 |
||||||
|
*region: 地区 默认为:SCN |
||||||
|
*/ |
||||||
|
callback({ |
||||||
|
visitPrefix: data.visitPrefix, |
||||||
|
token: data.token, |
||||||
|
folderPath: data.folderPath, |
||||||
|
region: "SCN" |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 文件说明 |
||||||
|
1. `request => core` 请求方法的目录 |
||||||
|
2. `request => core => request.js` 请求方法的class文件 |
||||||
|
3. `request => core => utils.js` 请求方法的源码文件 |
||||||
|
4. `request => upload` 上传方法的目录 |
||||||
|
5. `request => upload => upload.js` 上传方法的class文件 |
||||||
|
6. `request => upload => utils.js` 上传方法源码文件 |
||||||
|
7. `request => upload => qiniuUploader.js` 七牛云官方上传文件 |
||||||
|
8. `request => index.js` 输出方法的文件 |
||||||
|
9. `requestConfig.js` 请求配置文件(具体看代码) |
||||||
|
|
||||||
|
### 在main.js引入并挂在Vue上 |
||||||
|
``` |
||||||
|
import $http from '@/zhouWei-request/requestConfig'; |
||||||
|
Vue.prototype.$http = $http; |
||||||
|
``` |
||||||
|
|
||||||
|
### `requestConfig.js`配置说明(requestConfig.js有配置示例) |
||||||
|
``` |
||||||
|
import request from "@/plugins/request"; |
||||||
|
//可以new多个request来支持多个域名请求 |
||||||
|
let $http = new request({ |
||||||
|
//接口请求地址 |
||||||
|
baseUrl: "https://twin-ui.com/", //示例域名,请自行设计 |
||||||
|
//服务器本地上传文件地址 |
||||||
|
fileUrl: "https://twin-ui.com/", //示例域名,请自行设计 |
||||||
|
// 服务器上传图片默认url |
||||||
|
defaultUploadUrl: "api/common/v1/upload_image", |
||||||
|
//设置请求头(如果使用报错跨域问题,可能是content-type请求类型和后台那边设置的不一致) |
||||||
|
header: { |
||||||
|
'Content-Type': 'application/json;charset=UTF-8' |
||||||
|
}, |
||||||
|
// 请求超时时间(默认6000) |
||||||
|
timeout: 6000, |
||||||
|
// 默认配置(可不写) |
||||||
|
config: { |
||||||
|
// 是否自动提示错误 |
||||||
|
isPrompt: true, |
||||||
|
// 是否显示加载动画 |
||||||
|
load: true, |
||||||
|
// 是否使用数据工厂 |
||||||
|
isFactory: true, |
||||||
|
// ... 可写更多配置 |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加获取七牛云token的方法 |
||||||
|
$http.getQnToken = function(callback){ |
||||||
|
//该地址需要开发者自行配置(每个后台的接口风格都不一样) |
||||||
|
$http.get("api/common/v1/qn_upload").then(data => { |
||||||
|
/* |
||||||
|
*接口返回参数: |
||||||
|
*visitPrefix:访问文件的域名 |
||||||
|
*token:七牛云上传token |
||||||
|
*folderPath:上传的文件夹 |
||||||
|
*region: 地区 默认为:SCN |
||||||
|
*/ |
||||||
|
callback({ |
||||||
|
visitPrefix: data.visitPrefix, |
||||||
|
token: data.token, |
||||||
|
folderPath: data.folderPath |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
//当前接口请求数 |
||||||
|
let requestNum = 0; |
||||||
|
//请求开始拦截器 |
||||||
|
$http.requestStart = function(options) { |
||||||
|
if (options.load) { |
||||||
|
if (requestNum <= 0) { |
||||||
|
//打开加载动画 |
||||||
|
uni.showLoading({ |
||||||
|
title: '加载中', |
||||||
|
mask: true |
||||||
|
}); |
||||||
|
} |
||||||
|
requestNum += 1; |
||||||
|
} |
||||||
|
// 图片上传大小限制 |
||||||
|
if (options.method == "FILE" && options.maxSize) { |
||||||
|
// 文件最大字节: options.maxSize 可以在调用方法的时候加入参数 |
||||||
|
let maxSize = options.maxSize; |
||||||
|
for (let item of options.files) { |
||||||
|
if (item.size > maxSize) { |
||||||
|
setTimeout(() => { |
||||||
|
uni.showToast({ |
||||||
|
title: "图片过大,请重新上传", |
||||||
|
icon: "none" |
||||||
|
}); |
||||||
|
}, 500); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//请求前加入token |
||||||
|
options.header['token'] = "你的项目登录token"; |
||||||
|
return options; // return false 表示请求拦截,不会继续请求 |
||||||
|
} |
||||||
|
//请求结束 |
||||||
|
$http.requestEnd = function(options) { |
||||||
|
//判断当前接口是否需要加载动画 |
||||||
|
if (options.load) { |
||||||
|
requestNum = requestNum - 1; |
||||||
|
if (requestNum <= 0) { |
||||||
|
uni.hideLoading(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//所有接口数据处理(可在接口里设置不调用此方法) |
||||||
|
//此方法需要开发者根据各自的接口返回类型修改,以下只是模板 |
||||||
|
$http.dataFactory = async function(res) { |
||||||
|
console.log("接口请求数据", { |
||||||
|
url: res.url, |
||||||
|
resolve: res.response, |
||||||
|
header: res.header, |
||||||
|
data: res.data, |
||||||
|
method: res.method, |
||||||
|
}); |
||||||
|
if (res.response.statusCode && res.response.statusCode == 200) { |
||||||
|
let httpData = res.response.data; |
||||||
|
if (typeof (httpData) == "string") { |
||||||
|
httpData = JSON.parse(httpData); |
||||||
|
} |
||||||
|
//判断数据是否请求成功 |
||||||
|
if (httpData.success || httpData.code == 200) { |
||||||
|
// ---------重点----------返回正确的结果(then接受数据)------------重点------------ |
||||||
|
return Promise.resolve(httpData.data); |
||||||
|
} else { |
||||||
|
//其他错误提示 |
||||||
|
if (res.isPrompt) { // 是否提示 |
||||||
|
uni.showToast({ |
||||||
|
title: httpData.info || httpData.msg, |
||||||
|
icon: "none", |
||||||
|
duration: 3000 |
||||||
|
}); |
||||||
|
} |
||||||
|
// --------重点---------返回错误的结果(catch接受数据)------------重点------------ |
||||||
|
return Promise.reject({ |
||||||
|
statusCode: 0, |
||||||
|
errMsg: "【request】" + (httpData.info || httpData.msg) |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// 返回错误的结果(catch接受数据) |
||||||
|
return Promise.reject({ |
||||||
|
statusCode: res.response.statusCode, |
||||||
|
errMsg: "【request】数据工厂验证不通过" |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
// 错误回调 |
||||||
|
$http.requestError = function (e) { |
||||||
|
if (e.statusCode === 0) { |
||||||
|
throw e; |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: "网络错误,请检查一下网络", |
||||||
|
icon: "none" |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 通用请求方法 |
||||||
|
``` |
||||||
|
this.$http.request({ |
||||||
|
url: 'aid/region', |
||||||
|
method: "GET", // POST、GET、PUT、DELETE、JSONP,具体说明查看官方文档 |
||||||
|
data: {pid:0}, |
||||||
|
timeout: 30000, // 默认 30000 说明:超时时间,单位 ms,具体说明查看官方文档 |
||||||
|
dataType: "json", // 默认 json 说明:如果设为 json,会尝试对返回的数据做一次 JSON.parse,具体说明查看官方文档 |
||||||
|
responseType: "text", // 默认 text 说明:设置响应的数据类型。合法值:text、arraybuffer,具体说明查看官方文档 |
||||||
|
withCredentials: false, // 默认 false 说明:跨域请求时是否携带凭证(cookies),具体说明查看官方文档 |
||||||
|
isPrompt: true,//(默认 true 说明:本接口抛出的错误是否提示) |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
header: { //默认 无 说明:请求头 |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
isFactory: true, //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数将失去作用) |
||||||
|
}).then(function (response) { |
||||||
|
//这里只会在接口是成功状态返回 |
||||||
|
}).catch(function (error) { |
||||||
|
//这里只会在接口是失败状态返回,不需要去处理错误提示 |
||||||
|
console.log(error); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### get请求 正常写法 |
||||||
|
``` |
||||||
|
this.$http.get('aid/region',{pid:0}). |
||||||
|
then(function (response) { |
||||||
|
//这里只会在接口是成功状态返回 |
||||||
|
}).catch(function (error) { |
||||||
|
//这里只会在接口是失败状态返回,不需要去处理错误提示 |
||||||
|
console.log(error); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### post请求 async写法 |
||||||
|
``` |
||||||
|
async request(){ |
||||||
|
let data = await this.$http.post('aid/region',{pid:0}); |
||||||
|
console.log(data); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 其他功能配置项 |
||||||
|
``` |
||||||
|
let data = await this.$http.post( |
||||||
|
'http://www.aaa.com/aid/region', //可以直接放链接(将不启用全局定义域名) |
||||||
|
{ |
||||||
|
pid:0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
isPrompt: true,//(默认 true 说明:本接口抛出的错误是否提示) |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
header: { //默认 无 说明:请求头 |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
isFactory: true //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数将失去作用) |
||||||
|
} |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
### `requestConfig.js`可以设置服务器上传图片默认url |
||||||
|
``` |
||||||
|
//可以new多个request来支持多个域名请求 |
||||||
|
let $http = new request({ |
||||||
|
//服务器本地上传文件地址 |
||||||
|
fileUrl: base.baseUrl, |
||||||
|
// 服务器上传图片默认url |
||||||
|
defaultUploadUrl: "api/common/v1/upload_image", |
||||||
|
}); |
||||||
|
``` |
||||||
|
``` |
||||||
|
// 上传可以不用传递url(使用全局的上传图片url) |
||||||
|
this.$http.urlImgUpload({ |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
count:"最大选择数",//默认 9 |
||||||
|
sizeType:"选择压缩图原图,默认两个都选",//默认 ['original', 'compressed'] |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选",//默认 ['album','camera'] |
||||||
|
data:"而外参数" //可不填, |
||||||
|
}); |
||||||
|
// 上传可以不用传递url(使用全局的上传图片url) |
||||||
|
this.$http.urlVideoUpload({ |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选",//默认 ['album','camera'] |
||||||
|
compressed:"是否压缩所选的视频源文件,默认值为 true,需要压缩",//默认 false |
||||||
|
maxDuration: "拍摄视频最长拍摄时间,单位秒。最长支持 60 秒", //默认 60 |
||||||
|
camera: '前置还是后置摄像头', //'front'、'back',默认'back' |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
data:"而外参数" //可不填, |
||||||
|
}); |
||||||
|
// 上传可以不用传递url(使用全局的上传图片url) |
||||||
|
this.$http.urlFileUpload({ |
||||||
|
files: [], // 必填 临时文件路径 格式: [{path: "图片地址"}] |
||||||
|
data:"向服务器传递的参数", //可不填 |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### 本地服务器图片上传(支持多张上传) |
||||||
|
``` |
||||||
|
this.$http.urlImgUpload('flie/upload',{ |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
count:"最大选择数",//默认 9 |
||||||
|
sizeType:"选择压缩图原图,默认两个都选",//默认 ['original', 'compressed'] |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选",//默认 ['album','camera'] |
||||||
|
data:"而外参数" //可不填, |
||||||
|
isPrompt: true,//(默认 true 说明:本接口抛出的错误是否提示) |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
header: { //默认 无 说明:请求头 |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
isFactory: true, //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数奖失去作用) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onSelectComplete: res => { |
||||||
|
console.log("选择完成返回:",res); |
||||||
|
}, |
||||||
|
onEachUpdate: res => { |
||||||
|
console.log("单张上传成功返回:",res); |
||||||
|
}, |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
} |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
### 本地服务器视频上传 |
||||||
|
``` |
||||||
|
this.$http.urlVideoUpload('flie/upload',{ |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选",//默认 ['album','camera'] |
||||||
|
compressed:"是否压缩所选的视频源文件,默认值为 true,需要压缩",//默认 false |
||||||
|
maxDuration: "拍摄视频最长拍摄时间,单位秒。最长支持 60 秒", //默认 60 |
||||||
|
camera: '前置还是后置摄像头', //'front'、'back',默认'back' |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
data:"而外参数" //可不填, |
||||||
|
isPrompt: true,//(默认 true 说明:本接口抛出的错误是否提示) |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
header: { //默认 无 说明:请求头 |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
isFactory: true, //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数奖失去作用) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
}, |
||||||
|
onSelectComplete: res => { |
||||||
|
console.log("选择完成返回:",res); |
||||||
|
}, |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
### 本地服务器文件上传(支持多张上传) |
||||||
|
``` |
||||||
|
this.$http.urlFileUpload("flie/upload",{ |
||||||
|
files: [], // 必填 临时文件路径 格式: [{path: "图片地址"}] |
||||||
|
data:"向服务器传递的参数", //可不填 |
||||||
|
name:"后台接受文件key名称", //默认 file |
||||||
|
isPrompt: true,//(默认 true 说明:本接口抛出的错误是否提示) |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
header: { //默认 无 说明:请求头 |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
isFactory: true, //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数奖失去作用) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onEachUpdate: res => { |
||||||
|
console.log("单张上传成功返回:",res); |
||||||
|
}, |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
} |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### 七牛云图片上传(支持多张上传) |
||||||
|
``` |
||||||
|
this.$http.qnImgUpload({ |
||||||
|
count:"最大选择数", // 默认 9 |
||||||
|
sizeType:"选择压缩图原图,默认两个都选", // 默认 ['original', 'compressed'] |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选", // 默认 ['album','camera'] |
||||||
|
load: true, //(默认 true 说明:本接口是否提示加载动画) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onSelectComplete: res => { |
||||||
|
console.log("选择完成返回:",res); |
||||||
|
}, |
||||||
|
onEachUpdate: res => { |
||||||
|
console.log("单张上传成功返回:",res); |
||||||
|
}, |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
} |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
### 七牛云视频上传 |
||||||
|
``` |
||||||
|
this.$http.qnVideoUpload({ |
||||||
|
sourceType:"选择相机拍照或相册上传 默认两个都选",//默认 ['album','camera'] |
||||||
|
compressed:"是否压缩所选的视频源文件,默认值为 true,需要压缩",//默认 false |
||||||
|
maxDuration: "拍摄视频最长拍摄时间,单位秒。最长支持 60 秒", //默认 60 |
||||||
|
camera: '前置还是后置摄像头', //'front'、'back',默认'back' |
||||||
|
load: true,//(默认 true 说明:本接口是否提示加载动画) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onSelectComplete: res => { |
||||||
|
console.log("选择完成返回:",res); |
||||||
|
}, |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
} |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
### 七牛云文件上传(支持多张上传) |
||||||
|
``` |
||||||
|
this.$http.qnFileUpload( |
||||||
|
{ |
||||||
|
files:[], // 必填 临时文件路径 格式: [{path: "图片地址"}] |
||||||
|
load: true, //(默认 true 说明:本接口是否提示加载动画) |
||||||
|
maxSize: 300000, //(默认 无 说明:上传的文件最大字节数限制,默认不限制) |
||||||
|
onEachUpdate: res => { |
||||||
|
console.log("单张上传成功返回:",res); |
||||||
|
}, |
||||||
|
onProgressUpdate: res => { |
||||||
|
console.log("上传进度返回:",res); |
||||||
|
} |
||||||
|
}).then(res => { |
||||||
|
console.log("全部上传完返回结果:",res); |
||||||
|
}); |
||||||
|
``` |
||||||
|
### jsonp 跨域请求(只支持H5) |
||||||
|
``` |
||||||
|
let data = await this.$http.jsonp('http://www.aaa.com/aid/region',{pid:0}, { |
||||||
|
isFactory: false, //(默认 true 说明:本接口是否调用公共的数据处理方法,设置false后isPrompt参数奖失去作用) |
||||||
|
}); |
||||||
|
``` |
@ -0,0 +1,169 @@ |
|||||||
|
// created by gpake
|
||||||
|
(function () { |
||||||
|
|
||||||
|
var config = { |
||||||
|
qiniuRegion: '', |
||||||
|
qiniuImageURLPrefix: '', |
||||||
|
qiniuUploadToken: '', |
||||||
|
qiniuUploadTokenURL: '', |
||||||
|
qiniuUploadTokenFunction: null, |
||||||
|
qiniuShouldUseQiniuFileName: false |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
init: init, |
||||||
|
upload: upload, |
||||||
|
} |
||||||
|
|
||||||
|
// 在整个程序生命周期中,只需要 init 一次即可
|
||||||
|
// 如果需要变更参数,再调用 init 即可
|
||||||
|
function init(options) { |
||||||
|
config = { |
||||||
|
qiniuRegion: '', |
||||||
|
qiniuImageURLPrefix: '', |
||||||
|
qiniuUploadToken: '', |
||||||
|
qiniuUploadTokenURL: '', |
||||||
|
qiniuUploadTokenFunction: null, |
||||||
|
qiniuShouldUseQiniuFileName: false |
||||||
|
}; |
||||||
|
updateConfigWithOptions(options); |
||||||
|
} |
||||||
|
|
||||||
|
function updateConfigWithOptions(options) { |
||||||
|
if (options.region) { |
||||||
|
config.qiniuRegion = options.region; |
||||||
|
} else { |
||||||
|
console.error('qiniu uploader need your bucket region'); |
||||||
|
} |
||||||
|
if (options.uptoken) { |
||||||
|
config.qiniuUploadToken = options.uptoken; |
||||||
|
} else if (options.uptokenURL) { |
||||||
|
config.qiniuUploadTokenURL = options.uptokenURL; |
||||||
|
} else if (options.uptokenFunc) { |
||||||
|
config.qiniuUploadTokenFunction = options.uptokenFunc; |
||||||
|
} |
||||||
|
if (options.domain) { |
||||||
|
config.qiniuImageURLPrefix = options.domain; |
||||||
|
} |
||||||
|
config.qiniuShouldUseQiniuFileName = options.shouldUseQiniuFileName |
||||||
|
} |
||||||
|
|
||||||
|
function upload(filePath, success, fail, options, progress, cancelTask) { |
||||||
|
if (null == filePath) { |
||||||
|
console.error('qiniu uploader need filePath to upload'); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (options) { |
||||||
|
updateConfigWithOptions(options); |
||||||
|
} |
||||||
|
if (config.qiniuUploadToken) { |
||||||
|
doUpload(filePath, success, fail, options, progress, cancelTask); |
||||||
|
} else if (config.qiniuUploadTokenURL) { |
||||||
|
getQiniuToken(function () { |
||||||
|
doUpload(filePath, success, fail, options, progress, cancelTask); |
||||||
|
}); |
||||||
|
} else if (config.qiniuUploadTokenFunction) { |
||||||
|
config.qiniuUploadToken = config.qiniuUploadTokenFunction(); |
||||||
|
if (null == config.qiniuUploadToken && config.qiniuUploadToken.length > 0) { |
||||||
|
console.error('qiniu UploadTokenFunction result is null, please check the return value'); |
||||||
|
return |
||||||
|
} |
||||||
|
doUpload(filePath, success, fail, options, progress, cancelTask); |
||||||
|
} else { |
||||||
|
console.error('qiniu uploader need one of [uptoken, uptokenURL, uptokenFunc]'); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function doUpload(filePath, success, fail, options, progress, cancelTask) { |
||||||
|
if (null == config.qiniuUploadToken && config.qiniuUploadToken.length > 0) { |
||||||
|
console.error('qiniu UploadToken is null, please check the init config or networking'); |
||||||
|
return |
||||||
|
} |
||||||
|
var url = uploadURLFromRegionCode(config.qiniuRegion); |
||||||
|
var fileName = filePath.split('//')[1]; |
||||||
|
if (options && options.key) { |
||||||
|
fileName = options.key; |
||||||
|
} |
||||||
|
var formData = { |
||||||
|
'token': config.qiniuUploadToken |
||||||
|
}; |
||||||
|
if (!config.qiniuShouldUseQiniuFileName) { |
||||||
|
formData['key'] = fileName |
||||||
|
} |
||||||
|
var uploadTask = wx.uploadFile({ |
||||||
|
url: url, |
||||||
|
filePath: filePath, |
||||||
|
name: 'file', |
||||||
|
formData: formData, |
||||||
|
success: function (res) { |
||||||
|
var dataString = res.data |
||||||
|
if (res.data.hasOwnProperty('type') && res.data.type === 'Buffer') { |
||||||
|
dataString = String.fromCharCode.apply(null, res.data.data) |
||||||
|
} |
||||||
|
try { |
||||||
|
var dataObject = JSON.parse(dataString); |
||||||
|
//do something
|
||||||
|
var imageUrl = config.qiniuImageURLPrefix + '/' + dataObject.key; |
||||||
|
dataObject.imageURL = imageUrl; |
||||||
|
if (success) { |
||||||
|
success(dataObject); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
console.log('parse JSON failed, origin String is: ' + dataString) |
||||||
|
if (fail) { |
||||||
|
fail(e); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: function (error) { |
||||||
|
console.error(error); |
||||||
|
if (fail) { |
||||||
|
fail(error); |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
uploadTask.onProgressUpdate((res) => { |
||||||
|
progress && progress(res) |
||||||
|
}) |
||||||
|
|
||||||
|
cancelTask && cancelTask(() => { |
||||||
|
uploadTask.abort() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function getQiniuToken(callback) { |
||||||
|
wx.request({ |
||||||
|
url: config.qiniuUploadTokenURL, |
||||||
|
success: function (res) { |
||||||
|
var token = res.data.uptoken; |
||||||
|
if (token && token.length > 0) { |
||||||
|
config.qiniuUploadToken = token; |
||||||
|
if (callback) { |
||||||
|
callback(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
console.error('qiniuUploader cannot get your token, please check the uptokenURL or server') |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: function (error) { |
||||||
|
console.error('qiniu UploadToken is null, please check the init config or networking: ' + error); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function uploadURLFromRegionCode(code) { |
||||||
|
var uploadURL = null; |
||||||
|
switch (code) { |
||||||
|
case 'ECN': uploadURL = 'https://up.qbox.me'; break; |
||||||
|
case 'NCN': uploadURL = 'https://up-z1.qbox.me'; break; |
||||||
|
case 'SCN': uploadURL = 'https://up-z2.qbox.me'; break; |
||||||
|
case 'NA': uploadURL = 'https://up-na0.qbox.me'; break; |
||||||
|
case 'ASG': uploadURL = 'https://up-as0.qbox.me'; break; |
||||||
|
default: console.error('please make the region is with one of [ECN, SCN, NCN, NA, ASG]'); |
||||||
|
} |
||||||
|
return uploadURL; |
||||||
|
} |
||||||
|
|
||||||
|
})(); |
@ -0,0 +1,208 @@ |
|||||||
|
import request from "./../core/request.js"; |
||||||
|
const { |
||||||
|
chooseImage, |
||||||
|
chooseVideo, |
||||||
|
qiniuUpload, |
||||||
|
urlUpload |
||||||
|
} = require("./utils"); |
||||||
|
import { |
||||||
|
mergeConfig |
||||||
|
} from "./../core/utils.js"; |
||||||
|
export default class fileUpload extends request { |
||||||
|
constructor(props) { |
||||||
|
// 调用实现父类的构造函数
|
||||||
|
super(props); |
||||||
|
} |
||||||
|
//七牛云上传图片
|
||||||
|
async qnImgUpload(options = {}) { |
||||||
|
let files; |
||||||
|
try { |
||||||
|
files = await chooseImage(options); |
||||||
|
// 选择完成回调
|
||||||
|
options.onSelectComplete && options.onSelectComplete(files); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} |
||||||
|
if (files) { |
||||||
|
return this.qnFileUpload({ |
||||||
|
...options, |
||||||
|
files: files |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
//七牛云上传视频
|
||||||
|
async qnVideoUpload(options = {}) { |
||||||
|
let files; |
||||||
|
try { |
||||||
|
files = await chooseVideo(options); |
||||||
|
// 选择完成回调
|
||||||
|
options.onSelectComplete && options.onSelectComplete(files); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} |
||||||
|
if (files) { |
||||||
|
return this.qnFileUpload({ |
||||||
|
...options, |
||||||
|
files: files |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//七牛云文件上传(支持多张上传)
|
||||||
|
async qnFileUpload(options = {}) { |
||||||
|
let requestInfo; |
||||||
|
try { |
||||||
|
// 数据合并
|
||||||
|
requestInfo = { |
||||||
|
...this.config, |
||||||
|
...options, |
||||||
|
header: {}, |
||||||
|
method: "FILE" |
||||||
|
}; |
||||||
|
//请求前回调
|
||||||
|
if (this.requestStart) { |
||||||
|
let requestStart = this.requestStart(requestInfo); |
||||||
|
if (typeof requestStart == "object") { |
||||||
|
let changekeys = ["load", "files"]; |
||||||
|
changekeys.forEach(key => { |
||||||
|
requestInfo[key] = requestStart[key]; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
throw { |
||||||
|
errMsg: "【request】请求开始拦截器未通过", |
||||||
|
statusCode: 0, |
||||||
|
data: requestInfo.data, |
||||||
|
method: requestInfo.method, |
||||||
|
header: requestInfo.header, |
||||||
|
url: requestInfo.url, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let requestResult = await qiniuUpload(requestInfo, this.getQnToken); |
||||||
|
return Promise.resolve(requestResult); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} finally { |
||||||
|
this.requestEnd && this.requestEnd(requestInfo); |
||||||
|
} |
||||||
|
} |
||||||
|
//本地服务器图片上传
|
||||||
|
async urlImgUpload() { |
||||||
|
let options = {}; |
||||||
|
if (arguments[0]) { |
||||||
|
if (typeof(arguments[0]) == "string") { |
||||||
|
options.url = arguments[0]; |
||||||
|
} else if (typeof(arguments[0]) == "object") { |
||||||
|
options = Object.assign(options, arguments[0]); |
||||||
|
} |
||||||
|
} |
||||||
|
if (arguments[1] && typeof(arguments[1]) == "object") { |
||||||
|
options = Object.assign(options, arguments[1]); |
||||||
|
} |
||||||
|
try { |
||||||
|
options.files = await chooseImage(options); |
||||||
|
// 选择完成回调
|
||||||
|
options.onSelectComplete && options.onSelectComplete(options.files); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} |
||||||
|
if (options.files) { |
||||||
|
return this.urlFileUpload(options); |
||||||
|
} |
||||||
|
} |
||||||
|
//本地服务器上传视频
|
||||||
|
async urlVideoUpload() { |
||||||
|
let options = {}; |
||||||
|
if (arguments[0]) { |
||||||
|
if (typeof(arguments[0]) == "string") { |
||||||
|
options.url = arguments[0]; |
||||||
|
} else if (typeof(arguments[0]) == "object") { |
||||||
|
options = Object.assign(options, arguments[0]); |
||||||
|
} |
||||||
|
} |
||||||
|
if (arguments[1] && typeof(arguments[1]) == "object") { |
||||||
|
options = Object.assign(options, arguments[1]); |
||||||
|
} |
||||||
|
try { |
||||||
|
options.files = await chooseVideo(options); |
||||||
|
// 选择完成回调
|
||||||
|
options.onSelectComplete && options.onSelectComplete(options.files); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} |
||||||
|
if (options.files) { |
||||||
|
return this.urlFileUpload(options); |
||||||
|
} |
||||||
|
} |
||||||
|
//本地服务器文件上传方法
|
||||||
|
async urlFileUpload() { |
||||||
|
let requestInfo = { |
||||||
|
method: "FILE" |
||||||
|
}; |
||||||
|
if (arguments[0]) { |
||||||
|
if (typeof(arguments[0]) == "string") { |
||||||
|
requestInfo.url = arguments[0]; |
||||||
|
} else if (typeof(arguments[0]) == "object") { |
||||||
|
requestInfo = Object.assign(requestInfo, arguments[0]); |
||||||
|
} |
||||||
|
} |
||||||
|
if (arguments[1] && typeof(arguments[1]) == "object") { |
||||||
|
requestInfo = Object.assign(requestInfo, arguments[1]); |
||||||
|
} |
||||||
|
if (!requestInfo.url && this.defaultUploadUrl) { |
||||||
|
requestInfo.url = this.defaultUploadUrl; |
||||||
|
} |
||||||
|
// 请求数据
|
||||||
|
// 是否运行过请求开始钩子
|
||||||
|
let runRequestStart = false; |
||||||
|
try { |
||||||
|
if (!requestInfo.url) { |
||||||
|
throw { |
||||||
|
errMsg: "【request】文件上传缺失数据url", |
||||||
|
statusCode: 0, |
||||||
|
data: requestInfo.data, |
||||||
|
method: requestInfo.method, |
||||||
|
header: requestInfo.header, |
||||||
|
url: requestInfo.url, |
||||||
|
} |
||||||
|
} |
||||||
|
// 数据合并
|
||||||
|
requestInfo = mergeConfig(this, requestInfo); |
||||||
|
// 代表之前运行到这里
|
||||||
|
runRequestStart = true; |
||||||
|
//请求前回调
|
||||||
|
if (this.requestStart) { |
||||||
|
let requestStart = this.requestStart(requestInfo); |
||||||
|
if (typeof requestStart == "object") { |
||||||
|
let changekeys = ["data", "header", "isPrompt", "load", "isFactory", "files"]; |
||||||
|
changekeys.forEach(key => { |
||||||
|
requestInfo[key] = requestStart[key]; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
throw { |
||||||
|
errMsg: "【request】请求开始拦截器未通过", |
||||||
|
statusCode: 0, |
||||||
|
data: requestInfo.data, |
||||||
|
method: requestInfo.method, |
||||||
|
header: requestInfo.header, |
||||||
|
url: requestInfo.url, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let requestResult = await urlUpload(requestInfo, this.dataFactory); |
||||||
|
return Promise.resolve(requestResult); |
||||||
|
} catch (err) { |
||||||
|
this.requestError && this.requestError(err); |
||||||
|
return Promise.reject(err); |
||||||
|
} finally { |
||||||
|
if (runRequestStart) { |
||||||
|
this.requestEnd && this.requestEnd(requestInfo); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,288 @@ |
|||||||
|
const qiniuUploader = require("./qiniuUploader"); |
||||||
|
//七牛云上传文件命名
|
||||||
|
export const randomChar = function(l, url = "") { |
||||||
|
const x = "0123456789qwertyuioplkjhgfdsazxcvbnm"; |
||||||
|
let tmp = ""; |
||||||
|
let time = new Date(); |
||||||
|
for (let i = 0; i < l; i++) { |
||||||
|
tmp += x.charAt(Math.ceil(Math.random() * 100000000) % x.length); |
||||||
|
} |
||||||
|
return ( |
||||||
|
"file/" + |
||||||
|
url + |
||||||
|
time.getTime() + |
||||||
|
tmp |
||||||
|
); |
||||||
|
} |
||||||
|
//图片选择
|
||||||
|
export const chooseImage = function(data) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
uni.chooseImage({ |
||||||
|
count: data.count || 9, //默认9
|
||||||
|
sizeType: data.sizeType || ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
||||||
|
sourceType: data.sourceType || ['album', 'camera'], //从相册选择
|
||||||
|
success: function(res) { |
||||||
|
resolve(res.tempFiles); |
||||||
|
}, |
||||||
|
fail: err => { |
||||||
|
reject({ |
||||||
|
errMsg: err.errMsg,
|
||||||
|
errCode: err.errCode,
|
||||||
|
statusCode: 0, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
//视频选择
|
||||||
|
export const chooseVideo = function(data) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
uni.chooseVideo({ |
||||||
|
sourceType: data.sourceType || ['album', 'camera'], //从相册选择
|
||||||
|
compressed: data.compressed || false, //是否压缩所选的视频源文件,默认值为 true,需要压缩。
|
||||||
|
maxDuration: data.maxDuration || 60, //拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。
|
||||||
|
camera: data.camera || 'back', //'front'、'back',默认'back'
|
||||||
|
success: function(res) { |
||||||
|
let files = [{ |
||||||
|
path: res.tempFilePath |
||||||
|
}]; |
||||||
|
// #ifdef APP-PLUS || H5 || MP-WEIXIN
|
||||||
|
files[0].duration = res.duration; |
||||||
|
files[0].size = res.size; |
||||||
|
files[0].height = res.height; |
||||||
|
files[0].width = res.width; |
||||||
|
// #endif
|
||||||
|
// #ifdef H5
|
||||||
|
files[0].name = res.name; |
||||||
|
// #endif
|
||||||
|
resolve(files); |
||||||
|
}, |
||||||
|
fail: err => { |
||||||
|
reject({ |
||||||
|
errMsg: err.errMsg,
|
||||||
|
errCode: err.errCode,
|
||||||
|
statusCode: 0, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
// 七牛云上传
|
||||||
|
export const qiniuUpload = function(requestInfo, getQnToken) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (Array.isArray(requestInfo.files)) { |
||||||
|
let len = requestInfo.files.length; |
||||||
|
let fileList = new Array; |
||||||
|
if (getQnToken) { |
||||||
|
getQnToken(qnRes => { |
||||||
|
/* |
||||||
|
*接口返回参数: |
||||||
|
*visitPrefix:访问文件的域名 |
||||||
|
*token:七牛云上传token |
||||||
|
*folderPath:上传的文件夹 |
||||||
|
*region: 地区 默认为:SCN |
||||||
|
*/ |
||||||
|
let prefixLen = qnRes.visitPrefix.length; |
||||||
|
if(qnRes.visitPrefix.charAt(prefixLen - 1) == '/'){ |
||||||
|
qnRes.visitPrefix = qnRes.visitPrefix.substring(0, prefixLen - 1) |
||||||
|
} |
||||||
|
uploadFile(0); |
||||||
|
|
||||||
|
function uploadFile(i) { |
||||||
|
let item = requestInfo.files[i]; |
||||||
|
let updateUrl = randomChar(10, qnRes.folderPath); |
||||||
|
let fileData = { |
||||||
|
fileIndex: i, |
||||||
|
files: requestInfo.files, |
||||||
|
...item |
||||||
|
}; |
||||||
|
if (item.name) { |
||||||
|
fileData.name = item.name; |
||||||
|
let nameArr = item.name.split("."); |
||||||
|
updateUrl += "." + nameArr[nameArr.length - 1]; |
||||||
|
} |
||||||
|
// 交给七牛上传
|
||||||
|
qiniuUploader.upload(item.path || item, (res) => { |
||||||
|
fileData.url = res.imageURL; |
||||||
|
requestInfo.onEachUpdate && requestInfo.onEachUpdate({ |
||||||
|
url: res.imageURL, |
||||||
|
...fileData |
||||||
|
}); |
||||||
|
fileList.push(res.imageURL); |
||||||
|
if (len - 1 > i) { |
||||||
|
uploadFile(i + 1); |
||||||
|
} else { |
||||||
|
resolve(fileList); |
||||||
|
} |
||||||
|
}, (error) => { |
||||||
|
reject(error); |
||||||
|
}, { |
||||||
|
region: qnRes.region || 'SCN', //地区
|
||||||
|
domain: qnRes.visitPrefix, // bucket 域名,下载资源时用到。
|
||||||
|
key: updateUrl, |
||||||
|
uptoken: qnRes.token, // 由其他程序生成七牛 uptoken
|
||||||
|
uptokenURL: 'UpTokenURL.com/uptoken' // 上传地址
|
||||||
|
}, (res) => { |
||||||
|
console.log(requestInfo); |
||||||
|
requestInfo.onProgressUpdate && requestInfo.onProgressUpdate(Object.assign({}, fileData, res)); |
||||||
|
// console.log('上传进度', res.progress)
|
||||||
|
// console.log('已经上传的数据长度', res.totalBytesSent)
|
||||||
|
// console.log('预期需要上传的数据总长度', res.totalBytesExpectedToSend)
|
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
reject({ |
||||||
|
errMsg: "请添加七牛云回调方法:getQnToken", |
||||||
|
statusCode: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
reject({ |
||||||
|
errMsg: "files 必须是数组类型", |
||||||
|
statusCode: 0 |
||||||
|
}); |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
// 服务器URL上传
|
||||||
|
export const urlUpload = function(requestInfo, dataFactory) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
// 本地文件上传去掉默认Content-Type
|
||||||
|
if (requestInfo.header['Content-Type']) { |
||||||
|
delete requestInfo.header['Content-Type']; |
||||||
|
} |
||||||
|
// 本地文件上传去掉默认Content-Type
|
||||||
|
if (requestInfo.header['content-type']) { |
||||||
|
delete requestInfo.header['content-type']; |
||||||
|
} |
||||||
|
if (Array.isArray(requestInfo.files)) { |
||||||
|
// #ifdef APP-PLUS || H5
|
||||||
|
let files = []; |
||||||
|
let fileData = { |
||||||
|
files: requestInfo.files, |
||||||
|
name: requestInfo.name || "file" |
||||||
|
}; |
||||||
|
requestInfo.files.forEach(item => { |
||||||
|
let fileInfo = { |
||||||
|
name: requestInfo.name || "file", |
||||||
|
}; |
||||||
|
if(item.path){ |
||||||
|
fileInfo.uri = item.path; |
||||||
|
} else { |
||||||
|
fileInfo.file = item; |
||||||
|
} |
||||||
|
files.push(fileInfo); |
||||||
|
}); |
||||||
|
let config = { |
||||||
|
url: requestInfo.url, |
||||||
|
files: files, |
||||||
|
header: requestInfo.header, //加入请求头
|
||||||
|
success: (response) => { |
||||||
|
//是否用外部的数据处理方法
|
||||||
|
if (requestInfo.isFactory && dataFactory) { |
||||||
|
//数据处理
|
||||||
|
dataFactory({ |
||||||
|
...requestInfo, |
||||||
|
response: response, |
||||||
|
}).then(data => { |
||||||
|
requestInfo.onEachUpdate && requestInfo.onEachUpdate({ |
||||||
|
data: data, |
||||||
|
...fileData |
||||||
|
}); |
||||||
|
resolve(data); |
||||||
|
},err => { |
||||||
|
reject(err); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
requestInfo.onEachUpdate && requestInfo.onEachUpdate({ |
||||||
|
data: response, |
||||||
|
...fileData |
||||||
|
}); |
||||||
|
resolve(response); |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
reject(err); |
||||||
|
} |
||||||
|
}; |
||||||
|
if (requestInfo.data) { |
||||||
|
config.formData = requestInfo.data; |
||||||
|
} |
||||||
|
const uploadTask = uni.uploadFile(config); |
||||||
|
uploadTask.onProgressUpdate(res => { |
||||||
|
requestInfo.onProgressUpdate && requestInfo.onProgressUpdate(Object.assign({}, fileData, res)); |
||||||
|
}); |
||||||
|
// #endif
|
||||||
|
// #ifdef MP
|
||||||
|
const len = requestInfo.files.length - 1; |
||||||
|
let fileList = new Array; |
||||||
|
fileUpload(0); |
||||||
|
|
||||||
|
function fileUpload(i) { |
||||||
|
let item = requestInfo.files[i]; |
||||||
|
let fileData = { |
||||||
|
fileIndex: i, |
||||||
|
files: requestInfo.files, |
||||||
|
...item |
||||||
|
}; |
||||||
|
let config = { |
||||||
|
url: requestInfo.url, |
||||||
|
filePath: item.path, |
||||||
|
header: requestInfo.header, //加入请求头
|
||||||
|
name: requestInfo.name || "file", |
||||||
|
success: (response) => { |
||||||
|
//是否用外部的数据处理方法
|
||||||
|
if (requestInfo.isFactory && dataFactory) { |
||||||
|
//数据处理
|
||||||
|
dataFactory({ |
||||||
|
...requestInfo, |
||||||
|
response: response, |
||||||
|
}).then(data => { |
||||||
|
fileList.push(data); |
||||||
|
requestInfo.onEachUpdate && requestInfo.onEachUpdate({ |
||||||
|
data: data, |
||||||
|
...fileData |
||||||
|
}); |
||||||
|
if (len <= i) { |
||||||
|
resolve(fileList); |
||||||
|
} else { |
||||||
|
fileUpload(i + 1); |
||||||
|
} |
||||||
|
},err => { |
||||||
|
reject(err); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
requestInfo.onEachUpdate && requestInfo.onEachUpdate({ |
||||||
|
data: response, |
||||||
|
...fileData |
||||||
|
}); |
||||||
|
fileList.push(response); |
||||||
|
if (len <= i) { |
||||||
|
resolve(fileList); |
||||||
|
} else { |
||||||
|
fileUpload(i + 1); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
reject(err); |
||||||
|
} |
||||||
|
}; |
||||||
|
if (requestInfo.data) { |
||||||
|
config.formData = requestInfo.data; |
||||||
|
} |
||||||
|
const uploadTask = uni.uploadFile(config); |
||||||
|
uploadTask.onProgressUpdate(res => { |
||||||
|
requestInfo.onProgressUpdate && requestInfo.onProgressUpdate(Object.assign({}, fileData, res)); |
||||||
|
}); |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
} else { |
||||||
|
reject({ |
||||||
|
errMsg: "files 必须是数组类型", |
||||||
|
statusCode: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
import request from "./request"; |
||||||
|
// 全局配置的请求域名
|
||||||
|
let baseUrl = "https://www.huorantech.cn"; |
||||||
|
//可以new多个request来支持多个域名请求
|
||||||
|
let $http = new request({ |
||||||
|
//接口请求地址
|
||||||
|
baseUrl: baseUrl, |
||||||
|
//服务器本地上传文件地址
|
||||||
|
fileUrl: baseUrl, |
||||||
|
// 服务器上传图片默认url
|
||||||
|
defaultUploadUrl: "api/common/v1/upload_image", |
||||||
|
//设置请求头(如果使用报错跨域问题,可能是content-type请求类型和后台那边设置的不一致)
|
||||||
|
header: { |
||||||
|
'content-type': 'application/json;charset=UTF-8' |
||||||
|
}, |
||||||
|
// 请求超时时间(默认6000)
|
||||||
|
timeout: 6000, |
||||||
|
// 默认配置(可不写)
|
||||||
|
config: { |
||||||
|
// 是否自动提示错误
|
||||||
|
isPrompt: true, |
||||||
|
// 是否显示加载动画
|
||||||
|
load: true, |
||||||
|
// 是否使用数据工厂
|
||||||
|
isFactory: true |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加获取七牛云token的方法
|
||||||
|
$http.getQnToken = function(callback){ |
||||||
|
//该地址需要开发者自行配置(每个后台的接口风格都不一样)
|
||||||
|
$http.get("api/kemean/aid/qn_upload").then(data => { |
||||||
|
/* |
||||||
|
*接口返回参数: |
||||||
|
*visitPrefix:访问文件的域名 |
||||||
|
*token:七牛云上传token |
||||||
|
*folderPath:上传的文件夹 |
||||||
|
*region: 地区 默认为:SCN |
||||||
|
*/ |
||||||
|
callback({ |
||||||
|
visitPrefix: data.visitPrefix, |
||||||
|
token: data.token, |
||||||
|
folderPath: data.folderPath, |
||||||
|
region: "SCN" |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
//当前接口请求数
|
||||||
|
let requestNum = 0; |
||||||
|
//请求开始拦截器
|
||||||
|
$http.requestStart = function(options) { |
||||||
|
if (options.load) { |
||||||
|
if (requestNum <= 0) { |
||||||
|
//打开加载动画
|
||||||
|
uni.showLoading({ |
||||||
|
title: '加载中', |
||||||
|
mask: true |
||||||
|
}); |
||||||
|
} |
||||||
|
requestNum += 1; |
||||||
|
} |
||||||
|
// 图片上传大小限制
|
||||||
|
if (options.method == "FILE" && options.maxSize) { |
||||||
|
// 文件最大字节: options.maxSize 可以在调用方法的时候加入参数
|
||||||
|
const maxSize = options.maxSize; |
||||||
|
for (let item of options.files) { |
||||||
|
if (item.size > maxSize) { |
||||||
|
setTimeout(() => { |
||||||
|
uni.showToast({ |
||||||
|
title: "图片过大,请重新上传", |
||||||
|
icon: "none" |
||||||
|
}); |
||||||
|
}, 500); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//请求前加入token
|
||||||
|
// options.header['token'] = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoidG9rZW4iLCJpc3MiOiJBZG1pbiIsImlhdCI6MTYxMTEzMzk1MywiZXhwIjoxNjExMjIwMzUzfQ.ZB_zJgzmgk8uw4wwH5TY8r1IOvObd2rPSfTU3cFA69U";
|
||||||
|
return options; // return false 表示请求拦截,不会继续请求
|
||||||
|
} |
||||||
|
//请求结束
|
||||||
|
$http.requestEnd = function(options) { |
||||||
|
//判断当前接口是否需要加载动画
|
||||||
|
if (options.load) { |
||||||
|
requestNum = requestNum - 1; |
||||||
|
if (requestNum <= 0) { |
||||||
|
uni.hideLoading(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//登录弹窗次数
|
||||||
|
let loginPopupNum = 0; |
||||||
|
//所有接口数据处理(可在接口里设置不调用此方法)
|
||||||
|
//此方法需要开发者根据各自的接口返回类型修改,以下只是模板
|
||||||
|
$http.dataFactory = async function(res) { |
||||||
|
if (res.response.statusCode && res.response.statusCode == 200) { |
||||||
|
let httpData = res.response.data; |
||||||
|
if (typeof (httpData) == "string") { |
||||||
|
httpData = JSON.parse(httpData); |
||||||
|
} |
||||||
|
/*********以下只是模板(及共参考),需要开发者根据各自的接口返回类型修改*********/ |
||||||
|
|
||||||
|
//判断数据是否请求成功
|
||||||
|
if (httpData.code == 10000) { |
||||||
|
// 返回正确的结果(then接受数据)
|
||||||
|
return Promise.resolve(httpData); |
||||||
|
} else { //其他错误提示
|
||||||
|
if (res.isPrompt) { |
||||||
|
uni.showToast({ |
||||||
|
title: httpData.message, |
||||||
|
icon: "none", |
||||||
|
duration: 3000 |
||||||
|
}); |
||||||
|
} |
||||||
|
// 返回错误的结果(catch接受数据)
|
||||||
|
return Promise.reject({ |
||||||
|
statusCode: 0, |
||||||
|
errMsg: httpData.message |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/*********以上只是模板(及共参考),需要开发者根据各自的接口返回类型修改*********/ |
||||||
|
|
||||||
|
} else { |
||||||
|
// 返回错误的结果(catch接受数据)
|
||||||
|
return Promise.reject({ |
||||||
|
statusCode: res.response.statusCode, |
||||||
|
errMsg: "数据请求出错" |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
// 错误回调
|
||||||
|
$http.requestError = function (e) { |
||||||
|
// e.statusCode === 0 是参数效验错误抛出的
|
||||||
|
if (e.statusCode === 0) { |
||||||
|
throw e; |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: "网络错误,请检查一下网络", |
||||||
|
icon: "none" |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
export default $http; |
@ -0,0 +1,27 @@ |
|||||||
|
import Vue from 'vue' |
||||||
|
import App from './App' |
||||||
|
import cuCustom from 'colorui/components/cu-custom.vue' |
||||||
|
import MescrollBody from "@/components/mescroll-uni/mescroll-body.vue" |
||||||
|
import MescrollUni from "@/components/mescroll-uni/mescroll-uni.vue" |
||||||
|
import {goto} from "@/util/util.js" |
||||||
|
import core from '@/util/core.js' |
||||||
|
import '@/plugins/utils' |
||||||
|
// 接口请求
|
||||||
|
import $http from '@/js_sdk/zhouWei-request/requestConfig'; |
||||||
|
Vue.prototype.$http = $http; |
||||||
|
|
||||||
|
Vue.prototype.goto = goto; |
||||||
|
Vue.component('mescroll-body', MescrollBody) |
||||||
|
Vue.component('mescroll-uni', MescrollUni) |
||||||
|
|
||||||
|
Vue.prototype.core = core |
||||||
|
|
||||||
|
Vue.component('cu-custom',cuCustom) |
||||||
|
Vue.config.productionTip = false |
||||||
|
|
||||||
|
App.mpType = 'app' |
||||||
|
|
||||||
|
const app = new Vue({ |
||||||
|
...App |
||||||
|
}) |
||||||
|
app.$mount() |
@ -0,0 +1,79 @@ |
|||||||
|
{ |
||||||
|
"name" : "大庆小程序", |
||||||
|
"appid" : "__UNI__EB9AA47", |
||||||
|
"description" : "", |
||||||
|
"versionName" : "1.0.0", |
||||||
|
"versionCode" : "100", |
||||||
|
"transformPx" : true, |
||||||
|
/* 5+App特有相关 */ |
||||||
|
"app-plus" : { |
||||||
|
"usingComponents" : true, |
||||||
|
"nvueCompiler" : "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.READ_CONTACTS\"/>", |
||||||
|
"<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.WRITE_CONTACTS\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>", |
||||||
|
"<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.CALL_PHONE\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>", |
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", |
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" |
||||||
|
] |
||||||
|
}, |
||||||
|
/* ios打包配置 */ |
||||||
|
"ios" : {}, |
||||||
|
/* SDK配置 */ |
||||||
|
"sdkConfigs" : {} |
||||||
|
} |
||||||
|
}, |
||||||
|
/* 快应用特有相关 */ |
||||||
|
"quickapp" : {}, |
||||||
|
/* 小程序特有相关 */ |
||||||
|
"mp-weixin" : { |
||||||
|
"appid" : "wx77a8a2a23138998b", |
||||||
|
"setting" : { |
||||||
|
"urlCheck" : false, |
||||||
|
"minified" : true |
||||||
|
}, |
||||||
|
"usingComponents" : true |
||||||
|
}, |
||||||
|
"mp-alipay" : { |
||||||
|
"usingComponents" : true |
||||||
|
}, |
||||||
|
"mp-baidu" : { |
||||||
|
"usingComponents" : true |
||||||
|
}, |
||||||
|
"mp-toutiao" : { |
||||||
|
"usingComponents" : true |
||||||
|
}, |
||||||
|
"uniStatistics" : { |
||||||
|
"enable" : false |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014-present yiminghe |
||||||
|
|
||||||
|
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. |
@ -0,0 +1,462 @@ |
|||||||
|
# async-validator |
||||||
|
|
||||||
|
[![NPM version][npm-image]][npm-url] |
||||||
|
[![build status][travis-image]][travis-url] |
||||||
|
[![Test coverage][coveralls-image]][coveralls-url] |
||||||
|
[![node version][node-image]][node-url] |
||||||
|
[![npm download][download-image]][download-url] |
||||||
|
[![npm bundle size (minified + gzip)][bundlesize-image]][bundlesize-url] |
||||||
|
|
||||||
|
[npm-image]: https://img.shields.io/npm/v/async-validator.svg?style=flat-square |
||||||
|
[npm-url]: https://npmjs.org/package/async-validator |
||||||
|
[travis-image]: https://img.shields.io/travis/yiminghe/async-validator.svg?style=flat-square |
||||||
|
[travis-url]: https://travis-ci.org/yiminghe/async-validator |
||||||
|
[coveralls-image]: https://img.shields.io/coveralls/yiminghe/async-validator.svg?style=flat-square |
||||||
|
[coveralls-url]: https://coveralls.io/r/yiminghe/async-validator?branch=master |
||||||
|
[node-image]: https://img.shields.io/badge/node.js-%3E=4.0.0-green.svg?style=flat-square |
||||||
|
[node-url]: https://nodejs.org/download/ |
||||||
|
[download-image]: https://img.shields.io/npm/dm/async-validator.svg?style=flat-square |
||||||
|
[download-url]: https://npmjs.org/package/async-validator |
||||||
|
[bundlesize-image]: https://img.shields.io/bundlephobia/minzip/async-validator.svg?label=gzip%20size |
||||||
|
[bundlesize-url]: https://bundlephobia.com/result?p=async-validator |
||||||
|
|
||||||
|
Validate form asynchronous. A variation of https://github.com/freeformsystems/async-validate |
||||||
|
|
||||||
|
## Install |
||||||
|
|
||||||
|
```bash |
||||||
|
npm i async-validator |
||||||
|
``` |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
Basic usage involves defining a descriptor, assigning it to a schema and passing the object to be validated and a callback function to the `validate` method of the schema: |
||||||
|
|
||||||
|
```js |
||||||
|
import Schema from 'async-validator'; |
||||||
|
const descriptor = { |
||||||
|
name: { |
||||||
|
type: 'string', |
||||||
|
required: true, |
||||||
|
validator: (rule, value) => value === 'muji', |
||||||
|
}, |
||||||
|
age: { |
||||||
|
type: 'number', |
||||||
|
asyncValidator: (rule, value) => { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (value < 18) { |
||||||
|
reject('too young'); // reject with error message |
||||||
|
} else { |
||||||
|
resolve(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
validator.validate({ name: 'muji' }, (errors, fields) => { |
||||||
|
if (errors) { |
||||||
|
// validation failed, errors is an array of all errors |
||||||
|
// fields is an object keyed by field name with an array of |
||||||
|
// errors per field |
||||||
|
return handleErrors(errors, fields); |
||||||
|
} |
||||||
|
// validation passed |
||||||
|
}); |
||||||
|
|
||||||
|
// PROMISE USAGE |
||||||
|
validator.validate({ name: 'muji', age: 16 }).then(() => { |
||||||
|
// validation passed or without error message |
||||||
|
}).catch(({ errors, fields }) => { |
||||||
|
return handleErrors(errors, fields); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
## API |
||||||
|
|
||||||
|
### Validate |
||||||
|
|
||||||
|
```js |
||||||
|
function(source, [options], callback): Promise |
||||||
|
``` |
||||||
|
|
||||||
|
* `source`: The object to validate (required). |
||||||
|
* `options`: An object describing processing options for the validation (optional). |
||||||
|
* `callback`: A callback function to invoke when validation completes (required). |
||||||
|
|
||||||
|
The method will return a Promise object like: |
||||||
|
* `then()`,validation passed |
||||||
|
* `catch({ errors, fields })`,validation failed, errors is an array of all errors, fields is an object keyed by field name with an array of |
||||||
|
|
||||||
|
### Options |
||||||
|
|
||||||
|
* `suppressWarning`: Boolean, whether to suppress internal warning about invalid value. |
||||||
|
|
||||||
|
* `first`: Boolean, Invoke `callback` when the first validation rule generates an error, |
||||||
|
no more validation rules are processed. |
||||||
|
If your validation involves multiple asynchronous calls (for example, database queries) and you only need the first error use this option. |
||||||
|
|
||||||
|
* `firstFields`: Boolean|String[], Invoke `callback` when the first validation rule of the specified field generates an error, |
||||||
|
no more validation rules of the same field are processed. `true` means all fields. |
||||||
|
|
||||||
|
### Rules |
||||||
|
|
||||||
|
Rules may be functions that perform validation. |
||||||
|
|
||||||
|
```js |
||||||
|
function(rule, value, callback, source, options) |
||||||
|
``` |
||||||
|
|
||||||
|
* `rule`: The validation rule in the source descriptor that corresponds to the field name being validated. It is always assigned a `field` property with the name of the field being validated. |
||||||
|
* `value`: The value of the source object property being validated. |
||||||
|
* `callback`: A callback function to invoke once validation is complete. It expects to be passed an array of `Error` instances to indicate validation failure. If the check is synchronous, you can directly return a ` false ` or ` Error ` or ` Error Array `. |
||||||
|
* `source`: The source object that was passed to the `validate` method. |
||||||
|
* `options`: Additional options. |
||||||
|
* `options.messages`: The object containing validation error messages, will be deep merged with defaultMessages. |
||||||
|
|
||||||
|
The options passed to `validate` or `asyncValidate` are passed on to the validation functions so that you may reference transient data (such as model references) in validation functions. However, some option names are reserved; if you use these properties of the options object they are overwritten. The reserved properties are `messages`, `exception` and `error`. |
||||||
|
|
||||||
|
```js |
||||||
|
import Schema from 'async-validator'; |
||||||
|
const descriptor = { |
||||||
|
name(rule, value, callback, source, options) { |
||||||
|
const errors = []; |
||||||
|
if (!/^[a-z0-9]+$/.test(value)) { |
||||||
|
errors.push(new Error( |
||||||
|
util.format('%s must be lowercase alphanumeric characters', rule.field), |
||||||
|
)); |
||||||
|
} |
||||||
|
return errors; |
||||||
|
}, |
||||||
|
}; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
validator.validate({ name: 'Firstname' }, (errors, fields) => { |
||||||
|
if (errors) { |
||||||
|
return handleErrors(errors, fields); |
||||||
|
} |
||||||
|
// validation passed |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
It is often useful to test against multiple validation rules for a single field, to do so make the rule an array of objects, for example: |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
email: [ |
||||||
|
{ type: 'string', required: true, pattern: Schema.pattern.email }, |
||||||
|
{ |
||||||
|
validator(rule, value, callback, source, options) { |
||||||
|
const errors = []; |
||||||
|
// test if email address already exists in a database |
||||||
|
// and add a validation error to the errors array if it does |
||||||
|
return errors; |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
#### Type |
||||||
|
|
||||||
|
Indicates the `type` of validator to use. Recognised type values are: |
||||||
|
|
||||||
|
* `string`: Must be of type `string`. `This is the default type.` |
||||||
|
* `number`: Must be of type `number`. |
||||||
|
* `boolean`: Must be of type `boolean`. |
||||||
|
* `method`: Must be of type `function`. |
||||||
|
* `regexp`: Must be an instance of `RegExp` or a string that does not generate an exception when creating a new `RegExp`. |
||||||
|
* `integer`: Must be of type `number` and an integer. |
||||||
|
* `float`: Must be of type `number` and a floating point number. |
||||||
|
* `array`: Must be an array as determined by `Array.isArray`. |
||||||
|
* `object`: Must be of type `object` and not `Array.isArray`. |
||||||
|
* `enum`: Value must exist in the `enum`. |
||||||
|
* `date`: Value must be valid as determined by `Date` |
||||||
|
* `url`: Must be of type `url`. |
||||||
|
* `hex`: Must be of type `hex`. |
||||||
|
* `email`: Must be of type `email`. |
||||||
|
* `any`: Can be any type. |
||||||
|
|
||||||
|
#### Required |
||||||
|
|
||||||
|
The `required` rule property indicates that the field must exist on the source object being validated. |
||||||
|
|
||||||
|
#### Pattern |
||||||
|
|
||||||
|
The `pattern` rule property indicates a regular expression that the value must match to pass validation. |
||||||
|
|
||||||
|
#### Range |
||||||
|
|
||||||
|
A range is defined using the `min` and `max` properties. For `string` and `array` types comparison is performed against the `length`, for `number` types the number must not be less than `min` nor greater than `max`. |
||||||
|
|
||||||
|
#### Length |
||||||
|
|
||||||
|
To validate an exact length of a field specify the `len` property. For `string` and `array` types comparison is performed on the `length` property, for the `number` type this property indicates an exact match for the `number`, ie, it may only be strictly equal to `len`. |
||||||
|
|
||||||
|
If the `len` property is combined with the `min` and `max` range properties, `len` takes precedence. |
||||||
|
|
||||||
|
#### Enumerable |
||||||
|
|
||||||
|
> Since version 3.0.0 if you want to validate the values `0` or `false` inside `enum` types, you have to include them explicitly. |
||||||
|
|
||||||
|
To validate a value from a list of possible values use the `enum` type with a `enum` property listing the valid values for the field, for example: |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
role: { type: 'enum', enum: ['admin', 'user', 'guest'] }, |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
#### Whitespace |
||||||
|
|
||||||
|
It is typical to treat required fields that only contain whitespace as errors. To add an additional test for a string that consists solely of whitespace add a `whitespace` property to a rule with a value of `true`. The rule must be a `string` type. |
||||||
|
|
||||||
|
You may wish to sanitize user input instead of testing for whitespace, see [transform](#transform) for an example that would allow you to strip whitespace. |
||||||
|
|
||||||
|
|
||||||
|
#### Deep Rules |
||||||
|
|
||||||
|
If you need to validate deep object properties you may do so for validation rules that are of the `object` or `array` type by assigning nested rules to a `fields` property of the rule. |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
address: { |
||||||
|
type: 'object', |
||||||
|
required: true, |
||||||
|
fields: { |
||||||
|
street: { type: 'string', required: true }, |
||||||
|
city: { type: 'string', required: true }, |
||||||
|
zip: { type: 'string', required: true, len: 8, message: 'invalid zip' }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
name: { type: 'string', required: true }, |
||||||
|
}; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
validator.validate({ address: {} }, (errors, fields) => { |
||||||
|
// errors for address.street, address.city, address.zip |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
Note that if you do not specify the `required` property on the parent rule it is perfectly valid for the field not to be declared on the source object and the deep validation rules will not be executed as there is nothing to validate against. |
||||||
|
|
||||||
|
Deep rule validation creates a schema for the nested rules so you can also specify the `options` passed to the `schema.validate()` method. |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
address: { |
||||||
|
type: 'object', |
||||||
|
required: true, |
||||||
|
options: { first: true }, |
||||||
|
fields: { |
||||||
|
street: { type: 'string', required: true }, |
||||||
|
city: { type: 'string', required: true }, |
||||||
|
zip: { type: 'string', required: true, len: 8, message: 'invalid zip' }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
name: { type: 'string', required: true }, |
||||||
|
}; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
|
||||||
|
validator.validate({ address: {} }) |
||||||
|
.catch(({ errors, fields }) => { |
||||||
|
// now only errors for street and name |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
The parent rule is also validated so if you have a set of rules such as: |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
roles: { |
||||||
|
type: 'array', |
||||||
|
required: true, |
||||||
|
len: 3, |
||||||
|
fields: { |
||||||
|
0: { type: 'string', required: true }, |
||||||
|
1: { type: 'string', required: true }, |
||||||
|
2: { type: 'string', required: true }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
And supply a source object of `{ roles: ['admin', 'user'] }` then two errors will be created. One for the array length mismatch and one for the missing required array entry at index 2. |
||||||
|
|
||||||
|
#### defaultField |
||||||
|
|
||||||
|
The `defaultField` property can be used with the `array` or `object` type for validating all values of the container. |
||||||
|
It may be an `object` or `array` containing validation rules. For example: |
||||||
|
|
||||||
|
```js |
||||||
|
const descriptor = { |
||||||
|
urls: { |
||||||
|
type: 'array', |
||||||
|
required: true, |
||||||
|
defaultField: { type: 'url' }, |
||||||
|
}, |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
Note that `defaultField` is expanded to `fields`, see [deep rules](#deep-rules). |
||||||
|
|
||||||
|
#### Transform |
||||||
|
|
||||||
|
Sometimes it is necessary to transform a value before validation, possibly to coerce the value or to sanitize it in some way. To do this add a `transform` function to the validation rule. The property is transformed prior to validation and re-assigned to the source object to mutate the value of the property in place. |
||||||
|
|
||||||
|
```js |
||||||
|
import Schema from 'async-validator'; |
||||||
|
const descriptor = { |
||||||
|
name: { |
||||||
|
type: 'string', |
||||||
|
required: true, |
||||||
|
pattern: /^[a-z]+$/, |
||||||
|
transform(value) { |
||||||
|
return value.trim(); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
const source = { name: ' user ' }; |
||||||
|
validator.validate(source) |
||||||
|
.then(() => assert.equal(source.name, 'user')); |
||||||
|
``` |
||||||
|
|
||||||
|
Without the `transform` function validation would fail due to the pattern not matching as the input contains leading and trailing whitespace, but by adding the transform function validation passes and the field value is sanitized at the same time. |
||||||
|
|
||||||
|
|
||||||
|
#### Messages |
||||||
|
|
||||||
|
Depending upon your application requirements, you may need i18n support or you may prefer different validation error messages. |
||||||
|
|
||||||
|
The easiest way to achieve this is to assign a `message` to a rule: |
||||||
|
|
||||||
|
```js |
||||||
|
{ name: { type: 'string', required: true, message: 'Name is required' } } |
||||||
|
``` |
||||||
|
|
||||||
|
Message can be any type, such as jsx format. |
||||||
|
|
||||||
|
```js |
||||||
|
{ name: { type: 'string', required: true, message: '<b>Name is required</b>' } } |
||||||
|
``` |
||||||
|
|
||||||
|
Message can also be a function, e.g. if you use vue-i18n: |
||||||
|
```js |
||||||
|
{ name: { type: 'string', required: true, message: () => this.$t( 'name is required' ) } } |
||||||
|
``` |
||||||
|
|
||||||
|
Potentially you may require the same schema validation rules for different languages, in which case duplicating the schema rules for each language does not make sense. |
||||||
|
|
||||||
|
In this scenario you could just provide your own messages for the language and assign it to the schema: |
||||||
|
|
||||||
|
```js |
||||||
|
import Schema from 'async-validator'; |
||||||
|
const cn = { |
||||||
|
required: '%s 必填', |
||||||
|
}; |
||||||
|
const descriptor = { name: { type: 'string', required: true } }; |
||||||
|
const validator = new Schema(descriptor); |
||||||
|
// deep merge with defaultMessages |
||||||
|
validator.messages(cn); |
||||||
|
... |
||||||
|
``` |
||||||
|
|
||||||
|
If you are defining your own validation functions it is better practice to assign the message strings to a messages object and then access the messages via the `options.messages` property within the validation function. |
||||||
|
|
||||||
|
#### asyncValidator |
||||||
|
|
||||||
|
You can customize the asynchronous validation function for the specified field: |
||||||
|
|
||||||
|
```js |
||||||
|
const fields = { |
||||||
|
asyncField: { |
||||||
|
asyncValidator(rule, value, callback) { |
||||||
|
ajax({ |
||||||
|
url: 'xx', |
||||||
|
value: value, |
||||||
|
}).then(function(data) { |
||||||
|
callback(); |
||||||
|
}, function(error) { |
||||||
|
callback(new Error(error)); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
promiseField: { |
||||||
|
asyncValidator(rule, value) { |
||||||
|
return ajax({ |
||||||
|
url: 'xx', |
||||||
|
value: value, |
||||||
|
}); |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
#### validator |
||||||
|
|
||||||
|
You can custom validate function for specified field: |
||||||
|
|
||||||
|
```js |
||||||
|
const fields = { |
||||||
|
field: { |
||||||
|
validator(rule, value, callback) { |
||||||
|
return value === 'test'; |
||||||
|
}, |
||||||
|
message: 'Value is not equal to "test".', |
||||||
|
}, |
||||||
|
|
||||||
|
field2: { |
||||||
|
validator(rule, value, callback) { |
||||||
|
return new Error(`${value} is not equal to 'test'.`); |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
arrField: { |
||||||
|
validator(rule, value) { |
||||||
|
return [ |
||||||
|
new Error('Message 1'), |
||||||
|
new Error('Message 2'), |
||||||
|
]; |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
``` |
||||||
|
|
||||||
|
## FAQ |
||||||
|
|
||||||
|
### How to avoid warning |
||||||
|
|
||||||
|
```js |
||||||
|
import Schema from 'async-validator'; |
||||||
|
Schema.warning = function(){}; |
||||||
|
``` |
||||||
|
|
||||||
|
### How to check if it is `true` |
||||||
|
|
||||||
|
Use `enum` type passing `true` as option. |
||||||
|
|
||||||
|
```js |
||||||
|
{ |
||||||
|
type: 'enum', |
||||||
|
enum: [true], |
||||||
|
message: '', |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Test Case |
||||||
|
|
||||||
|
```bash |
||||||
|
npm test |
||||||
|
``` |
||||||
|
|
||||||
|
## Coverage |
||||||
|
|
||||||
|
```bash |
||||||
|
npm run coverage |
||||||
|
``` |
||||||
|
|
||||||
|
Open coverage/ dir |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
Everything is [MIT](https://en.wikipedia.org/wiki/MIT_License). |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,97 @@ |
|||||||
|
// Type definitions for async-validator 3.0.4
|
||||||
|
// Project: http://github.com/yiminghe/async-validator
|
||||||
|
// Definitions by: iamdhj <https://github.com/iamdhj>
|
||||||
|
// TypeScript Version: 3.6.2
|
||||||
|
|
||||||
|
export default class { |
||||||
|
constructor(rule: Rules); |
||||||
|
|
||||||
|
/** |
||||||
|
* Validate source |
||||||
|
* @param source The object to validate (required) |
||||||
|
* @param options An object describing processing options for the validation |
||||||
|
* @param callback A callback function to invoke when validation completes |
||||||
|
* @returns Promise |
||||||
|
*/ |
||||||
|
validate( |
||||||
|
source: ValidateSource, |
||||||
|
options?: ValidateOption, |
||||||
|
callback?: (errors: ErrorList, fields: FieldErrorList) => void, |
||||||
|
): Promise<void>; |
||||||
|
} |
||||||
|
|
||||||
|
export type RuleType = |
||||||
|
| 'string' |
||||||
|
| 'number' |
||||||
|
| 'boolean' |
||||||
|
| 'method' |
||||||
|
| 'regexp' |
||||||
|
| 'integer' |
||||||
|
| 'float' |
||||||
|
| 'array' |
||||||
|
| 'object' |
||||||
|
| 'enum' |
||||||
|
| 'date' |
||||||
|
| 'url' |
||||||
|
| 'hex' |
||||||
|
| 'email' |
||||||
|
| 'any'; |
||||||
|
|
||||||
|
export interface RuleItem { |
||||||
|
type?: RuleType; // default type is 'string'
|
||||||
|
required?: boolean; |
||||||
|
pattern?: RegExp | string; |
||||||
|
min?: number; // Range of type 'string' and 'array'
|
||||||
|
max?: number; // Range of type 'string' and 'array'
|
||||||
|
len?: number; // Length of type 'string' and 'array'
|
||||||
|
enum?: Array<string | number | boolean | null | undefined>; // possible values of type 'enum'
|
||||||
|
whitespace?: boolean; |
||||||
|
fields?: Rules; // ignore when without required
|
||||||
|
options?: ValidateOption; |
||||||
|
defaultField?: RuleItem; // 'object' or 'array' containing validation rules
|
||||||
|
transform?: (value: any) => any; |
||||||
|
message?: string | (() => string); |
||||||
|
asyncValidator?: ( |
||||||
|
rule: Rules, |
||||||
|
value: any, |
||||||
|
callback: (error: string | string[] | void) => void, |
||||||
|
source: ValidateSource, |
||||||
|
options: ValidateOption, |
||||||
|
) => void | Promise<void>; |
||||||
|
validator?: ( |
||||||
|
rule: Rules, |
||||||
|
value: any, |
||||||
|
callback: (error: string | string[] | void) => void, |
||||||
|
source: ValidateSource, |
||||||
|
options: ValidateOption, |
||||||
|
) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Rules { |
||||||
|
[field: string]: RuleItem | RuleItem[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ValidateSource { |
||||||
|
[field: string]: any; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ValidateOption { |
||||||
|
// whether to suppress internal warning
|
||||||
|
suppressWarning?: boolean; |
||||||
|
|
||||||
|
// when the first validation rule generates an error stop processed
|
||||||
|
first?: boolean; |
||||||
|
|
||||||
|
// when the first validation rule of the specified field generates an error stop the field processed, 'true' means all fields.
|
||||||
|
firstFields?: boolean | string[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ValidateError { |
||||||
|
message: string; |
||||||
|
field: string; |
||||||
|
} |
||||||
|
|
||||||
|
export type ErrorList = ValidateError[]; |
||||||
|
export interface FieldErrorList { |
||||||
|
[field: string]: ValidateError[]; |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,71 @@ |
|||||||
|
{ |
||||||
|
"_from": "async-validator", |
||||||
|
"_id": "async-validator@3.5.1", |
||||||
|
"_inBundle": false, |
||||||
|
"_integrity": "sha1-zWK5aIskZfSEIOJ620d2CrG1VZ8=", |
||||||
|
"_location": "/async-validator", |
||||||
|
"_phantomChildren": {}, |
||||||
|
"_requested": { |
||||||
|
"type": "tag", |
||||||
|
"registry": true, |
||||||
|
"raw": "async-validator", |
||||||
|
"name": "async-validator", |
||||||
|
"escapedName": "async-validator", |
||||||
|
"rawSpec": "", |
||||||
|
"saveSpec": null, |
||||||
|
"fetchSpec": "latest" |
||||||
|
}, |
||||||
|
"_requiredBy": [ |
||||||
|
"#USER", |
||||||
|
"/" |
||||||
|
], |
||||||
|
"_resolved": "https://registry.npm.taobao.org/async-validator/download/async-validator-3.5.1.tgz?cache=0&sync_timestamp=1605749896979&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fasync-validator%2Fdownload%2Fasync-validator-3.5.1.tgz", |
||||||
|
"_shasum": "cd62b9688b2465f48420e27adb47760ab1b5559f", |
||||||
|
"_spec": "async-validator", |
||||||
|
"_where": "D:\\大庆\\ui文件\\大庆小程序", |
||||||
|
"bugs": { |
||||||
|
"url": "https://github.com/yiminghe/async-validator/issues" |
||||||
|
}, |
||||||
|
"bundleDependencies": false, |
||||||
|
"dependencies": {}, |
||||||
|
"deprecated": false, |
||||||
|
"description": "validate form asynchronous", |
||||||
|
"devDependencies": { |
||||||
|
"@babel/preset-env": "^7.8.7", |
||||||
|
"@pika/pack": "^0.5.0", |
||||||
|
"@pika/plugin-build-types": "^0.6.0", |
||||||
|
"@pika/plugin-standard-pkg": "^0.6.0", |
||||||
|
"@pika/types": "^0.6.0", |
||||||
|
"babel-jest": "^24.8.0", |
||||||
|
"coveralls": "^2.13.1", |
||||||
|
"jest": "^24.8.0", |
||||||
|
"lint-staged": "^7.2.0", |
||||||
|
"np": "^5.0.3", |
||||||
|
"pika-plugin-build-web-babel": "^0.8.0", |
||||||
|
"pika-plugin-clean-dist-src": "^0.1.1", |
||||||
|
"pre-commit": "^1.2.2", |
||||||
|
"prettier": "^1.11.1" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"dist-*/", |
||||||
|
"bin/" |
||||||
|
], |
||||||
|
"homepage": "https://github.com/yiminghe/async-validator", |
||||||
|
"keywords": [ |
||||||
|
"validator", |
||||||
|
"validate", |
||||||
|
"async" |
||||||
|
], |
||||||
|
"license": "MIT", |
||||||
|
"main": "dist-node/index.js", |
||||||
|
"module": "dist-web/index.js", |
||||||
|
"name": "async-validator", |
||||||
|
"pika": true, |
||||||
|
"repository": { |
||||||
|
"type": "git", |
||||||
|
"url": "git+ssh://git@github.com/yiminghe/async-validator.git" |
||||||
|
}, |
||||||
|
"sideEffects": false, |
||||||
|
"types": "dist-types/index.d.ts", |
||||||
|
"version": "3.5.1" |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
{ |
||||||
|
"requires": true, |
||||||
|
"lockfileVersion": 1, |
||||||
|
"dependencies": { |
||||||
|
"async-validator": { |
||||||
|
"version": "3.5.1", |
||||||
|
"resolved": "https://registry.npm.taobao.org/async-validator/download/async-validator-3.5.1.tgz?cache=0&sync_timestamp=1605749896979&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fasync-validator%2Fdownload%2Fasync-validator-3.5.1.tgz", |
||||||
|
"integrity": "sha1-zWK5aIskZfSEIOJ620d2CrG1VZ8=" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,248 @@ |
|||||||
|
{ |
||||||
|
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages |
||||||
|
{ |
||||||
|
"path": "pages/index/index", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "待办事项" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/workbench/workbench", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "工作台" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/user/user", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "我的" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/customer/customer", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "客户资源管理系统" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/guarantee/guarantee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保业务管理系统" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/addcustomer/addcustomer", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "添加客户信息" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/aboutUs/aboutUs", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "关于我们" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/personalInfo/personalInfo", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "个人信息" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/bindPhone/bindPhone", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "绑定手机号" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/application/application", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "业务申请" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/addApplication/addApplication", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "新增业务申请" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/investigation/investigation", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/assignAB/assignAB", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "指派AB角" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/investigationSee/investigationSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/asset/asset", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "资产部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/assignCommissioner/assignCommissioner", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "指派资产部专员" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/assetSee/assetSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "资产部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/Information/Information", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "信息部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/assignInformation/assignInformation", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "指派信息部专员" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/InformationSee/InformationSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "信息部调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/regulation/regulation", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "合规调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/regulationSee/regulationSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "合规调查" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/workMeeting/workMeeting", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "工作会" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/workMeetingSee/workMeetingSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "工作会" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/reviewJudges/reviewJudges", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "贷审会评委" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/review/review", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "贷审会" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/reviewSee/reviewSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "贷审会" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/letter/letter", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保函" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/letterSee/letterSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保承诺函" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/payment/payment", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "回款确认" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/notice/notice", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "放款通知" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/noticeSee/noticeSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "放款通知" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/paymentSee/paymentSee", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "担保承诺函" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/statistics/statistics", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "进度查询" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"path": "pages/refuse/refuse", |
||||||
|
"style": { |
||||||
|
"navigationBarTitleText": "拒绝查询" |
||||||
|
} |
||||||
|
} |
||||||
|
], |
||||||
|
"globalStyle": { |
||||||
|
"navigationBarTextStyle": "black", |
||||||
|
"navigationBarTitleText": "uni-app", |
||||||
|
"navigationBarBackgroundColor": "#F8F8F8", |
||||||
|
"backgroundColor": "#F8F8F8", |
||||||
|
"backgroundColorTop":"#FFFFFF", // iOS APP真机bounce回弹区域默认灰色,建议统一重置为白色 |
||||||
|
"usingComponents": { |
||||||
|
"ly-tree-node": "/components/ly-tree/ly-tree-node" |
||||||
|
} |
||||||
|
}, |
||||||
|
"tabBar": { |
||||||
|
"color": "#707070", |
||||||
|
"selectedColor": "#00B9FF", |
||||||
|
"borderStyle": "black", |
||||||
|
"backgroundColor": "#FFF", |
||||||
|
"list": [{ |
||||||
|
"pagePath": "pages/index/index", |
||||||
|
"iconPath": "static/img/icon_4_blue.png", |
||||||
|
"selectedIconPath": "static/img/icon_4.png", |
||||||
|
"text": "待办事项" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"pagePath": "pages/workbench/workbench", |
||||||
|
"iconPath": "static/img/icon_5.png", |
||||||
|
"selectedIconPath": "static/img/icon_5_blue.png", |
||||||
|
"text": "工作台" |
||||||
|
}, { |
||||||
|
"pagePath": "pages/user/user", |
||||||
|
"iconPath": "static/img/icon_2.png", |
||||||
|
"selectedIconPath": "static/img/icon_2_blue.png", |
||||||
|
"text": "我的" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,282 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<!-- <index-list :list="chargeList"></index-list> --> |
||||||
|
<!-- <touch-list :list="chargeList" :listTouchDirection="listTouchDirection" :listTouchStart="listTouchStart" :modalName="modalName" |
||||||
|
@TouchStart="ListTouchStart" @TouchMove="ListTouchMove" @TouchEnd="ListTouchEnd" |
||||||
|
></touch-list> --> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in chargeList" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/pages/InformationSee/InformationSee')">查看</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/pages/assignInformation/assignInformation')">指派</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
import {apiSearch} from "@/api/mock.js" |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'待处理', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已处理', num:1, y:0, curPageLen:0, hasNext:true} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
chargeList: [{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 4 |
||||||
|
}], |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.chargeList.map(e =>{ |
||||||
|
e.status = this.core.auditStatus(e.status) |
||||||
|
}) |
||||||
|
console.log(this.chargeList) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
//联网加载数据 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.mescroll.hideUpScroll(); // 切换菜单,不显示mescroll进度, 显示系统进度条 |
||||||
|
uni.showLoading(); |
||||||
|
} |
||||||
|
let keyword = this.tabs[this.tabIndex].name; |
||||||
|
apiSearch(page.num, page.size, keyword).then(curPageData=>{ |
||||||
|
//联网成功的回调 |
||||||
|
|
||||||
|
// 当前tab数据 |
||||||
|
let curTab = this.tabs[this.tabIndex] |
||||||
|
|
||||||
|
//设置列表数据 |
||||||
|
if(page.num == 1) curTab.goods = []; //如果是第一页需手动制空列表 |
||||||
|
curTab.goods=curTab.goods.concat(curPageData); //追加新数据 |
||||||
|
|
||||||
|
// 数据渲染完毕再隐藏加载状态 this.$nextTick在iOS真机不触发,需改成setTimeout |
||||||
|
// this.$nextTick(()=>{ |
||||||
|
setTimeout(()=>{ |
||||||
|
// 需先隐藏加载状态 |
||||||
|
this.mescroll.endSuccess(curPageData.length); |
||||||
|
// 再记录当前页的数据 |
||||||
|
curTab.num = page.num; // 页码 |
||||||
|
curTab.curPageLen = curPageData.length; // 当前页长 |
||||||
|
curTab.hasNext = this.mescroll.optUp.hasNext; // 是否还有下一页 |
||||||
|
|
||||||
|
// 设置nav到顶部的距离 (需根据自身的情况获取navTop的值, 这里放到列表数据渲染完毕之后) |
||||||
|
// 也可以放到onReady里面,或者菜单顶部的数据(轮播等)加载完毕之后.. |
||||||
|
if(!this.navTop) this.setNavTop() |
||||||
|
// 保持tab悬浮,列表数据显示第一条 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.isChangeTab = false; |
||||||
|
uni.hideLoading(); |
||||||
|
if(this.isShowSticky) this.mescroll.scrollTo(this.navTop, 0) |
||||||
|
} |
||||||
|
},20) |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
console.log('tabInList基本信息 = ' + JSON.stringify(data)); |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
console.log('滚动条位置 = ' + this.mescroll.getScrollTop() + ', navTop = ' + this.navTop); |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
console.log('滚动条位置 = ' + e.scrollTop + ', navTop = ' + this.navTop); |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,882 @@ |
|||||||
|
<template> |
||||||
|
<view class="evan-form-show"> |
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">基本信息</text></view> |
||||||
|
</view> |
||||||
|
<uni-forms :value="formData" ref="form" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item required name="cusType" label="客户类型"> |
||||||
|
<uni-data-checkbox v-model="cusType" :localdata="cusTypeList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="code" required label="客户编号"> |
||||||
|
<uni-easyinput type="text" :inputBorder="true" v-model="enterpriseForm.code" placeholder="请输入客户编号"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="客户名称"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="客户名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="socialUnifiedCode" required label="社会统一代码"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.socialUnifiedCode" placeholder="社会统一代码"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="联系电话"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="联系电话"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="注册时间"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="注册时间"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="注册资金(万元)"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="注册资金"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="注册地址"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="注册地址"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="员工人数"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="员工人数"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="经营地址"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="经营地址"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="业务类别"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="业务类别"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="经营范围"> |
||||||
|
<uni-easyinput disabled type="textarea" v-model="enterpriseForm.remarks" placeholder="经营范围"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
|
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">法人信息</text></view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<uni-forms :value="enterpriseFrom" ref="form" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item required name="cusType" label="姓名"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="姓名"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item required name="gender" label="性别"> |
||||||
|
<uni-data-checkbox disabled v-model="enterpriseFrom.gender" :localdata="genderList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="身份证号"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="身份证号"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="socialUnifiedCode" required label="联系电话"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.socialUnifiedCode" placeholder="联系电话"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="户口所在地"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="户口所在地"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="家庭住址"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="家庭住址"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item required name="isExist" label="是否存在关联人"> |
||||||
|
<uni-data-checkbox v-model="enterpriseForm.isExist" :localdata="isExistList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
|
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">关联人信息</text></view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">申请贷款相关信息</text></view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<uni-forms :value="enterpriseFrom" ref="form" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item required name="cusType" label="申请额度(万元)"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="申请额度"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="申请期限"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="申请期限"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="socialUnifiedCode" required label="贷款银行"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.socialUnifiedCode" placeholder="贷款银行"></uni-easyinput> |
||||||
|
<text>—</text> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.socialUnifiedCode" placeholder="分行名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="贷款用途"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="贷款用途"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item required name="enGuaranteeMeasures" label="反担保措施"> |
||||||
|
<uni-data-checkbox mode="list" v-model="enterpriseFrom.enGuaranteeMeasures" :localdata="easuresList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="反担保措施描述"> |
||||||
|
<uni-easyinput type="textarea" v-model="enterpriseForm.remarks" placeholder="请输入反担保措施描述"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
|
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">附件</text></view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传营业执照</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传法定代表人夫妻及企业实际经营者身份证附件</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传法定代表人夫妻户口本、结婚证(离婚证)</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传法定代表人身份证证明</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传公司章程</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传由会计师事务所审计的上一年度及本年度审计报告</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传企业信用报告</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传企业法人夫妇信用报告、实际经营者信用报告</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传征信业务授权书、承诺书</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传增值税纳税报表首表/完税证明</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传股东会会议纪要</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">上传反担保资料和评估报告</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-bar bg-white"> |
||||||
|
<view class="action"> |
||||||
|
<text class="is-required">*</text> |
||||||
|
<text class="label-color">其他</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-form-group"> |
||||||
|
<view class="grid col-4 grid-square flex-sub"> |
||||||
|
<view class="bg-img" v-for="(item,index) in imgList" :key="index" @tap="ViewImage" :data-url="imgList[index]"> |
||||||
|
<image :src="imgList[index]" mode="aspectFill"></image> |
||||||
|
<view class="cu-tag bg-red" @tap.stop="DelImg" :data-index="index"> |
||||||
|
<text class='cuIcon-close'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="solids" @tap="ChooseImage" v-if="imgList.length<4"> |
||||||
|
<text class='cuIcon-cameraadd'></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="foot-btn btn-rig pad-bt"> |
||||||
|
<button plain class="mini-btn round plain-btn" type="primary" size="mini" @click="goto('/pages/application/application')">返回</button> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/pages/application/application')">保存</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/pages/application/application')">提交</button> |
||||||
|
</view> |
||||||
|
|
||||||
|
<timeline></timeline> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import utils from '@/components/evan-form/utils.js' |
||||||
|
const CONTACT_INFO={ |
||||||
|
name:'', |
||||||
|
phone:'', |
||||||
|
duty:'' |
||||||
|
} |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
cusTypeList: [{ |
||||||
|
text: '个人', |
||||||
|
value: '0' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '企业', |
||||||
|
value: '1' |
||||||
|
}],//客户类型列表 |
||||||
|
cusType: '1', |
||||||
|
EvaluationList: [{ |
||||||
|
text: '房产', |
||||||
|
value: '0', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '车辆', |
||||||
|
value: '1', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '设备', |
||||||
|
value: '2', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '其他', |
||||||
|
value: '3', |
||||||
|
InputValue: '' |
||||||
|
}],//评估价值列表 |
||||||
|
employeeMsg: [{ |
||||||
|
name: '个人', |
||||||
|
value: '3' |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '企业', |
||||||
|
value: '4' |
||||||
|
}],//客户经理列表 |
||||||
|
genderList: [{ |
||||||
|
text: '男', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '女', |
||||||
|
value: 2 |
||||||
|
}],//性别列表 |
||||||
|
easuresList: [{ |
||||||
|
text: '第三方保证', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '抵押', |
||||||
|
value: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '个人无限责任连带', |
||||||
|
value: 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '质押', |
||||||
|
value: 4 |
||||||
|
}], |
||||||
|
imgList: [], |
||||||
|
maritalList: ['未婚','已婚','离异','再婚'],//婚姻状况列表 |
||||||
|
educationList: ['本科','大专','高职','中专','其他'],//学历列表 |
||||||
|
isExistList: [{ |
||||||
|
text: '否', |
||||||
|
value: 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '是', |
||||||
|
value: 1 |
||||||
|
}],//是否存在关联人列表 |
||||||
|
manager: '', |
||||||
|
enterpriseFrom: { |
||||||
|
maritalStatus: 0,//婚姻状况 |
||||||
|
gender: 1,//性别 |
||||||
|
education: 0, //学历 |
||||||
|
legalGender: 1, //关联人性别 |
||||||
|
isExistRelated: 0, //是否存在关联人 |
||||||
|
enGuaranteeMeasures: '' |
||||||
|
},//个人form |
||||||
|
enterpriseForm: {},//企业form |
||||||
|
hideRequiredAsterisk: false, |
||||||
|
// 表单的内容必须初始化 |
||||||
|
info: { |
||||||
|
name: '', |
||||||
|
email: '', |
||||||
|
desc: '', |
||||||
|
phone: '', |
||||||
|
sex: '', |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
cusType: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户' |
||||||
|
}, |
||||||
|
code: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户编号' |
||||||
|
}, |
||||||
|
manager: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户经理' |
||||||
|
}, |
||||||
|
name: { |
||||||
|
required: true, |
||||||
|
message: '请输入姓名' |
||||||
|
}, |
||||||
|
email: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入邮箱' |
||||||
|
}, { |
||||||
|
type: 'email', |
||||||
|
message: '邮箱格式不正确' |
||||||
|
}], |
||||||
|
desc: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入简介' |
||||||
|
}, |
||||||
|
{ |
||||||
|
min: 10, |
||||||
|
max: 30, |
||||||
|
message: '简介必须在10到30个字之间' |
||||||
|
} |
||||||
|
], |
||||||
|
phone: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入手机号' |
||||||
|
}, |
||||||
|
{ |
||||||
|
validator: (rule, value, callback) => { |
||||||
|
// 注意这里如果用的是methods里的isMobilePhone将不生效 |
||||||
|
if (this.$utils.isMobilePhone(value)) { |
||||||
|
callback() |
||||||
|
} else { |
||||||
|
callback(new Error('手机号格式不正确')) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 或者这样也是可以的 |
||||||
|
// { |
||||||
|
// validator: this.isMobile |
||||||
|
// } |
||||||
|
], |
||||||
|
sex: { |
||||||
|
required: true, |
||||||
|
message: '请选择性别' |
||||||
|
} |
||||||
|
}, |
||||||
|
info2:{ |
||||||
|
name:'', |
||||||
|
email:'', |
||||||
|
phone:'' |
||||||
|
}, |
||||||
|
rules2:{ |
||||||
|
name: { |
||||||
|
required: true, |
||||||
|
message: '请输入姓名' |
||||||
|
}, |
||||||
|
email: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入邮箱' |
||||||
|
}, { |
||||||
|
type: 'email', |
||||||
|
message: '邮箱格式不正确' |
||||||
|
}], |
||||||
|
phone:[{ |
||||||
|
required: true, |
||||||
|
message: '请输入手机号' |
||||||
|
}, |
||||||
|
{ |
||||||
|
pattern:'^1\\d{10}$', // 注意这里由于小程序的缘故正则表达式需要通过string的方式传递并且去除两边的斜杠,中间的斜杠变成两个斜杠 |
||||||
|
message:'手机号格式不正确' |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
dynamicInfo:{ |
||||||
|
test1:'', |
||||||
|
test2:'', |
||||||
|
list:[{...CONTACT_INFO}] |
||||||
|
}, |
||||||
|
dynamicRules:{ |
||||||
|
test1:[{required:true,message:'请输入rule规则字段'},{min:4,max:8,message:'必须4-8位'}] |
||||||
|
}, |
||||||
|
showRuleParam:true, |
||||||
|
showRequiredParam:true, |
||||||
|
mobileRules:[{required:true,message:'请输入手机号'},{pattern:'^1\\d{10}$',message:'手机号格式不正确'}] // 注意这里由于小程序的缘故正则表达式需要通过string的方式传递并且去除两边的斜杠,中间的斜杠变成两个斜杠 |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// 这里必须放在mounted中,不然h5,支付宝小程序等会找不到this.$refs.form |
||||||
|
this.$refs.form.setRules(this.rules) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//客户类型选择 |
||||||
|
cusChange(e) { |
||||||
|
this.cusType = e.detail.value |
||||||
|
}, |
||||||
|
//性别选择 |
||||||
|
genderChange(e) { |
||||||
|
this.personageForm.gender = e.detail.value |
||||||
|
}, |
||||||
|
//婚姻状况选择 |
||||||
|
maritalChange(e) { |
||||||
|
this.personageForm.maritalStatus = e.detail.value |
||||||
|
}, |
||||||
|
//学历选择 |
||||||
|
educationChange(e) { |
||||||
|
this.personageForm.education = e.detail.value |
||||||
|
}, |
||||||
|
//关联人性别选择 |
||||||
|
legalGenderChange(e) { |
||||||
|
this.personageForm.legalGender = e.detail.value |
||||||
|
}, |
||||||
|
isExistChange(e) { |
||||||
|
this.personageForm.isExistRelated = e.detail.value |
||||||
|
}, |
||||||
|
bindPickerChange: function(e) { |
||||||
|
// this.cusType = e.target.value |
||||||
|
}, |
||||||
|
save() { |
||||||
|
this.$refs.form.validate((res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
promiseSave(){ |
||||||
|
this.$refs.form.validate().then((res)=>{ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
}).catch(()=>{ |
||||||
|
}) |
||||||
|
}, |
||||||
|
async asyncSave(){ |
||||||
|
try{ |
||||||
|
const result=await this.$refs.form.validate() |
||||||
|
if(result){ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
} catch(e){ |
||||||
|
console.log(e) |
||||||
|
} |
||||||
|
}, |
||||||
|
saveForm2(){ |
||||||
|
this.$refs.form2.validate((res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
utilsSave() { |
||||||
|
utils.validate(this.info, this.rules, (res, errors) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
validateSingle() { |
||||||
|
this.$refs.form.validateField('email', (res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
validateMultiple() { |
||||||
|
this.$refs.form.validateField(['email', 'phone'], (res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
isMobilePhone() { |
||||||
|
const reg = /^1\d{10}$/ |
||||||
|
if (reg.test(value)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
}, |
||||||
|
isMobile(rule, value, callback) { |
||||||
|
if (this.$utils.isMobilePhone(value)) { |
||||||
|
callback() |
||||||
|
} else { |
||||||
|
callback(new Error('手机号格式不正确')) |
||||||
|
} |
||||||
|
}, |
||||||
|
sexChange(e) { |
||||||
|
this.info.sex = e.detail.value |
||||||
|
}, |
||||||
|
addContact(){ |
||||||
|
this.dynamicInfo.list.push({...CONTACT_INFO}) |
||||||
|
}, |
||||||
|
deleteContact(index){ |
||||||
|
this.dynamicInfo.list.splice(index,1) |
||||||
|
}, |
||||||
|
toggleRuleParam(){ |
||||||
|
this.showRuleParam=!this.showRuleParam |
||||||
|
}, |
||||||
|
toggleRequiredParam(){ |
||||||
|
this.showRequiredParam=!this.showRequiredParam |
||||||
|
}, |
||||||
|
dynamicSave(){ |
||||||
|
this.$refs.dynamicForm.validate((res)=>{ |
||||||
|
if(res){ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
ViewImage(e) { |
||||||
|
uni.previewImage({ |
||||||
|
urls: this.imgList, |
||||||
|
current: e.currentTarget.dataset.url |
||||||
|
}); |
||||||
|
}, |
||||||
|
DelImg(e) { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '确定要删除该文件吗?', |
||||||
|
cancelText: '取消', |
||||||
|
confirmText: '确定', |
||||||
|
success: res => { |
||||||
|
if (res.confirm) { |
||||||
|
this.imgList.splice(e.currentTarget.dataset.index, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
ChooseImage() { |
||||||
|
uni.chooseImage({ |
||||||
|
count: 4, //默认9 |
||||||
|
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 |
||||||
|
sourceType: ['album'], //从相册选择 |
||||||
|
success: (res) => { |
||||||
|
if (this.imgList.length != 0) { |
||||||
|
this.imgList = this.imgList.concat(res.tempFilePaths) |
||||||
|
} else { |
||||||
|
this.imgList = res.tempFilePaths |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.evan-form-show { |
||||||
|
padding: 0 30rpx; |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.form-input { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
text-align: right; |
||||||
|
width: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
height: 60rpx; |
||||||
|
&.textarea{ |
||||||
|
height: 240rpx; |
||||||
|
padding: 24rpx 0; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.form-input-placeholder { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #999; |
||||||
|
} |
||||||
|
|
||||||
|
&__button { |
||||||
|
width: 100%; |
||||||
|
height: 88rpx; |
||||||
|
border-radius: 8rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
padding: 0; |
||||||
|
font-size: 36rpx; |
||||||
|
color: #fff; |
||||||
|
margin-top: 20rpx; |
||||||
|
background-color: #2D87D5; |
||||||
|
|
||||||
|
&::before, |
||||||
|
&::after { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.customize-form-item { |
||||||
|
&__label { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-bottom: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
&__radio { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 16rpx; |
||||||
|
|
||||||
|
&__text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.label-color{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.checklist-group { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
margin: 10rpx 0; |
||||||
|
margin-right: 50rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-left: 10rpx; |
||||||
|
transition: color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color 0.1s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner-icon { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 16rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
width: 6rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .2s; |
||||||
|
transform-origin: center; |
||||||
|
transform: rotate(40deg) scaleY(0.4); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,29 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<view class="bg-img bg-mask flex align-center" style="background-image: url('../../static/img/aboutUs.png');height: 300rpx;"> |
||||||
|
|
||||||
|
</view> |
||||||
|
<view class="cu-list menu sm-border card-menu margin-top box-sha"> |
||||||
|
<view class="about-card"> |
||||||
|
深圳市立德融资担保有限公司广州分公司,主要是以商品房产为媒介进行操作银行贷款担保为主营业务的专业机构企业, |
||||||
|
于2010通过重组兼并改制而成立注册。2017年经省政府金融管理部门批准、深圳市经济贸易和信息化委员会注册,变更为深圳市立德融资担保有限公司, |
||||||
|
注册资本为一亿人民币,经营业务范围从传统的商品房贷款担保扩大到票据承兑担保、诉讼保全担保、履约担保等。 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
page{ |
||||||
|
background-color: #F5F5F5; |
||||||
|
} |
||||||
|
.box-sha{ |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
} |
||||||
|
.about-card{ |
||||||
|
padding: 30rpx 40rpx; |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,329 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="add-customer mat15 flex-between"> |
||||||
|
<text>共{{total}}条</text> |
||||||
|
<view> |
||||||
|
<button class="mini-btn" type="primary" size="mini" @click="goto('/pages/addApplication/addApplication')"><text class="cuIcon-add mar-icon"></text>添加</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in businessApplyListData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="index"> |
||||||
|
<navigator hover-class='none' :url="'/pages/addcustomer/addcustomer?id=' + item.id + '&companyId=' + item.companyId + '&cusType=' + cusType" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatus}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operatingStatus}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>提单人:</text> |
||||||
|
<text class="mgl30">{{item.account}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所属部门:</text> |
||||||
|
<text class="mgl30">{{item.deptName}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="repaelPop(item)">撤销</button> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="businessApplyBtn(item)">审核</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
import {apiSearch} from "@/api/mock.js" |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'全部', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审批中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已通过', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
businessApplyListData: [], |
||||||
|
page: 1, |
||||||
|
pageSize: 10, |
||||||
|
total: 0, |
||||||
|
CustomerNumberOrName: '', |
||||||
|
scrolltop: false, |
||||||
|
|
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.CustomerNumberOrName.map(e =>{ |
||||||
|
// e.businessStatus = this.core.auditStatus(e.businessStatus) |
||||||
|
// }) |
||||||
|
this.getBusinessApply() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 业务-列表 |
||||||
|
getBusinessApply(){ |
||||||
|
let params = { |
||||||
|
page: this.page, |
||||||
|
size: this.pageSize, |
||||||
|
CustomerNumberOrName: this.CustomerNumberOrName |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-apply-amount-info/businessApplicationList',{params}).then(res => { |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.status = this.core.auditStatus(e.status) |
||||||
|
}) |
||||||
|
this.businessApplyListData = res.data.list |
||||||
|
this.total = res.data.totalCount |
||||||
|
}).catch(function (error) {}); |
||||||
|
}, |
||||||
|
// 撤销业务 |
||||||
|
repaelPop(item){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '是否撤销该申请', |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
this.$http.get('/api-guarantee/dg-apply-amount-info/revokeBusinessApplication',{id: item.id}).then(res => { |
||||||
|
uni.showToast({title: '撤销成功'}) |
||||||
|
this.getBusinessApply() |
||||||
|
}).catch(function (error) {}); |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
//联网加载数据 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.mescroll.hideUpScroll(); // 切换菜单,不显示mescroll进度, 显示系统进度条 |
||||||
|
uni.showLoading(); |
||||||
|
} |
||||||
|
let keyword = this.tabs[this.tabIndex].name; |
||||||
|
apiSearch(page.num, page.size, keyword).then(curPageData=>{ |
||||||
|
//联网成功的回调 |
||||||
|
|
||||||
|
// 当前tab数据 |
||||||
|
let curTab = this.tabs[this.tabIndex] |
||||||
|
|
||||||
|
//设置列表数据 |
||||||
|
if(page.num == 1) curTab.goods = []; //如果是第一页需手动制空列表 |
||||||
|
curTab.goods=curTab.goods.concat(curPageData); //追加新数据 |
||||||
|
|
||||||
|
// 数据渲染完毕再隐藏加载状态 this.$nextTick在iOS真机不触发,需改成setTimeout |
||||||
|
// this.$nextTick(()=>{ |
||||||
|
setTimeout(()=>{ |
||||||
|
// 需先隐藏加载状态 |
||||||
|
this.mescroll.endSuccess(curPageData.length); |
||||||
|
// 再记录当前页的数据 |
||||||
|
curTab.num = page.num; // 页码 |
||||||
|
curTab.curPageLen = curPageData.length; // 当前页长 |
||||||
|
curTab.hasNext = this.mescroll.optUp.hasNext; // 是否还有下一页 |
||||||
|
|
||||||
|
// 设置nav到顶部的距离 (需根据自身的情况获取navTop的值, 这里放到列表数据渲染完毕之后) |
||||||
|
// 也可以放到onReady里面,或者菜单顶部的数据(轮播等)加载完毕之后.. |
||||||
|
if(!this.navTop) this.setNavTop() |
||||||
|
// 保持tab悬浮,列表数据显示第一条 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.isChangeTab = false; |
||||||
|
uni.hideLoading(); |
||||||
|
if(this.isShowSticky) this.mescroll.scrollTo(this.navTop, 0) |
||||||
|
} |
||||||
|
},20) |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
console.log('tabInList基本信息 = ' + JSON.stringify(data)); |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
console.log('滚动条位置 = ' + this.mescroll.getScrollTop() + ', navTop = ' + this.navTop); |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
console.log('滚动条位置 = ' + e.scrollTop + ', navTop = ' + this.navTop); |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,282 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<!-- <index-list :list="chargeList"></index-list> --> |
||||||
|
<!-- <touch-list :list="chargeList" :listTouchDirection="listTouchDirection" :listTouchStart="listTouchStart" :modalName="modalName" |
||||||
|
@TouchStart="ListTouchStart" @TouchMove="ListTouchMove" @TouchEnd="ListTouchEnd" |
||||||
|
></touch-list> --> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in chargeList" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/pages/assetSee/assetSee')">查看</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/pages/assignCommissioner/assignCommissioner')">指派</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
import {apiSearch} from "@/api/mock.js" |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'待处理', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已处理', num:1, y:0, curPageLen:0, hasNext:true} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
chargeList: [{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '小王', |
||||||
|
IDNumber: '202004020002', |
||||||
|
department: '担保一部', |
||||||
|
cusName: '江南服装厂', |
||||||
|
time: '2020-07-13', |
||||||
|
status: 4 |
||||||
|
}], |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.chargeList.map(e =>{ |
||||||
|
e.status = this.core.auditStatus(e.status) |
||||||
|
}) |
||||||
|
console.log(this.chargeList) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
//联网加载数据 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.mescroll.hideUpScroll(); // 切换菜单,不显示mescroll进度, 显示系统进度条 |
||||||
|
uni.showLoading(); |
||||||
|
} |
||||||
|
let keyword = this.tabs[this.tabIndex].name; |
||||||
|
apiSearch(page.num, page.size, keyword).then(curPageData=>{ |
||||||
|
//联网成功的回调 |
||||||
|
|
||||||
|
// 当前tab数据 |
||||||
|
let curTab = this.tabs[this.tabIndex] |
||||||
|
|
||||||
|
//设置列表数据 |
||||||
|
if(page.num == 1) curTab.goods = []; //如果是第一页需手动制空列表 |
||||||
|
curTab.goods=curTab.goods.concat(curPageData); //追加新数据 |
||||||
|
|
||||||
|
// 数据渲染完毕再隐藏加载状态 this.$nextTick在iOS真机不触发,需改成setTimeout |
||||||
|
// this.$nextTick(()=>{ |
||||||
|
setTimeout(()=>{ |
||||||
|
// 需先隐藏加载状态 |
||||||
|
this.mescroll.endSuccess(curPageData.length); |
||||||
|
// 再记录当前页的数据 |
||||||
|
curTab.num = page.num; // 页码 |
||||||
|
curTab.curPageLen = curPageData.length; // 当前页长 |
||||||
|
curTab.hasNext = this.mescroll.optUp.hasNext; // 是否还有下一页 |
||||||
|
|
||||||
|
// 设置nav到顶部的距离 (需根据自身的情况获取navTop的值, 这里放到列表数据渲染完毕之后) |
||||||
|
// 也可以放到onReady里面,或者菜单顶部的数据(轮播等)加载完毕之后.. |
||||||
|
if(!this.navTop) this.setNavTop() |
||||||
|
// 保持tab悬浮,列表数据显示第一条 |
||||||
|
if(this.isChangeTab){ |
||||||
|
this.isChangeTab = false; |
||||||
|
uni.hideLoading(); |
||||||
|
if(this.isShowSticky) this.mescroll.scrollTo(this.navTop, 0) |
||||||
|
} |
||||||
|
},20) |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
console.log('tabInList基本信息 = ' + JSON.stringify(data)); |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
console.log('滚动条位置 = ' + this.mescroll.getScrollTop() + ', navTop = ' + this.navTop); |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
console.log('滚动条位置 = ' + e.scrollTop + ', navTop = ' + this.navTop); |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,578 @@ |
|||||||
|
<template> |
||||||
|
<view class="evan-form-show"> |
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">基本信息</text></view> |
||||||
|
</view> |
||||||
|
<uni-forms :value="formData" ref="form" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item required name="cusType" label="客户类型"> |
||||||
|
<uni-data-checkbox v-model="cusType" :localdata="cusTypeList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="code" required label="客户编号"> |
||||||
|
<uni-easyinput type="text" :inputBorder="true" v-model="enterpriseForm.code" placeholder="请输入客户编号"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="客户名称"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="客户名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="time" label="业务类别"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.name" placeholder="客户名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="联系电话"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="联系电话"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="申请额度"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="申请额度"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="申请期限"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="申请期限"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="资产评估报告名称"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="资产评估报告名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="资产评估报告编号"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="资产评估报告编号"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="评估价值(万元)"> |
||||||
|
<uni-data-checkbox :inputVisble="true" :multiple="true" mode="list" v-model="enterpriseFrom.gender" :localdata="EvaluationList"></uni-data-checkbox> |
||||||
|
<view class="flex-end"> |
||||||
|
<text>合计</text> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="自动合计"></uni-easyinput> |
||||||
|
</view> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="time" label="评估日期"> |
||||||
|
<uni-datetime-picker v-model="formData.time" :timestamp="true" @change="datetimeChange"></uni-datetime-picker> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="审计编号"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="审计编号"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="审计报告(名称)"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="审计报告(名称)"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="time" label="审计日期"> |
||||||
|
<uni-datetime-picker v-model="formData.time" :timestamp="true" @change="datetimeChange"></uni-datetime-picker> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="审计价值(万元)"> |
||||||
|
<uni-easyinput disabled type="text" :inputBorder="true" v-model="enterpriseForm.phone" placeholder="审计价值"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item required name="enGuaranteeMeasures" label="反担保措施"> |
||||||
|
<uni-data-checkbox mode="list" v-model="enterpriseFrom.enGuaranteeMeasures" :localdata="easuresList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="name" required label="反担保措施描述"> |
||||||
|
<uni-easyinput type="textarea" v-model="enterpriseForm.remarks" placeholder="请输入反担保措施描述"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
|
||||||
|
<view class="foot-btn btn-rig pad-bt"> |
||||||
|
<button plain class="mini-btn round plain-btn" type="primary" size="mini" @click="goto('/pages/application/application')">返回</button> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/pages/application/application')">保存</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/pages/application/application')">提交</button> |
||||||
|
</view> |
||||||
|
|
||||||
|
<timeline></timeline> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import utils from '@/components/evan-form/utils.js' |
||||||
|
const CONTACT_INFO={ |
||||||
|
name:'', |
||||||
|
phone:'', |
||||||
|
duty:'' |
||||||
|
} |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
cusTypeList: [{ |
||||||
|
text: '个人', |
||||||
|
value: '0' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '企业', |
||||||
|
value: '1' |
||||||
|
}],//客户类型列表 |
||||||
|
cusType: '1', |
||||||
|
EvaluationList: [{ |
||||||
|
text: '房产', |
||||||
|
value: '0', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '车辆', |
||||||
|
value: '1', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '设备', |
||||||
|
value: '2', |
||||||
|
InputValue: '' |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '其他', |
||||||
|
value: '3', |
||||||
|
InputValue: '' |
||||||
|
}],//评估价值列表 |
||||||
|
employeeMsg: [{ |
||||||
|
name: '个人', |
||||||
|
value: '3' |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '企业', |
||||||
|
value: '4' |
||||||
|
}],//客户经理列表 |
||||||
|
genderList: [{ |
||||||
|
text: '男', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '女', |
||||||
|
value: 2 |
||||||
|
}],//性别列表 |
||||||
|
easuresList: [{ |
||||||
|
text: '第三方保证', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '抵押', |
||||||
|
value: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '个人无限责任连带', |
||||||
|
value: 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '质押', |
||||||
|
value: 4 |
||||||
|
}], |
||||||
|
imgList: [], |
||||||
|
maritalList: ['未婚','已婚','离异','再婚'],//婚姻状况列表 |
||||||
|
educationList: ['本科','大专','高职','中专','其他'],//学历列表 |
||||||
|
isExistList: [{ |
||||||
|
text: '否', |
||||||
|
value: 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '是', |
||||||
|
value: 1 |
||||||
|
}],//是否存在关联人列表 |
||||||
|
manager: '', |
||||||
|
enterpriseFrom: { |
||||||
|
maritalStatus: 0,//婚姻状况 |
||||||
|
gender: 1,//性别 |
||||||
|
education: 0, //学历 |
||||||
|
legalGender: 1, //关联人性别 |
||||||
|
isExistRelated: 0, //是否存在关联人 |
||||||
|
enGuaranteeMeasures: '' |
||||||
|
},//个人form |
||||||
|
enterpriseForm: {},//企业form |
||||||
|
hideRequiredAsterisk: false, |
||||||
|
// 表单的内容必须初始化 |
||||||
|
info: { |
||||||
|
name: '', |
||||||
|
email: '', |
||||||
|
desc: '', |
||||||
|
phone: '', |
||||||
|
sex: '', |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
cusType: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户' |
||||||
|
}, |
||||||
|
code: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户编号' |
||||||
|
}, |
||||||
|
manager: { |
||||||
|
required: true, |
||||||
|
message: '请选择客户经理' |
||||||
|
}, |
||||||
|
name: { |
||||||
|
required: true, |
||||||
|
message: '请输入姓名' |
||||||
|
}, |
||||||
|
email: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入邮箱' |
||||||
|
}, { |
||||||
|
type: 'email', |
||||||
|
message: '邮箱格式不正确' |
||||||
|
}], |
||||||
|
desc: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入简介' |
||||||
|
}, |
||||||
|
{ |
||||||
|
min: 10, |
||||||
|
max: 30, |
||||||
|
message: '简介必须在10到30个字之间' |
||||||
|
} |
||||||
|
], |
||||||
|
phone: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入手机号' |
||||||
|
}, |
||||||
|
{ |
||||||
|
validator: (rule, value, callback) => { |
||||||
|
// 注意这里如果用的是methods里的isMobilePhone将不生效 |
||||||
|
if (this.$utils.isMobilePhone(value)) { |
||||||
|
callback() |
||||||
|
} else { |
||||||
|
callback(new Error('手机号格式不正确')) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 或者这样也是可以的 |
||||||
|
// { |
||||||
|
// validator: this.isMobile |
||||||
|
// } |
||||||
|
], |
||||||
|
sex: { |
||||||
|
required: true, |
||||||
|
message: '请选择性别' |
||||||
|
} |
||||||
|
}, |
||||||
|
info2:{ |
||||||
|
name:'', |
||||||
|
email:'', |
||||||
|
phone:'' |
||||||
|
}, |
||||||
|
rules2:{ |
||||||
|
name: { |
||||||
|
required: true, |
||||||
|
message: '请输入姓名' |
||||||
|
}, |
||||||
|
email: [{ |
||||||
|
required: true, |
||||||
|
message: '请输入邮箱' |
||||||
|
}, { |
||||||
|
type: 'email', |
||||||
|
message: '邮箱格式不正确' |
||||||
|
}], |
||||||
|
phone:[{ |
||||||
|
required: true, |
||||||
|
message: '请输入手机号' |
||||||
|
}, |
||||||
|
{ |
||||||
|
pattern:'^1\\d{10}$', // 注意这里由于小程序的缘故正则表达式需要通过string的方式传递并且去除两边的斜杠,中间的斜杠变成两个斜杠 |
||||||
|
message:'手机号格式不正确' |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
dynamicInfo:{ |
||||||
|
test1:'', |
||||||
|
test2:'', |
||||||
|
list:[{...CONTACT_INFO}] |
||||||
|
}, |
||||||
|
dynamicRules:{ |
||||||
|
test1:[{required:true,message:'请输入rule规则字段'},{min:4,max:8,message:'必须4-8位'}] |
||||||
|
}, |
||||||
|
showRuleParam:true, |
||||||
|
showRequiredParam:true, |
||||||
|
mobileRules:[{required:true,message:'请输入手机号'},{pattern:'^1\\d{10}$',message:'手机号格式不正确'}] // 注意这里由于小程序的缘故正则表达式需要通过string的方式传递并且去除两边的斜杠,中间的斜杠变成两个斜杠 |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// 这里必须放在mounted中,不然h5,支付宝小程序等会找不到this.$refs.form |
||||||
|
this.$refs.form.setRules(this.rules) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//客户类型选择 |
||||||
|
cusChange(e) { |
||||||
|
this.cusType = e.detail.value |
||||||
|
}, |
||||||
|
//性别选择 |
||||||
|
genderChange(e) { |
||||||
|
this.personageForm.gender = e.detail.value |
||||||
|
}, |
||||||
|
//婚姻状况选择 |
||||||
|
maritalChange(e) { |
||||||
|
this.personageForm.maritalStatus = e.detail.value |
||||||
|
}, |
||||||
|
//学历选择 |
||||||
|
educationChange(e) { |
||||||
|
this.personageForm.education = e.detail.value |
||||||
|
}, |
||||||
|
//关联人性别选择 |
||||||
|
legalGenderChange(e) { |
||||||
|
this.personageForm.legalGender = e.detail.value |
||||||
|
}, |
||||||
|
isExistChange(e) { |
||||||
|
this.personageForm.isExistRelated = e.detail.value |
||||||
|
}, |
||||||
|
bindPickerChange: function(e) { |
||||||
|
// this.cusType = e.target.value |
||||||
|
}, |
||||||
|
save() { |
||||||
|
this.$refs.form.validate((res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
promiseSave(){ |
||||||
|
this.$refs.form.validate().then((res)=>{ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
}).catch(()=>{ |
||||||
|
}) |
||||||
|
}, |
||||||
|
async asyncSave(){ |
||||||
|
try{ |
||||||
|
const result=await this.$refs.form.validate() |
||||||
|
if(result){ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
} catch(e){ |
||||||
|
console.log(e) |
||||||
|
} |
||||||
|
}, |
||||||
|
saveForm2(){ |
||||||
|
this.$refs.form2.validate((res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
utilsSave() { |
||||||
|
utils.validate(this.info, this.rules, (res, errors) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
validateSingle() { |
||||||
|
this.$refs.form.validateField('email', (res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
validateMultiple() { |
||||||
|
this.$refs.form.validateField(['email', 'phone'], (res) => { |
||||||
|
if (res) { |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
isMobilePhone() { |
||||||
|
const reg = /^1\d{10}$/ |
||||||
|
if (reg.test(value)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
}, |
||||||
|
isMobile(rule, value, callback) { |
||||||
|
if (this.$utils.isMobilePhone(value)) { |
||||||
|
callback() |
||||||
|
} else { |
||||||
|
callback(new Error('手机号格式不正确')) |
||||||
|
} |
||||||
|
}, |
||||||
|
sexChange(e) { |
||||||
|
this.info.sex = e.detail.value |
||||||
|
}, |
||||||
|
addContact(){ |
||||||
|
this.dynamicInfo.list.push({...CONTACT_INFO}) |
||||||
|
}, |
||||||
|
deleteContact(index){ |
||||||
|
this.dynamicInfo.list.splice(index,1) |
||||||
|
}, |
||||||
|
toggleRuleParam(){ |
||||||
|
this.showRuleParam=!this.showRuleParam |
||||||
|
}, |
||||||
|
toggleRequiredParam(){ |
||||||
|
this.showRequiredParam=!this.showRequiredParam |
||||||
|
}, |
||||||
|
dynamicSave(){ |
||||||
|
this.$refs.dynamicForm.validate((res)=>{ |
||||||
|
if(res){ |
||||||
|
uni.showToast({ |
||||||
|
title: '验证通过' |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
ViewImage(e) { |
||||||
|
uni.previewImage({ |
||||||
|
urls: this.imgList, |
||||||
|
current: e.currentTarget.dataset.url |
||||||
|
}); |
||||||
|
}, |
||||||
|
DelImg(e) { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '确定要删除该文件吗?', |
||||||
|
cancelText: '取消', |
||||||
|
confirmText: '确定', |
||||||
|
success: res => { |
||||||
|
if (res.confirm) { |
||||||
|
this.imgList.splice(e.currentTarget.dataset.index, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
ChooseImage() { |
||||||
|
uni.chooseImage({ |
||||||
|
count: 4, //默认9 |
||||||
|
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 |
||||||
|
sourceType: ['album'], //从相册选择 |
||||||
|
success: (res) => { |
||||||
|
if (this.imgList.length != 0) { |
||||||
|
this.imgList = this.imgList.concat(res.tempFilePaths) |
||||||
|
} else { |
||||||
|
this.imgList = res.tempFilePaths |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.evan-form-show { |
||||||
|
padding: 0 30rpx; |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.form-input { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
text-align: right; |
||||||
|
width: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
height: 60rpx; |
||||||
|
&.textarea{ |
||||||
|
height: 240rpx; |
||||||
|
padding: 24rpx 0; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.form-input-placeholder { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #999; |
||||||
|
} |
||||||
|
|
||||||
|
&__button { |
||||||
|
width: 100%; |
||||||
|
height: 88rpx; |
||||||
|
border-radius: 8rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
padding: 0; |
||||||
|
font-size: 36rpx; |
||||||
|
color: #fff; |
||||||
|
margin-top: 20rpx; |
||||||
|
background-color: #2D87D5; |
||||||
|
|
||||||
|
&::before, |
||||||
|
&::after { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.customize-form-item { |
||||||
|
&__label { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-bottom: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
&__radio { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 16rpx; |
||||||
|
|
||||||
|
&__text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.label-color{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.checklist-group { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
margin: 10rpx 0; |
||||||
|
margin-right: 50rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-left: 10rpx; |
||||||
|
transition: color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color 0.1s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner-icon { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 16rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
width: 6rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .2s; |
||||||
|
transform-origin: center; |
||||||
|
transform: rotate(40deg) scaleY(0.4); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,241 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>A角选择</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="filterText" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<ly-tree :tree-data="data" |
||||||
|
:props="defaultProps" |
||||||
|
ref="tree" |
||||||
|
node-key="id" |
||||||
|
show-radio |
||||||
|
checkOnlyLeaf |
||||||
|
@check="handleCheck" |
||||||
|
@radio-change="handleRadioChange" |
||||||
|
@node-click="handleNodeClick" |
||||||
|
:filter-node-method="filterNode" |
||||||
|
child-visible-for-filter-node |
||||||
|
/> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>B角选择</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="filterText" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<ly-tree :tree-data="data" |
||||||
|
:props="defaultProps" |
||||||
|
ref="tree" |
||||||
|
node-key="id" |
||||||
|
show-radio |
||||||
|
checkOnlyLeaf |
||||||
|
@check="handleCheck" |
||||||
|
@radio-change="handleRadioChange" |
||||||
|
@node-click="handleNodeClick" |
||||||
|
:filter-node-method="filterNode" |
||||||
|
child-visible-for-filter-node |
||||||
|
/> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选员工</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="'A角: '+A" placeholder="请选择A角"/> |
||||||
|
<text class="cuIcon-roundclosefill" @click="A = ''"></text> |
||||||
|
</view> |
||||||
|
<view class="flex mat15"> |
||||||
|
<input disabled type="text" :value="'B角: '+B" placeholder="请选择B角"/> |
||||||
|
<text class="cuIcon-roundclosefill" @click="B = ''"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<navigator url="/pages/user/user" open-type="redirect"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="bindPhoneClick">确定</button> |
||||||
|
</navigator> |
||||||
|
<navigator url="/pages/investigation/investigation" open-type="redirect"> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="bindPhoneClick">取消</button> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterNodeData: null, |
||||||
|
filterText: '', |
||||||
|
A: '小明', |
||||||
|
B: '小红', |
||||||
|
data: [{ |
||||||
|
id: 1, |
||||||
|
label: '一级 1', |
||||||
|
children: [{ |
||||||
|
id: 4, |
||||||
|
label: '二级 1-1', |
||||||
|
children: [{ |
||||||
|
id: 9, |
||||||
|
label: '三级 1-1-1' |
||||||
|
}, { |
||||||
|
id: 10, |
||||||
|
label: '三级 1-1-2' |
||||||
|
}] |
||||||
|
}] |
||||||
|
}, { |
||||||
|
id: 2, |
||||||
|
label: '一级 2', |
||||||
|
children: [{ |
||||||
|
id: 5, |
||||||
|
label: '二级 2-1' |
||||||
|
}, { |
||||||
|
id: 6, |
||||||
|
label: '二级 2-2' |
||||||
|
}] |
||||||
|
}, { |
||||||
|
id: 3, |
||||||
|
label: '一级 3', |
||||||
|
children: [{ |
||||||
|
id: 7, |
||||||
|
label: '二级 3-1' |
||||||
|
}, { |
||||||
|
id: 8, |
||||||
|
label: '二级 3-2' |
||||||
|
}] |
||||||
|
}], |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'label' |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
this.$nextTick(() => { |
||||||
|
this.$refs.tree.setCheckedKeys([6]) |
||||||
|
}) |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
filterText(val) { |
||||||
|
this.$refs.tree.filter(val); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
// obj: { |
||||||
|
// checkedKeys: [9, 5], // 当前选中节点的id数组 |
||||||
|
// checkedNodes: [{...}, {...}], // 当前选中节点数组 |
||||||
|
// data: {...}, // 当前节点的数据 |
||||||
|
// halfCheckedKeys: [1, 4, 2], // 半选中节点的id数组 |
||||||
|
// halfCheckedNodes: [{...}, {...}, {...}], // 半选中节点的数组 |
||||||
|
// node: Node {...} // 当前节点实例 |
||||||
|
// } |
||||||
|
|
||||||
|
console.log('handleCheck', obj); |
||||||
|
}, |
||||||
|
|
||||||
|
// 只要节点的选中状态改变就触发(包括设置默认选中,点击选中/取消选中),会被触发多次 |
||||||
|
handleRadioChange(obj) { |
||||||
|
console.log('handleRadioChange', obj); |
||||||
|
}, |
||||||
|
|
||||||
|
handleNodeClick(obj) { |
||||||
|
console.log('handleNodeClick', JSON.stringify(obj)); |
||||||
|
console.log('getNodePath', this.$refs.tree.getNodePath(obj.data)); |
||||||
|
}, |
||||||
|
// 查询过滤 |
||||||
|
filterNode(value, data) { |
||||||
|
if (!value) return true; |
||||||
|
return data.label.indexOf(value) !== -1; |
||||||
|
}, |
||||||
|
|
||||||
|
// handleCheck(obj) { |
||||||
|
// if (obj.node.checked) { |
||||||
|
// this.placeholder = `过滤"${obj.data.label}"的子节点`; |
||||||
|
// this.filterNodeData = obj.data; |
||||||
|
// } else { |
||||||
|
// this.placeholder = `过滤所有节点`; |
||||||
|
// this.filterNodeData = null; |
||||||
|
// } |
||||||
|
|
||||||
|
// // filter(val, nodeData),假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
// this.$refs.tree.filter(this.filterText, this.filterNodeData); |
||||||
|
// } |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 600rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,210 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>调查专员选择</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="filterText" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<ly-tree :tree-data="data" |
||||||
|
:props="defaultProps" |
||||||
|
ref="tree" |
||||||
|
node-key="id" |
||||||
|
show-radio |
||||||
|
checkOnlyLeaf |
||||||
|
@check="handleCheck" |
||||||
|
@radio-change="handleRadioChange" |
||||||
|
@node-click="handleNodeClick" |
||||||
|
:filter-node-method="filterNode" |
||||||
|
child-visible-for-filter-node |
||||||
|
/> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选调查专员</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="A" placeholder="请选择调查专员"/> |
||||||
|
<text class="cuIcon-roundclosefill" @click="A = ''"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<navigator url="/pages/user/user" open-type="redirect"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="bindPhoneClick">确定</button> |
||||||
|
</navigator> |
||||||
|
<navigator url="/pages/investigation/investigation" open-type="redirect"> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="bindPhoneClick">取消</button> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterNodeData: null, |
||||||
|
filterText: '', |
||||||
|
A: '小明', |
||||||
|
B: '小红', |
||||||
|
data: [{ |
||||||
|
id: 1, |
||||||
|
label: '一级 1', |
||||||
|
children: [{ |
||||||
|
id: 4, |
||||||
|
label: '二级 1-1', |
||||||
|
children: [{ |
||||||
|
id: 9, |
||||||
|
label: '三级 1-1-1' |
||||||
|
}, { |
||||||
|
id: 10, |
||||||
|
label: '三级 1-1-2' |
||||||
|
}] |
||||||
|
}] |
||||||
|
}, { |
||||||
|
id: 2, |
||||||
|
label: '一级 2', |
||||||
|
children: [{ |
||||||
|
id: 5, |
||||||
|
label: '二级 2-1' |
||||||
|
}, { |
||||||
|
id: 6, |
||||||
|
label: '二级 2-2' |
||||||
|
}] |
||||||
|
}, { |
||||||
|
id: 3, |
||||||
|
label: '一级 3', |
||||||
|
children: [{ |
||||||
|
id: 7, |
||||||
|
label: '二级 3-1' |
||||||
|
}, { |
||||||
|
id: 8, |
||||||
|
label: '二级 3-2' |
||||||
|
}] |
||||||
|
}], |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'label' |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
this.$nextTick(() => { |
||||||
|
this.$refs.tree.setCheckedKeys([6]) |
||||||
|
}) |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
filterText(val) { |
||||||
|
this.$refs.tree.filter(val); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
// obj: { |
||||||
|
// checkedKeys: [9, 5], // 当前选中节点的id数组 |
||||||
|
// checkedNodes: [{...}, {...}], // 当前选中节点数组 |
||||||
|
// data: {...}, // 当前节点的数据 |
||||||
|
// halfCheckedKeys: [1, 4, 2], // 半选中节点的id数组 |
||||||
|
// halfCheckedNodes: [{...}, {...}, {...}], // 半选中节点的数组 |
||||||
|
// node: Node {...} // 当前节点实例 |
||||||
|
// } |
||||||
|
|
||||||
|
console.log('handleCheck', obj); |
||||||
|
}, |
||||||
|
|
||||||
|
// 只要节点的选中状态改变就触发(包括设置默认选中,点击选中/取消选中),会被触发多次 |
||||||
|
handleRadioChange(obj) { |
||||||
|
console.log('handleRadioChange', obj); |
||||||
|
}, |
||||||
|
|
||||||
|
handleNodeClick(obj) { |
||||||
|
console.log('handleNodeClick', JSON.stringify(obj)); |
||||||
|
console.log('getNodePath', this.$refs.tree.getNodePath(obj.data)); |
||||||
|
}, |
||||||
|
// 查询过滤 |
||||||
|
filterNode(value, data) { |
||||||
|
if (!value) return true; |
||||||
|
return data.label.indexOf(value) !== -1; |
||||||
|
}, |
||||||
|
|
||||||
|
// handleCheck(obj) { |
||||||
|
// if (obj.node.checked) { |
||||||
|
// this.placeholder = `过滤"${obj.data.label}"的子节点`; |
||||||
|
// this.filterNodeData = obj.data; |
||||||
|
// } else { |
||||||
|
// this.placeholder = `过滤所有节点`; |
||||||
|
// this.filterNodeData = null; |
||||||
|
// } |
||||||
|
|
||||||
|
// // filter(val, nodeData),假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
// this.$refs.tree.filter(this.filterText, this.filterNodeData); |
||||||
|
// } |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 800rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue