代码更新

This commit is contained in:
jia 2023-11-18 10:51:32 +08:00
parent 81aba0c9b7
commit 6352e2f3d8
496 changed files with 51895 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

427
static/css/base.css Normal file
View File

@ -0,0 +1,427 @@
@charset "UTF-8";
* {
scrollbar-color: #e5e5e5 #f7f7f9;
scrollbar-width: thin;
}
html {
margin: 0 auto;
}
body {
overflow-x: hidden;
}
.font-color,
.font-color-red {
color: #fc4141 !important
}
.bg-color {
background-color: #e93323
}
.icon-color {
color: #ff3c2b
}
.cart-color {
color: #ff3700 !important;
border: 1px solid #ff3700 !important
}
.padding20 {
padding: 20rpx
}
.pad20 {
padding: 0 20rpx
}
.padding30 {
padding: 30rpx
}
.pad30 {
padding: 0 30rpx
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.clearfix:after {
content: '';
display: block;
height: 0;
clear: both
}
.clearfix {
zoom: 1
}
.acea-row {
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-lines: multiple;
-moz-box-lines: multiple;
-o-box-lines: multiple;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap
}
.acea-row.row-middle {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center
}
.acea-row.row-top {
-webkit-box-align: start;
-moz-box-align: start;
-o-box-align: start;
-ms-flex-align: start;
-webkit-align-items: flex-start;
align-items: flex-start
}
.acea-row.row-bottom {
-webkit-box-align: end;
-moz-box-align: end;
-o-box-align: end;
-ms-flex-align: end;
-webkit-align-items: flex-end;
align-items: flex-end
}
.acea-row.row-center {
-webkit-box-pack: center;
-moz-box-pack: center;
-o-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center
}
.acea-row.row-right {
-webkit-box-pack: end;
-moz-box-pack: end;
-o-box-pack: end;
-ms-flex-pack: end;
-webkit-justify-content: flex-end;
justify-content: flex-end
}
.acea-row.row-left {
-webkit-box-pack: start;
-moz-box-pack: start;
-o-box-pack: start;
-ms-flex-pack: start;
-webkit-justify-content: flex-start;
justify-content: flex-start
}
.acea-row.row-between {
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between
}
.acea-row.row-around {
justify-content: space-around;
-webkit-justify-content: space-around
}
.acea-row.row-column-around {
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
justify-content: space-around;
-webkit-justify-content: space-around
}
.acea-row.row-column {
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-o-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column
}
.acea-row.row-column-between {
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-o-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between
}
.acea-row.row-center-wrapper {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: center;
-moz-box-pack: center;
-o-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center
}
.acea-row.row-between-wrapper {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between
}
.start {
width: 122rpx;
height: 30rpx;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHoAAADMCAYAAAC8yreMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4BpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpDMDg0NEE2QTVFNUQxMUU4QUI3RkNGOTgwNDYyRUZDOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozODU5QzVENDMwRjcxMUU5OTQ0QzlEOTQ5RkE1MTlBRiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozODU5QzVEMzMwRjcxMUU5OTQ0QzlEOTQ5RkE1MTlBRiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpkN2ZhZTM5NC0wNmE4LTkzNGYtODA0OS0zNjBjNTcxOTU2YjAiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDpmYWI1M2NhMC04MWE1LTE5NGItYmJlYi1jMzI2MjIwNmNhOTYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4ctYL1AAAHsElEQVR42uycS4gcVRiFq5Mm4yuKihoVlKg7IVlkI4oGFV9R4wPSJChBMW5ECYooKuIbEReCBBGJEDS+BkVJYEIMJgvJIossFATxCW58boRJEDEZz83cDk3TXc/7T9Xt+g783O7p6tP/9Km6dbvp+jpzc3MJmnwt4i0gaETQiKBRM+UWY8OVpt9XXXySakcZ34Xwp/fR3mWO6LWqW/Wilxnte5b+re29TNB3+fEeozfL0r+1vXdGTRmdTmfc9HGGG1Rd1d+qZecc/PGfcVPI2Bc19Kf3uSBHdM+/mNNpqtsD77WW/q3uvTtmDzpPw9kDxm5XW6q6b2jTB7XtrEZXc35vc/rNV7LQ/vQ+2rs75v/Zproux45yhWrniL/vUq1JeZ6lP70XmLpvVr1SYoo5qnrBrQ4ztrP0p/eiizFND9dreHdgOknTr25lqEXCvrwLGgt/ei+xGNOTP9ewUrUn48XclLGi/2J5ZelP7wVX3TJxJ/f3Mjbbpu3+KrOctPSn9+Ifr9ZWfLxOf3rPE7TOF1Mabhg68e/yy/q+1mi7bpn/xNKf3osd0deqTva3f1ZdpenCLeFXq37wfz9ddWXJvdbSn94LBH2bH9/2J/79/hzypV8wvD60XVFZ+tN7xhcmgzrRnQv0AjtHLBgOa9is6eOzCm+WpT+9p32ORpMnfmFC0IigEUEjgkYEjQgaETQiaETQBI0IGhE0ImhE0IigEUGj8oIaEEfvTtPT0ytVF5bxh3gQl/9TqkcWauqGeFCDv47kM5P5y2PP1+0lpkH7K+9v9HfX6/4JIf8ZS/+Ye/dap/pI5U4Nt1gf0RAP6vO/WrVX9YnqjqJPhnjQoN5Tpu0LNPzS6/XcZTmHdH9WdZbu/1kp6ARqQC29Kzw39d+rOqL6V7XY33Y/5n9pYNOtqpdd4BqX+G3dNqck81dgzuQN2l15766gf7zgDHHUN/RcxnaW/jH3fkC1QbVbR+v7Y88Rvd5BDZsGdhA3qzykWqXaP+o5EA8a2LuCe0DDparHFOqhHKvxLaovtO3Wcf4QDxrYuwJ7Q8Nbqu0KckVKyO5Kyu2qF/shl151Qw2ox1/BfaXhbtVrKZs9r7pT234T6uMV1IB6/N3F8N+mPP6dX7BV/xwNNaA+f786n+kvuFT3qzYMPL5n4Euaykc01ID6/N3HuL0K91yNH/odaKnuu3P3Mt1264SbQgUNNaAGfwU55fNxX3e+qXrCLbhUbpH2tF9pO4Dcqdp2cYig+1feb1LNDi1GDqs26+Y1Fc5Dlv4x977azwSX+AXXTwMLNTd7rPOzhfti5vIsM4gHDdIgxllH6XJ3tPrVd9qRf5GGWW33R/9vI78bIeh2iJ8SETQiaETQiKARQSOCRgSNCBoRNEEjgkYEjQgaETQiaETQiKARQSMnYDXheneqApSx9AZWE16lgTKW3sBqAqoqUMbSG1hNWFUCylh6A6sJq0pAGUtvYDUBYDJ+aq0MlLH0BlZTsHdLoAywmmL+1r2bAWUSYDX5/ReqdwugjKl32gv6N8xdcP1OxnTopruNg6yOPG+Whf9C9u6+wNDwrOoZvclfjwnCXfr6pOrRQdZI1vse2htYTYXeQwNlLL2B1VTvPRhQxtIbWE01mEySBATKWHoDq6nWe5IEBMpYegOrqdB7aKCMpTewmmq9BwXKWHrDMCkoU6AMsBpUVfyUiKARQSOCRgSNCBoRNCJoRNCIoAkaETQiaETQiKARQSOCRgSNMtQmholTFQ4IDJOwsva3ZIxY+8MwySNLxoi1PwyTYrJkjFj7wzApIEvGiLU/DJOcU58ZY8TaH4bJ6DfFjANi7Q/DpJi/JWPE2h+GSVKQYWLBAbH2h2FSkmESmgNi7Q/DpKS/JWPE2h+GSXFZMkas/YN5Z7I7xnBAdvsvNvpzzTEOiI66/0p8SWLqnwxxQPwixl1P/IF/vM8B+bhkGJb+wbwnnWGSJLaMEWt/GCY5v2gwY4xY+8MwKSZLxoi1PwyT1H/IkANi7Q/DBFUWPyUiaETQiKARQSOCRgSNCBoRNCJogkYEjQgaETQiaETQiKARQaMMNY1h4tRUDggMk/BqJAck5t4bxzBpMgck5t4bxTDxaiwHJObem8YwcWosByTm3mthmKRMT43mgMTcey0Mk1g5IDH3XhfDJEoOSMy918owiY0DEnPvtTNMYuKAxNx77QyTmDggMffeFIZJFByQmHuvnWHSf34SAQck5t6bwDBJkkg4IDH3XjvDJCYOSMy9N4FhEg0HJObeR56jh5bnGxM/Z6TIfXbeVxKS8b2bDcaxOvR3Z7tFe+2M/6q1Kd5R9Q7DpCXip0QEjQgaETQiaETQiKARQSOCRgRN0IigEUEjgkYEjQgamQhqQDt6L3tEt5IaEHPvhYNuMzUg5t7LHNGtpQbE3HuZoFtLDYi590UFp4/jV977yzmPXXkfcHoy8297790xxlADWkI8gBowYb13oAZAPDj+IT2BGhB975mLMagBk9F73lU31IDIe+/mfEGoAZH3nveIhhoQee95FmNT/nPbDn/OeLh/UbYeW67hVdWnyTwXdL0eO1JwQRPUn95LLsYSqAET0XueczTUgAnoHeJBS8RPiQgaETQiaNRM/S/AAOykxVBJG5QXAAAAAElFTkSuQmCC');
background-repeat: no-repeat;
background-size: 122rpx auto;
}
.start.star5 {
background-position: 0 3rpx;
}
.start.star4 {
background-position: 0 -30rpx;
}
.start.star3 {
background-position: 0 -70rpx;
}
.start.star2 {
background-position: 0 -105rpx;
}
.start.star1 {
background-position: 0 -140rpx;
}
.start.star0 {
background-position: 0 -175rpx;
}
* {
box-sizing: border-box
}
page {
font-size: 28rpx;
background-color: #f5f5f5;
color: #333
}
body,
html {
height: unset
}
button {
padding: 0;
margin: 0;
line-height: normal;
background-color: #fff
}
button::after {
border: 0
}
radio .wx-radio-input {
border-radius: 50%;
width: 38rpx;
height: 38rpx
}
radio .wx-radio-input.wx-radio-input-checked {
border: 1px solid #e93323;
background-color: #e93323;
}
radio .uni-radio-input {
border-radius: 50%;
width: 38rpx;
height: 38rpx
}
radio .uni-radio-input.uni-radio-input-checked {
border: 1px solid #e93323;
background-color: #e93323;
}
.store-list uni-radio .uni-radio-input.uni-radio-input-checked,
.store-list uni-radio .uni-radio-input {
/* border-color: transparent;
background-color: transparent; */
}
.store-list uni-radio .uni-radio-input.uni-radio-input-checked:before {
/* color: #e93323!important; */
}
checkbox .wx-checkbox-input {
width: 38rpx;
height: 38rpx
}
checkbox .wx-checkbox-input.wx-checkbox-input-checked::before {
color: #fff !important;
}
checkbox .uni-checkbox-input {
/* border-radius: 50%; */
width: 38rpx;
height: 38rpx
}
checkbox .uni-checkbox-input.uni-checkbox-input-checked,
checkbox .wx-checkbox-input.wx-checkbox-input-checked {
border: 1px solid #20A162;
background-color: #20A162;
color: #fff !important;
}
checkbox .uni-checkbox-input.uni-checkbox-input-checked::before {
font-size: 35rpx
}
.line1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.line2 {
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: pre-wrap;
}
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
opacity: .5;
z-index: 30
}
@keyframes load {
from {
transform: rotate(0)
}
to {
transform: rotate(360deg)
}
}
@-webkit-keyframes load {
from {
transform: rotate(0)
}
to {
transform: rotate(360deg)
}
}
.loadingpic {
animation: load 3s linear 1s infinite;
--webkit-animation: load 3s linear 1s infinite
}
.loading-list {
animation: load linear 1s infinite;
-webkit-animation: load linear 1s infinite;
font-size: 40rpx;
margin-right: 22rpx
}
.loading {
width: 100%;
height: 100rpx;
line-height: 100rpx;
align-items: center;
justify-content: center;
position: relative;
text-align: center
}
.loading .line {
position: absolute;
width: 450rpx;
left: 150rpx;
top: 50rpx;
height: 1px;
border-top: 1px solid #eee
}
.loading .text {
position: relative;
display: inline-block;
padding: 0 20rpx;
background: #fff;
z-index: 2;
color: #777
}
.loadingicon .loading {
animation: load linear 1s infinite;
font-size: 45rpx;
color: #000
}
.loadingicon {
width: 100%;
height: 80rpx;
overflow: hidden
}

1
static/css/global.scss Normal file
View File

@ -0,0 +1 @@
$base-color: #0122c7;

BIN
static/empty/data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/empty/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

2530
static/iconfont/iconfont.css Normal file

File diff suppressed because it is too large Load Diff

BIN
static/images/DH.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
static/images/QS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/images/SJ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/images/che.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/images/er.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/images/xiangyou.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/images/yan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/images/zanwu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
static/images/zzw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/mp3/im.mp3 Normal file

Binary file not shown.

BIN
static/mp3/im.wav Normal file

Binary file not shown.

BIN
static/mp3/order.mp3 Normal file

Binary file not shown.

13
static/server/archives.js Normal file
View File

@ -0,0 +1,13 @@
export const comonentList = [{
id: 7,
name: 'plant'
},
{
id: 8,
name: 'store'
},
{
id: 32,
name: 'breeding'
}
]

View File

@ -0,0 +1,4 @@
export const companyContractType = [23, 24, 25, 29] // 公司合同
export const personnerContractType = [19, 20, 21, 22] // 个人合同
export const shareholderContractType = [40] // 股金合同

469
static/server/server.js Normal file
View File

@ -0,0 +1,469 @@
export const avatar = 'https://cdn.uviewui.com/uview/album/1.jpg'
export const defaultAvatar =
'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132'
export const prefix = 'https://lihai001.oss-cn-chengdu.aliyuncs.com/public/kk/'
export const urls = [
'https://cdn.uviewui.com/uview/album/1.jpg',
'https://cdn.uviewui.com/uview/album/2.jpg',
'https://cdn.uviewui.com/uview/album/3.jpg',
'https://cdn.uviewui.com/uview/album/4.jpg',
'https://cdn.uviewui.com/uview/album/7.jpg',
'https://cdn.uviewui.com/uview/album/6.jpg',
'https://cdn.uviewui.com/uview/album/5.jpg'
]
export const wenluData = [
'gxsy/fangsahn@2x.png',
'gxsy/laojiao@2x.png',
'gxsy/zbgy@2x.png'
]
export const marketData = [{
title: '江阳区',
text: '醉美泸州 • 中国酒城',
src: 'gxsy/jiangyang@2x.png',
bg: 'gxsy/jiangyang@2x(1).png',
code: '510502'
},
{
title: '龙马潭区',
text: '中国酒城 • 文明泸州',
src: 'gxsy/longmatan@2x.png',
bg: 'gxsy/longmatan@2x(1).png',
code: '510504'
},
{
title: '纳溪区',
text: '山水神韵 • 醇香酒城',
src: 'gxsy/naxiqu@2x.png',
bg: 'gxsy/naxi@2x.png',
code: '510503'
},
{
title: '泸县',
text: '千年古县 • 宋韵龙城',
src: 'gxsy/luxian@2x.png',
bg: 'gxsy/luxian@2x(1).png',
code: '510521'
},
{
title: '叙永县',
text: '康养竹乡 • 画稿叙永',
src: 'gxsy/xuyongxian@2x.png',
bg: 'gxsy/xuyong@2x.png',
code: '510524'
},
{
title: '古蔺县',
text: '梦里郎酒 • 画里古蔺',
src: 'gxsy/gulinxian@2x.png',
bg: 'gxsy/jiangyang@2x(1).png',
code: '510525'
}, {
title: '合江县',
text: '千年荔城 • 甜美合江',
src: 'gxsy/hejaingxian@2x.png',
bg: 'gxsy/hejaing@2x.png',
code: '510522'
}
]
export const shichangData = [{
url: 'img4@2x.png',
title: 'rexiao@2x.png',
text: '农业生产产品'
},
{
url: 'img5@2x.png',
title: 'dangji@2x.png',
text: '村名生活用品'
}
]
export const openList = [{
title: '党建在线',
text: '党建资讯文章',
src: 'djzx@2x.png',
color: '#FF614D',
pid: 295,
navCallBack: (pid, t, id) => {
uni.navigateTo({
url: `/pages/service_hall/party_building?pid=${pid}&title=${t}&village_id=${id}`
})
}
},
{
title: '村务动态',
text: '村务信息公开',
src: 'cwdt@2x.png',
color: '#4DB896',
pid: 300,
navCallBack: (pid, t, id) => {
uni.navigateTo({
url: `/pages/service_hall/party_building?pid=${pid}&title=${t}&village_id=${id}`
})
}
},
{
title: '村镇新闻',
text: '村镇新闻资讯',
src: 'czxw@2x.png',
color: '#FFAA33',
pid: 304,
navCallBack: (pid, t, id) => {
uni.navigateTo({
url: `/pages/service_hall/list?id=${pid}&title=${t}&village_id=${id}`
})
}
},
{
title: '文明实践',
text: '文明创建实践',
src: 'wmsj@2x.png',
color: '#FF7A3D',
pid: '',
navCallBack: (pid, t, id) => {
uni.navigateTo({
url: `/pages/service_hall/opens`
})
}
}
]
export const quickLink = [{
icon: 'scfw',
src: 'scfw.png',
name: '商超服务',
url: '/pages/fast_track/production',
category_id: 25
},
{
icon: 'nfcp',
src: 'nfcp.png',
name: '农副产品',
url: '/pages/fast_track/production',
category_id: 26
}, {
icon: 'sczl',
src: 'sczl.png',
name: '生产资料',
url: '/pages/fast_track/production',
category_id: 22
}, {
icon: 'shfw',
src: 'shfw.png',
name: '生活服务',
// url: '/pages/fast_track/service_life',
url: '/pages/fast_track/production',
category_id: 23
}, {
icon: 'hbxs',
src: 'hbxs.png',
name: '红白喜事',
// url: '/pages/fast_track/red_white_thing',
url: '/pages/fast_track/production',
category_id: 21
}, {
icon: 'wyly',
src: 'wyly.png',
name: '文娱旅游',
// url: '/pages/fast_track/travel'
}, {
icon: 'fwzx',
src: 'fwzx.png',
name: '房屋装修',
// url: '/pages/fast_track/fitment'
}, {
icon: 'jypx',
src: 'jypx.png',
name: '教育资讯',
// url: '/pages/fast_track/education'
}, {
icon: 'msgy',
src: 'msgy.png',
name: '民生资讯',
// url: '/pages/fast_track/public_benefit'
}, {
icon: 'ylbj',
src: 'ylbj.png',
name: '医疗资讯'
}
]
// oaHOme快速入口数据
export const oaHomeData = [{
name: '公司信息',
icon: '../../static/img/home/GSXX.png',
paths: '/subpkg/companyInfo/companyInfo',
admin: true
},
{
name: '人员管理',
icon: '../../static/img/home/RYGL.png',
paths: '/subpkg/personnel/personnel',
admin: true
},
{
name: '固定资产',
icon: '../../static/img/home/GDZC.png',
paths: '/subpkg/property/index',
admin: true
},
{
name: '合同管理',
icon: '../../static/img/home/HTGL.png',
paths: '/subpkg/contract/contract'
},
// {
// name: '公司管理',
// icon: '../../static/img/home/GSXX.png',
// paths: '/subpkg/companyAdmin/companyAdmin',
// admin: true
// },
{
name: '任务管理',
icon: '../../static/img/home/RWGL.png',
paths: '/subpkg/taskAdmin/taskAdmin',
},
{
name: '档案管理',
icon: '../../static/img/home/DAGL.png',
paths: '/subpkg/captain/captain',
admin: true
},
{
name: '档案管理',
icon: '../../static/img/home/DAGL.png',
paths: '/subpkg/archives/archives',
captain: true
},
// {
// name: '片区经理',
// icon: '../../static/img/home/GRCW.png',
// paths: '/pages/oaManager/oaManager',
// admin: true
// },
{
name: '个人财务',
icon: '../../static/img/home/GRCW.png',
paths: '/subpkg/finance/finance'
},
{
name: '待取驿站',
icon: '../../static/img/home/YZ.png',
paths: '/pages/logistics/post',
// captain: true
},
// {
// name: '出差申请',
// icon: prefix + 'oa/ccsq@2x.png'
// },
// {
// name: '外出申请',
// icon: prefix + 'oa/wcsq@2x.png'
// },
// {
// name: '采购申请',
// icon: prefix + 'oa/cgsq@2x.png'
// },
// {
// name: '物品维修',
// icon: prefix + 'oa/bxsq@2x.png'
// },
// {
// name: '用章申请',
// icon: prefix + 'oa/yzsq@2x.png'
// },
// {
// name: '报销申请',
// icon: prefix + 'oa/gengduo@2x.png'
// },
{
name: '更多',
icon: prefix + 'oa/wpwx@2x.png',
paths: '/pages/views/application'
}
]
/**
* oa-应用中心数据
*/
export const appDataList = [{
title: '假勤',
data: [{
name: '请假申请',
src: prefix + 'oa/qjsq@2x.png',
url: '/pages/views/leave_request'
},
{
name: '出差申请',
src: prefix + 'oa/ccsq@2x.png',
url: ''
},
{
name: '外出申请',
src: prefix + 'oa/wcsq@2x.png',
url: ''
}
]
},
{
title: '行政',
data: [{
name: '物品维修',
src: prefix + 'oa/bxsq@2x.png',
url: ''
},
{
name: '用章审批',
src: prefix + 'oa/yzsq@2x.png',
url: ''
},
{
name: '领用审批',
src: prefix + 'oa/lysp@2x.png',
url: ''
}
]
},
{
title: '财务',
data: [{
name: '借款申请',
src: prefix + 'oa/jksq@2x.png',
url: ''
},
{
name: '付款申请',
src: prefix + 'oa/fksq@2x.png',
url: ''
},
{
name: '报销申请',
src: prefix + 'oa/gengduo@2x.png',
url: ''
},
{
name: '采购申请',
src: prefix + 'oa/cgsq@2x.png',
url: ''
},
{
name: '奖励申请',
src: prefix + 'oa/jlsq@2x.png',
url: ''
},
{
name: '活动经费',
src: prefix + 'oa/hdjf@2x.png',
url: ''
}
]
},
{
title: '人事',
data: [{
name: '招聘需求',
src: prefix + 'oa/zpxq@2x.png',
url: ''
}]
},
{
title: '其他',
data: [{
name: '通用审批',
src: prefix + 'oa/tysp@2x.png',
url: '/pages/views/com_approve'
}]
}
]
/**
* oa-个人中心
*/
export const myOaData = [
// {
// name: '流水详情',
// icon: '../../static/icons/runningWater.png',
// url: '/subpkg/orderDetail/orderDetail'
// },
// {
// name: '管理后台',
// icon: '../../static/icons/backstage.png',
// // url: '/pages/views/personal_center_two'
// },
{
name: '片区经理',
icon: '../../static/icons/manager.png',
url: '/pages/oaManager/oaManager'
},
{
name: '安全设置',
icon: '../../static/icons/setting.png',
url: '/pages/updatePassword/updatePassword'
},
// {
// name: '管理后台',
// icon: 'custom-icongongzi',
// // url: '/pages/views/personal_center_two'
// },
// {
// name: '片区经理',
// icon: 'custom-icongongzi',
// url: '/pages/oaManager/oaManager'
// },
// {
// name: '工资详情',
// icon: 'custom-icongongzi',
// url: '/pages/views/personal_center_two'
// },
// {
// name: '公示文档',
// icon: 'custom-iconwendang',
// url: '/pages/views/public_document'
// },
// {
// name: '绑定公众号',
// icon: 'custom-iconweixin'
// },
// {
// name: '意见反馈',
// icon: 'custom-iconyijian'
// }
]
/*
oa-请假类型
*/
export const oaLeaveData = [{
name: '事假',
id: 1
},
{
name: '年假',
id: 2
},
{
name: '调休假',
id: 3
},
{
name: '病假',
id: 4
},
{
name: '婚假',
id: 5
},
{
name: '丧假',
id: 6
},
{
name: '产假',
id: 7
},
{
name: '陪产假',
id: 8
},
{
name: '其他',
id: 9
}
]

BIN
static/tabbar_icon/a-a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/tabbar_icon/a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/tabbar_icon/b-b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/tabbar_icon/b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/tabbar_icon/c-c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
static/tabbar_icon/c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/tabbar_icon/d-d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
static/tabbar_icon/d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

13
store/getters.js Normal file
View File

@ -0,0 +1,13 @@
export default {
token: state => state.app.token,
isLogin: state => !!state.app.token,
userInfo: state => state.app.userInfo || {},
eyeType: state => state.config.eyeType || true,
config: state => state.config.config || {}
};
// export default {
// token: state => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJrYWlmYS5jcm1lYi5uZXQiLCJhdWQiOiJrYWlmYS5jcm1lYi5uZXQiLCJpYXQiOjE1NzcwODM1MzQsIm5iZiI6MTU3NzA4MzUzNCwiZXhwIjoxNTc3MDk0MzM0LCJqdGkiOnsiaWQiOjExMCwidHlwZSI6InVzZXIifX0.U-i1pbdRjyXI1gr79Uq2XBPZ89T8f5Ai9jwrR8woTwE',
// isLogin: state => true,
// backgroundColor: state => state.app.backgroundColor,
// userInfo: state => state.app.userInfo || {}
// };

20
store/index.js Normal file
View File

@ -0,0 +1,20 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2021 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import Vue from "vue";
import Vuex from "vuex";
import modules from "./modules";
import getters from "./getters";
Vue.use(Vuex);
export default new Vuex.Store({
modules,
getters,
});

121
store/modules/app.js Normal file
View File

@ -0,0 +1,121 @@
import { commonAuth } from '@/api/pubic.js'
import { loginMobile } from '@/api/user.js'
import { loginAccount } from '@/api/oaUser.js'
import Routine from '@/libs/routine.js'
import Cache from '@/utils/cache';
import encrypt from '@/utils/encrypt.js';
import oaHttp from '@/utils/oahttp.js';
const state = {
userInfo: JSON.parse(Cache.get('USER_INFO') || '{}') || null,
token: Cache.get("TOKEN") || null
};
const mutations = {
setUserInfo(state, data) {
state.userInfo = data
Cache.set("USER_INFO", data)
},
LOGOUT(state) {
Cache.clear('USER_INFO')
Cache.clear('TOKEN')
uni.showModal({
content: '登录已过期,是否重新登录?',
success(e) {
if (e.confirm) uni.reLaunch({
url: '/pages/oaLogin/oaLogin'
})
}
})
},
CLEAR(state) {
state.userInfo = null;
state.token = null;
Cache.clear('USER_INFO')
Cache.clear('TOKEN')
},
UPDATE_USERINFO(state, data) {
let time = res.data.result.expires_time - Cache.time();
state.userInfo = data.result.user
state.token = data.result.token
Cache.set("USER_INFO", data.result.user, time)
Cache.set("TOKEN", data.result.token, time)
},
SET_USERINFO(state, data) {
let time = Cache.time();
state.userInfo = data.user
state.token = data.token
Cache.set("USER_INFO", data.user, time)
Cache.set("TOKEN", data.token, time)
},
SET_TOKEN(state, data) {
let time = Cache.time();
state.token = data.token;
Cache.set("TOKEN", data.token, time);
},
};
const actions = {
RE_LOGIN({ state, commit }, data) {
return new Promise((resolve, reject) => {
let fromData = encrypt.decode('ACT');
if(fromData) {
loginAccount({ ...fromData }, true).then((res) => {
commit('SET_TOKEN', res.data);
oaHttp[data.method](data.url, data.data, data.opt).then((e) => {
resolve(e);
}).catch((err) => {
reject(err)
})
}).catch((err) => {
commit('LOGOUT')
reject(err)
})
}else {
commit('LOGOUT')
reject();
}
})
},
MobileLogin({ state, commit }, force) {
let data = {
auth_token: uni.getStorageSync('auth_token'),
phone: force.account,
sms_code: force.captcha,
spread: that.$Cache.get("spread"),
// #ifdef APP-PLUS
user_type: 'app',
// #endif
// #ifdef H5
user_type: 'h5',
// #endif
}
loginMobile(data).then(res => {
console.log('手机号登录', res);
})
},
async getWxLogin({ state, commit }, force) {
let newCode = null
Routine.getCode().then(code => {
newCode = code;
})
Routine.getUserProfile().then(res => {
let userInfo = res.userInfo;
userInfo.code = newCode;
commonAuth({
auth: {
type: 'routine',
auth: userInfo
}
}).then(res => {
commit("UPDATE_USERINFO", res.data);
})
})
}
};
export default {
state,
mutations,
actions
};

83
store/modules/config.js Normal file
View File

@ -0,0 +1,83 @@
import Cache from '@/utils/cache';
import { getConfig } from "@/api/config.js";
const state = {
eyeType: Cache.get('eyeType') || true, // 小眼睛
request: Cache.get('request') || true, // 网络请求
config: JSON.parse(Cache.get('config')||'{}'),
updateFlag: true
};
const mutations = {
SET_EYE_TYPE(state){
state.eyeType=!state.eyeType;
Cache.set('eyeType', state.eyeType);
},
SET_REQUEST(state, data=true){
state.request = data;
Cache.set('request', state.request);
},
SET_CONFIG(state, data){
state.config = {...data};
Cache.set('config', JSON.stringify(state.config));
},
SET_UPDATEFLAG(state, data){
state.updateFlag = data;
},
};
const actions = {
async initConfig({ state, commit }, data = false) {
let res = await getConfig();
commit('SET_CONFIG', res.data);
// console.log(compareVersions(res.data.version, '1.0.0')==1&&compareVersions(res.data.version, Cache.get('wgt_version'))==1);
if(uni.getStorageSync('uniMP')||!state.updateFlag) return ;//是小程序环境时不进行更新
let os = uni.getSystemInfoSync();
// uni.showModal({
// title: `当前:${os.appVersion},WGT:${Cache.get('wgt_version')},返回:${res.data.version}`
// })
// #ifdef APP-PLUS
if(data) uni.showLoading({
title: '检查更新中'
})
const wgt_v = uni.getStorageSync('wgt_version')||'1.0.0';
commit('SET_UPDATEFLAG', false);
// 版本更新
if(compareVersions(res.data.version, os.appWgtVersion||wgt_v)==1&&compareVersions(res.data.version, wgt_v)==1){
try{
let info = res.data.version_info||{};
let version = {
title: info.title||'发现新版本',
content: info.content||'修复了部分BUG',
versionName: info.version||'1.0.1',
downUrl: info.dow_url||'',
force: info.force==1?true:false, // 是否强制更新
quiet: info.quiet==1?true:false // 是否静默更新
}
Updater.update(version);;
}catch(e){
console.log(e);
}
if(data) uni.hideLoading();
}else if(data){
uni.hideLoading();
uni.showToast({
title: '已经是最新版本了',
icon: 'none'
})
}
// #endif
}
};
export default {
state,
mutations,
actions
};

15
store/modules/index.js Normal file
View File

@ -0,0 +1,15 @@
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2021 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
import app from "./app";
import config from "./config";
export default {
app,
config
};

View File

@ -0,0 +1,2 @@
## 1.0.02022-04-15
1.0.0

View File

@ -0,0 +1,152 @@
<template>
<view class="lottie" :options="options" :change:options="Lottie.optionsChange" :change:fun="Lottie.funChange">
<canvas :class="canvasId" :canvas-id="canvasId" type="2d" ref="lottie" style="width:100%;height:100%;"></canvas>
</view>
</template>
<script>
import uuid from './uuid.js'
// #ifdef MP-WEIXIN
import { loadAnimation, setup } from './lottie-miniprogram.min.js'
// #endif
export default {
props:{
options:{
type: Object,
default:()=>{}
}
},
options: {
styleIsolation: "isolated",
addGlobalClass: true,
virtualHost: true,
},
data(){
return {
canvasId: uuid(10),
fun:{
name: null,
args: null
},
}
},
// #ifdef MP-WEIXIN
watch: {
//
fun:{
deep: true,
handler(){
let {name, args} = this.fun
if(this?.lottie&&name) this.lottie[name](args)
}
},
// options
options:{
deep: true,
handler(){
// destroy ,stop
this.lottie.stop()
//
this.loadData()
}
},
},
// #endif
mounted(){
// ,
this.fun.name = this.options?.autoplay??true?'play':null
// #ifdef MP-WEIXIN
this.$nextTick(function(){
this.init()
})
// #endif
},
methods:{
// lottie
call(name=null,args=null){
if(name&&this.fun.name!=name) this.fun.name = name
if(args&&this.fun.args!=args) this.fun.args = args
},
// #ifdef MP-WEIXIN
init(){
const query = uni.createSelectorQuery().in(this);
query.select(`.${this.canvasId}`).node(res => {
this.canvas = res.node
setup(this.canvas)
this.loadData()
}).exec()
},
loadData(){
this.lottie = loadAnimation({
//
loop: this.options?.autoplay??true,
//
autoplay: this.options?.autoplay??true,
// json
// animationData: this.options?.data,
// json
path: this.options?.path,
rendererSettings: {
context: this.canvas.getContext('2d')
},
})
}
// #endif
}
}
</script>
<script module="Lottie" lang="renderjs">
// #ifdef APP-PLUS || H5
import { loadAnimation } from './lottie-web.min.js'
export default {
data(){
return {
lottie: null,
}
},
mounted() {
this.init()
},
methods:{
//
optionsChange(newValue, oldValue, ownerInstance, instance){
//
this.lottie?.destroy()
this.init()
},
//
funChange(newValue, oldValue, ownerInstance, instance){
let {name, args} = newValue
this.lottie[name](args)
},
// lottie
init(){
this.lottie = loadAnimation({
// dom
container: this.$refs.lottie.$el,
//
renderer: this.options?.renderer??'canvas',
//
loop: this.options?.autoplay??true,
//
autoplay: this.options?.autoplay??true,
// json
animationData: this.options?.data,
// json
path: this.options?.path
});
},
}
}
// #endif
</script>
<style>
.lottie{
width: 100%;
height: 100%;
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
export default (len = 32, radix = null) => {
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
let uuid = [];
radix = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}

View File

@ -0,0 +1,83 @@
{
"id": "hx-lottie",
"displayName": "hx-lottie",
"version": "1.0.0",
"description": "uniapp lottie动画插件, 适配H5, App, 微信小程序, 其他小程序暂未测试",
"keywords": [
"hx-lottie",
"lottie",
"lottie动画",
"微信lottie"
],
"repository": "",
"engines": {
"HBuilderX": "^3.3.0"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://gitee.com/Xiaohuixiong_123/hx-lottie.git"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "n"
},
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,47 @@
<h3 align="center" style="margin:30px 0 30px;font-weight: bold;font-size:32px;">hx-lottie 使用说明</h3>
## 预览
[![L8dXn0.gif](https://s1.ax1x.com/2022/04/15/L8dXn0.gif)](https://imgtu.com/i/L8dXn0)
[![L8wmND.gif](https://s1.ax1x.com/2022/04/15/L8wmND.gif)](https://imgtu.com/i/L8wmND)
## 使用说明
* 引入使用
``` html
<!-- options 动画参数 -->
<hx-lottie :options="options" ref="lottie">
```
* 参数说明
| 属性值 | 参数说明 |
|----------|---------------------------|
| loop | 是否循环播放动画Boolean 默认 true |
| autoplay | 是否自动播放Boolean 默认 true |
| data | 动画数据Object |
| path | 动画网络路径 String |
* 方法说名 `组件内方法统一使用 call(funName, args) 调用
`
1. 使用示例
```js
// 播放动画
this.$refs.lottie.call('play')
// 暂停动画
this.$refs.lottie.call('pause')
```
1. 说明 `方法与lottie-web 方法保持一致` [详情可参考](http://airbnb.io/lottie/#/web?id=usage)
| 方法名 | 说明 |
|----------|---------------------|
| play | 播放动画 |
| pause | 暂停动画 |
| stop | 停止动画 |
| setSpeed | 设置动画速度args = speed |
| … | ... |

View File

@ -0,0 +1,18 @@
## 1.0.82023-06-26
优化
## 1.0.72023-06-08
增加预览二维码
## 1.0.62023-06-08
增加禁用左滑参数
## 1.0.52023-06-08
增加禁用参数
## 1.0.42023-05-31
增加license
## 1.0.32023-05-19
优化代码
## 1.0.22023-05-19
增加示例代码
## 1.0.12023-05-19
增加示例
## 1.0.02023-05-19
初始化发布

View File

@ -0,0 +1,166 @@
<template>
<view class="liu-slide">
<view class="liu-slide-left" :style="'position: relative;left:'+left+'rpx'" @touchstart="touchstart"
@touchmove="touchmove" @touchend="touchend">
<slot></slot>
</view>
<view class="liu-slide-right">
<view class="btn-item" v-for="(item,index) in btnList" :key="index"
:style="'width:'+item.width+';height:'+100+'rpx;line-height:'+100+'rpx; background-color:'+item.bgColor+';color:'+item.color+';font-size:'+item.fontSize"
@click.stop="clickItem(item)">
<!-- <view class="liu-slide-right-img">
<image src="@/static/images/delete.png" mode="aspectFit"></image>
</view> -->
<view class="">
{{item.name}}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
//
btnList: {
type: Array,
default: () => {
return [{
id: '1',
name: '编辑',
width: '100rpx',
height: '100rpx',
bgColor: '#5f92f7',
color: '#FFFFFF',
fontSize: '28rpx'
}, {
id: '2',
name: '删除',
height: '100rpx',
width: '100rpx',
bgColor: '#ed656d',
color: '#FFFFFF',
fontSize: '28rpx'
}]
}
},
//
index: {
type: Number,
require: true,
default: 0
},
//
disable: {
type: Boolean,
default: false
}
},
data() {
return {
x: 0,
left: 0,
operation: 0,
height: 0,
screenWidth: 0
};
},
mounted() {
this.$nextTick(res => {
const systemInfo = uni.getSystemInfoSync()
this.screenWidth = systemInfo.screenWidth
this.getBtnWidth()
this.getListHeight()
})
},
methods: {
clickItem(e) {
this.$emit('clickItem', {
index: this.index,
id: e.id
})
},
//
reset() {
this.left = 0
},
getBtnWidth() {
let view = uni.createSelectorQuery().in(this).select(".liu-slide-right");
view.boundingClientRect(rect => {
this.operation = this.px2rpx(rect.width, this.screenWidth)
}).exec()
},
getListHeight() {
let view = uni.createSelectorQuery().in(this).select(".liu-slide-left");
view.boundingClientRect(rect => {
this.height = this.px2rpx(rect.height, this.screenWidth)
}).exec()
},
px2rpx(px, screenWidth) {
return px / (screenWidth / 750)
},
touchstart(e) {
if (this.disable) return
this.x = this.px2rpx(e.touches[0].clientX, this.screenWidth)
},
touchmove(e) {
if (this.disable) return
let clientX = this.x - this.px2rpx(e.touches[0].clientX, this.screenWidth)
if (clientX <= 0) this.left = 0
else if (this.operation <= clientX) this.left = this.operation * -1
else this.left = clientX * -1
},
touchend(e) {
if (this.disable) return
let clientX = this.x - this.px2rpx(e.changedTouches[0].clientX, this.screenWidth)
this.left = clientX > this.operation / 2 ? this.operation * -1 : 0
},
}
}
</script>
<style scoped>
.liu-slide {
width: 100%;
position: relative;
overflow: hidden;
}
.liu-slide-right-img {
width: 40rpx;
height: 40rpx;
image {
width: 100%;
height: 100%;
}
}
.liu-slide-left {
width: 100%;
overflow: hidden;
background-color: #FFFFFF;
transition: left 0.2s ease-in-out;
z-index: 10;
}
.liu-slide-right {
position: absolute;
top: 35%;
right: 0;
z-index: 1;
display: flex;
/* justify-content: flex-end; */
}
.btn-item {
text-align: center;
}
</style>

View File

@ -0,0 +1,6 @@
### 1、本插件可免费下载使用
### 2、未经许可严禁复制本插件派生同类插件上传插件市场
### 3、未经许可严禁在插件市场恶意复制抄袭本插件进行违规获利;
### 4、对本软件的任何使用都必须遵守这些条款违反这些条款的个人或组织将面临法律追究。

View File

@ -0,0 +1,85 @@
{
"id": "liu-swipe-action",
"displayName": "滑动操作、左滑删除、滑动删除",
"version": "1.0.8",
"description": "简单好用的滑动操作、左滑删除、滑动删除组件,可自定义样式,源码简单易修改",
"keywords": [
"滑动操作",
"左滑删除",
"滑动删除",
"滑动",
"列表"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,86 @@
# liu-watermark适用于uni-app项目的滑动操作、左滑删除、滑动删除组件
### 本组件目前兼容微信小程序、H5
### 本组件是简单好用的滑动操作、左滑删除、滑动删除组件,可自定义样式,源码简单易修改
# --- 扫码预览、关注我们 ---
## 扫码关注公众号,查看更多插件信息,预览插件效果!
![](https://uni.ckapi.pro/uniapp/publicize.png)
### 使用示例
```
<template>
<view class="slide-main">
<view class="list" v-for="(item,index) in list" :key="index">
<liu-swipe-action :index="index" @clickItem="clickItem">
<view class="item">
<image class="item-img" src="https://cdn.pixabay.com/photo/2022/03/31/14/53/camp-7103189_1280.png">
</image>
<view class="item-name">{{item}}</view>
</view>
</liu-swipe-action>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: ['第1条', '第2条', '第3条', '第4条', '第5条', '第6条']
}
},
methods: {
//点击操作回调事件
clickItem(e) {
console.log('所点击的列表索引:' + e.index)
console.log('所点击的按钮id' + e.id)
}
}
}
</script>
<style scoped>
.slide-main {
width: 100%;
background-color: #f0f0f0;
}
.list {
width: 100%;
margin-top: 1px;
}
.item {
width: 100%;
height: 120rpx;
display: flex;
align-items: center;
justify-content: flex-start;
}
.item-img {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-left: 28rpx;
}
.item-name {
margin-left: 16rpx;
font-size: 30rpx;
color: #666666;
}
</style>
```
### 如需图文水印可自行修改组件内容
### 属性说明
| 名称 | 类型 | 默认值 | 描述 |
| ----------------------------|--------------- | -------------------- | ---------------|
| index | Number | 0 | 当前列索引
| disable | Boolean | false | 是否禁用左滑
| btnList | Array | 默认显示编辑、删除 | 操作按钮数组信息,默认显示编辑、删除,可自定义传入
| @clickItem | Function | | 点击操作回调事件(返回当前列id和操作按钮id)

View File

@ -0,0 +1,114 @@
## 2.2.92023-06-01
优化将是否多选与count字段解绑(原逻辑是count>1为允许多选)改为新增multiple属性控制是否多选。
## 2.2.82023-06-01
修复上版本提交时accept测试值未删除导致h5端只能选择图片的问题。
## 2.2.72023-05-06
应群友建议当instantly为true时触发change事件后延迟1000毫秒再自动上传方便动态修改参数其实个人还是建议想在change事件动态设置参数的伙伴将instantly设置为false,修改参数后手动调用upload()
## 2.2.62023-02-09
修复多个文件同时选择时返回多次change回调的问题
## 2.2.52022-12-27
1.修复多选文件时未能正常校验数量的问题;
2.app端与H5端支持单选或多选文件通过count数量控制超过1开启多选。
## 2.2.42022-12-27
1.修复多选文件时未能正常校验数量的问题;
2.app端修复多选只取到第一个文件的问题。
## 2.2.32022-12-06
修复手动调用show()导致count失效的问题
## 2.2.22022-12-01
Vue3自行修改兼容
## 2.2.12022-10-19
修复childId警告提示
## 2.2.02022-10-10
更新app端webview窗口参数clidId默认值添加时间戳保证唯一性
## 2.1.92022-07-13
[修复] app端选择文件后初始化设置的文件列表被清空问题
## 2.1.82022-07-13
[新增] ref方法初始化文件列表用于已提交后再次编辑时需带入已上传文件setFiles(files)可传入数组或Map对象传入格式请与组件选择返回格式保持一致且name为必须属性。
## 2.1.72022-07-12
修复ios端偶现创建webview初始化参数未生效的问题
## 2.1.62022-07-11
[修复]修复上个版本更新导致nvue窗口组件不能选择文件的问题
[新增]
1.应群友建议(填写禁止格式太多)格式限制formats由原来填写禁止选择的格式改为填写允许被选择的格式
2.应群友建议(增加上传结束回调事件),上传结束回调事件@uploadEnd
3.如能帮到你请留下你的免费好评组件使用过程中有问题可以加QQ群交流至于Map对象怎么使用这类前端基础问题请自行百度
## 2.1.52022-07-01
app端组件销毁时添加自动销毁webview功能避免v-if销毁组件的情况控件还能被点击的问题
## 2.1.42022-07-01
修复小程序端回显问题
## 2.1.32022-06-30
回调事件返回参数新增path字段(文件临时地址),用于回显
## 2.1.22022-06-16
修复APP端Tabbar窗口无法选择文件的问题
## 2.1.12022-06-16
优化:
1.组件优化为允许在v-if中使用
2.允许option直接在data赋值不再强制在onRead中初始化
## 2.1.02022-06-13
h5 pc端更改为单次可多选
## 2.0.92022-06-10
更新演示内容,部分同学不知道怎么获取服务端返回的数据
## 2.0.82022-06-09
优化动态更新上传参数函数,具体查看下方说明:动态更新参数演示
## 2.0.72022-06-07
新增wxFileType属性用于小程序端选择附件时可选文件类型
## 2.0.62022-06-07
修复小程序端真机选择文件提示失败的问题
## 2.0.52022-06-02
优化小程序端调用hide()后未阻止触发文件选择问题
## 2.0.42022-06-01
优化APP端选择器初始定位
## 2.0.32022-05-31
修复nvue窗口选择文件报错问题
## 2.0.22022-05-20
修复ios端opiton设置过早未传入webview导致不自动上传问题
## 2.0.12022-05-19
修复APP端子窗口点击选择文件不响应问题
## 2.0.02022-05-18
此次组件更新至2.0版本与1.0版本使用上略有差异已使用1.0的同学请自行斟酌是否需要升级!
部分差异:
一、 2.0新增异步触发上传功能;
二、2.0新增文件批量上传功能;
三、2.0优化option剔除属性只保留上传接口所需字段且允许异步更改option的值
四、组件增加size(文件大小限制)、count(文件个数限制)、formats(文件后缀限制)、accept(文件类型限制)、instantly(是否立即自动上传)、debug(日志打印)等属性;
五、回调事件取消input事件、callback事件新增change事件和progress事件
六、ref事件新增upload事件、clear事件
七、优化组件代码show和hide函数改为显示隐藏不再重复开关webview
## 1.2.32022-03-22
修复Demo里传入待完善功能[手动上传属性manual=true]导致不自动上传的问题,手动提交上传待下个版本更新
## 1.2.22022-02-21
修复上版本APP优化导致H5和小程序端不自动初始化的问题此次更新仅修复此问题。异步提交功能下个版本更新~
## 1.2.12022-01-25
QQ1群已满已开放2群469580165
## 1.2.02021-12-09
优化APP端页面中DOM重排后每次需要重新定位的问题
## 1.1.12021-12-09
优化与上版本使用方式有改变请检查后确认是否需要更新create更名为show, close更名为hide取消初始化时手动create, 传参方式改为props=>option
## 1.1.02021-12-09
新增refresh方法用于DOM发生重排时重新定位控件(APP端)
## 1.0.92021-07-15
修复上传进度未同步渲染直接返回100%的BUG
## 1.0.82021-07-12
修复H5端传入height和width未生效的bug
## 1.0.72021-07-07
修复h5和小程序端上传完成callback未返回fileName字段问题
## 1.0.62021-07-07
修复h5端提示信息debug
## 1.0.52021-06-29
感谢小伙伴找出bug,上传成功回调success未置为true,已修复
## 1.0.42021-06-28
新增兼容APP,H5,小程序手动关闭控件关闭后不再弹出文件选择框需要重新create再次开启
## 1.0.32021-06-28
close增加条件编译除app端外不需要close
## 1.0.22021-06-28
1.修复页面滚动位置后再create控件导致控件位置不正确的问题
2.修复nvue无法create控件
3.示例项目新增nvue使用案例
## 1.0.12021-06-28
因为有的朋友不清楚app端切换tab时应该怎么处理webview现重新上传一版示例项目需要做tab切换的朋友可以导入示例项目查看
## 1.0.02021-06-25
此插件为l-file插件中上传功能改版更新内容为
1. 按钮内嵌入页面,不再强制固定底部,可跟随页面滚动
2.无需再单独弹框点击上传,减去中间层
3.通过slot自定义按钮样式

View File

@ -0,0 +1,400 @@
export class LsjFile {
constructor(data) {
this.dom = null;
// files.type = waiting等待上传|| loading上传中|| success成功 || fail失败
this.files = new Map();
this.debug = data.debug || false;
this.id = data.id;
this.width = data.width;
this.height = data.height;
this.option = data.option;
this.instantly = data.instantly;
this.prohibited = data.prohibited;
this.onchange = data.onchange;
this.onprogress = data.onprogress;
this.uploadHandle = this._uploadHandle;
// #ifdef MP-WEIXIN
this.uploadHandle = this._uploadHandleWX;
// #endif
}
/**
* 创建File节点
* @param {string}path webview地址
*/
create(path) {
if (!this.dom) {
// #ifdef H5
let dom = document.createElement('input');
dom.type = 'file'
dom.value = ''
dom.style.height = this.height
dom.style.width = this.width
dom.style.position = 'absolute'
dom.style.top = 0
dom.style.left = 0
dom.style.right = 0
dom.style.bottom = 0
dom.style.opacity = 0
dom.style.zIndex = 999
dom.accept = this.prohibited.accept;
if (this.prohibited.multiple) {
dom.multiple = 'multiple';
}
dom.onchange = event => {
for (let file of event.target.files) {
if (this.files.size >= this.prohibited.count) {
this.toast(`只允许上传${this.prohibited.count}个文件`);
this.dom.value = '';
break;
}
this.addFile(file);
}
this._uploadAfter();
this.dom.value = '';
};
this.dom = dom;
// #endif
// #ifdef APP-PLUS
let styles = {
top: '-200px',
left: 0,
width: '1px',
height: '200px',
background: 'transparent'
};
let extras = {
debug: this.debug,
instantly: this.instantly,
prohibited: this.prohibited,
}
this.dom = plus.webview.create(path, this.id, styles,extras);
this.setData(this.option);
this._overrideUrlLoading();
// #endif
return this.dom;
}
}
/**
* 设置上传参数
* @param {object|string}name 上传参数,支持a.b a[b]
*/
setData() {
let [name,value = ''] = arguments;
if (typeof name === 'object') {
Object.assign(this.option,name);
}
else {
this._setValue(this.option,name,value);
}
this.debug&&console.log(JSON.stringify(this.option));
// #ifdef APP-PLUS
this.dom.evalJS(`vm.setData('${JSON.stringify(this.option)}')`);
// #endif
}
/**
* 上传
* @param {string}name 文件名称
*/
async upload(name='') {
if (!this.option.url) {
throw Error('未设置上传地址');
}
// #ifndef APP-PLUS
if (name && this.files.has(name)) {
await this.uploadHandle(this.files.get(name));
}
else {
for (let item of this.files.values()) {
if (item.type === 'waiting' || item.type === 'fail') {
await this.uploadHandle(item);
}
}
}
// #endif
// #ifdef APP-PLUS
this.dom&&this.dom.evalJS(`vm.upload('${name}')`);
// #endif
}
// 选择文件change
addFile(file,isCallChange) {
let name = file.name;
this.debug&&console.log('文件名称',name,'大小',file.size);
if (file) {
// 限制文件格式
let path = '';
let suffix = name.substring(name.lastIndexOf(".")+1).toLowerCase();
let formats = this.prohibited.formats.toLowerCase();
// #ifndef MP-WEIXIN
path = URL.createObjectURL(file);
// #endif
// #ifdef MP-WEIXIN
path = file.path;
// #endif
if (formats&&!formats.includes(suffix)) {
this.toast(`不支持上传${suffix.toUpperCase()}格式文件`);
return false;
}
// 限制文件大小
if (file.size > 1024 * 1024 * Math.abs(this.prohibited.size)) {
this.toast(`附件大小请勿超过${this.prohibited.size}M`)
return false;
}
this.files.set(file.name,{file,path,name: file.name,size: file.size,progress: 0,type: 'waiting'});
return true;
}
}
/**
* 移除文件
* @param {string}name 不传name默认移除所有文件传入name移除指定name的文件
*/
clear(name='') {
// #ifdef APP-PLUS
this.dom&&this.dom.evalJS(`vm.clear('${name}')`);
// #endif
if (!name) {
this.files.clear();
}
else {
this.files.delete(name);
}
return this.onchange(this.files);
}
/**
* 提示框
* @param {string}msg 轻提示内容
*/
toast(msg) {
uni.showToast({
title: msg,
icon: 'none'
});
}
/**
* 微信小程序选择文件
* @param {number}count 可选择文件数量
*/
chooseMessageFile(type,count) {
wx.chooseMessageFile({
count: count,
type: type,
success: ({ tempFiles }) => {
for (let file of tempFiles) {
this.addFile(file);
}
this._uploadAfter();
},
fail: () => {
this.toast(`打开失败`);
}
})
}
_copyObject(obj) {
if (typeof obj !== "undefined") {
return JSON.parse(JSON.stringify(obj));
} else {
return obj;
}
}
/**
* 自动根据字符串路径设置对象中的值 支持.[]
* @param {Object} dataObj 数据源
* @param {String} name 支持a.b a[b]
* @param {String} value
* setValue(dataObj, name, value);
*/
_setValue(dataObj, name, value) {
// 通过正则表达式 查找路径数据
let dataValue;
if (typeof value === "object") {
dataValue = this._copyObject(value);
} else {
dataValue = value;
}
let regExp = new RegExp("([\\w$]+)|\\[(:\\d)\\]", "g");
const patten = name.match(regExp);
// 遍历路径 逐级查找 最后一级用于直接赋值
for (let i = 0; i < patten.length - 1; i++) {
let keyName = patten[i];
if (typeof dataObj[keyName] !== "object") dataObj[keyName] = {};
dataObj = dataObj[keyName];
}
// 最后一级
dataObj[patten[patten.length - 1]] = dataValue;
this.debug&&console.log('参数更新后',JSON.stringify(this.option));
}
_uploadAfter() {
this.onchange(this.files);
setTimeout(()=>{
this.instantly&&this.upload();
},1000)
}
_overrideUrlLoading() {
this.dom.overrideUrlLoading({ mode: 'reject' }, e => {
let {retype,item,files,end} = this._getRequest(
e.url
);
let _this = this;
switch (retype) {
case 'updateOption':
this.dom.evalJS(`vm.setData('${JSON.stringify(_this.option)}')`);
break
case 'change':
try {
_this.files = new Map([..._this.files,...JSON.parse(unescape(files))]);
} catch (e) {
return console.error('出错了,请检查代码')
}
_this.onchange(_this.files);
break
case 'progress':
try {
item = JSON.parse(unescape(item));
} catch (e) {
return console.error('出错了,请检查代码')
}
_this._changeFilesItem(item,end);
break
default:
break
}
})
}
_getRequest(url) {
let theRequest = new Object()
let index = url.indexOf('?')
if (index != -1) {
let str = url.substring(index + 1)
let strs = str.split('&')
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
}
}
return theRequest
}
_changeFilesItem(item,end=false) {
this.debug&&console.log('onprogress',JSON.stringify(item));
this.onprogress(item,end);
this.files.set(item.name,item);
}
_uploadHandle(item) {
item.type = 'loading';
delete item.responseText;
return new Promise((resolve,reject)=>{
this.debug&&console.log('option',JSON.stringify(this.option));
let {url,name,method='POST',header,formData} = this.option;
let form = new FormData();
for (let keys in formData) {
form.append(keys, formData[keys])
}
form.append(name, item.file);
let xmlRequest = new XMLHttpRequest();
xmlRequest.open(method, url, true);
for (let keys in header) {
xmlRequest.setRequestHeader(keys, header[keys])
}
xmlRequest.upload.addEventListener(
'progress',
event => {
if (event.lengthComputable) {
let progress = Math.ceil((event.loaded * 100) / event.total)
if (progress <= 100) {
item.progress = progress;
this._changeFilesItem(item);
}
}
},
false
);
xmlRequest.ontimeout = () => {
console.error('请求超时')
item.type = 'fail';
this._changeFilesItem(item,true);
return resolve(false);
}
xmlRequest.onreadystatechange = ev => {
if (xmlRequest.readyState == 4) {
if (xmlRequest.status == 200) {
this.debug&&console.log('上传完成:' + xmlRequest.responseText)
item['responseText'] = xmlRequest.responseText;
item.type = 'success';
this._changeFilesItem(item,true);
return resolve(true);
} else if (xmlRequest.status == 0) {
console.error('status = 0 :请检查请求头Content-Type与服务端是否匹配服务端已正确开启跨域并且nginx未拦截阻止请求')
}
console.error('--ERROR--status = ' + xmlRequest.status)
item.type = 'fail';
this._changeFilesItem(item,true);
return resolve(false);
}
}
xmlRequest.send(form)
});
}
_uploadHandleWX(item) {
item.type = 'loading';
delete item.responseText;
return new Promise((resolve,reject)=>{
this.debug&&console.log('option',JSON.stringify(this.option));
let form = {filePath: item.file.path,...this.option };
form['fail'] = ({ errMsg = '' }) => {
console.error('--ERROR--' + errMsg)
item.type = 'fail';
this._changeFilesItem(item,true);
return resolve(false);
}
form['success'] = res => {
if (res.statusCode == 200) {
this.debug&&console.log('上传完成,微信端返回不一定是字符串根据接口返回格式判断是否需要JSON.parse' + res.data)
item['responseText'] = res.data;
item.type = 'success';
this._changeFilesItem(item,true);
return resolve(true);
}
item.type = 'fail';
this._changeFilesItem(item,true);
return resolve(false);
}
let xmlRequest = uni.uploadFile(form);
xmlRequest.onProgressUpdate(({ progress = 0 }) => {
if (progress <= 100) {
item.progress = progress;
this._changeFilesItem(item);
}
})
});
}
}

View File

@ -0,0 +1,321 @@
<template>
<view class="lsj-file" :style="[getStyles]">
<view ref="lsj" class="hFile" :style="[getStyles]" @click="onClick">
<slot><view class="defview" :style="[getStyles]">附件上传</view></slot>
</view>
</view>
</template>
<script>
// https://ext.dcloud.net.cn/plugin?id=5459
import {LsjFile} from './LsjFile.js'
export default {
name: 'Lsj-upload',
props: {
//
debug: {type: Boolean,default: false},
//
instantly: {type: Boolean,default: false},
//
option: {type: Object,default: ()=>{}},
//
size: { type: Number, default: 10 },
// ,
count: { type: Number, default: 9 },
//
multiple: {type:Boolean, default: true},
//
formats: { type: String, default:''},
// input file
accept: {type: String,default: ''},
//
//all=
//video=
//image=
//file=
wxFileType: { type: String, default: 'all' },
// webviewIDId
childId: { type: String, default: 'lsjUpload' },
//
width: { type: String, default: '100%' },
//
height: { type: String, default: '80rpx' },
// top,left,bottom,rightposition=absolute
top: { type: [String, Number], default: '' },
left: { type: [String, Number], default: '' },
bottom: { type: [String, Number], default: '' },
right: { type: [String, Number], default: '' },
// nvue
position: {
type: String,
// #ifdef APP-NVUE
default: 'absolute',
// #endif
// #ifndef APP-NVUE
default: 'static',
// #endif
},
},
data() {
return {
}
},
watch: {
option(v) {
// #ifdef APP-PLUS
this.lsjFile&&this.show();
// #endif
}
},
updated() {
// #ifdef APP-PLUS
if (this.isShow) {
this.lsjFile&&this.show();
}
// #endif
},
computed: {
getStyles() {
let styles = {
width: this.width,
height: this.height
}
if (this.position == 'absolute') {
styles['top'] = this.top
styles['bottom'] = this.bottom
styles['left'] = this.left
styles['right'] = this.right
styles['position'] = 'fixed'
}
return styles
}
},
mounted() {
this._size = 0;
let WEBID = this.childId + new Date().getTime();
this.lsjFile = new LsjFile({
id: WEBID,
debug: this.debug,
width: this.width,
height: this.height,
option: this.option,
instantly: this.instantly,
//
prohibited: {
//
size: this.size,
//
formats: this.formats,
//
accept: this.accept,
count: this.count,
//
multiple: this.multiple,
},
onchange: this.onchange,
onprogress: this.onprogress,
});
this.create();
//
uni.$on('lsjShow',this.show);
},
beforeDestroy() {
uni.$off('lsjShow',this.show);
// #ifdef APP-PLUS
this.lsjFile.dom.close();
// #endif
},
methods: {
setFiles(array) {
if (array instanceof Map) {
for (let [key, item] of array) {
item['progress'] = 100;
item['type'] = 'success';
this.lsjFile.files.set(key,item);
}
}
else if (Array.isArray(array)) {
array.forEach(item=>{
if (item.name) {
item['progress'] = 100;
item['type'] = 'success';
this.lsjFile.files.set(item.name,item);
}
});
}
this.onchange(this.lsjFile.files);
},
setData() {
this.lsjFile&&this.lsjFile.setData(...arguments);
},
getDomStyles(callback) {
// #ifndef APP-NVUE
let view = uni
.createSelectorQuery()
.in(this)
.select('.lsj-file')
view.fields(
{
size: true,
rect: true
},
({ height, width, top, left, right, bottom }) => {
uni.createSelectorQuery()
.selectViewport()
.scrollOffset(({ scrollTop }) => {
return callback({
top: parseInt(top) + parseInt(scrollTop) + 'px',
left: parseInt(left) + 'px',
width: parseInt(width) + 'px',
height: parseInt(height) + 'px'
})
})
.exec()
}
).exec()
// #endif
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
dom.getComponentRect(this.$refs.lsj, ({ size: { height, width, top, left, right, bottom } }) => {
return callback({
top: parseInt(top) + 'px',
left: parseInt(left) + 'px',
width: parseInt(width) + 'px',
height: parseInt(height) + 'px',
right: parseInt(right) + 'px',
bottom: parseInt(bottom) + 'px'
})
})
// #endif
},
show() {
if (this._size && (this._size >= this.count)) {
return;
}
this.isShow = true;
// #ifdef APP-PLUS
this.lsjFile&&this.getDomStyles(styles => {
this.lsjFile.dom.setStyle(styles)
});
// #endif
// #ifdef H5
this.lsjFile.dom.style.display = 'inline'
// #endif
},
hide() {
this.isShow = false;
// #ifdef APP-PLUS
this.lsjFile&&this.lsjFile.dom.setStyle({
top: '-100px',
left:'0px',
width: '1px',
height: '100px',
});
// #endif
// #ifdef H5
this.lsjFile.dom.style.display = 'none'
// #endif
},
/**
* 手动提交上传
* @param {string}name 文件名称不传则上传所有type等于waiting和fail的文件
*/
upload(name) {
this.lsjFile&&this.lsjFile.upload(name);
},
/**
* @returns {Map} 已选择的文件Map集
*/
onchange(files) {
this.$emit('change',files);
this._size = files.size;
return files.size >= this.count ? this.hide() : this.show();
},
/**
* @returns {object} 当前上传中的对象
*/
onprogress(item,end=false) {
this.$emit('progress',item);
if (end) {
setTimeout(()=>{
this.$emit('uploadEnd',item);
},0);
}
},
/**
* 移除组件内缓存的某条数据
* @param {string}name 文件名称,不指定默认清除所有文件
*/
clear(name) {
this.lsjFile.clear(name);
},
//
create() {
// iOShybridhtmlpath
let path = '/uni_modules/lsj-upload/hybrid/html/uploadFile.html';
let dom = this.lsjFile.create(path);
// #ifdef H5
this.$refs.lsj.$el.appendChild(dom);
// #endif
// #ifndef APP-PLUS
this.show();
// #endif
// #ifdef APP-PLUS
dom.setStyle({position: this.position});
dom.loadURL(path);
setTimeout(()=>{
// #ifdef APP-NVUE
plus.webview.currentWebview().append(dom);
// #endif
// #ifndef APP-NVUE
this.$root.$scope.$getAppWebview().append(dom);
// #endif
this.show();
},300)
// #endif
},
//
onClick() {
if (this._size >= this.count) {
this.toast(`只允许上传${this.count}个文件`);
return;
}
// #ifdef MP-WEIXIN
if (!this.isShow) {return;}
let count = this.count - this._size;
this.lsjFile.chooseMessageFile(this.wxFileType,count);
// #endif
},
toast(msg) {
uni.showToast({
title: msg,
icon: 'none'
});
}
}
}
</script>
<style scoped>
.lsj-file {
display: inline-block;
}
.defview {
background-color: #007aff;
color: #fff;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.hFile {
position: relative;
overflow: hidden;
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title class="title">[文件管理器]</title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<style type="text/css">
.content {background: transparent;}
.btn {position: relative;top: 0;left: 0;bottom: 0;right: 0;}
.btn .file {position: fixed;z-index: 93;left: 0;right: 0;top: 0;bottom: 0;width: 100%;opacity: 0;}
</style>
</head>
<body>
<div id="content" class="content">
<div class="btn">
<input :multiple="multiple" @change="onChange" :accept="accept" ref="file" class="file" type="file" />
</div>
</div>
<script type="text/javascript" src="js/vue.min.js"></script>
<script type="text/javascript">
let _this;
var vm = new Vue({
el: '#content',
data: {
accept: '',
multiple: true,
},
mounted() {
console.log('加载webview');
_this = this;
this.files = new Map();
document.addEventListener('plusready', (e)=>{
let {debug,instantly,prohibited} = plus.webview.currentWebview();
this.debug = debug;
this.instantly = instantly;
this.prohibited = prohibited;
this.accept = prohibited.accept;
if (prohibited.multiple === 'false') {
prohibited.multiple = false;
}
this.multiple = prohibited.multiple;
location.href = 'callback?retype=updateOption';
}, false);
},
methods: {
toast(msg) {
plus.nativeUI.toast(msg);
},
clear(name) {
if (!name) {
this.files.clear();
return;
}
this.files.delete(name);
},
setData(option='{}') {
this.debug&&console.log('更新参数:'+option);
try{
_this.option = JSON.parse(option);
}catch(e){
console.error('参数设置错误')
}
},
async upload(name=''){
if (name && this.files.has(name)) {
await this.createUpload(this.files.get(name));
}
else {
for (let item of this.files.values()) {
if (item.type === 'waiting' || item.type === 'fail') {
await this.createUpload(item);
}
}
}
},
onChange(e) {
let fileDom = this.$refs.file;
for (let file of fileDom.files) {
if (this.files.size >= this.prohibited.count) {
this.toast(`只允许上传${this.prohibited.count}个文件`);
fileDom.value = '';
break;
}
this.addFile(file);
}
this.uploadAfter();
fileDom.value = '';
},
addFile(file) {
if (file) {
let name = file.name;
this.debug&&console.log('文件名称',name,'大小',file.size);
// 限制文件格式
let suffix = name.substring(name.lastIndexOf(".")+1).toLowerCase();
let formats = this.prohibited.formats.toLowerCase();
if (formats&&!formats.includes(suffix)) {
this.toast(`不支持上传${suffix.toUpperCase()}格式文件`);
return;
}
// 限制文件大小
if (file.size > 1024 * 1024 * Math.abs(this.prohibited.size)) {
this.toast(`附件大小请勿超过${this.prohibited.size}M`)
return;
}
// let itemBlob = new Blob([file]);
let path = URL.createObjectURL(file);
this.files.set(file.name,{file,path,name: file.name,size: file.size,progress: 0,type: 'waiting'});
}
},
/**
* @returns {Map} 已选择的文件Map集
*/
callChange() {
location.href = 'callback?retype=change&files=' + escape(JSON.stringify([...this.files]));
},
/**
* @returns {object} 正在处理的当前对象
*/
changeFilesItem(item,end='') {
this.files.set(item.name,item);
location.href = 'callback?retype=progress&end='+ end +'&item=' + escape(JSON.stringify(item));
},
uploadAfter() {
this.callChange();
setTimeout(()=>{
this.instantly&&this.upload();
},1000)
},
createUpload(item) {
this.debug&&console.log('准备上传,option='+JSON.stringify(this.option));
item.type = 'loading';
delete item.responseText;
return new Promise((resolve,reject)=>{
let {url,name,method='POST',header={},formData={}} = this.option;
let form = new FormData();
for (let keys in formData) {
form.append(keys, formData[keys])
}
form.append(name, item.file);
let xmlRequest = new XMLHttpRequest();
xmlRequest.open(method, url, true);
for (let keys in header) {
xmlRequest.setRequestHeader(keys, header[keys])
}
xmlRequest.upload.addEventListener(
'progress',
event => {
if (event.lengthComputable) {
let progress = Math.ceil((event.loaded * 100) / event.total)
if (progress <= 100) {
item.progress = progress;
this.changeFilesItem(item);
}
}
},
false
);
xmlRequest.ontimeout = () => {
console.error('请求超时')
item.type = 'fail';
this.changeFilesItem(item,true);
return resolve(false);
}
xmlRequest.onreadystatechange = ev => {
if (xmlRequest.readyState == 4) {
this.debug && console.log('接口是否支持跨域',xmlRequest.withCredentials);
if (xmlRequest.status == 200) {
this.debug && console.log('上传完成:' + xmlRequest.responseText)
item['responseText'] = xmlRequest.responseText;
item.type = 'success';
this.changeFilesItem(item,true);
return resolve(true);
} else if (xmlRequest.status == 0) {
console.error('status = 0 :请检查请求头Content-Type与服务端是否匹配服务端已正确开启跨域并且nginx未拦截阻止请求')
}
console.error('--ERROR--status = ' + xmlRequest.status)
item.type = 'fail';
this.changeFilesItem(item,true);
return resolve(false);
}
}
xmlRequest.send(form)
});
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,79 @@
{
"id": "lsj-upload",
"displayName": "全文件上传选择非原生2.0版",
"version": "2.2.9",
"description": "文件选择上传-支持APP-H5网页-微信小程序",
"keywords": [
"附件",
"file",
"upload",
"上传",
"文件管理器"
],
"repository": "",
"engines": {
"HBuilderX": "^3.4.9"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue"
},
"uni_modules": {
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,353 @@
# lsj-upload
### 插件地址https://ext.dcloud.net.cn/plugin?id=5459
### 不清楚使用方式可点击右侧导入示例项目运行完整示例
### 此次更新2.0与1.0使用方式略有差异已使用1.0的同学自行斟酌是否更新到2.0版本!!!
使用插件有任何问题欢迎加入QQ讨论群
- 群1701468256已满
- 群2469580165已满
- 群3667530868
若能帮到你请高抬贵手点亮5颗星~
------
## 重要提示
### 组件是窗口级滚动不要在scroll-view内使用
### 组件是窗口级滚动不要在scroll-view内使用
### 组件是窗口级滚动不要在scroll-view内使用
### 控件的height高度应与slot自定义内容高度保持一致
### nvue窗口只能使用固定模式position=absolute
### show() 当DOM重排后在this.$nextTick内调用show(),控件定位会更加准确
### hide() APP端webview层级比view高如不希望触发点击时应调用hide隐藏控件反之调用show
### 若iOS端跨域服务端同学实在配置不好可把hybrid下html目录放到服务器去同源则不存在跨域问题。
### 小程序端因hybrid不能使用本地HTML所以插件提供的是从微信消息列表拉取文件并选择请知悉。
### file对象不是object对象也不能转json字符串如果你打印file那就是{}可以打印file.name和file.size。
### 返回的path是个blob类型仅供用于文件回显插件已内置好上传函数调用上传会自动提交待上传文件若非要自己拿path去搞上传那你自己处理。
------
## 使用说明
| 属性 | 是否必填 | 值类型 | 默认值 | 说明 |
| --------- | -------- | -----: | --: | :------------:|
| width | 否 | String |100% | 容器宽度 |
| height | 是 | String |80rpx | 容器高度 |
| debug | 否 | Boolean |false | 打印调试日志 |
| option | 是 | Object |- | [文件上传接口相关参数](#p1)|
| instantly | 否 | Boolean |false | true=自动上传 |
| count | 否 | Number |10 | 附件选择上限(个)|
| size | 否 | Number |10 | 附件大小上限(M)|
| wxFileType | 否 | String |all | 微信小程序文件选择器格式限制(all=从所有文件选择video=只能选择视频文件image=只能选择图片文件file=可以选择除了图片和视频之外的其它的文件)|
| accept | 否 | String |- | 文件选择器input file格式限制(部分机型不兼容建议使用formats)|
| formats | 否 | String |- | 限制允许上传的格式,空串=不限制默认为空多个格式以逗号隔开例如png,jpg,pdf|
| childId | 否 | String |lsjUpload| 控件的id(仅APP有效应用内每个控件命名一个唯一Id不同窗口也不要同名Id)|
| position | 否 | String |static | 控件的定位模式(static=控件随页面滚动;absolute=控件在页面中绝对定位,不随窗口内容滚动)|
| top,left,right,bottom | 否 | [Number,String] |0 | 设置控件绝对位置position=absolute时有效|
| @change | 否 | Function |Map | 选择文件触发返回所有已选择文件Map集合|
| @progress | 否 | Function |Object | 上传过程中发生状态变化的文件对象需通过set更新至Map集合|
| @uploadEnd| 否 | Function |Object | 上传结束回调返回参数与progress一致|
## <a id="p1">option说明</a>
|参数 | 是否必填 | 说明|
|---- | ---- | :--: |
|url | 是 | 上传接口地址|
|name| 否 |上传接口文件key默认为file|
|header| 否 |上传接口请求头|
|formData| 否 |上传接口额外参数|
## ref调用
|作用 | 方法名| 传入参数| 说明|
|---- | --------- | -------- | :--: |
|显示控件| show|-| 控件显示状态下可触发点击|
|隐藏控件| hide|-| 控件隐藏状态下不触发点击|
|动态设置文件列表| setFiles|[Array,Map] files| 传入格式请与组件选择返回格式保持一致且name为必须属性可查看下方演示|
|动态更新参数| setData|[String] name,[any] value| name支持a.b 和 a[b],可查看下方演示|
|移除选择的文件| clear|[String] name| 不传参数清空所有文件传入文件name时删除该name的文件|
|手动上传| upload|[String] name| 不传参数默认依次上传所有type=waiting的文件传入文件name时不关心type是否为waiting单独上传指定name的文件|
## progress返回对象字段说明
|字段 | 说明|
|---- | :--: |
|file | 文件对象|
|name |文件名称|
|size |文件大小|
|type |文件上传状态waiting等待上传、loading上传中、success成功 、fail失败|
|responseText|上传成功后服务端返回数据(仅type为success时存在)|
## 以下演示为vue窗口使用方式nvue使用区别是必须传入控件绝对位置如topbottomleftright且position只能为absolute如不清楚可点击右侧导入示例项目有详细演示代码。
### vue:
``` javascript
<lsj-upload
ref="lsjUpload"
childId="upload1"
:width="width"
:height="height"
:option="option"
:size="size"
:formats="formats"
:debug="debug"
:instantly="instantly"
@uploadEnd="onuploadEnd"
@progress="onprogre"
@change="onChange">
<view class="btn" :style="{width: width,height: height}">选择附件</view>
</lsj-upload>
<view class="padding">
<view>已选择文件列表:</view>
<!-- #ifndef MP-WEIXIN -->
<view v-for="(item,index) in files.values()" :key="index">
<image style="width: 100rpx;height: 100rpx;" :src="item.path" mode="widthFix"></image>
<text>提示【path主要用于图片视频类文件回显他用自行处理】{{item.path}}</text>
<text>{{item.name}}</text>
<text style="margin-left: 10rpx;">大小:{{item.size}}</text>
<text style="margin-left: 10rpx;">状态:{{item.type}}</text>
<text style="margin-left: 10rpx;">进度:{{item.progress}}</text>
<text style="margin-left: 10rpx;" v-if="item.responseText">服务端返回演示:{{item.responseText}}</text>
<text @click="resetUpload(item.name)" v-if="item.type=='fail'" style="margin-left: 10rpx;padding: 0 10rpx;border: 1rpx solid #007AFF;">重新上传</text>
<text @click="clear(item.name)" style="margin-left: 10rpx;padding: 0 10rpx;border: 1rpx solid #007AFF;">删除</text>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view v-for="(item,index) in wxFiles" :key="index">
<text>{{item.name}}</text>
<text style="margin-left: 10rpx;">大小:{{item.size}}</text>
<text style="margin-left: 10rpx;">状态:{{item.type}}</text>
<text style="margin-left: 10rpx;">进度:{{item.progress}}</text>
<view>
<button @click="resetUpload(item.name)">重新上传</button>
<button @click="clear(item.name)">删除</button>
</view>
</view>
<!-- #endif -->
</view>
```
---
* 函数说明
``` javascript
export default {
data() {
return {
// 上传接口参数
option: {
// 上传服务器地址,需要替换为你的接口地址
url: 'http://hl.j56.com/dropbox/document/upload', // 该地址非真实路径,需替换为你项目自己的接口地址
// 上传附件的key
name: 'file',
// 根据你接口需求自定义请求头,默认不要写content-type,让浏览器自适配
header: {
// 示例参数可删除
'Authorization': 'bearer eyJhbGciOiJSUzI1NiIsI',
'uid': '99',
'client': 'app',
'accountid': 'DP',
},
// 根据你接口需求自定义body参数
formData: {
// 'orderId': 1000
}
},
// 选择文件后是否立即自动上传true=选择后立即上传
instantly: true,
// 必传宽高且宽高应与slot宽高保持一致
width: '180rpx',
height: '180rpx',
// 限制允许上传的格式,空串=不限制,默认为空
formats: '',
// 文件上传大小限制
size: 30,
// 文件数量限制
count: 2,
// 文件回显列表
files: new Map(),
// 微信小程序Map对象for循环不显示所以转成普通数组不要问为什么我也不知道
wxFiles: [],
// 是否打印日志
debug: true,
// 演示用
tabIndex: 0,
list:[],
}
},
onReady() {
setTimeout(()=>{
console.log('----演示动态更新参数-----');
this.$refs['lsjUpload'+this.tabIndex].setData('formData.orderId','动态设置的参数');
console.log('以下注释内容为-动态更新参数更多演示,放开后可查看演示效果');
// 修改option对象的name属性
// this.$refs.lsjUpload.setData('name','myFile');
// 修改option对象的formData内的属性
// this.$refs.lsjUpload.setData('formData.appid','1111');
// 替换option对象的formData
// this.$refs.lsjUpload.setData('formData',{appid:'222'});
// option对象的formData新增属性
// this.$refs.lsjUpload.setData('formData.newkey','新插入到formData的属性');
// ---------演示初始化值,用于已提交后再次编辑时需带入已上传文件-------
// 方式1=传入数组
// let files1 = [{name: '1.png'},{name: '2.png',}];
// 方式2=传入Map对象
// let files2 = new Map();
// files2.set('1.png',{name: '1.png'})
// 此处调用setFiles设置初始files
// this.$refs.lsjUpload.setFiles(files1);
// 初始化tab
this.onTab(0);
},2000)
},
methods: {
// 某文件上传结束回调(成功失败都回调)
onuploadEnd(item) {
console.log(`${item.name}已上传结束,上传状态=${item.type}`);
// 更新当前窗口状态变化的文件
this.files.set(item.name,item);
// ---可删除--演示上传完成后取服务端数据
if (item['responseText']) {
console.log('演示服务器返回的字符串JSON转Object对象');
this.files.get(item.name).responseText = JSON.parse(item.responseText);
}
// 微信小程序Map对象for循环不显示所以转成普通数组
// 如果你用不惯Map对象也可以像这样转普通数组组件使用Map主要是避免反复文件去重操作
// #ifdef MP-WEIXIN
this.wxFiles = [...this.files.values()];
// #endif
// 强制更新视图
this.$forceUpdate();
// ---可删除--演示判断是否所有文件均已上传成功
let isAll = [...this.files.values()].find(item=>item.type!=='success');
if (!isAll) {
console.log('已全部上传完毕');
}
else {
console.log(isAll.name+'待上传');
}
},
// 上传进度回调,如果网页上md文档没有渲染出事件名称onprogre请复制代码的小伙伴自行添加上哈没有哪个事件是只(item)的
onprogre(item) {
// 更新当前状态变化的文件
this.files.set(item.name,item);
console.log('打印对象',JSON.stringify(this.files.get(item.name)));
// 微信小程序Map对象for循环不显示所以转成普通数组不要问为什么我也不知道
// #ifdef MP-WEIXIN
this.wxFiles = [...this.files.values()];
// #endif
// 强制更新视图
this.$forceUpdate();
},
// 文件选择回调
onChange(files) {
console.log('当前选择的文件列表:',JSON.stringify([...files.values()]));
// 更新选择的文件
this.files = files;
// 强制更新视图
this.$forceUpdate();
// 微信小程序Map对象for循环不显示所以转成普通数组不要问为什么我也不知道
// #ifdef MP-WEIXIN
this.wxFiles = [...this.files.values()];
// #endif
// ---可删除--演示重新定位覆盖层控件
this.$nextTick(()=>{
console.log('演示重新定位');
this.$refs.lsjUpload0.show();
this.$refs.lsjUpload1.show();
this.$refs.lsjUpload2.show();
});
},
// 手动上传
upload() {
// name=指定文件名不指定则上传所有type等于waiting和fail的文件
this.$refs['lsjUpload'+this.tabIndex].upload();
},
// 指定上传某个文件
resetUpload(name) {
this.$refs['lsjUpload'+this.tabIndex].upload(name);
},
// 移除某个文件
clear(name) {
// name=指定文件名不传name默认移除所有文件
this.$refs['lsjUpload'+this.tabIndex].clear(name);
},
/**
* ---可删除--演示在组件上方添加新内容DOM变化
* DOM重排演示重排后组件内部updated默认会触发show方法,若特殊情况未能触发updated也可以手动调用一次show()
* 什么是DOM重排自行百度去
*/
add() {
this.list.push('DOM重排测试');
},
/**
* ---可删除--演示Tab切换时覆盖层是否能被点击
* APP端因为是webview层级比view高此时若不希望点击触发选择文件需要手动调用hide()
* 手动调用hide后需要调用show()才能恢复覆盖层的点击
*/
onTab(tabIndex) {
this.$refs.lsjUpload0.hide();
this.$refs.lsjUpload1.hide();
this.tabIndex = tabIndex;
this.$nextTick(()=>{
this.$refs['lsjUpload'+this.tabIndex].show();
})
},
/**
* 打开nvue窗口查看非跟随窗口滚动效果
*/
open() {
uni.navigateTo({
url: '/pages/nvue-demo/nvue-demo'
});
}
}
}
```
## 温馨提示
* 文件上传
0. 如说明表达还不够清楚,不清楚怎么使用可导入完整示例项目运行体验和查看
1. APP端请优先联调Android,上传成功后再运行iOS端如iOS返回status=0则需要后端开启允许跨域
2. header的Content-Type类型需要与服务端要求一致否则收不到附件服务端若没有明文规定则可不写使用默认匹配
3. 服务端不清楚怎么配置跨域可加群咨询,具体百度~
4. 欢迎加入QQ讨论群701468256(已满)
5. 欢迎加入QQ讨论群469580165(已满)
6. 欢迎加入QQ讨论群667530868
7. 若能帮到你还请点亮5颗小星星以作鼓励哈~
8. 若能帮到你还请点亮5颗小星星以作鼓励哈~
9. 若能帮到你还请点亮5颗小星星以作鼓励哈~

View File

@ -0,0 +1,13 @@
## 1.2.02021-07-30
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.12021-05-12
- 新增 示例地址
- 修复 示例项目缺少组件的Bug
## 1.1.02021-04-22
- 新增 通过方法自定义动画
- 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式
- 优化 动画触发逻辑,使动画更流畅
- 优化 支持单独的动画类型
- 优化 文档示例
## 1.0.22021-02-05
- 调整为uni_modules目录规范

View File

@ -0,0 +1,128 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
this.animation = uni.createAnimation(options)
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@ -0,0 +1,277 @@
<template>
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式 css 样式注意带-连接符的属性需要使用小驼峰写法如`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
}
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// close,
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
//
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
//
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
//
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj))
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
//
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// nextTick wx
this.$nextTick(() => {
// TODO
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
//
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
//
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
//
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
//
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@ -0,0 +1,83 @@
{
"id": "uni-transition",
"displayName": "uni-transition 过渡动画",
"version": "1.2.0",
"description": "元素的简单过渡动画",
"keywords": [
"uni-ui",
"uniui",
"动画",
"过渡",
"过渡动画"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,397 @@
## Transition 过渡动画
> **组件名uni-transition**
> 代码块: `uTransition`
元素过渡动画
> **注意事项**
> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。
> - 组件需要依赖 `sass` 插件 ,请自行手动安装
> - rotate 旋转动画不需要填写 deg 单位,在小程序上填写单位动画不会执行
> - NVUE 下修改宽高动画,不能定位到中心点
> - 百度小程序下修改宽高 ,可能会影响其他动画,需注意
> - nvue 不支持 costom-class , 请使用 styles
> - 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839
### 安装方式
本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55)
### 基本用法
在 ``template`` 中使用组件
```html
<template>
<view>
<button type="primary" @click="open">fade</button>
<uni-transition mode-class="fade" :styles="{'width':'100px','height':'100px','backgroundColor':'red'}" :show="show" @change="change" />
</view>
</template>
<script>
export default {
data() {
return {
show: false,
}
},
onLoad() {},
methods: {
open(mode) {
this.show = !this.show
},
change() {
<!-- console.log('触发动画') -->
}
}
}
</script>
```
### 样式覆盖
**注意:`nvue` 不支持 `custom-class` 属性 ,需要使用 `styles` 属性进行兼容**
使用 `custom-class` 属性绑定样式,可以自定义 `uni-transition` 的样式
```html
<template>
<view class="content">
<uni-transition custom-class="custom-transition" mode-class="fade" :duration="0" :show="true" />
</view>
</template>
<style>
/* 常规样式覆盖 */
.content >>> .custom-transition {
width:100px;
height:100px;
background-color:red;
}
</style>
<style lang="scss">
/* 如果使用 scss 需要使用 /deep/ */
.content /deep/ .custom-transition {
width:100px;
height:100px;
background-color:red;
}
</style>
```
如果使用 `styles` 注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
```html
<template>
<view class="content">
<uni-transition :styles="styles" mode-class="fade" :duration="0" :show="true" />
</view>
</template>
<script>
export default {
data() {
return {
styles:{
'width':'100px',
'height':'100px',
'backgroundColor':'red'
}
}
}
}
</script>
```
### 自定义动画
当内置动画类型不能满足需求的时候 ,可以使用 `step()``run()` 自定义动画,入参以及具体用法参考下方属性说明
`init()` 方法可以覆盖默认配置
```html
<template>
<view>
<button type="primary" @click="run">执行动画</button>
<uni-transition ref="ani" :styles="{'width':'100px','height':'100px','backgroundColor':'red'}" :show="show" />
</view>
</template>
<script>
export default {
data() {
return {
show: true,
}
},
onReady() {
this.$refs.ani.init({
duration: 1000,
timingFunction: 'linear',
transformOrigin: '50% 50%',
delay: 500
})
},
methods: {
run() {
// 同时右平移到 100px,旋转 360 读
this.$refs.ani.step({
translateX: '100px',
rotate: '360'
})
// 上面的动画执行完成后等待200毫秒平移到 0px,旋转到 0 读
this.$refs.ani.step({
translateX: '0px',
rotate: '0'
},
{
timingFunction: 'ease-in',
duration: 200
})
// 开始执行动画
this.$refs.ani.run(()=>{
// console.log('动画支持完毕')
})
}
}
}
</script>
```
## API
### Transition Props
|属性名 |类型 |默认值 |说明 |
|:-: |:-: |:-: |:-:|
|show |Boolean|false |控制组件显示或隐藏 |
|mode-class |Array/String |- |内置过渡动画类型 |
|custom-class |String |- |自定义类名 |
|duration |Number |300 |过渡动画持续时间 |
|styles |Object |- |组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red` |
#### mode-class 内置过渡动画类型说明
**格式为** `'fade'` 或者 `['fade','slide-top']`
|属性名 |说明 |
|:-: |:-: |
|fade |渐隐渐出过渡 |
|slide-top |由上至下过渡 |
|slide-right |由右至左过渡 |
|slide-bottom |由下至上过渡 |
|slide-left |由左至右过渡 |
|zoom-in |由小到大过渡 |
|zoom-out |由大到小过渡 |
**注意**
组合使用时同一种类型相反的过渡动画如slide-top、slide-bottom同时使用时只有最后一个生效
### Transition Events
|事件名 |说明 |返回值 |
|:-: |:-: |:-: |
|click |点击组件触发 |- |
|change |过渡动画结束时触发 | e = {detail:true} |
### Transition Methons
|方法名|说明|参数|
|:-:|:-:|:-:|
|init()|手动初始化配置|Function(OBJECT:config)|
|step()|动画队列|Function(OBJECT:type,OBJECT:config)|
|run()|执行动画|Function(FUNCTION:callback) |
### init(OBJECT:config)
**通过 ref 调用方法**
手动设置动画配置,需要在页面渲染完毕后调用
```javascript
this.$refs.ani.init({
duration: 1000,
timingFunction:'ease',
delay:500,
transformOrigin:'left center'
})
```
### step(OBJECT:type,OBJECT:config) 动画队列
**通过 ref 调用方法**
调用 `step()` 来表示一组动画完成,`step` 第一个参数可以传入任意多个动画方法,一组动画中的所有动画会同时开始,一组动画完成后才会进行下一组动画。`step` 第二个参数可以传入一个跟 `uni.createAnimation()` 一样的配置参数用于指定当前组动画的配置。
Tips
- 第一个参数支持的动画参考下面的 `支持的动画`
- 第二个参数参考下面的 `动画配置`,可省略,如果省略继承`init`的配置
```javascript
this.$refs.ani.step({
translateX: '100px'
},{
duration: 1000,
timingFunction:'ease',
delay:500,
transformOrigin:'left center'
})
```
### run(FUNCTION:callback) 执行动画
**通过 ref 调用方法**
在执行 `step()` 后,需要调用 `run()` 来运行动画 ,否则动画会一直等待
`run()` 方法可以传入一个 `callback` 函数 ,会在所有动画执行完毕后回调
```javascript
this.$refs.ani.step({
translateX: '100px'
})
this.$refs.ani.run(()=>{
// console.log('动画执行完毕')
})
```
### 动画配置
动画配置 `init()``step()` 第二个参数配置相同 ,如果配置`step() `第二个参数,将会覆盖 `init()` 的配置
|属性名|值|必填|默认值|说明|平台差异|
|:-:|:-:|:-:|:-:|:-:|:-:|
|duration|Number|否|400|动画持续时间单位ms|-|
|timingFunction|String|否|"linear"|定义动画的效果|-|
|delay|Number|否|0|动画延迟时间,单位 ms|-|
|needLayout|Boolean|否|false |动画执行是否影响布局|仅 nvue 支持|
|transformOrigin|String |否|"center center"|设置 [transform-origin](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin)|-|
### timingFunction 属性说明
|值|说明|平台差异|
|:-:|:-:|:-:|
|linear|动画从头到尾的速度是相同的|-|
|ease|动画以低速开始,然后加快,在结束前变慢|-|
|ease-in| 动画以低速开始|-|
|ease-in-out| 动画以低速开始和结束|-|
|ease-out|动画以低速结束|-|
|step-start|动画第一帧就跳至结束状态直到结束|nvue不支持|
|step-end|动画一直保持开始状态,最后一帧跳到结束状态|nvue不支持|
```javascript
// init 配置
this.$refs.ani.init({
duration: 1000,
timingFunction:'ease',
delay:500,
transformOrigin:'left center'
})
// step 配置
this.$refs.ani.step({
translateX: '100px'
},{
duration: 1000,
timingFunction:'ease',
delay:500,
transformOrigin:'left center'
})
```
### 支持的动画
动画方法
如果同一个动画方法有多个值,多个值使用数组分隔
```javascript
this.$refs.ani.step({
width:'100px',
scale: [1.2,0.8],
})
```
**样式:**
|属性名|值|说明|平台差异|
|:-:|:-:|:-:|:-:|
|opacity|value|透明度,参数范围 0~1|-|
|backgroundColor|color|颜色值|-|
|width|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|-|
|height|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|-|
|top|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|nvue 不支持|
|left|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|nvue 不支持|
|bottom|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|nvue 不支持|
|right|length|长度值,如果传入 Number 则默认使用 px可传入其他自定义单位的长度值|nvue 不支持|
```javascript
this.$refs.ani.step({
opacity: 1,
backgroundColor: '#ff5a5f',
widht:'100px',
height:'50rpx',
})
```
**旋转:**
旋转属性的值不需要填写单位
|属性名|值|说明|平台差异 |
|:-:|:-:|:-:|:-:|
|rotate|deg|deg的范围-180~180从原点顺时针旋转一个deg角度 |-|
|rotateX|deg|deg的范围-180~180在X轴旋转一个deg角度 |-|
|rotateY|deg|deg的范围-180~180在Y轴旋转一个deg角度 |-|
|rotateZ|deg|deg的范围-180~180在Z轴旋转一个deg角度 |nvue不支持|
|rotate3d|x,y,z,deg| 同 [transform-function rotate3d](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate3d()) |nvue不支持|
```javascript
this.$refs.ani.step({
rotateX: 45,
rotateY: 45
})
```
**缩放:**
|属性名|值|说明|平台差异|
|:-:|:-:|:-: |:-:|
|scale|sx,[sy]|一个参数时表示在X轴、Y轴同时缩放sx倍数两个参数时表示在X轴缩放sx倍数在Y轴缩放sy倍数|-|
|scaleX|sx|在X轴缩放sx倍数|-|
|scaleY|sy|在Y轴缩放sy倍数|-|
|scaleZ|sz|在Z轴缩放sy倍数|nvue不支持|
|scale3d|sx,sy,sz|在X轴缩放sx倍数在Y轴缩放sy倍数在Z轴缩放sz倍数|nvue不支持|
```javascript
this.$refs.ani.step({
scale: [1.2,0.8]
})
```
**偏移:**
|属性名|值|说明|平台差异|
|:-:|:-:|:-:|:-:|
|translate|tx,[ty]|一个参数时表示在X轴偏移tx单位px两个参数时表示在X轴偏移tx在Y轴偏移ty单位px。|-|
|translateX|tx| 在X轴偏移tx单位px|-|
|translateY|ty| 在Y轴偏移tx单位px|-|
|translateZ|tz| 在Z轴偏移tx单位px|nvue不支持|
|translate3d|tx,ty,tz| 在X轴偏移tx在Y轴偏移ty在Z轴偏移tz单位px|nvue不支持|
```javascript
this.$refs.ani.step({
translateX: '100px'
})
```
## 组件示例
点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/transition/transition](https://hellouniapp.dcloud.net.cn/pages/extUI/transition/transition)

View File

@ -0,0 +1,27 @@
## 1.0.112023-10-29
1. imgMode默认值改成aspectFit
## 1.0.102023-08-13
1. 优化nvue方便自定义图标
## 1.0.92023-07-28
1. 修改几个对应错误图标的BUG
## 1.0.82023-07-24
1. 优化 支持base64图片
## 1.0.72023-07-17
1. 修复 uv-icon 恢复uv-empty相关的图标
## 1.0.62023-07-13
1. 修复icon设置name属性对应图标错误的BUG
## 1.0.52023-07-04
1. 更新图标,删除一些不常用的图标
2. 删除base64修改成ttf文件引入读取图标
3. 自定义图标文档说明https://www.uvui.cn/guide/customIcon.html
## 1.0.42023-07-03
1. 修复主题颜色在APP不生效的BUG
## 1.0.32023-05-24
1. 将线上ttf字体包替换成base64避免加载时或者网络差时候显示白色方块
## 1.0.22023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.12023-05-10
1. 修复小程序中异常显示
## 1.0.02023-05-04
新发版

View File

@ -0,0 +1,160 @@
export default {
'uvicon-level': 'e68f',
'uvicon-checkbox-mark': 'e659',
'uvicon-folder': 'e694',
'uvicon-movie': 'e67c',
'uvicon-star-fill': 'e61e',
'uvicon-star': 'e618',
'uvicon-phone-fill': 'e6ac',
'uvicon-phone': 'e6ba',
'uvicon-apple-fill': 'e635',
'uvicon-backspace': 'e64d',
'uvicon-attach': 'e640',
'uvicon-empty-data': 'e671',
'uvicon-empty-address': 'e68a',
'uvicon-empty-favor': 'e662',
'uvicon-empty-car': 'e657',
'uvicon-empty-order': 'e66b',
'uvicon-empty-list': 'e672',
'uvicon-empty-search': 'e677',
'uvicon-empty-permission': 'e67d',
'uvicon-empty-news': 'e67e',
'uvicon-empty-history': 'e685',
'uvicon-empty-coupon': 'e69b',
'uvicon-empty-page': 'e60e',
'uvicon-empty-wifi-off': 'e6cc',
'uvicon-reload': 'e627',
'uvicon-order': 'e695',
'uvicon-server-man': 'e601',
'uvicon-search': 'e632',
'uvicon-more-dot-fill': 'e66f',
'uvicon-scan': 'e631',
'uvicon-map': 'e665',
'uvicon-map-fill': 'e6a8',
'uvicon-tags': 'e621',
'uvicon-tags-fill': 'e613',
'uvicon-eye': 'e664',
'uvicon-eye-fill': 'e697',
'uvicon-eye-off': 'e69c',
'uvicon-eye-off-outline': 'e688',
'uvicon-mic': 'e66d',
'uvicon-mic-off': 'e691',
'uvicon-calendar': 'e65c',
'uvicon-trash': 'e623',
'uvicon-trash-fill': 'e6ce',
'uvicon-play-left': 'e6bf',
'uvicon-play-right': 'e6b3',
'uvicon-minus': 'e614',
'uvicon-plus': 'e625',
'uvicon-info-circle': 'e69f',
'uvicon-info-circle-fill': 'e6a7',
'uvicon-question-circle': 'e622',
'uvicon-question-circle-fill': 'e6bc',
'uvicon-close': 'e65a',
'uvicon-checkmark': 'e64a',
'uvicon-checkmark-circle': 'e643',
'uvicon-checkmark-circle-fill': 'e668',
'uvicon-setting': 'e602',
'uvicon-setting-fill': 'e6d0',
'uvicon-heart': 'e6a2',
'uvicon-heart-fill': 'e68b',
'uvicon-camera': 'e642',
'uvicon-camera-fill': 'e650',
'uvicon-more-circle': 'e69e',
'uvicon-more-circle-fill': 'e684',
'uvicon-chat': 'e656',
'uvicon-chat-fill': 'e63f',
'uvicon-bag': 'e647',
'uvicon-error-circle': 'e66e',
'uvicon-error-circle-fill': 'e655',
'uvicon-close-circle': 'e64e',
'uvicon-close-circle-fill': 'e666',
'uvicon-share': 'e629',
'uvicon-share-fill': 'e6bb',
'uvicon-share-square': 'e6c4',
'uvicon-shopping-cart': 'e6cb',
'uvicon-shopping-cart-fill': 'e630',
'uvicon-bell': 'e651',
'uvicon-bell-fill': 'e604',
'uvicon-list': 'e690',
'uvicon-list-dot': 'e6a9',
'uvicon-zhifubao-circle-fill': 'e617',
'uvicon-weixin-circle-fill': 'e6cd',
'uvicon-weixin-fill': 'e620',
'uvicon-qq-fill': 'e608',
'uvicon-qq-circle-fill': 'e6b9',
'uvicon-moments-circel-fill': 'e6c2',
'uvicon-moments': 'e6a0',
'uvicon-car': 'e64f',
'uvicon-car-fill': 'e648',
'uvicon-warning-fill': 'e6c7',
'uvicon-warning': 'e6c1',
'uvicon-clock-fill': 'e64b',
'uvicon-clock': 'e66c',
'uvicon-edit-pen': 'e65d',
'uvicon-edit-pen-fill': 'e679',
'uvicon-email': 'e673',
'uvicon-email-fill': 'e683',
'uvicon-minus-circle': 'e6a5',
'uvicon-plus-circle': 'e603',
'uvicon-plus-circle-fill': 'e611',
'uvicon-file-text': 'e687',
'uvicon-file-text-fill': 'e67f',
'uvicon-pushpin': 'e6d1',
'uvicon-pushpin-fill': 'e6b6',
'uvicon-grid': 'e68c',
'uvicon-grid-fill': 'e698',
'uvicon-play-circle': 'e6af',
'uvicon-play-circle-fill': 'e62a',
'uvicon-pause-circle-fill': 'e60c',
'uvicon-pause': 'e61c',
'uvicon-pause-circle': 'e696',
'uvicon-gift-fill': 'e6b0',
'uvicon-gift': 'e680',
'uvicon-kefu-ermai': 'e660',
'uvicon-server-fill': 'e610',
'uvicon-coupon-fill': 'e64c',
'uvicon-coupon': 'e65f',
'uvicon-integral': 'e693',
'uvicon-integral-fill': 'e6b1',
'uvicon-home-fill': 'e68e',
'uvicon-home': 'e67b',
'uvicon-account': 'e63a',
'uvicon-account-fill': 'e653',
'uvicon-thumb-down-fill': 'e628',
'uvicon-thumb-down': 'e60a',
'uvicon-thumb-up': 'e612',
'uvicon-thumb-up-fill': 'e62c',
'uvicon-lock-fill': 'e6a6',
'uvicon-lock-open': 'e68d',
'uvicon-lock-opened-fill': 'e6a1',
'uvicon-lock': 'e69d',
'uvicon-red-packet': 'e6c3',
'uvicon-photo-fill': 'e6b4',
'uvicon-photo': 'e60d',
'uvicon-volume-off-fill': 'e6c8',
'uvicon-volume-off': 'e6bd',
'uvicon-volume-fill': 'e624',
'uvicon-volume': 'e605',
'uvicon-download': 'e670',
'uvicon-arrow-up-fill': 'e636',
'uvicon-arrow-down-fill': 'e638',
'uvicon-play-left-fill': 'e6ae',
'uvicon-play-right-fill': 'e6ad',
'uvicon-arrow-downward': 'e634',
'uvicon-arrow-leftward': 'e63b',
'uvicon-arrow-rightward': 'e644',
'uvicon-arrow-upward': 'e641',
'uvicon-arrow-down': 'e63e',
'uvicon-arrow-right': 'e63c',
'uvicon-arrow-left': 'e646',
'uvicon-arrow-up': 'e633',
'uvicon-skip-back-left': 'e6c5',
'uvicon-skip-forward-right': 'e61f',
'uvicon-arrow-left-double': 'e637',
'uvicon-man': 'e675',
'uvicon-woman': 'e626',
'uvicon-en': 'e6b8',
'uvicon-twitte': 'e607',
'uvicon-twitter-circle-fill': 'e6cf'
}

View File

@ -0,0 +1,90 @@
export default {
props: {
// 图标类名
name: {
type: String,
default: ''
},
// 图标颜色,可接受主题色
color: {
type: String,
default: '#606266'
},
// 字体大小单位px
size: {
type: [String, Number],
default: '16px'
},
// 是否显示粗体
bold: {
type: Boolean,
default: false
},
// 点击图标的时候传递事件出去的index用于区分点击了哪一个
index: {
type: [String, Number],
default: null
},
// 触摸图标时的类名
hoverClass: {
type: String,
default: ''
},
// 自定义扩展前缀,方便用户扩展自己的图标库
customPrefix: {
type: String,
default: 'uvicon'
},
// 图标右边或者下面的文字
label: {
type: [String, Number],
default: ''
},
// label的位置只能右边或者下边
labelPos: {
type: String,
default: 'right'
},
// label的大小
labelSize: {
type: [String, Number],
default: '15px'
},
// label的颜色
labelColor: {
type: String,
default: '#606266'
},
// label与图标的距离
space: {
type: [String, Number],
default: '3px'
},
// 图片的mode
imgMode: {
type: String,
default: 'aspectFit'
},
// 用于显示图片小图标时,图片的宽度
width: {
type: [String, Number],
default: ''
},
// 用于显示图片小图标时,图片的高度
height: {
type: [String, Number],
default: ''
},
// 用于解决某些情况下,让图标垂直居中的用途
top: {
type: [String, Number],
default: 0
},
// 是否阻止事件传播
stop: {
type: Boolean,
default: false
},
...uni.$uv?.props?.icon
}
}

View File

@ -0,0 +1,226 @@
<template>
<view
class="uv-icon"
@tap="clickHandler"
:class="['uv-icon--' + labelPos]"
>
<image
class="uv-icon__img"
v-if="isImg"
:src="name"
:mode="imgMode"
:style="[imgStyle, $uv.addStyle(customStyle)]"
></image>
<text
v-else
class="uv-icon__icon"
:class="uClasses"
:style="[iconStyle, $uv.addStyle(customStyle)]"
:hover-class="hoverClass"
>{{icon}}</text>
<!-- 这里进行空字符串判断如果仅仅是v-if="label"可能会出现传递0的时候结果也无法显示 -->
<text
v-if="label !== ''"
class="uv-icon__label"
:style="{
color: labelColor,
fontSize: $uv.addUnit(labelSize),
marginLeft: labelPos == 'right' ? $uv.addUnit(space) : 0,
marginTop: labelPos == 'bottom' ? $uv.addUnit(space) : 0,
marginRight: labelPos == 'left' ? $uv.addUnit(space) : 0,
marginBottom: labelPos == 'top' ? $uv.addUnit(space) : 0
}"
>{{ label }}</text>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
// #ifdef APP-NVUE
// nvueweexdom
// https://weex.apache.org/zh/docs/modules/dom.html#addrule
import iconUrl from './uvicons.ttf';
const domModule = weex.requireModule('dom')
domModule.addRule('fontFace', {
'fontFamily': "uvicon-iconfont",
'src': "url('" + iconUrl + "')"
})
// #endif
// unicode
import icons from './icons';
import props from './props.js';
/**
* icon 图标
* @description 基于字体的图标集包含了大多数常见场景的图标
* @tutorial https://www.uvui.cn/components/icon.html
* @property {String} name 图标名称见示例图标集
* @property {String} color 图标颜色,可接受主题色 默认 color['uv-content-color']
* @property {String | Number} size 图标字体大小单位px 默认 '16px'
* @property {Boolean} bold 是否显示粗体 默认 false
* @property {String | Number} index 点击图标的时候传递事件出去的index用于区分点击了哪一个
* @property {String} hoverClass 图标按下去的样式类用法同uni的view组件的hoverClass参数详情见官网
* @property {String} customPrefix 自定义扩展前缀方便用户扩展自己的图标库 默认 'uicon'
* @property {String | Number} label 图标右侧的label文字
* @property {String} labelPos label相对于图标的位置只能right或bottom 默认 'right'
* @property {String | Number} labelSize label字体大小单位px 默认 '15px'
* @property {String} labelColor 图标右侧的label文字颜色 默认 color['uv-content-color']
* @property {String | Number} space label与图标的距离单位px 默认 '3px'
* @property {String} imgMode 图片的mode
* @property {String | Number} width 显示图片小图标时的宽度
* @property {String | Number} height 显示图片小图标时的高度
* @property {String | Number} top 图标在垂直方向上的定位 用于解决某些情况下让图标垂直居中的用途 默认 0
* @property {Boolean} stop 是否阻止事件传播 默认 false
* @property {Object} customStyle icon的样式对象形式
* @event {Function} click 点击图标时触发
* @event {Function} touchstart 事件触摸时触发
* @example <uv-icon name="photo" color="#2979ff" size="28"></uv-icon>
*/
export default {
name: 'uv-icon',
emits: ['click'],
mixins: [mpMixin, mixin, props],
data() {
return {
colorType: [
'primary',
'success',
'info',
'error',
'warning'
]
}
},
computed: {
uClasses() {
let classes = []
classes.push(this.customPrefix)
classes.push(this.customPrefix + '-' + this.name)
//
if (this.color && this.colorType.includes(this.color)) classes.push('uv-icon__icon--' + this.color)
// 使[a, b, c]
//
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(' ')
//#endif
return classes
},
iconStyle() {
let style = {}
style = {
fontSize: this.$uv.addUnit(this.size),
lineHeight: this.$uv.addUnit(this.size),
fontWeight: this.bold ? 'bold' : 'normal',
//
top: this.$uv.addUnit(this.top)
}
//
if (this.color && !this.colorType.includes(this.color)) style.color = this.color
return style
},
// name"/"
isImg() {
const isBase64 = this.name.indexOf('data:') > -1 && this.name.indexOf('base64') > -1;
return this.name.indexOf('/') !== -1 || isBase64;
},
imgStyle() {
let style = {}
// widthheight使使size
style.width = this.width ? this.$uv.addUnit(this.width) : this.$uv.addUnit(this.size)
style.height = this.height ? this.$uv.addUnit(this.height) : this.$uv.addUnit(this.size)
return style
},
//
icon() {
// nameunicode
const code = icons['uvicon-' + this.name];
// #ifdef APP-NVUE
if(!code) {
return code ? unescape(`%u${code}`) : ['uvicon'].indexOf(this.customPrefix) > -1 ? unescape(`%u${this.name}`) : '';
}
// #endif
return code ? unescape(`%u${code}`) : ['uvicon'].indexOf(this.customPrefix) > -1 ? this.name : '';
}
},
methods: {
clickHandler(e) {
this.$emit('click', this.index)
//
this.stop && this.preventEvent(e)
}
}
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
//
$uv-icon-primary: $uv-primary !default;
$uv-icon-success: $uv-success !default;
$uv-icon-info: $uv-info !default;
$uv-icon-warning: $uv-warning !default;
$uv-icon-error: $uv-error !default;
$uv-icon-label-line-height: 1 !default;
/* #ifndef APP-NVUE */
// nvue
@font-face {
font-family: 'uvicon-iconfont';
src: url('./uvicons.ttf') format('truetype');
}
/* #endif */
.uv-icon {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
font-family: uvicon-iconfont;
position: relative;
@include flex;
align-items: center;
&--primary {
color: $uv-icon-primary;
}
&--success {
color: $uv-icon-success;
}
&--error {
color: $uv-icon-error;
}
&--warning {
color: $uv-icon-warning;
}
&--info {
color: $uv-icon-info;
}
}
&__img {
/* #ifndef APP-NVUE */
height: auto;
will-change: transform;
/* #endif */
}
&__label {
/* #ifndef APP-NVUE */
line-height: $uv-icon-label-line-height;
/* #endif */
}
}
</style>

Binary file not shown.

View File

@ -0,0 +1,83 @@
{
"id": "uv-icon",
"displayName": "uv-icon 图标 全面兼容vue3+2、app、h5、小程序等多端",
"version": "1.0.11",
"description": "基于字体的图标集,包含了大多数常见场景的图标,支持自定义,支持自定义图片图标等。可自定义颜色、大小。",
"keywords": [
"uv-ui,uvui,uv-icon,icon,图标,字体图标"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
## uv-icon 图标库
> **组件名uv-icon**
基于字体的图标集,包含了大多数常见场景的图标,支持自定义,支持自定义图片图标等。
# <a href="https://www.uvui.cn/components/icon.html" target="_blank">查看文档</a>
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui)
### [更多插件请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png)
#### 如使用过程中有任何问题反馈或者您对uv-ui有一些好的建议欢迎加入uv-ui官方交流群<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

View File

@ -0,0 +1,11 @@
## 1.0.42023-10-13
1. 优化
## 1.0.32023-10-13
1. unmounted兼容vue3
## 1.0.22023-05-27
1. 不支持抖音小程序说明
## 1.0.12023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.02023-05-10
uv-swipe-action 滑动单元格

View File

@ -0,0 +1,256 @@
/**
* 此为wxs模块只支持APP-VUE微信和QQ小程序以及H5平台
* wxs内部不支持es6语法变量只能使用var定义无法使用解构箭头函数等特性
*/
// 开始触摸
function touchstart(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照此快照是属于整个组件的在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disable) return
var touches = event.touches
// 如果进行的是多指触控,不允许进行操作
if (touches && touches.length > 1) return
// 标识当前为滑动中状态
state.moving = true
// 记录触摸开始点的坐标值
state.startX = touches[0].pageX
state.startY = touches[0].pageY
}
// 触摸滑动
function touchmove(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (state.disabled || !state.moving) return
var touches = event.touches
var pageX = touches[0].pageX
var pageY = touches[0].pageY
var moveX = pageX - state.startX
var moveY = pageY - state.startY
var buttonsWidth = state.buttonsWidth
// 移动的X轴距离大于Y轴距离也即终点与起点位置连线与X轴夹角小于45度时禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > state.threshold) {
event.preventDefault()
event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离也即终点位置与起点位置连线与Y轴夹角小于45度时认为是页面上下滑动而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离不允许内容部分往右偏移右滑会导致X轴偏移值大于0以此做判断
// 此处不能直接return因为滑动过程中会缺失某些关键点坐标会导致错乱最好的办法就是
// 在超出后设置为0
if (state.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
moveSwipeAction(-buttonsWidth + moveX, instance, ownerInstance)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动菜单的内容部分,从而使隐藏的菜单显示出来
moveSwipeAction(moveX, instance, ownerInstance)
}
}
// 触摸结束
function touchend(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (!state.moving || state.disabled) return
var touches = event.changedTouches ? event.changedTouches[0] : {}
var pageX = touches.pageX
var pageY = touches.pageY
var moveX = pageX - state.startX
if (state.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (moveX < 0) return
// 在开启状态下点击一下内容区域moveX为0也即没有进行移动这时执行收起菜单逻辑
if (moveX === 0) {
return closeSwipeAction(instance, ownerInstance)
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(moveX) < state.threshold) {
openSwipeAction(instance, ownerInstance)
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
closeSwipeAction(instance, ownerInstance)
}
} else {
// 在关闭的状态下,右滑,无需操作
if (moveX > 0) return
// 理由同上
if (Math.abs(moveX) < state.threshold) {
closeSwipeAction(instance, ownerInstance)
} else {
openSwipeAction(instance, ownerInstance)
}
}
}
// 获取过渡时间
function getDuration(value) {
if (value.toString().indexOf('s') >= 0) return value
return value > 30 ? value + 'ms' : value + 's'
}
// 滑动结束时判断滑动的方向
function getMoveDirection(instance, ownerInstance) {
var state = instance.getState()
}
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
function moveSwipeAction(moveX, instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
var previewButtonsMoveX = 0
// 设置菜单内容部分的偏移
instance.requestAnimationFrame(function() {
instance.setStyle({
// 设置translateX的值
'transition': 'none',
transform: 'translateX(' + moveX + 'px)',
'-webkit-transform': 'translateX(' + moveX + 'px)'
})
// 折叠按钮动画
for (var i = len - 1; i >= 0; i--) {
// 通过比例,得出元素自身该移动的距离
var translateX = state.buttons[i].width / state.buttonsWidth * moveX
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
var realTranslateX = translateX + previewButtonsMoveX
buttons[i].setStyle({
// 在移动期间,不能使用过渡效果,否则会造成卡顿,本质原因是每次移动一点,就要花一定时间去过渡这个过程
'transition': 'none',
'transform': 'translateX(' + realTranslateX + 'px)',
'-webkit-transform': 'translateX(' + realTranslateX + 'px)'
})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
})
}
// 一次性展开滑动菜单
function openSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
const duration = getDuration(state.duration)
// 展开过程中是向左移动所以X的偏移应该为负值
var buttonsWidth = -state.buttonsWidth
var previewButtonsMoveX = 0
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(' + buttonsWidth + 'px)',
'-webkit-transform': 'translateX(' + buttonsWidth + 'px)',
})
// 设置各个隐藏的按钮为展开的状态
for (var i = len - 1; i >= 0; i--) {
// 通过比例,得出元素自身该移动的距离
var translateX = state.buttons[i].width / state.buttonsWidth * buttonsWidth
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
var realTranslateX = translateX + previewButtonsMoveX
buttons[i].setStyle({
// 在移动期间,需要加上动画效果
'transition': 'transform ' + duration,
'transform': 'translateX(' + realTranslateX + 'px)',
'-webkit-transform': 'translateX(' + realTranslateX + 'px)'
})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
})
setStatus('open', instance)
}
// 标记菜单的当前状态open-已经打开close-已经关闭
function setStatus(status, instance) {
var state = instance.getState()
state.status = status
}
// 一次性收起滑动菜单
function closeSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
const duration = getDuration(state.duration)
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
// 设置各个隐藏的按钮为收起的状态
for (var i = len - 1; i >= 0; i--) {
buttons[i].setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
}
})
setStatus('close', instance)
}
// show的状态发生变化
function showChange(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
if (state.disabled) return
// 打开或关闭单元格
if (newValue) {
openSwipeAction(instance, ownerInstance)
} else {
closeSwipeAction(instance, ownerInstance)
}
}
// 菜单尺寸发生变化
function sizeChange(newValue, oldValue, ownerInstance, instance) {
// wxs内的局部变量快照
var state = instance.getState()
state.disabled = newValue.disabled
state.duration = newValue.duration
state.show = newValue.show
state.threshold = newValue.threshold
state.buttons = newValue.buttons
var len = state.buttons.length
if (len) {
var buttonsWidth = 0
var buttons = newValue.buttons
for (var i = 0; i < len; i++) {
buttonsWidth += buttons[i].width
}
}
state.buttonsWidth = buttonsWidth
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend,
sizeChange: sizeChange
}

View File

@ -0,0 +1,225 @@
/**
* 此为wxs模块只支持APP-VUE微信和QQ小程序以及H5平台
* wxs内部不支持es6语法变量只能使用var定义无法使用解构箭头函数等特性
*/
// 开始触摸
function touchstart(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照此快照是属于整个组件的在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disabled) return
var touches = event.touches
// 如果进行的是多指触控,不允许进行操作
if (touches && touches.length > 1) return
// 标识当前为滑动中状态
state.moving = true
// 记录触摸开始点的坐标值
state.startX = touches[0].pageX
state.startY = touches[0].pageY
ownerInstance.callMethod('closeOther')
}
// 触摸滑动
function touchmove(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (state.disabled || !state.moving) return
var touches = event.touches
var pageX = touches[0].pageX
var pageY = touches[0].pageY
var moveX = pageX - state.startX
var moveY = pageY - state.startY
var buttonsWidth = state.buttonsWidth
// 移动的X轴距离大于Y轴距离也即终点与起点位置连线与X轴夹角小于45度时禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > state.threshold) {
event.preventDefault && event.preventDefault()
event.stopPropagation && event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离也即终点位置与起点位置连线与Y轴夹角小于45度时认为是页面上下滑动而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离不允许内容部分往右偏移右滑会导致X轴偏移值大于0以此做判断
// 此处不能直接return因为滑动过程中会缺失某些关键点坐标会导致错乱最好的办法就是
// 在超出后设置为0
if (state.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
moveSwipeAction(-buttonsWidth + moveX, instance, ownerInstance)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动单元格内容部分,从而使隐藏的菜单显示出来
moveSwipeAction(moveX, instance, ownerInstance)
}
}
// 触摸结束
function touchend(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (!state.moving || state.disabled) return
var touches = event.changedTouches ? event.changedTouches[0] : {}
var pageX = touches.pageX
var pageY = touches.pageY
var moveX = pageX - state.startX
if (state.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (moveX < 0) return
// 在开启状态下点击一下内容区域moveX为0也即没有进行移动这时执行收起菜单逻辑
if (moveX === 0) {
return closeSwipeAction(instance, ownerInstance)
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(moveX) < state.threshold) {
openSwipeAction(instance, ownerInstance)
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
closeSwipeAction(instance, ownerInstance)
}
} else {
// 在关闭的状态下,右滑,无需操作
if (moveX > 0) return
// 理由同上
if (Math.abs(moveX) < state.threshold) {
closeSwipeAction(instance, ownerInstance)
} else {
openSwipeAction(instance, ownerInstance)
}
}
}
// 获取过渡时间
function getDuration(value) {
if (value.toString().indexOf('s') >= 0) return value
return value > 30 ? value + 'ms' : value + 's'
}
// 滑动结束时判断滑动的方向
function getMoveDirection(instance, ownerInstance) {
var state = instance.getState()
}
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
function moveSwipeAction(moveX, instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
// 设置菜单内容部分的偏移
instance.requestAnimationFrame(function() {
instance.setStyle({
// 设置translateX的值
'transition': 'none',
transform: 'translateX(' + moveX + 'px)',
'-webkit-transform': 'translateX(' + moveX + 'px)'
})
})
}
// 一次性展开滑动菜单
function openSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
// 处理duration单位问题
var duration = getDuration(state.duration)
// 展开过程中是向左移动所以X的偏移应该为负值
var buttonsWidth = -state.buttonsWidth
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(' + buttonsWidth + 'px)',
'-webkit-transform': 'translateX(' + buttonsWidth + 'px)',
})
})
setStatus('open', instance, ownerInstance)
}
// 标记菜单的当前状态open-已经打开close-已经关闭
function setStatus(status, instance, ownerInstance) {
var state = instance.getState()
state.status = status
ownerInstance.callMethod('setState', status)
}
// 一次性收起滑动菜单
function closeSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
var duration = getDuration(state.duration)
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
// 设置各个隐藏的按钮为收起的状态
for (var i = len - 1; i >= 0; i--) {
buttons[i].setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
}
})
setStatus('close', instance, ownerInstance)
}
// status的状态发生变化
function statusChange(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
if (state.disabled) return
// 打开或关闭单元格
if (newValue === 'close' && state.status === 'open') {
closeSwipeAction(instance, ownerInstance)
} else if(newValue === 'open' && state.status === 'close') {
openSwipeAction(instance, ownerInstance)
}
}
// 菜单尺寸发生变化
function sizeChange(newValue, oldValue, ownerInstance, instance) {
// wxs内的局部变量快照
var state = instance.getState()
state.disabled = newValue.disabled
state.duration = newValue.duration
state.show = newValue.show
state.threshold = newValue.threshold
state.buttons = newValue.buttons
if (state.buttons) {
var len = state.buttons.length
var buttonsWidth = 0
var buttons = newValue.buttons
for (var i = 0; i < len; i++) {
buttonsWidth += buttons[i].width
}
}
state.buttonsWidth = buttonsWidth
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend,
sizeChange: sizeChange,
statusChange: statusChange
}

View File

@ -0,0 +1,264 @@
// nvue操作dom的库用于获取dom的尺寸信息
const dom = uni.requireNativePlugin('dom')
// nvue中用于操作元素动画的库类似于uni.animation只不过uni.animation不能用于nvue
const animation = uni.requireNativePlugin('animation')
import { sleep } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
export default {
data() {
return {
// 是否滑动中
moving: false,
// 状态open-打开状态close-关闭状态
status: 'close',
// 开始触摸点的X和Y轴坐标
startX: 0,
startY: 0,
// 所有隐藏按钮的尺寸信息数组
buttons: [],
// 所有按钮的总宽度
buttonsWidth: 0,
// 记录上一次移动的位置值
moveX: 0,
// 记录上一次滑动的位置,用于前后两次做对比,如果移动的距离小于某一阈值,则认为前后之间没有移动,为了解决可能存在的通信阻塞问题
lastX: 0
}
},
computed: {
// 获取过渡时间
getDuratin() {
let duration = String(this.duration)
// 如果ms为单位返回ms的数值部分
if (duration.indexOf('ms') >= 0) return parseInt(duration)
// 如果s为单位为了得到ms的数值需要乘以1000
if (duration.indexOf('s') >= 0) return parseInt(duration) * 1000
// 如果值传了数值且小于30认为是s单位
duration = Number(duration)
return duration < 30 ? duration * 1000 : duration
}
},
watch: {
show: {
immediate: true,
handler(n) {
}
}
},
mounted() {
sleep(20).then(() => {
this.queryRect()
})
},
methods: {
close() {
this.closeSwipeAction()
},
// 触摸单元格
touchstart(event) {
if (this.disabled) return
this.closeOther()
const { touches } = event
// 记录触摸开始点的坐标值
this.startX = touches[0].pageX
this.startY = touches[0].pageY
},
// // 触摸滑动
touchmove(event) {
if (this.disabled) return
const { touches } = event
const { pageX } = touches[0]
const { pageY } = touches[0]
let moveX = pageX - this.startX
const moveY = pageY - this.startY
const { buttonsWidth } = this
const len = this.buttons.length
// 判断前后两次的移动距离,如果小于一定值,则不进行移动处理
if (Math.abs(pageX - this.lastX) < 0.3) return
this.lastX = pageX
// 移动的X轴距离大于Y轴距离也即终点与起点位置连线与X轴夹角小于45度时禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > this.threshold) {
event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离也即终点位置与起点位置连线与Y轴夹角小于45度时认为是页面上下滑动而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离不允许内容部分往右偏移右滑会导致X轴偏移值大于0以此做判断
// 此处不能直接return因为滑动过程中会缺失某些关键点坐标会导致错乱最好的办法就是
// 在超出后设置为0
if (this.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
this.moveSwipeAction(-buttonsWidth + moveX)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动菜单的内容部分,从而使隐藏的菜单显示出来
this.moveSwipeAction(moveX)
}
},
// 单元格结束触摸
touchend(event) {
if (this.disabled) return
const touches = event.changedTouches ? event.changedTouches[0] : {}
const { pageX } = touches
const { pageY } = touches
const { buttonsWidth } = this
this.moveX = pageX - this.startX
if (this.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (this.moveX < 0) this.moveX = 0
if (this.moveX > buttonsWidth) this.moveX = buttonsWidth
// 在开启状态下点击一下内容区域moveX为0也即没有进行移动这时执行收起菜单逻辑
if (this.moveX === 0) {
return this.closeSwipeAction()
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(this.moveX) < this.threshold) {
this.openSwipeAction()
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
this.closeSwipeAction()
}
} else {
// 在关闭的状态下,右滑,无需操作
if (this.moveX >= 0) this.moveX = 0
if (this.moveX <= -buttonsWidth) this.moveX = -buttonsWidth
// 理由同上
if (Math.abs(this.moveX) < this.threshold) {
this.closeSwipeAction()
} else {
this.openSwipeAction()
}
}
},
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
moveSwipeAction(moveX) {
if (this.moving) return
this.moving = true
let previewButtonsMoveX = 0
const len = this.buttons.length
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: `translateX(${moveX}px)`
},
timingFunction: 'linear'
}, () => {
this.moving = false
})
// 按钮的组的长度
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 通过比例,得出元素自身该移动的距离
const translateX = this.buttons[i].width / this.buttonsWidth * moveX
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
const realTranslateX = translateX + previewButtonsMoveX
animation.transition(buttonRef, {
styles: {
transform: `translateX(${realTranslateX}px)`
},
duration: 0,
delay: 0,
timingFunction: 'linear'
}, () => {})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
},
// 关闭菜单
closeSwipeAction() {
if (this.status === 'close') return
this.moving = true
const { buttonsWidth } = this
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: 'translateX(0px)'
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {
this.status = 'close'
this.moving = false
this.closeHandler()
})
// 按钮的组的长度
const len = this.buttons.length
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 如果不满足边界条件,返回
if (this.buttons.length === 0 || !this.buttons[i] || !this.buttons[i].width) return
animation.transition(buttonRef, {
styles: {
transform: 'translateX(0px)'
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {})
}
},
// 打开菜单
openSwipeAction() {
if (this.status === 'open') return
this.moving = true
const buttonsWidth = -this.buttonsWidth
let previewButtonsMoveX = 0
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: `translateX(${buttonsWidth}px)`
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {
this.status = 'open'
this.moving = false
this.openHandler()
})
// 按钮的组的长度
const len = this.buttons.length
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 如果不满足边界条件,返回
if (this.buttons.length === 0 || !this.buttons[i] || !this.buttons[i].width) return
// 通过比例,得出元素自身该移动的距离
const translateX = this.buttons[i].width / this.buttonsWidth * buttonsWidth
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
const realTranslateX = translateX + previewButtonsMoveX
animation.transition(buttonRef, {
styles: {
transform: `translateX(${realTranslateX}px)`
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {})
previewButtonsMoveX += translateX
}
},
// 查询按钮节点信息
queryRect() {
// 历遍所有按钮数组通过getRectByDom返回一个promise
const promiseAll = this.rightOptions.map((item, index) => this.getRectByDom(this.$refs[`uv-swipe-action-item__right__button-${index}`][0]))
// 通过promise.all方法让所有按钮的查询结果返回一个数组的形式
Promise.all(promiseAll).then((sizes) => {
this.buttons = sizes
// 计算所有按钮总宽度
this.buttonsWidth = sizes.reduce((sum, cur) => sum + cur.width, 0)
})
},
// 通过nvue的dom模块查询节点信息
getRectByDom(ref) {
return new Promise((resolve) => {
dom.getComponentRect(ref, (res) => {
resolve(res.size)
})
})
}
}
}

View File

@ -0,0 +1,182 @@
// nvue操作dom的库用于获取dom的尺寸信息
const dom = uni.requireNativePlugin('dom');
const bindingX = uni.requireNativePlugin('bindingx');
const animation = uni.requireNativePlugin('animation');
import { getDuration, getPx } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
export default {
data() {
return {
// 所有按钮的总宽度
buttonsWidth: 0,
// 是否正在移动中
moving: false
}
},
computed: {
// 获取过渡时间
getDuratin() {
let duration = String(this.duration)
// 如果ms为单位返回ms的数值部分
if (duration.indexOf('ms') >= 0) return parseInt(duration)
// 如果s为单位为了得到ms的数值需要乘以1000
if (duration.indexOf('s') >= 0) return parseInt(duration) * 1000
// 如果值传了数值且小于30认为是s单位
duration = Number(duration)
return duration < 30 ? duration * 1000 : duration
}
},
watch: {
show(n) {
if(n) {
this.moveCellByAnimation('open')
} else {
this.moveCellByAnimation('close')
}
}
},
mounted() {
setTimeout(()=>{
this.initialize()
},20)
},
methods: {
initialize() {
this.queryRect()
},
// 关闭单元格,用于打开一个,自动关闭其他单元格的场景
closeHandler() {
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
},
// 点击单元格
clickHandler() {
// 如果在移动中被点击,进行忽略
if(this.moving) return
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
},
// 滑动单元格
onTouchstart(e) {
// 如果当前正在移动中或者disabled状态则返回
if(this.moving || this.disabled) {
return this.unbindBindingX()
}
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
// 特殊情况下e可能不为一个对象
e?.stopPropagation && e.stopPropagation()
e?.preventDefault && e.preventDefault()
this.moving = true
// 获取元素ref
const content = this.getContentRef()
let expression = `min(max(${-this.buttonsWidth}, x), 0)`
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
// 阿里为了KPI而开源的BindingX
this.panEvent = bindingX.bind({
anchor: content,
eventType: 'pan',
props: [{
element: content,
// 绑定width属性设置其宽度值
property: 'transform.translateX',
expression
}]
}, (res) => {
this.moving = false
if (res.state === 'end' || res.state === 'exit') {
const deltaX = res.deltaX
if(deltaX <= -this.buttonsWidth || deltaX >= 0) {
// 如果触摸滑动的过程中大于单元格的总宽度或者大于0意味着已经动过滑动达到了打开或者关闭的状态
// 这里直接进行状态的标记
this.$nextTick(() => {
this.status = deltaX <= -this.buttonsWidth ? 'open' : 'close'
})
} else if(Math.abs(deltaX) > getPx(this.threshold)) {
// 在移动大于阈值、并且小于总按钮宽度时,进行自动打开或者关闭
// 移动距离大于0时意味着需要关闭状态
if(Math.abs(deltaX) < this.buttonsWidth) {
this.moveCellByAnimation(deltaX > 0 ? 'close' : 'open')
}
} else {
// 在小于阈值时,进行关闭操作(如果在打开状态下将不会执行bindingX)
this.moveCellByAnimation('close')
}
}
})
},
// 释放bindingX
unbindBindingX() {
// 释放上一次的资源
if (this?.panEvent?.token != 0) {
bindingX.unbind({
token: this.panEvent?.token,
// pan为手势事件
eventType: 'pan'
})
}
},
// 查询按钮节点信息
queryRect() {
// 历遍所有按钮数组通过getRectByDom返回一个promise
const promiseAll = this.options.map(async(item, index) => {
return await this.getRectByDom(this.$refs[`uv-swipe-action-item__right__button-${index}`][0])
})
// 通过promise.all方法让所有按钮的查询结果返回一个数组的形式
Promise.all(promiseAll).then(sizes => {
this.buttons = sizes
// 计算所有按钮总宽度
this.buttonsWidth = sizes.reduce((sum, cur) => sum + cur.width, 0)
})
},
// 通过nvue的dom模块查询节点信息
getRectByDom(ref) {
return new Promise(resolve => {
dom.getComponentRect(ref, res => {
resolve(res.size)
})
})
},
// 移动单元格到左边或者右边尽头
moveCellByAnimation(status = 'open') {
if(this.moving) return
// 标识当前状态
this.moveing = true
const content = this.getContentRef()
const x = status === 'open' ? -this.buttonsWidth : 0
animation.transition(content, {
styles: {
transform: `translateX(${x}px)`,
},
duration: getDuration(this.duration, false),
timingFunction: 'ease-in-out'
}, () => {
this.moving = false
this.status = status
this.unbindBindingX()
})
},
// 获取元素ref
getContentRef() {
return this.$refs['uv-swipe-action-item__content'].ref
}
},
// #ifdef VUE2
beforeDestroy() {
this.unbindBindingX()
},
// #endif
// #ifdef VUE3
unmounted() {
this.unbindBindingX()
}
// #endif
}

View File

@ -0,0 +1,40 @@
export default {
props: {
// 控制打开或者关闭
show: {
type: Boolean,
default: false
},
// 标识符如果是v-for可用index索引值
name: {
type: [String, Number],
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
},
// 滑动距离阈值,只有大于此值,才被认为是要打开菜单
threshold: {
type: Number,
default: 20
},
// 右侧按钮内容
options: {
type: Array,
default: () => []
},
// 动画过渡时间单位ms
duration: {
type: [String, Number],
default: 300
},
...uni.$uv?.props?.swipeActionItem
}
}

View File

@ -0,0 +1,200 @@
<template>
<view class="uv-swipe-action-item" ref="uv-swipe-action-item">
<view class="uv-swipe-action-item__right">
<slot name="button">
<view v-for="(item,index) in options" :key="index" class="uv-swipe-action-item__right__button"
:ref="`uv-swipe-action-item__right__button-${index}`" :style="[{
alignItems: item.style && item.style.borderRadius ? 'center' : 'stretch'
}]" @tap="buttonClickHandler(item, index)">
<view class="uv-swipe-action-item__right__button__wrapper" :style="[{
backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
borderRadius: item.style && item.style.borderRadius ? item.style.borderRadius : '0',
padding: item.style && item.style.borderRadius ? '0' : '0 15px',
}, item.style]">
<uv-icon v-if="item.icon" :name="item.icon"
:color="item.style && item.style.color ? item.style.color : '#ffffff'"
:size="item.iconSize ? $uv.addUnit(item.iconSize) : item.style && item.style.fontSize ? $uv.getPx(item.style.fontSize) * 1.2 : 17"
:customStyle="{
marginRight: item.text ? '2px' : 0
}"></uv-icon>
<text v-if="item.text" class="uv-swipe-action-item__right__button__wrapper__text uv-line-1"
:style="[{
color: item.style && item.style.color ? item.style.color : '#ffffff',
fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px',
lineHeight: item.style && item.style.fontSize ? item.style.fontSize : '16px',
}]">{{ item.text }}</text>
</view>
</view>
</slot>
</view>
<!-- #ifndef APP-NVUE -->
<view class="uv-swipe-action-item__content" @touchstart="wxs.touchstart" @touchmove="wxs.touchmove"
@touchend="wxs.touchend" :status="status" :change:status="wxs.statusChange" :size="size"
:change:size="wxs.sizeChange">
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view class="uv-swipe-action-item__content" ref="uv-swipe-action-item__content" @panstart="onTouchstart"
@tap="clickHandler">
<!-- #endif -->
<slot />
</view>
</view>
</template>
<!-- #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ -->
<script src="./index.wxs" module="wxs" lang="wxs"></script>
<!-- #endif -->
<script>
import touch from '@/uni_modules/uv-ui-tools/libs/mixin/touch.js'
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
// #ifdef APP-NVUE
import nvue from './nvue.js';
// #endif
// #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ
import wxs from './wxs.js';
// #endif
/**
* SwipeActionItem 滑动单元格子组件
* @description 该组件一般用于左滑唤出操作菜单的场景用的最多的是左滑删除操作
* @tutorial https://www.uvui.cn/components/swipeAction.html
* @property {Boolean} show 控制打开或者关闭默认 false
* @property {String | Number} index 标识符如果是v-for可用index索引
* @property {Boolean} disabled 是否禁用默认 false
* @property {Boolean} autoClose 是否自动关闭其他swipe按钮组默认 true
* @property {Number} threshold 滑动距离阈值只有大于此值才被认为是要打开菜单默认 30
* @property {Array} options 右侧按钮内容
* @property {String | Number} duration 动画过渡时间单位ms默认 350
* @event {Function(index)} open 组件打开时触发
* @event {Function(index)} close 组件关闭时触发
* @example <uv-swipe-action><uv-swipe-action-item :options="options1" ></uv-swipe-action-item></uv-swipe-action>
*/
export default {
name: 'uv-swipe-action-item',
emits: ['click'],
// #ifndef APP-NVUE
mixins: [mpMixin, mixin, props, touch],
// #endif
// #ifdef APP-NVUE
mixins: [mpMixin, mixin, props, nvue , touch],
// #endif
// #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ
mixins: [mpMixin, mixin, props, touch, wxs],
// #endif
data() {
return {
//
size: {},
// uv-swipe-action
parentData: {
autoClose: true,
},
// open-close-
status: 'close',
}
},
watch: {
// wxs
wxsInit(newValue, oldValue) {
this.queryRect()
}
},
computed: {
wxsInit() {
return [this.disabled, this.autoClose, this.threshold, this.options, this.duration]
}
},
mounted() {
// #ifdef MP-TOUTIAO
this.$uv.error('抖音小程序暂不支持wxs故该组件暂不支持抖音小程序');
// #endif
this.init()
},
methods: {
init() {
//
this.updateParentData()
// #ifndef APP-NVUE
this.$uv.sleep().then(() => {
this.queryRect()
})
// #endif
},
updateParentData() {
// mixin
this.getParentData('uv-swipe-action')
},
// #ifndef APP-NVUE
//
queryRect() {
this.$uvGetRect('.uv-swipe-action-item__right__button', true).then(buttons => {
this.size = {
buttons,
show: this.show,
disabled: this.disabled,
threshold: this.threshold,
duration: this.duration
}
})
},
// #endif
//
buttonClickHandler(item, index) {
this.$emit('click', {
index,
name: this.name
})
}
},
}
</script>
<style lang="scss" scoped>
$show-lines: 1;
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-swipe-action-item {
position: relative;
overflow: hidden;
/* #ifndef APP-NVUE || MP-WEIXIN */
touch-action: pan-y;
/* #endif */
&__content {
background-color: #FFFFFF;
z-index: 10;
}
&__right {
position: absolute;
top: 0;
bottom: 0;
right: 0;
@include flex;
&__button {
@include flex;
justify-content: center;
overflow: hidden;
align-items: center;
&__wrapper {
@include flex;
align-items: center;
justify-content: center;
padding: 0 15px;
&__text {
@include flex;
align-items: center;
color: #FFFFFF;
font-size: 15px;
text-align: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,15 @@
export default {
methods: {
// 关闭时执行
closeHandler() {
this.status = 'close'
},
setState(status) {
this.status = status
},
closeOther() {
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
}
}
}

View File

@ -0,0 +1,10 @@
export default {
props: {
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
},
...uni.$uv?.props?.swipeAction
}
}

View File

@ -0,0 +1,65 @@
<template>
<view class="uv-swipe-action">
<slot></slot>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* SwipeAction 滑动单元格
* @description 该组件一般用于左滑唤出操作菜单的场景用的最多的是左滑删除操作
* @tutorial https://www.uvui.cn/components/swipeAction.html
* @property {Boolean} autoClose 是否自动关闭其他swipe按钮组
* @event {Function(index)} click 点击组件时触发
* @example <uv-swipe-action><uv-swipe-action-item :rightOptions="options1" ></uv-swipe-action-item></uv-swipe-action>
*/
export default {
name: 'uv-swipe-action',
mixins: [mpMixin, mixin, props],
data() {
return {}
},
provide() {
return {
swipeAction: this
}
},
computed: {
// computeduv-swipe-action-item
// parentDatawatch(uv-swipe-action-item)
//
parentData() {
return [this.autoClose]
}
},
watch: {
//
parentData() {
if (this.children.length) {
this.children.map(child => {
// (uv-swipe-action-item)updateParentData()
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
},
created() {
this.children = []
},
methods: {
closeOther(child) {
if (this.autoClose) {
//
this.children.map((item, index) => {
if (child !== item) {
item.closeHandler()
}
})
}
}
}
}
</script>

View File

@ -0,0 +1,88 @@
{
"id": "uv-swipe-action",
"displayName": "uv-swipe-action 滑动单元格 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.4",
"description": "滑动单元格组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。",
"keywords": [
"uv-swipe-action",
"uvui",
"uv-ui",
"swipe-action",
"滑动单元格"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-icon"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
## SwipeAction 滑动单元格
> **组件名uv-swipe-action**
该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。
### <a href="https://www.uvui.cn/components/swipeAction.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题或者您对uv-ui有一些好的建议欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a><a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>

View File

@ -0,0 +1,66 @@
## 1.1.202023-10-30
1. 1.1.16版本
## 1.1.192023-10-13
1. 兼容vue3
## 1.1.182023-10-12
1. 1.1.15版本
## 1.1.172023-09-27
1. 1.1.14版本发布
## 1.1.162023-09-15
1. 1.1.13版本发布
## 1.1.152023-09-15
1. 更新button.js相关按钮支持open-type="agreePrivacyAuthorization"
## 1.1.142023-09-14
1. 优化dayjs
## 1.1.132023-09-13
1. 优化,$uv中增加unit参数方便组件中使用
## 1.1.122023-09-10
1. 升级版本
## 1.1.112023-09-04
1. 1.1.11版本
## 1.1.102023-08-31
1. 修复customStyle和customClass存在冲突的问题
## 1.1.92023-08-27
1. 版本升级
2. 优化
## 1.1.82023-08-24
1. 版本升级
## 1.1.72023-08-22
1. 版本升级
## 1.1.62023-08-18
uvui版本1.1.6
## 1.0.152023-08-14
1. 更新uvui版本号
## 1.0.132023-08-06
1. 优化
## 1.0.122023-08-06
1. 修改版本号
## 1.0.112023-08-06
1. 路由增加events参数
2. 路由拦截修复
## 1.0.102023-08-01
1. 优化
## 1.0.92023-06-28
优化openType.js
## 1.0.82023-06-15
1. 修改支付宝报错的BUG
## 1.0.72023-06-07
1. 解决微信小程序使用uvui提示 Some selectors are not allowed in component wxss, including tag name selectors, ID selectors, and attribute selectors
2. 解决上述提示需要在uni.scss配置$uvui-nvue-style: false; 然后在APP.vue下面引入uvui内置的基础样式:@import '@/uni_modules/uv-ui-tools/index.scss';
## 1.0.62023-06-04
1. uv-ui-tools 优化工具组件,兼容更多功能
2. 小程序分享功能优化等
## 1.0.52023-06-02
1. 修改扩展使用mixin中方法的问题
## 1.0.42023-05-23
1. 兼容百度小程序修改bem函数
## 1.0.32023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.22023-05-10
1. 增加Http请求封装
2. 优化
## 1.0.12023-05-04
1. 修改名称及备注
## 1.0.02023-05-04
1. uv-ui工具集首次发布

View File

@ -0,0 +1,6 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@ -0,0 +1,79 @@
// 全局挂载引入http相关请求拦截插件
import Request from './libs/luch-request'
// 引入全局mixin
import mixin from './libs/mixin/mixin.js'
// 小程序特有的mixin
import mpMixin from './libs/mixin/mpMixin.js'
// #ifdef MP
import mpShare from '@/uni_modules/uv-ui-tools/libs/mixin/mpShare.js'
// #endif
// 路由封装
import route from './libs/util/route.js'
// 公共工具函数
import * as index from './libs/function/index.js'
// 防抖方法
import debounce from './libs/function/debounce.js'
// 节流方法
import throttle from './libs/function/throttle.js'
// 规则检验
import * as test from './libs/function/test.js'
// 颜色渐变相关,colorGradient-颜色渐变,hexToRgb-十六进制颜色转rgb颜色,rgbToHex-rgb转十六进制
import * as colorGradient from './libs/function/colorGradient.js'
// 配置信息
import config from './libs/config/config.js'
// 平台
import platform from './libs/function/platform'
const $uv = {
route,
config,
test,
date: index.timeFormat, // 另名date
...index,
colorGradient: colorGradient.colorGradient,
hexToRgb: colorGradient.hexToRgb,
rgbToHex: colorGradient.rgbToHex,
colorToRgba: colorGradient.colorToRgba,
http: new Request(),
debounce,
throttle,
platform,
mixin,
mpMixin
}
uni.$uv = $uv;
const install = (Vue,options={}) => {
// #ifndef APP-NVUE
const cloneMixin = index.deepClone(mixin);
delete cloneMixin?.props?.customClass;
delete cloneMixin?.props?.customStyle;
Vue.mixin(cloneMixin);
// #ifdef MP
if(options.mpShare){
Vue.mixin(mpShare);
}
// #endif
// #endif
// #ifdef VUE2
// 时间格式化同时两个名称date和timeFormat
Vue.filter('timeFormat', (timestamp, format) => uni.$uv.timeFormat(timestamp, format));
Vue.filter('date', (timestamp, format) => uni.$uv.timeFormat(timestamp, format));
// 将多久以前的方法,注入到全局过滤器
Vue.filter('timeFrom', (timestamp, format) => uni.$uv.timeFrom(timestamp, format));
// 同时挂载到uni和Vue.prototype中
// #ifndef APP-NVUE
// 只有vue挂载到Vue.prototype才有意义因为nvue中全局Vue.prototype和Vue.mixin是无效的
Vue.prototype.$uv = $uv;
// #endif
// #endif
// #ifdef VUE3
Vue.config.globalProperties.$uv = $uv;
// #endif
}
export default {
install
}

View File

@ -0,0 +1,7 @@
// 引入公共基础类
@import "./libs/css/common.scss";
// 非nvue的样式
/* #ifndef APP-NVUE */
@import "./libs/css/vue.scss";
/* #endif */

View File

@ -0,0 +1,34 @@
// 此版本发布于2023-10-30
const version = '1.1.16'
// 开发环境才提示,生产环境不会提示
if (process.env.NODE_ENV === 'development') {
console.log(`\n %c uvui V${version} https://www.uvui.cn/ \n\n`, 'color: #ffffff; background: #3c9cff; padding:5px 0; border-radius: 5px;');
}
export default {
v: version,
version,
// 主题名称
type: [
'primary',
'success',
'info',
'error',
'warning'
],
// 颜色部分本来可以通过scss的:export导出供js使用但是奈何nvue不支持
color: {
'uv-primary': '#2979ff',
'uv-warning': '#ff9900',
'uv-success': '#19be6b',
'uv-error': '#fa3534',
'uv-info': '#909399',
'uv-main-color': '#303133',
'uv-content-color': '#606266',
'uv-tips-color': '#909399',
'uv-light-color': '#c0c4cc'
},
// 默认单位可以通过配置为rpx那么在用于传入组件大小参数为数值时就默认为rpx
unit: 'px'
}

View File

@ -0,0 +1,32 @@
$uv-main-color: #303133 !default;
$uv-content-color: #606266 !default;
$uv-tips-color: #909193 !default;
$uv-light-color: #c0c4cc !default;
$uv-border-color: #dadbde !default;
$uv-bg-color: #f3f4f6 !default;
$uv-disabled-color: #c8c9cc !default;
$uv-primary: #3c9cff !default;
$uv-primary-dark: #398ade !default;
$uv-primary-disabled: #9acafc !default;
$uv-primary-light: #ecf5ff !default;
$uv-warning: #f9ae3d !default;
$uv-warning-dark: #f1a532 !default;
$uv-warning-disabled: #f9d39b !default;
$uv-warning-light: #fdf6ec !default;
$uv-success: #5ac725 !default;
$uv-success-dark: #53c21d !default;
$uv-success-disabled: #a9e08f !default;
$uv-success-light: #f5fff0;
$uv-error: #f56c6c !default;
$uv-error-dark: #e45656 !default;
$uv-error-disabled: #f7b2b2 !default;
$uv-error-light: #fef0f0 !default;
$uv-info: #909399 !default;
$uv-info-dark: #767a82 !default;
$uv-info-disabled: #c4c6c9 !default;
$uv-info-light: #f4f4f5 !default;

View File

@ -0,0 +1,100 @@
// 超出行数自动显示行尾省略号最多5行
// 来自uvui的温馨提示当您在控制台看到此报错说明需要在App.vue的style标签加上lang="scss"
@for $i from 1 through 5 {
.uv-line-#{$i} {
/* #ifdef APP-NVUE */
// nvue下可以直接使用lines属性这是weex特有样式
lines: $i;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
// vue下单行和多行显示省略号需要单独处理
@if $i == '1' {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} @else {
display: -webkit-box!important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: $i;
-webkit-box-orient: vertical!important;
}
/* #endif */
}
}
$uv-bordercolor: #dadbde;
@if variable-exists(uv-border-color) {
$uv-bordercolor: $uv-border-color;
}
// 此处加上!important并非随意乱用而是因为目前*.nvue页面编译到H5时
// App.vue的样式会被uni-app的view元素的自带border属性覆盖导致无效
// 综上这是uni-app的缺陷导致我们为了多端兼容而必须要加上!important
// 移动端兼容性较好直接使用0.5px去实现细边框不使用伪元素形式实现
.uv-border {
border-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-style: solid;
}
.uv-border-top {
border-top-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-top-style: solid;
}
.uv-border-left {
border-left-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-left-style: solid;
}
.uv-border-right {
border-right-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-right-style: solid;
}
.uv-border-bottom {
border-bottom-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-bottom-style: solid;
}
.uv-border-top-bottom {
border-top-width: 0.5px!important;
border-bottom-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-top-style: solid;
border-bottom-style: solid;
}
// 去除button的所有默认样式让其表现跟普通的viewtext元素一样
.uv-reset-button {
padding: 0;
background-color: transparent;
/* #ifndef APP-PLUS */
font-size: inherit;
line-height: inherit;
color: inherit;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
/* #ifndef APP-NVUE */
.uv-reset-button::after {
border: none;
}
/* #endif */
.uv-hover-class {
opacity: 0.7;
}

View File

@ -0,0 +1,23 @@
@mixin flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: $direction;
}
/* #ifndef APP-NVUE */
// 由于uvui是基于nvue环境进行开发的此环境中普通元素默认为flex-direction: column;
// 所以在非nvue中需要对元素进行重置为flex-direction: column; 否则可能会表现异常
$uvui-nvue-style: true !default;
@if $uvui-nvue-style == true {
view, scroll-view, swiper-item {
display: flex;
flex-direction: column;
flex-shrink: 0;
flex-grow: 0;
flex-basis: auto;
align-items: stretch;
align-content: flex-start;
}
}
/* #endif */

View File

@ -0,0 +1,111 @@
// 超出行数自动显示行尾省略号最多5行
// 来自uvui的温馨提示当您在控制台看到此报错说明需要在App.vue的style标签加上lang="scss"
@if variable-exists(show-lines) {
@for $i from 1 through 5 {
.uv-line-#{$i} {
/* #ifdef APP-NVUE */
// nvue下可以直接使用lines属性这是weex特有样式
lines: $i;
text-overflow: ellipsis;
overflow: hidden;
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
// vue下单行和多行显示省略号需要单独处理
@if $i == '1' {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} @else {
display: -webkit-box!important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: $i;
-webkit-box-orient: vertical!important;
}
/* #endif */
}
}
}
@if variable-exists(show-border) {
$uv-bordercolor: #dadbde;
@if variable-exists(uv-border-color) {
$uv-bordercolor: $uv-border-color;
}
// 此处加上!important并非随意乱用而是因为目前*.nvue页面编译到H5时
// App.vue的样式会被uni-app的view元素的自带border属性覆盖导致无效
// 综上这是uni-app的缺陷导致我们为了多端兼容而必须要加上!important
// 移动端兼容性较好直接使用0.5px去实现细边框不使用伪元素形式实现
@if variable-exists(show-border-surround) {
.uv-border {
border-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-style: solid;
}
}
@if variable-exists(show-border-top) {
.uv-border-top {
border-top-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-top-style: solid;
}
}
@if variable-exists(show-border-left) {
.uv-border-left {
border-left-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-left-style: solid;
}
}
@if variable-exists(show-border-right) {
.uv-border-right {
border-right-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-right-style: solid;
}
}
@if variable-exists(show-border-bottom) {
.uv-border-bottom {
border-bottom-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-bottom-style: solid;
}
}
@if variable-exists(show-border-top-bottom) {
.uv-border-top-bottom {
border-top-width: 0.5px!important;
border-bottom-width: 0.5px!important;
border-color: $uv-bordercolor!important;
border-top-style: solid;
border-bottom-style: solid;
}
}
}
@if variable-exists(show-reset-button) {
// 去除button的所有默认样式让其表现跟普通的viewtext元素一样
.uv-reset-button {
padding: 0;
background-color: transparent;
/* #ifndef APP-PLUS */
font-size: inherit;
line-height: inherit;
color: inherit;
/* #endif */
/* #ifdef APP-NVUE */
border-width: 0;
/* #endif */
}
/* #ifndef APP-NVUE */
.uv-reset-button::after {
border: none;
}
/* #endif */
}
@if variable-exists(show-hover) {
.uv-hover-class {
opacity: 0.7;
}
}

View File

@ -0,0 +1,40 @@
// 历遍生成4个方向的底部安全区
@each $d in top, right, bottom, left {
.uv-safe-area-inset-#{$d} {
padding-#{$d}: 0;
padding-#{$d}: constant(safe-area-inset-#{$d});
padding-#{$d}: env(safe-area-inset-#{$d});
}
}
//提升H5端uni.toast()的层级避免被uvui的modal等遮盖
/* #ifdef H5 */
uni-toast {
z-index: 10090;
}
uni-toast .uni-toast {
z-index: 10090;
}
/* #endif */
// 隐藏scroll-view的滚动条
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
$uvui-nvue-style: true !default;
@if $uvui-nvue-style == false {
view, scroll-view, swiper-item {
display: flex;
flex-direction: column;
flex-shrink: 0;
flex-grow: 0;
flex-basis: auto;
align-items: stretch;
align-content: flex-start;
}
}

View File

@ -0,0 +1,134 @@
/**
* 求两个颜色之间的渐变值
* @param {string} startColor 开始的颜色
* @param {string} endColor 结束的颜色
* @param {number} step 颜色等分的份额
* */
function colorGradient(startColor = 'rgb(0, 0, 0)', endColor = 'rgb(255, 255, 255)', step = 10) {
const startRGB = hexToRgb(startColor, false) // 转换为rgb数组模式
const startR = startRGB[0]
const startG = startRGB[1]
const startB = startRGB[2]
const endRGB = hexToRgb(endColor, false)
const endR = endRGB[0]
const endG = endRGB[1]
const endB = endRGB[2]
const sR = (endR - startR) / step // 总差值
const sG = (endG - startG) / step
const sB = (endB - startB) / step
const colorArr = []
for (let i = 0; i < step; i++) {
// 计算每一步的hex值
let hex = rgbToHex(`rgb(${Math.round((sR * i + startR))},${Math.round((sG * i + startG))},${Math.round((sB
* i + startB))})`)
// 确保第一个颜色值为startColor的值
if (i === 0) hex = rgbToHex(startColor)
// 确保最后一个颜色值为endColor的值
if (i === step - 1) hex = rgbToHex(endColor)
colorArr.push(hex)
}
return colorArr
}
// 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
function hexToRgb(sColor, str = true) {
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
sColor = String(sColor).toLowerCase()
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
let sColorNew = '#'
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
}
sColor = sColorNew
}
// 处理六位的颜色值
const sColorChange = []
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`))
}
if (!str) {
return sColorChange
}
return `rgb(${sColorChange[0]},${sColorChange[1]},${sColorChange[2]})`
} if (/^(rgb|RGB)/.test(sColor)) {
const arr = sColor.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',')
return arr.map((val) => Number(val))
}
return sColor
}
// 将rgb表示方式转换为hex表示方式
function rgbToHex(rgb) {
const _this = rgb
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
if (/^(rgb|RGB)/.test(_this)) {
const aColor = _this.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',')
let strHex = '#'
for (let i = 0; i < aColor.length; i++) {
let hex = Number(aColor[i]).toString(16)
hex = String(hex).length == 1 ? `${0}${hex}` : hex // 保证每个rgb的值为2位
if (hex === '0') {
hex += hex
}
strHex += hex
}
if (strHex.length !== 7) {
strHex = _this
}
return strHex
} if (reg.test(_this)) {
const aNum = _this.replace(/#/, '').split('')
if (aNum.length === 6) {
return _this
} if (aNum.length === 3) {
let numHex = '#'
for (let i = 0; i < aNum.length; i += 1) {
numHex += (aNum[i] + aNum[i])
}
return numHex
}
} else {
return _this
}
}
/**
* JS颜色十六进制转换为rgb或rgba,返回的格式为 rgba2552552550.5字符串
* sHex为传入的十六进制的色值
* alpha为rgba的透明度
*/
function colorToRgba(color, alpha) {
color = rgbToHex(color)
// 十六进制颜色值的正则表达式
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/
/* 16进制颜色转为RGB格式 */
let sColor = String(color).toLowerCase()
if (sColor && reg.test(sColor)) {
if (sColor.length === 4) {
let sColorNew = '#'
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1))
}
sColor = sColorNew
}
// 处理六位的颜色值
const sColorChange = []
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`))
}
// return sColorChange.join(',')
return `rgba(${sColorChange.join(',')},${alpha})`
}
return sColor
}
export {
colorGradient,
hexToRgb,
rgbToHex,
colorToRgba
}

View File

@ -0,0 +1,29 @@
let timeout = null
/**
* 防抖原理一定时间内只有最后一次操作再过wait毫秒后才执行函数
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function debounce(func, wait = 500, immediate = false) {
// 清除定时器
if (timeout !== null) clearTimeout(timeout)
// 立即执行,此类情况一般用不到
if (immediate) {
const callNow = !timeout
timeout = setTimeout(() => {
timeout = null
}, wait)
if (callNow) typeof func === 'function' && func()
} else {
// 设置定时器当最后一次操作后timeout不会再被清除所以在延时wait毫秒后执行func回调方法
timeout = setTimeout(() => {
typeof func === 'function' && func()
}, wait)
}
}
export default debounce

View File

@ -0,0 +1,167 @@
let _boundaryCheckingState = true; // 是否进行越界检查的全局开关
/**
* 把错误的数据转正
* @private
* @example strip(0.09999999999999998)=0.1
*/
function strip(num, precision = 15) {
return +parseFloat(Number(num).toPrecision(precision));
}
/**
* Return digits length of a number
* @private
* @param {*number} num Input number
*/
function digitLength(num) {
// Get digit length of e
const eSplit = num.toString().split(/[eE]/);
const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
return len > 0 ? len : 0;
}
/**
* 把小数转成整数,如果是小数则放大成整数
* @private
* @param {*number} num 输入数
*/
function float2Fixed(num) {
if (num.toString().indexOf('e') === -1) {
return Number(num.toString().replace('.', ''));
}
const dLen = digitLength(num);
return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}
/**
* 检测数字是否越界如果越界给出提示
* @private
* @param {*number} num 输入数
*/
function checkBoundary(num) {
if (_boundaryCheckingState) {
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
console.warn(`${num} 超出了精度限制,结果可能不正确`);
}
}
}
/**
* 把递归操作扁平迭代化
* @param {number[]} arr 要操作的数字数组
* @param {function} operation 迭代操作
* @private
*/
function iteratorOperation(arr, operation) {
const [num1, num2, ...others] = arr;
let res = operation(num1, num2);
others.forEach((num) => {
res = operation(res, num);
});
return res;
}
/**
* 高精度乘法
* @export
*/
export function times(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, times);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
const baseNum = digitLength(num1) + digitLength(num2);
const leftValue = num1Changed * num2Changed;
checkBoundary(leftValue);
return leftValue / Math.pow(10, baseNum);
}
/**
* 高精度加法
* @export
*/
export function plus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, plus);
}
const [num1, num2] = nums;
// 取最大的小数位
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
// 把小数都转为整数然后再计算
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}
/**
* 高精度减法
* @export
*/
export function minus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, minus);
}
const [num1, num2] = nums;
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
}
/**
* 高精度除法
* @export
*/
export function divide(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, divide);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
checkBoundary(num1Changed);
checkBoundary(num2Changed);
// 重要这里必须用strip进行修正
return times(num1Changed / num2Changed, strip(Math.pow(10, digitLength(num2) - digitLength(num1))));
}
/**
* 四舍五入
* @export
*/
export function round(num, ratio) {
const base = Math.pow(10, ratio);
let result = divide(Math.round(Math.abs(times(num, base))), base);
if (num < 0 && result !== 0) {
result = times(result, -1);
}
// 位数不足则补0
return result;
}
/**
* 是否进行边界检查默认开启
* @param flag 标记开关true 为开启false 为关闭默认为 true
* @export
*/
export function enableBoundaryChecking(flag = true) {
_boundaryCheckingState = flag;
}
export default {
times,
plus,
minus,
divide,
round,
enableBoundaryChecking,
};

View File

@ -0,0 +1,734 @@
import { number, empty } from './test.js'
import { round } from './digit.js'
/**
* @description 如果value小于min取min如果value大于max取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)))
}
/**
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx"取出其数值部分如果是"xxxrpx"还需要用过uni.upx2px进行转换
* @param {number|string} value 用户传递值的px值
* @param {boolean} unit
* @returns {number|string}
*/
function getPx(value, unit = false) {
if (number(value)) {
return unit ? `${value}px` : Number(value)
}
// 如果带有rpx先取出其数值部分再转为px值
if (/(rpx|upx)$/.test(value)) {
return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)))
}
return unit ? `${parseInt(value)}px` : parseInt(value)
}
/**
* @description 进行延时以达到可以简写代码的目的 比如: await uni.$uv.sleep(20)将会阻塞20ms
* @param {number} value 堵塞时间 单位ms 毫秒
* @returns {Promise} 返回promise
*/
function sleep(value = 30) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, value)
})
}
/**
* @description 运行期判断平台
* @returns {string} 返回所在平台(小写)
* @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台
*/
function os() {
return uni.getSystemInfoSync().platform.toLowerCase()
}
/**
* @description 获取系统信息同步接口
* @link 获取系统信息同步接口 https://uniapp.dcloud.io/api/system/info?id=getsysteminfosync
*/
function sys() {
return uni.getSystemInfoSync()
}
/**
* @description 取一个区间数
* @param {Number} min 最小值
* @param {Number} max 最大值
*/
function random(min, max) {
if (min >= 0 && max > 0 && max >= min) {
const gab = max - min + 1
return Math.floor(Math.random() * gab + min)
}
return 0
}
/**
* @param {Number} len uuid的长度
* @param {Boolean} firstU 将返回的首字母置为"u"
* @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
*/
function guid(len = 32, firstU = true, radix = null) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
const uuid = []
radix = radix || chars.length
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]
} else {
let r
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
uuid[14] = '4'
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
if (firstU) {
uuid.shift()
return `u${uuid.join('')}`
}
return uuid.join('')
}
/**
* @description 获取父组件的参数因为支付宝小程序不支持provide/inject的写法
this.$parent在非H5中可以准确获取到父组件但是在H5中需要多次this.$parent.$parent.xxx
这里默认值等于undefined有它的含义因为最顶层元素(组件)的$parent就是undefined意味着不传name
(默认为undefined)就是查找最顶层的$parent
* @param {string|undefined} name 父组件的参数名
*/
function $parent(name = undefined) {
let parent = this.$parent
// 通过while历遍这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等继续上一级寻找
parent = parent.$parent
} else {
return parent
}
}
return false
}
/**
* @description 样式转换
* 对象转字符串或者字符串转对象
* @param {object | string} customStyle 需要转换的目标
* @param {String} target 转换的目的object-转为对象string-转为字符串
* @returns {object|string}
*/
function addStyle(customStyle, target = 'object') {
// 字符串转字符串,对象转对象情形,直接返回
if (empty(customStyle) || typeof(customStyle) === 'object' && target === 'object' || target === 'string' &&
typeof(customStyle) === 'string') {
return customStyle
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格(中间的空格不能去掉比如padding: 20px 0如果去掉了就错了),空格是无用的
customStyle = trim(customStyle)
// 根据";"将字符串转为数组形式
const styleArray = customStyle.split(';')
const style = {}
// 历遍数组,拼接成对象
for (let i = 0; i < styleArray.length; i++) {
// 'font-size:20px;color:red;',如此最后字符串有";"的话会导致styleArray最后一个元素为空字符串这里需要过滤
if (styleArray[i]) {
const item = styleArray[i].split(':')
style[trim(item[0])] = trim(item[1])
}
}
return style
}
// 这里为对象转字符串形式
let string = ''
for (const i in customStyle) {
// 驼峰转为中划线的形式否则css内联样式无法识别驼峰样式属性名
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
string += `${key}:${customStyle[i]};`
}
// 去除两端空格
return trim(string)
}
/**
* @description 添加单位如果有rpxupx%px等单位结尾或者值为auto直接返回否则加上px单位结尾
* @param {string|number} value 需要添加单位的值
* @param {string} unit 添加的单位名 比如px
*/
function addUnit(value = 'auto', unit = uni?.$uv?.config?.unit ? uni?.$uv?.config?.unit : 'px') {
value = String(value)
// 用uvui内置验证规则中的number判断是否为数值
return number(value) ? `${value}${unit}` : value
}
/**
* @description 深度克隆
* @param {object} obj 需要深度克隆的对象
* @param cache 缓存
* @returns {*} 克隆后的对象或者原值不是对象
*/
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
let clone;
if (obj instanceof Date) {
clone = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
clone = new RegExp(obj);
} else if (obj instanceof Map) {
clone = new Map(Array.from(obj, ([key, value]) => [key, deepClone(value, cache)]));
} else if (obj instanceof Set) {
clone = new Set(Array.from(obj, value => deepClone(value, cache)));
} else if (Array.isArray(obj)) {
clone = obj.map(value => deepClone(value, cache));
} else if (Object.prototype.toString.call(obj) === '[object Object]') {
clone = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, clone);
for (const [key, value] of Object.entries(obj)) {
clone[key] = deepClone(value, cache);
}
} else {
clone = Object.assign({}, obj);
}
cache.set(obj, clone);
return clone;
}
/**
* @description JS对象深度合并
* @param {object} target 需要拷贝的对象
* @param {object} source 拷贝的来源对象
* @returns {object|boolean} 深度合并后的对象或者false入参有不是对象
*/
function deepMerge(target = {}, source = {}) {
target = deepClone(target)
if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) return target;
const merged = Array.isArray(target) ? target.slice() : Object.assign({}, target);
for (const prop in source) {
if (!source.hasOwnProperty(prop)) continue;
const sourceValue = source[prop];
const targetValue = merged[prop];
if (sourceValue instanceof Date) {
merged[prop] = new Date(sourceValue);
} else if (sourceValue instanceof RegExp) {
merged[prop] = new RegExp(sourceValue);
} else if (sourceValue instanceof Map) {
merged[prop] = new Map(sourceValue);
} else if (sourceValue instanceof Set) {
merged[prop] = new Set(sourceValue);
} else if (typeof sourceValue === 'object' && sourceValue !== null) {
merged[prop] = deepMerge(targetValue, sourceValue);
} else {
merged[prop] = sourceValue;
}
}
return merged;
}
/**
* @description error提示
* @param {*} err 错误内容
*/
function error(err) {
// 开发环境才提示,生产环境不会提示
if (process.env.NODE_ENV === 'development') {
console.error(`uvui提示${err}`)
}
}
/**
* @description 打乱数组
* @param {array} array 需要打乱的数组
* @returns {array} 打乱后的数组
*/
function randomArray(array = []) {
// 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0
return array.sort(() => Math.random() - 0.5)
}
// padStart 的 polyfill因为某些机型或情况还无法支持es7的padStart比如电脑版的微信小程序
// 所以这里做一个兼容polyfill的兼容处理
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function(maxLength, fillString = ' ') {
if (Object.prototype.toString.call(fillString) !== '[object String]') {
throw new TypeError(
'fillString must be String'
)
}
const str = this
// 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
if (str.length >= maxLength) return String(str)
const fillLength = maxLength - str.length
let times = Math.ceil(fillLength / fillString.length)
while (times >>= 1) {
fillString += fillString
if (times === 1) {
fillString += fillString
}
}
return fillString.slice(0, fillLength) + str
}
}
/**
* @description 格式化时间
* @param {String|Number} dateTime 需要格式化的时间戳
* @param {String} fmt 格式化规则 yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 默认yyyy-mm-dd
* @returns {string} 返回格式化后的字符串
*/
function timeFormat(dateTime = null, formatStr = 'yyyy-mm-dd') {
let date
// 若传入时间为假值,则取当前时间
if (!dateTime) {
date = new Date()
}
// 若为unix秒时间戳则转为毫秒时间戳逻辑有点奇怪但不敢改以保证历史兼容
else if (/^\d{10}$/.test(dateTime?.toString().trim())) {
date = new Date(dateTime * 1000)
}
// 若用户传入字符串格式时间戳new Date无法解析需做兼容
else if (typeof dateTime === 'string' && /^\d+$/.test(dateTime.trim())) {
date = new Date(Number(dateTime))
}
// 处理平台性差异在Safari/Webkit中new Date仅支持/作为分割符的字符串时间
// 处理 '2022-07-10 01:02:03',跳过 '2022-07-10T01:02:03'
else if (typeof dateTime === 'string' && dateTime.includes('-') && !dateTime.includes('T')) {
date = new Date(dateTime.replace(/-/g, '/'))
}
// 其他都认为符合 RFC 2822 规范
else {
date = new Date(dateTime)
}
const timeSource = {
'y': date.getFullYear().toString(), // 年
'm': (date.getMonth() + 1).toString().padStart(2, '0'), // 月
'd': date.getDate().toString().padStart(2, '0'), // 日
'h': date.getHours().toString().padStart(2, '0'), // 时
'M': date.getMinutes().toString().padStart(2, '0'), // 分
's': date.getSeconds().toString().padStart(2, '0') // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
}
for (const key in timeSource) {
const [ret] = new RegExp(`${key}+`).exec(formatStr) || []
if (ret) {
// 年可能只需展示两位
const beginIndex = key === 'y' && ret.length === 2 ? 2 : 0
formatStr = formatStr.replace(ret, timeSource[key].slice(beginIndex))
}
}
return formatStr
}
/**
* @description 时间戳转为多久之前
* @param {String|Number} timestamp 时间戳
* @param {String|Boolean} format
* 格式化规则如果为时间格式字符串超出一定时间范围返回固定的时间格式
* 如果为布尔值false无论什么时间都返回多久以前的格式
* @returns {string} 转化后的内容
*/
function timeFrom(timestamp = null, format = 'yyyy-mm-dd') {
if (timestamp == null) timestamp = Number(new Date())
timestamp = parseInt(timestamp)
// 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位)
if (timestamp.toString().length == 10) timestamp *= 1000
let timer = (new Date()).getTime() - timestamp
timer = parseInt(timer / 1000)
// 如果小于5分钟,则返回"刚刚",其他以此类推
let tips = ''
switch (true) {
case timer < 300:
tips = '刚刚'
break
case timer >= 300 && timer < 3600:
tips = `${parseInt(timer / 60)}分钟前`
break
case timer >= 3600 && timer < 86400:
tips = `${parseInt(timer / 3600)}小时前`
break
case timer >= 86400 && timer < 2592000:
tips = `${parseInt(timer / 86400)}天前`
break
default:
// 如果format为false则无论什么时间戳都显示xx之前
if (format === false) {
if (timer >= 2592000 && timer < 365 * 86400) {
tips = `${parseInt(timer / (86400 * 30))}个月前`
} else {
tips = `${parseInt(timer / (86400 * 365))}年前`
}
} else {
tips = timeFormat(timestamp, format)
}
}
return tips
}
/**
* @description 去除空格
* @param String str 需要去除空格的字符串
* @param String pos both(左右)|left|right|all 默认both
*/
function trim(str, pos = 'both') {
str = String(str)
if (pos == 'both') {
return str.replace(/^\s+|\s+$/g, '')
}
if (pos == 'left') {
return str.replace(/^\s*/, '')
}
if (pos == 'right') {
return str.replace(/(\s*$)/g, '')
}
if (pos == 'all') {
return str.replace(/\s+/g, '')
}
return str
}
/**
* @description 对象转url参数
* @param {object} data,对象
* @param {Boolean} isPrefix,是否自动加上"?"
* @param {string} arrayFormat 规则 indices|brackets|repeat|comma
*/
function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
const prefix = isPrefix ? '?' : ''
const _result = []
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets'
for (const key in data) {
const value = data[key]
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(`${key}[${i}]=${value[i]}`)
}
break
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`)
})
break
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach((_value) => {
_result.push(`${key}=${_value}`)
})
break
case 'comma':
// 结果: ids=1,2,3
let commaStr = ''
value.forEach((_value) => {
commaStr += (commaStr ? ',' : '') + _value
})
_result.push(`${key}=${commaStr}`)
break
default:
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`)
})
}
} else {
_result.push(`${key}=${value}`)
}
}
return _result.length ? prefix + _result.join('&') : ''
}
/**
* 显示消息提示框
* @param {String} title 提示的内容长度与 icon 取值有关
* @param {Number} duration 提示的延迟时间单位毫秒默认2000
*/
function toast(title, duration = 2000) {
uni.showToast({
title: String(title),
icon: 'none',
duration
})
}
/**
* @description 根据主题type值,获取对应的图标
* @param {String} type 主题名称,primary|info|error|warning|success
* @param {boolean} fill 是否使用fill填充实体的图标
*/
function type2icon(type = 'success', fill = false) {
// 如果非预置值,默认为success
if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success'
let iconName = ''
// 目前(2019-12-12),info和primary使用同一个图标
switch (type) {
case 'primary':
iconName = 'info-circle'
break
case 'info':
iconName = 'info-circle'
break
case 'error':
iconName = 'close-circle'
break
case 'warning':
iconName = 'error-circle'
break
case 'success':
iconName = 'checkmark-circle'
break
default:
iconName = 'checkmark-circle'
}
// 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的
if (fill) iconName += '-fill'
return iconName
}
/**
* @description 数字格式化
* @param {number|string} number 要格式化的数字
* @param {number} decimals 保留几位小数
* @param {string} decimalPoint 小数点符号
* @param {string} thousandsSeparator 千分位符号
* @returns {string} 格式化后的数字
*/
function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') {
number = (`${number}`).replace(/[^0-9+-Ee.]/g, '')
const n = !isFinite(+number) ? 0 : +number
const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals)
const sep = (typeof thousandsSeparator === 'undefined') ? ',' : thousandsSeparator
const dec = (typeof decimalPoint === 'undefined') ? '.' : decimalPoint
let s = ''
s = (prec ? round(n, prec) + '' : `${Math.round(n)}`).split('.')
const re = /(-?\d+)(\d{3})/
while (re.test(s[0])) {
s[0] = s[0].replace(re, `$1${sep}$2`)
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || ''
s[1] += new Array(prec - s[1].length + 1).join('0')
}
return s.join(dec)
}
/**
* @description 获取duration值
* 如果带有ms或者s直接返回如果大于一定值认为是ms单位小于一定值认为是s单位
* 比如以30位阈值那么300大于30可以理解为用户想要的是300ms而不是想花300s去执行一个动画
* @param {String|number} value 比如: "1s"|"100ms"|1|100
* @param {boolean} unit 提示: 如果是false 默认返回number
* @return {string|number}
*/
function getDuration(value, unit = true) {
const valueNum = parseInt(value)
if (unit) {
if (/s$/.test(value)) return value
return value > 30 ? `${value}ms` : `${value}s`
}
if (/ms$/.test(value)) return valueNum
if (/s$/.test(value)) return valueNum > 30 ? valueNum : valueNum * 1000
return valueNum
}
/**
* @description 日期的月或日补零操作
* @param {String} value 需要补零的值
*/
function padZero(value) {
return `00${value}`.slice(-2)
}
/**
* @description 在uv-form的子组件内容发生变化或者失去焦点时尝试通知uv-form执行校验方法
* @param {*} instance
* @param {*} event
*/
function formValidate(instance, event) {
const formItem = $parent.call(instance, 'uv-form-item')
const form = $parent.call(instance, 'uv-form')
// 如果发生变化的input或者textarea等其父组件中有uv-form-item或者uv-form等就执行form的validate方法
// 同时将form-item的pros传递给form让其进行精确对象验证
if (formItem && form) {
form.validateField(formItem.prop, () => {}, event)
}
}
/**
* @description 获取某个对象下的属性用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式
* @param {object} obj 对象
* @param {string} key 需要获取的属性字段
* @returns {*}
*/
function getProperty(obj, key) {
if (!obj) {
return
}
if (typeof key !== 'string' || key === '') {
return ''
}
if (key.indexOf('.') !== -1) {
const keys = key.split('.')
let firstObj = obj[keys[0]] || {}
for (let i = 1; i < keys.length; i++) {
if (firstObj) {
firstObj = firstObj[keys[i]]
}
}
return firstObj
}
return obj[key]
}
/**
* @description 设置对象的属性值如果'a.b.c'的形式进行设置
* @param {object} obj 对象
* @param {string} key 需要设置的属性
* @param {string} value 设置的值
*/
function setProperty(obj, key, value) {
if (!obj) {
return
}
// 递归赋值
const inFn = function(_obj, keys, v) {
// 最后一个属性key
if (keys.length === 1) {
_obj[keys[0]] = v
return
}
// 0~length-1个key
while (keys.length > 1) {
const k = keys[0]
if (!_obj[k] || (typeof _obj[k] !== 'object')) {
_obj[k] = {}
}
const key = keys.shift()
// 自调用判断是否存在属性,不存在则自动创建对象
inFn(_obj[k], keys, v)
}
}
if (typeof key !== 'string' || key === '') {
} else if (key.indexOf('.') !== -1) { // 支持多层级赋值操作
const keys = key.split('.')
inFn(obj, keys, value)
} else {
obj[key] = value
}
}
/**
* @description 获取当前页面路径
*/
function page() {
const pages = getCurrentPages();
const route = pages[pages.length - 1]?.route;
// 某些特殊情况下(比如页面进行redirectTo时的一些时机)pages可能为空数组
return `/${route ? route : ''}`
}
/**
* @description 获取当前路由栈实例数组
*/
function pages() {
const pages = getCurrentPages()
return pages
}
/**
* 获取页面历史栈指定层实例
* @param back {number} [0] - 0或者负数表示获取历史栈的哪一层0表示获取当前页面实例-1 表示获取上一个页面实例默认0
*/
function getHistoryPage(back = 0) {
const pages = getCurrentPages()
const len = pages.length
return pages[len - 1 + back]
}
/**
* @description 修改uvui内置属性值
* @param {object} props 修改内置props属性
* @param {object} config 修改内置config属性
* @param {object} color 修改内置color属性
* @param {object} zIndex 修改内置zIndex属性
*/
function setConfig({
props = {},
config = {},
color = {},
zIndex = {}
}) {
const {
deepMerge,
} = uni.$uv
uni.$uv.config = deepMerge(uni.$uv.config, config)
uni.$uv.props = deepMerge(uni.$uv.props, props)
uni.$uv.color = deepMerge(uni.$uv.color, color)
uni.$uv.zIndex = deepMerge(uni.$uv.zIndex, zIndex)
}
export {
range,
getPx,
sleep,
os,
sys,
random,
guid,
$parent,
addStyle,
addUnit,
deepClone,
deepMerge,
error,
randomArray,
timeFormat,
timeFrom,
trim,
queryParams,
toast,
type2icon,
priceFormat,
getDuration,
padZero,
formValidate,
getProperty,
setProperty,
page,
pages,
getHistoryPage,
setConfig
}

View File

@ -0,0 +1,75 @@
/**
* 注意
* 此部分内容在vue-cli模式下需要在vue.config.js加入如下内容才有效
* module.exports = {
* transpileDependencies: ['uview-v2']
* }
*/
let platform = 'none'
// #ifdef VUE3
platform = 'vue3'
// #endif
// #ifdef VUE2
platform = 'vue2'
// #endif
// #ifdef APP-PLUS
platform = 'plus'
// #endif
// #ifdef APP-NVUE
platform = 'nvue'
// #endif
// #ifdef H5
platform = 'h5'
// #endif
// #ifdef MP-WEIXIN
platform = 'weixin'
// #endif
// #ifdef MP-ALIPAY
platform = 'alipay'
// #endif
// #ifdef MP-BAIDU
platform = 'baidu'
// #endif
// #ifdef MP-TOUTIAO
platform = 'toutiao'
// #endif
// #ifdef MP-QQ
platform = 'qq'
// #endif
// #ifdef MP-KUAISHOU
platform = 'kuaishou'
// #endif
// #ifdef MP-360
platform = '360'
// #endif
// #ifdef MP
platform = 'mp'
// #endif
// #ifdef QUICKAPP-WEBVIEW
platform = 'quickapp-webview'
// #endif
// #ifdef QUICKAPP-WEBVIEW-HUAWEI
platform = 'quickapp-webview-huawei'
// #endif
// #ifdef QUICKAPP-WEBVIEW-UNION
platform = 'quckapp-webview-union'
// #endif
export default platform

View File

@ -0,0 +1,287 @@
/**
* 验证电子邮箱格式
*/
function email(value) {
return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value)
}
/**
* 验证手机格式
*/
function mobile(value) {
return /^1([3589]\d|4[5-9]|6[1-2,4-7]|7[0-8])\d{8}$/.test(value)
}
/**
* 验证URL格式
*/
function url(value) {
return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/
.test(value)
}
/**
* 验证日期格式
*/
function date(value) {
if (!value) return false
// 判断是否数值或者字符串数值(意味着为时间戳)转为数值否则new Date无法识别字符串时间戳
if (number(value)) value = +value
return !/Invalid|NaN/.test(new Date(value).toString())
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
}
/**
* 验证十进制数字
*/
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
}
/**
* 验证字符串
*/
function string(value) {
return typeof value === 'string'
}
/**
* 验证整数
*/
function digits(value) {
return /^\d+$/.test(value)
}
/**
* 验证身份证号码
*/
function idCard(value) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
value
)
}
/**
* 是否车牌号
*/
function carNo(value) {
// 新能源车牌
const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/
// 旧车牌
const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/
if (value.length === 7) {
return creg.test(value)
} if (value.length === 8) {
return xreg.test(value)
}
return false
}
/**
* 金额,只允许2位小数
*/
function amount(value) {
// 金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value)
}
/**
* 中文
*/
function chinese(value) {
const reg = /^[\u4e00-\u9fa5]+$/gi
return reg.test(value)
}
/**
* 只能输入字母
*/
function letter(value) {
return /^[a-zA-Z]*$/.test(value)
}
/**
* 只能是字母或者数字
*/
function enOrNum(value) {
// 英文或者数字
const reg = /^[0-9a-zA-Z]*$/g
return reg.test(value)
}
/**
* 验证是否包含某个值
*/
function contains(value, param) {
return value.indexOf(param) >= 0
}
/**
* 验证一个值范围[min, max]
*/
function range(value, param) {
return value >= param[0] && value <= param[1]
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value, param) {
return value.length >= param[0] && value.length <= param[1]
}
/**
* 是否固定电话
*/
function landline(value) {
const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/
return reg.test(value)
}
/**
* 判断是否为空
*/
function empty(value) {
switch (typeof value) {
case 'undefined':
return true
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true
break
case 'boolean':
if (!value) return true
break
case 'number':
if (value === 0 || isNaN(value)) return true
break
case 'object':
if (value === null || value.length === 0) return true
for (const i in value) {
return false
}
return true
}
return false
}
/**
* 是否json字符串
*/
function jsonString(value) {
if (typeof value === 'string') {
try {
const obj = JSON.parse(value)
if (typeof obj === 'object' && obj) {
return true
}
return false
} catch (e) {
return false
}
}
return false
}
/**
* 是否数组
*/
function array(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
}
/**
* 是否对象
*/
function object(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
/**
* 是否短信验证码
*/
function code(value, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value)
}
/**
* 是否函数方法
* @param {Object} value
*/
function func(value) {
return typeof value === 'function'
}
/**
* 是否promise对象
* @param {Object} value
*/
function promise(value) {
return object(value) && func(value.then) && func(value.catch)
}
/**
* @param {Object} value
*/
function image(value) {
const newValue = value.split('?')[0]
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i
return IMAGE_REGEXP.test(newValue)
}
/**
* 是否视频格式
* @param {Object} value
*/
function video(value) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i
return VIDEO_REGEXP.test(value)
}
/**
* 是否为正则对象
* @param {Object}
* @return {Boolean}
*/
function regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]'
}
export {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
jsonString,
landline,
object,
array,
code,
func,
promise,
video,
image,
regExp,
string
}

View File

@ -0,0 +1,30 @@
let timer; let
flag
/**
* 节流原理在一定时间内只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function throttle(func, wait = 500, immediate = true) {
if (immediate) {
if (!flag) {
flag = true
// 如果是立即执行则在wait毫秒内开始时执行
typeof func === 'function' && func()
timer = setTimeout(() => {
flag = false
}, wait)
}
} else if (!flag) {
flag = true
// 如果是非立即执行则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false
typeof func === 'function' && func()
}, wait)
}
}
export default throttle

View File

@ -0,0 +1,97 @@
import buildURL from '../helpers/buildURL'
import buildFullPath from '../core/buildFullPath'
import settle from '../core/settle'
import { isUndefined } from '../utils'
/**
* 返回可选值存在的配置
* @param {Array} keys - 可选值数组
* @param {Object} config2 - 配置
* @return {{}} - 存在的配置项
*/
const mergeKeys = (keys, config2) => {
const config = {}
keys.forEach((prop) => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
return config
}
export default (config) => new Promise((resolve, reject) => {
const fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params)
const _config = {
url: fullPath,
header: config.header,
complete: (response) => {
config.fullPath = fullPath
response.config = config
try {
// 对可能字符串不是json 的情况容错
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data)
}
// eslint-disable-next-line no-empty
} catch (e) {
}
settle(resolve, reject, response)
}
}
let requestTask
if (config.method === 'UPLOAD') {
delete _config.header['content-type']
delete _config.header['Content-Type']
const otherConfig = {
// #ifdef MP-ALIPAY
fileType: config.fileType,
// #endif
filePath: config.filePath,
name: config.name
}
const optionalKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef H5
'file',
// #endif
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData'
]
requestTask = uni.uploadFile({ ..._config, ...otherConfig, ...mergeKeys(optionalKeys, config) })
} else if (config.method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config.timeout)) {
_config.timeout = config.timeout
}
// #endif
requestTask = uni.downloadFile(_config)
} else {
const optionalKeys = [
'data',
'method',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4'
// #endif
]
requestTask = uni.request({ ..._config, ...mergeKeys(optionalKeys, config) })
}
if (config.getTask) {
config.getTask(requestTask, config)
}
})

View File

@ -0,0 +1,50 @@
'use strict'
function InterceptorManager() {
this.handlers = []
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled,
rejected
})
return this.handlers.length - 1
}
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h)
}
})
}
export default InterceptorManager

View File

@ -0,0 +1,198 @@
/**
* @Class Request
* @description luch-request http请求插件
* @version 3.0.7
* @Author lu-ch
* @Date 2021-09-04
* @Email webwork.s@qq.com
* 文档: https://www.quanzhan.co/luch-request/
* github: https://github.com/lei-mu/luch-request
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
* HBuilderX: beat-3.0.4 alpha-3.0.4
*/
import dispatchRequest from './dispatchRequest'
import InterceptorManager from './InterceptorManager'
import mergeConfig from './mergeConfig'
import defaults from './defaults'
import { isPlainObject } from '../utils'
import clone from '../utils/clone'
export default class Request {
/**
* @param {Object} arg - 全局配置
* @param {String} arg.baseURL - 全局根路径
* @param {Object} arg.header - 全局header
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
* @param {String} arg.dataType = [json] - 全局默认的dataType
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType支付宝小程序不支持
* @param {Object} arg.custom - 全局默认的自定义参数
* @param {Number} arg.timeout - 全局默认的超时时间单位 ms默认60000H5(HBuilderX 2.9.9+)APP(HBuilderX 2.9.9+)微信小程序2.10.0支付宝小程序
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书默认true.仅App安卓端支持HBuilderX 2.3.3+
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证cookies默认false仅H5支持HBuilderX 2.6.15+
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4默认false App-Android 支持 (HBuilderX 2.8.0+)
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器默认statusCode >= 200 && statusCode < 300
*/
constructor(arg = {}) {
if (!isPlainObject(arg)) {
arg = {}
console.warn('设置全局参数必须接收一个Object')
}
this.config = clone({ ...defaults, ...arg })
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
/**
* @Function
* @param {Request~setConfigCallback} f - 设置全局默认配置
*/
setConfig(f) {
this.config = f(this.config)
}
middleware(config) {
config = mergeConfig(this.config, config)
const chain = [dispatchRequest, undefined]
let promise = Promise.resolve(config)
this.interceptors.request.forEach((interceptor) => {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
this.interceptors.response.forEach((interceptor) => {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
/**
* @Function
* @param {Object} config - 请求配置项
* @prop {String} options.url - 请求路径
* @prop {Object} options.data - 请求参数
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json会尝试对返回的数据做一次 JSON.parse
* @prop {Object} [options.header = config.header] - 请求header
* @prop {Object} [options.method = config.method] - 请求方法
* @returns {Promise<unknown>}
*/
request(config = {}) {
return this.middleware(config)
}
get(url, options = {}) {
return this.middleware({
url,
method: 'GET',
...options
})
}
post(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'POST',
...options
})
}
// #ifndef MP-ALIPAY
put(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'PUT',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
delete(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'DELETE',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
connect(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'CONNECT',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
head(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'HEAD',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
options(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'OPTIONS',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
trace(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'TRACE',
...options
})
}
// #endif
upload(url, config = {}) {
config.url = url
config.method = 'UPLOAD'
return this.middleware(config)
}
download(url, config = {}) {
config.url = url
config.method = 'DOWNLOAD'
return this.middleware(config)
}
}
/**
* setConfig回调
* @return {Object} - 返回操作后的config
* @callback Request~setConfigCallback
* @param {Object} config - 全局默认config
*/

View File

@ -0,0 +1,20 @@
'use strict'
import isAbsoluteURL from '../helpers/isAbsoluteURL'
import combineURLs from '../helpers/combineURLs'
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL)
}
return requestedURL
}

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