feat: init
commit
0921f0105d
|
@ -0,0 +1,2 @@
|
|||
/.idea
|
||||
/.vscode
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"php_ext":"fileinfo",
|
||||
"chmod":[
|
||||
{"mode":777,"path":"/server/runtime"}
|
||||
],
|
||||
"success_url":"/index.php",
|
||||
"php_versions":"72",
|
||||
"db_config":"",
|
||||
"admin_username":"",
|
||||
"admin_password":"",
|
||||
"run_path":"/server/public",
|
||||
"remove_file":[]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
unpackage
|
||||
node_modules/
|
||||
.hbuilderx/
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import {
|
||||
mapActions,
|
||||
mapGetters,
|
||||
mapMutations
|
||||
} from 'vuex'
|
||||
import Cache from '@/utils/cache'
|
||||
export default {
|
||||
async onLaunch(options) {
|
||||
// 获取配置
|
||||
this.getConfig();
|
||||
// 获取个人信息
|
||||
this.getUser()
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getUser',
|
||||
'getConfig',
|
||||
'isLogin'
|
||||
]),
|
||||
...mapMutations(['logout'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/*每个页面公共css */
|
||||
@import 'styles/common.scss';
|
||||
@import "components/uview-ui/index.scss";
|
||||
</style>
|
|
@ -0,0 +1,26 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
|
||||
// 账号登录
|
||||
export const apiAccountLogin = params =>
|
||||
request.post("login/account", {...params})
|
||||
|
||||
// 小程序授权登录
|
||||
export const apiAuthLogin = params =>
|
||||
request.post('login/authLogin', params)
|
||||
|
||||
// 小程序静默登录
|
||||
export const apiSilentLogin = params =>
|
||||
request.post('login/silentLogin', params)
|
||||
|
||||
// 公众号登录
|
||||
export const apiOALogin = params =>
|
||||
request.post('login/oaLogin', params)
|
||||
|
||||
// 向微信请求code的链接
|
||||
export const apiCodeUrlGet = () =>
|
||||
request.get('login/codeUrl', {
|
||||
params: {
|
||||
url: encodeURIComponent(location.href)
|
||||
}
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
import request from '@/utils/request'
|
||||
import wechath5 from '@/utils/wechath5'
|
||||
import store from 'store'
|
||||
|
||||
// 账号登录
|
||||
export const apiAccountLogin = data =>
|
||||
request.post("account/login", {...data})
|
||||
|
||||
// 重置登录密码
|
||||
export const apiResetPassword = data =>
|
||||
request.post("user/resetPassword", data)
|
||||
|
||||
// 获取服务协议
|
||||
export const apiPolicyAgreement = (params) => request.get('config/getPolicyAgreement', {params})
|
||||
|
||||
// 获取公共配置
|
||||
export const apiConfig = () => request.get('index/config')
|
|
@ -0,0 +1,18 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/** S 商品 **/
|
||||
|
||||
// 商品搜索记录
|
||||
export const apiGoodsLists = (params) => request.get('goods/lists', {params})
|
||||
|
||||
// 清空商品搜索记录
|
||||
export const apiGoodsOperation = (params) => request.post('goods/operation', params)
|
||||
|
||||
// 商品详情
|
||||
export const apiGoodsDetail = (params) => request.get('goods/detail', {params})
|
||||
|
||||
// 商品编辑
|
||||
export const apiGoodsEdit = (params) => request.post('goods/edit', params)
|
||||
|
||||
|
||||
/** E 商品 **/
|
|
@ -0,0 +1,36 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/** S 订单 **/
|
||||
|
||||
// 订单列表
|
||||
export const apiOrderList = params => request.get('order/lists', { params })
|
||||
|
||||
// 订单详情
|
||||
export const apiOrderDetail = params => request.get('order/detail', { params })
|
||||
|
||||
// 取消订单
|
||||
export const apiOrderClose = params => request.post('order/cancel', params)
|
||||
|
||||
// 确认订单收货
|
||||
export const apiOrderConfirm = params => request.post('order/confirm', params)
|
||||
|
||||
// 订单发货
|
||||
export const apiOrderDelivery = params => request.post('order/delivery', params)
|
||||
|
||||
// 订单详情
|
||||
export const apiOrderExpress = params => request.get('order/getExpress', { params })
|
||||
|
||||
// 订单查看物流
|
||||
export const apiOrderLogistics = params => request.get('order/logistics', {params})
|
||||
|
||||
// 获取地址
|
||||
export const apiOrderGetAddress = params => request.get('order/getAddress', {params})
|
||||
|
||||
// 删除订单
|
||||
export const apiOrderDelete = params => request.post('order/del', params)
|
||||
|
||||
// 修改订单地址
|
||||
export const apiOrderEditAddress = params => request.post('order/editAddress', params)
|
||||
|
||||
/** E 订单 **/
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 商城首页
|
||||
export const apiIndex = () => request.get('Statistics/workbench')
|
||||
|
||||
// 设置店铺信息
|
||||
export const apiSetShopInfo = (params) => request.post('shop/shopSet', params)
|
||||
|
||||
// 商品交易分析
|
||||
export const apiStatisticsGoodslist = () => request.get('Statistics/goodslist')
|
||||
|
||||
// 交易分析
|
||||
export const apiStatisticsTrading = () => request.get('Statistics/trading')
|
||||
|
||||
// 商品交易分析
|
||||
export const apiStatisticsVisit = () => request.get('Statistics/visit')
|
|
@ -0,0 +1,76 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 个人中心
|
||||
export const apiUserCentre = () => request.get('shop/getShopInfo')
|
||||
|
||||
|
||||
// S 个人设置
|
||||
// 获取用户信息
|
||||
export const apiGetUserInfo = () => request.get('user/info')
|
||||
|
||||
// 退出u登录
|
||||
export const apiLogout = () => request.post('account/logout')
|
||||
|
||||
// 设置用户登录登录密码
|
||||
export const apiSetPassword = params => request.post('shop/changePwd', params)
|
||||
// E 个人设置
|
||||
|
||||
// 账户明细
|
||||
export const userBill = (params) => request.get('account_log/lists', {
|
||||
params
|
||||
})
|
||||
// E 转账
|
||||
|
||||
|
||||
// S 地址
|
||||
// 获取地址列表
|
||||
export const apiAddressLists = () => request.get('user_address/lists')
|
||||
|
||||
// 获取地址详情
|
||||
export const apiAddressDetail = params => request.get('user_address/detail', {
|
||||
params
|
||||
})
|
||||
|
||||
// 添加收货地址
|
||||
export const apiAddressAdd = params => request.post('user_address/add', params)
|
||||
|
||||
// 编辑收货地址
|
||||
export const apiAddressEdit = params => request.post('user_address/edit', params)
|
||||
|
||||
// 设置默认收货地址
|
||||
export const apiAddressEditDefault = params => request.post('user_address/setDefault', params)
|
||||
|
||||
// 删除收货地址
|
||||
export const apiAddressDel = params => request.post('user_address/del', params)
|
||||
|
||||
// 微信导入收货地址转ID
|
||||
export const apiAddresshandleRegion = params => request.get('user_address/handleRegion', {
|
||||
params
|
||||
})
|
||||
// E 地址
|
||||
|
||||
|
||||
// S 提现
|
||||
|
||||
// 提现记录列表
|
||||
export const apiWithdrawLog = () => request.get('shop/withdrawLog')
|
||||
|
||||
// 提现数据
|
||||
export const apiGetWithdrawInfo = () => request.get('shop/getWithdrawInfo')
|
||||
|
||||
// 添加银行卡
|
||||
export const apiAddBank = (params) => request.post('shop/addBank', params)
|
||||
|
||||
// 删除银行卡
|
||||
export const apiDelBank = (params) => request.post('shop/delBank', params)
|
||||
|
||||
// 编辑银行卡
|
||||
export const apiEditBank = (params) => request.post('shop/editBank', params)
|
||||
|
||||
// 银行卡详情
|
||||
export const apigetBankDetail = (params) => request.get('shop/getBank', {params})
|
||||
|
||||
// 提现申请
|
||||
export const apiWithdrawApply = (params) => request.post('shop/withdraw', params)
|
||||
|
||||
// E 提现
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "8656MXP6VT.com.gzyx.likeshop-plus",
|
||||
"paths": [ "/ulink/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
# 文件原路径
|
||||
srcPath="./unpackage/dist/build/h5"
|
||||
# 发布路径文件夹
|
||||
releasePath="../server/public/business"
|
||||
|
||||
#删除发布目录下的mobile文件
|
||||
rm -r $releasePath
|
||||
echo "已删除 ==> $releasePath 下的目录文件"
|
||||
mkdir $releasePath
|
||||
echo "已新建 ==> $releasePath 目录"
|
||||
|
||||
# 复制打包目录内的文件到发布目录
|
||||
cp -r $srcPath/* $releasePath
|
||||
echo "已复制 $srcPath/* ==> $releasePath"
|
||||
|
||||
cp $releasePath/../favicon.ico $releasePath
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<view>
|
||||
<view class="add-bank bg-white">
|
||||
<view class="black md flex row-between">
|
||||
<view>提现银行:</view>
|
||||
<input type="text" v-model="form.name" placeholder="请填写银行名称,如中国银行" />
|
||||
</view>
|
||||
<view class="black md flex row-between">
|
||||
<view>银行支行:</view>
|
||||
<input type="text" v-model="form.branch" placeholder="请填写银行支行,如北京路支行" />
|
||||
</view>
|
||||
<view class="black md flex row-between">
|
||||
<view>开户姓名:</view>
|
||||
<input type="text" v-model="form.nickname" placeholder="请填写开户人姓名,如张三" />
|
||||
</view>
|
||||
<view class="black md flex row-between">
|
||||
<view>银行账号:</view>
|
||||
<input type="text" v-model="form.account" placeholder="请填写银行卡账号" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="type == 1">
|
||||
<view class="btn flex row-center br60 md white" @click="addBankFunc">
|
||||
确认添加
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-if="type == 2">
|
||||
<view class="btn flex row-center br60 md white" @click="editBankFunc">
|
||||
确认编辑
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiAddBank,
|
||||
apiEditBank,
|
||||
apigetBankDetail
|
||||
} from "@/api/user.js"
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
type: 1, //type1 = 添加 2=编辑
|
||||
form: {
|
||||
name: '',
|
||||
branch: '',
|
||||
account: '',
|
||||
nickname: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try {
|
||||
const id = this.$Route.query.id;
|
||||
if (id) {
|
||||
this.getBankFunc(id)
|
||||
this.type = 2
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async addBankFunc() {
|
||||
const res = await apiAddBank({
|
||||
...this.form
|
||||
})
|
||||
this.$refs.uToast.show({
|
||||
title: '添加成功',
|
||||
type: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
async editBankFunc() {
|
||||
const res = await apiEditBank({
|
||||
...this.form
|
||||
})
|
||||
this.$refs.uToast.show({
|
||||
title: '修改成功',
|
||||
type: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
async getBankFunc(id) {
|
||||
const res = await apigetBankDetail({id: id})
|
||||
|
||||
for (const key in res) {
|
||||
console.log(res[key])
|
||||
this.$set(this.form, key, res[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.add-bank {
|
||||
padding: 0 30rpx;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 30rpx 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 30rpx;
|
||||
margin-top: 40rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<view>
|
||||
<view class="select-bank">
|
||||
<block v-for="(item, index) in bank_list" :key="item.id">
|
||||
<view class="m-t-20">
|
||||
<u-swipe-action :vibrate-short="true" :index="index" bg-color="transparent" :btn-width="130"
|
||||
@click="clickBtn($event, item.id)" :options="options">
|
||||
<view class="bank-item flex row-between bg-white" @click="selectBankFunc(item)">
|
||||
<view class="normal nr">{{item.name}} ({{item.account.substring(item.account.length-4)}})</view>
|
||||
<view class="nr setting" @click.stop="toAddBankFunc(item)">编辑</view>
|
||||
</view>
|
||||
</u-swipe-action>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<router-link to="/bundle/pages/bank_add/bank_add">
|
||||
<view class="btn flex row-center br60 md white">
|
||||
新增提现账户
|
||||
</view>
|
||||
</router-link>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiGetWithdrawInfo,
|
||||
apiDelBank
|
||||
} from "@/api/user.js"
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
bank_list: [],
|
||||
selectIndex: -1,
|
||||
|
||||
options: [{
|
||||
text: '删除',
|
||||
style: {
|
||||
backgroundColor: '#FF2C3C'
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getWithdrawInfoFunc();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getWithdrawInfoFunc() {
|
||||
const {
|
||||
bank_list
|
||||
} = await apiGetWithdrawInfo();
|
||||
console.log(bank_list)
|
||||
this.bank_list = bank_list;
|
||||
},
|
||||
|
||||
async clickBtn(event, id) {
|
||||
const res = await apiDelBank({id})
|
||||
this.$refs.uToast.show({
|
||||
title: '删除成功',
|
||||
type: 'success'
|
||||
})
|
||||
this.getWithdrawInfoFunc();
|
||||
},
|
||||
|
||||
toAddBankFunc(data) {
|
||||
this.$Router.push({
|
||||
path: '/bundle/pages/bank_add/bank_add',
|
||||
query: {
|
||||
id: data.id
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
selectBankFunc(item) {
|
||||
uni.$emit('getBank', item)
|
||||
this.$Router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.select-bank {
|
||||
width: 100%;
|
||||
|
||||
.bank-item {
|
||||
width: 100vw;
|
||||
padding: 30rpx;
|
||||
|
||||
.setting {
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 30rpx;
|
||||
margin-top: 40rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<view class="m-t-20">
|
||||
|
||||
<view class="item bb">
|
||||
<view>快递方式</view>
|
||||
<view class="flex row-right">
|
||||
<u-radio-group v-model="form.send_type">
|
||||
<u-radio v-for="(item, index) in list" :key="index" :name="item.val">
|
||||
{{item.name}}
|
||||
</u-radio>
|
||||
</u-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template v-if="form.send_type == 1">
|
||||
<view class="item bb" @click="show = true">
|
||||
<view>快递公司</view>
|
||||
<view class="flex row-right m-r-16">
|
||||
<view class="m-r-12">
|
||||
{{!curData.label?'未选择':curData.label}}
|
||||
</view>
|
||||
<u-icon name="arrow-down"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="item bb" style="align-items:flex-start">
|
||||
<view>快递单号</view>
|
||||
<view style="text-align: left;" class="m-t-4">
|
||||
<input v-model="form.invoice_no" placeholder="请输入物流单号" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="br60 btn flex row-center white md" @click="onSubmit(type)">
|
||||
确认
|
||||
</view>
|
||||
|
||||
|
||||
<u-select v-model="show" @confirm="confirm" :list="express" mode="single-column" value-name="id" label-name="name"></u-select>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiOrderDelivery,
|
||||
apiOrderExpress
|
||||
} from '@/api/order'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
id: 0,
|
||||
invoice_no: '',
|
||||
send_type: 1,
|
||||
shipping_id: 0
|
||||
},
|
||||
|
||||
show: false,
|
||||
express: [],
|
||||
curData: {},
|
||||
|
||||
list: [{
|
||||
name: '快递配送',
|
||||
val: 1
|
||||
},
|
||||
{
|
||||
name: '无需快递',
|
||||
val: 2
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try {
|
||||
const id = this.$Route.query.id;
|
||||
if (id) {
|
||||
this.form.id = id;
|
||||
this.getExpressFunc(id)
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
if(this.form.send_type == 1) {
|
||||
if(this.curData.value == undefined) return this.$toast({title: '请选择快递公司'})
|
||||
if(this.form.invoice_no == '') return this.$toast({title: '请输入快递单号'})
|
||||
}
|
||||
|
||||
const params = {
|
||||
id: this.form.id,
|
||||
send_type: this.form.send_type,
|
||||
invoice_no: this.form.send_type == 1 ? this.form.invoice_no : '',
|
||||
shipping_id: this.form.send_type == 1 ? this.curData.value : '',
|
||||
}
|
||||
|
||||
await apiOrderDelivery({...params})
|
||||
|
||||
this.$refs.uToast.show({
|
||||
title: '发货成功',
|
||||
type: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$Router.back()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
confirm(event) {
|
||||
this.curData = event[0];
|
||||
},
|
||||
|
||||
async getExpressFunc(id) {
|
||||
const res = await apiOrderExpress({id})
|
||||
this.express = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: $-color-white;
|
||||
justify-content: space-between;
|
||||
|
||||
>view:first-child {
|
||||
width: 180rpx;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
>view:last-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
textarea {
|
||||
width: 560rpx;
|
||||
height: 300rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 auto;
|
||||
margin-top: 40rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<view>
|
||||
<view class="run-time bg-white m-t-20">
|
||||
<view class="black nr m-b-20">工作日: </view>
|
||||
<view class="days flex row-between">
|
||||
<block v-for="(item, index) in weekdaysText" :key="index">
|
||||
<template>
|
||||
<view @click="selectDayFunc(index)" class="item" :class="weekdays[index] == null ? 'not-select' : 'select'">{{item}}</view>
|
||||
</template>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<view class="black nr m-t-40 m-b-20">营业时间: </view>
|
||||
|
||||
<view class="time flex row-between">
|
||||
<view class="flex row-between" @click="openTimePickerFunc(1)">
|
||||
{{start[0]}}:{{start[1]}}
|
||||
<u-icon name="arrow-down" size="20"></u-icon>
|
||||
</view>
|
||||
-
|
||||
<view class="flex row-between" @click="openTimePickerFunc(2)">
|
||||
{{end[0]}}:{{end[1]}}
|
||||
<u-icon name="arrow-down" size="20"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="my-btn white md flex br60 row-center" @click="onSubmit">完成</button>
|
||||
|
||||
<!-- 时间选择组件 -->
|
||||
<u-picker v-model="show" @confirm="confirm" mode="time" :params="params"></u-picker>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiSetShopInfo
|
||||
} from '@/api/store'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
weekdays: [],
|
||||
weekdaysText: ['日',"一","二",'三','四','五','六'],
|
||||
|
||||
type: 1, //1 = 开始时间 2=结束时间
|
||||
show: false,
|
||||
params: {
|
||||
year: false,
|
||||
month: false,
|
||||
day: false,
|
||||
hour: true,
|
||||
minute: true,
|
||||
second: false,
|
||||
timestamp: true, // 1.3.7版本提供
|
||||
},
|
||||
start: ['00','00'],//时 分
|
||||
end: ['00','00'],//时 分
|
||||
startTimestamp: '',//开始时间戳
|
||||
endTimestamp: ''//结束时间戳
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 工作日计算
|
||||
let arr = [null, null, null, null, null, null, null];
|
||||
let weekdays = this.shopInfo.weekdays;
|
||||
let weekdaysText = [0, 1, 2, 3, 4, 5, 6]
|
||||
for (let i = 0; i < weekdaysText.length; i++) {
|
||||
for (let j = 0; j < weekdays.length; j++) {
|
||||
if (weekdaysText[i] == weekdays[j]) {
|
||||
arr[i] = weekdays[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.weekdays = arr;
|
||||
|
||||
// 营业时间计算
|
||||
let start = this.shopInfo.run_start_time;
|
||||
let end = this.shopInfo.run_end_time;
|
||||
this.start = start.split(':');
|
||||
this.end = end.split(':');
|
||||
this.startTimestamp = ((start.split(':')[0])*60*60) + ((start.split(':')[1])*60) + (Number(new Date(new Date().toLocaleDateString()).getTime())/1000)
|
||||
this.endTimestamp = ((end.split(':')[0])*60*60) + ((end.split(':')[1])*60) + (Number(new Date(new Date().toLocaleDateString()).getTime())/1000)
|
||||
},
|
||||
methods: {
|
||||
// 选择营业工作日
|
||||
selectDayFunc(index) {
|
||||
let weekdaysText = [0, 1, 2, 3, 4, 5, 6]
|
||||
weekdaysText.forEach(item => {
|
||||
if(item == index) {
|
||||
this.weekdays[index] = this.weekdays[index] == null ? item : null
|
||||
}
|
||||
})
|
||||
this.weekdaysText = [...this.weekdaysText]
|
||||
},
|
||||
|
||||
openTimePickerFunc(type) {
|
||||
this.type = type;
|
||||
this.show = true;
|
||||
},
|
||||
|
||||
confirm(time) {
|
||||
if(this.type == 1) {
|
||||
this.start = [time.hour, time.minute]
|
||||
this.startTimestamp = time.timestamp;
|
||||
} else {
|
||||
this.end = [time.hour, time.minute]
|
||||
this.endTimestamp = time.timestamp;
|
||||
}
|
||||
},
|
||||
|
||||
async onSubmit() {
|
||||
const params = {
|
||||
run_start_time: Number(this.startTimestamp),
|
||||
run_end_time: Number(this.endTimestamp),
|
||||
weekdays: this.weekdays.filter(item => typeof item == 'string' || typeof item == 'number')
|
||||
}
|
||||
|
||||
await apiSetShopInfo({...params})
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$Router.back()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.run-time {
|
||||
padding: 30rpx;
|
||||
|
||||
.days {
|
||||
.item {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 50%;
|
||||
line-height: 76rpx;
|
||||
text-align: center;
|
||||
color: $-color-white;
|
||||
font-size: $-font-size-nr;
|
||||
}
|
||||
.select {
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
.not-select {
|
||||
background-color: #EEEEEE;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
padding-bottom: 20rpx;
|
||||
>view {
|
||||
width: 322rpx;
|
||||
height: 64rpx;
|
||||
padding: 20rpx;
|
||||
font-size: $-font-size-nr;
|
||||
border: 1rpx solid #dbdbdb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.my-btn {
|
||||
height: 88rpx;
|
||||
margin: 30rpx 26rpx;
|
||||
margin-top: 40rpx;
|
||||
text-align: center;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<view class="p-24">
|
||||
<u-parse :html="content" :selectable="true" :show-with-animation="true"></u-parse>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiPolicyAgreement,
|
||||
} from "@/api/app"
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
content: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取服务协议
|
||||
getPolicyAgreement() {
|
||||
apiPolicyAgreement({
|
||||
type: this.type
|
||||
}).then(res => {
|
||||
this.content = res[this.type];
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.type = this.$Route.query.type;
|
||||
this.getPolicyAgreement()
|
||||
this.type == 1 ? uni.setNavigationBarTitle({
|
||||
title: '服务协议'
|
||||
}) : uni.setNavigationBarTitle({
|
||||
title: '隐私政策'
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<view>
|
||||
<u-tabs :list="list" :is-scroll="false" :current="current" @change="change"></u-tabs>
|
||||
|
||||
<view class="m-t-20" style="padding: 0 20rpx;">
|
||||
|
||||
<!-- 交易数据 -->
|
||||
<template v-if="current == 0">
|
||||
<view class="normal m-t-20 m-b-20 sm">最近7天</view>
|
||||
<view class="flex row-between">
|
||||
<view class="card">
|
||||
<view class="xs lighter">成交订单(笔)</view>
|
||||
<view class="normal">{{tradingInfo.order_num}}</view>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="xs lighter">营业额(元)</view>
|
||||
<view class="primary">{{tradingInfo.order_amount}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="e-data m-t-30">
|
||||
<view class="sm normal">成交趋势图</view>
|
||||
|
||||
<view class="e-content m-t-20">
|
||||
<charts ids="canvasColumn" width="100%" height="544rpx" :chartData="dealData"></charts>
|
||||
</view>
|
||||
</view>
|
||||
<view class="e-data m-t-30 m-b-50">
|
||||
<view class="e-title sm">营业额趋势图</view>
|
||||
|
||||
<view class="e-content m-t-20">
|
||||
<charts ids="canvasColumn2" width="100%" height="544rpx" :chartData="turnoverData"></charts>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 商品数据 -->
|
||||
<template v-if="current == 1">
|
||||
<block v-for="(item, index) in goodsInfo">
|
||||
<view class="goods flex m-b-20" @click="toDetail(item.id)">
|
||||
<u-image :src="item.image" width="160rpx" height="160rpx"></u-image>
|
||||
<view class="m-l-16 line-2" style="width: 100%;">
|
||||
<!-- 订单名称 -->
|
||||
<view class="m-b-10 flex row-between">
|
||||
<view class="nr black line-2">
|
||||
{{item.goods_name}}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商品规格 -->
|
||||
<view class="m-t-20 flex row-between">
|
||||
<view class="muted">销量 {{item.sales_volume}}</view>
|
||||
<view class="muted">销售额: <text class="black">{{item.sales_price}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</template>
|
||||
|
||||
<!-- 人数数据 -->
|
||||
<template v-if="current == 2">
|
||||
<view class="flex row-between">
|
||||
<view class="card" style="width: 100%;">
|
||||
<view class="xs lighter">进店人数(人)</view>
|
||||
<view class="normal">{{visitInfo.user_count}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="e-data m-t-30">
|
||||
<view class="sm normal">进店人数趋势图</view>
|
||||
|
||||
<view class="e-content m-t-20">
|
||||
<charts ids="canvasColumn" width="100%" height="544rpx" :chartData="visitData"></charts>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapGetters,
|
||||
mapActions
|
||||
} from 'vuex'
|
||||
import {
|
||||
apiStatisticsGoodslist, //商品
|
||||
apiStatisticsTrading, //交易
|
||||
apiStatisticsVisit //访问
|
||||
} from '@/api/store'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
list: [{
|
||||
name: '交易分析'
|
||||
}, {
|
||||
name: '商品分析'
|
||||
}, {
|
||||
name: '访问分析'
|
||||
}],
|
||||
|
||||
current: 0,
|
||||
|
||||
// 交易
|
||||
tradingInfo: {},
|
||||
dealData: {}, //成交趋势
|
||||
turnoverData: {}, //营业额
|
||||
|
||||
goodsInfo: [],
|
||||
|
||||
// 进店
|
||||
visitInfo: {},
|
||||
visitData: {},
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.getDataFunc(this.current)
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(index) {
|
||||
this.current = index;
|
||||
this.getDataFunc(index);
|
||||
},
|
||||
|
||||
toDetail(id) {
|
||||
this.$Router.push({
|
||||
path: '/pages/goods_detail/goods_detail',
|
||||
query: {
|
||||
id
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async getDataFunc(index) {
|
||||
if (index == 0) {
|
||||
const res = await apiStatisticsTrading()
|
||||
this.tradingInfo = res;
|
||||
const turnover = {
|
||||
categories: res.days,
|
||||
series: [{
|
||||
"name": "数据",
|
||||
data: res.echarts_order_num_add
|
||||
}]
|
||||
}
|
||||
const deal = {
|
||||
categories: res.days,
|
||||
series: [{
|
||||
"name": "数据",
|
||||
data: res.echarts_order_amount_add
|
||||
}]
|
||||
}
|
||||
this.turnoverData = turnover
|
||||
this.dealData = deal
|
||||
} else if (index == 1) {
|
||||
const res = await apiStatisticsGoodslist()
|
||||
this.goodsInfo = res.lists;
|
||||
} else if (index == 2) {
|
||||
const res = await apiStatisticsVisit()
|
||||
this.visitInfo = res;
|
||||
const visit = {
|
||||
categories: res.days,
|
||||
series: [{
|
||||
"name": "数据",
|
||||
data: res.echarts_add
|
||||
}]
|
||||
}
|
||||
this.visitData = visit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
width: 346rpx;
|
||||
padding: 30rpx 36rpx;
|
||||
border-radius: 14rpx;
|
||||
background-color: $-color-white;
|
||||
|
||||
view:last-child {
|
||||
font-size: 44rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据图
|
||||
.e-data {
|
||||
.e-content {
|
||||
padding: 20rpx 0;
|
||||
border-radius: 14rpx;
|
||||
background-color: $-color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.goods {
|
||||
padding: 20rpx;
|
||||
border-radius: 14rpx;
|
||||
background-color: $-color-white;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,245 @@
|
|||
<template>
|
||||
<view>
|
||||
<u-tabs :list="list" :is-scroll="false" :current="current" @change="change"></u-tabs>
|
||||
|
||||
<view class="m-t-20">
|
||||
<template v-if="current == 0">
|
||||
<view class="item bb">
|
||||
<view>商家名称</view>
|
||||
<view>{{info.name}}</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>商家类型</view>
|
||||
<view>{{info.name}}</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>主营类目</view>
|
||||
<view>点啊好难过</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>商家评分</view>
|
||||
<view>
|
||||
<u-rate disabled active-color="#F6B125" :count="5" v-model="info.score"></u-rate>
|
||||
</view>
|
||||
</view>
|
||||
<router-link to="/bundle/pages/shop_setting_edit/shop_setting_edit?type=1">
|
||||
<view class="item bb">
|
||||
<view>联系人</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-10">{{info.nickname}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
<router-link to="/bundle/pages/shop_setting_edit/shop_setting_edit?type=2">
|
||||
<view class="item bb">
|
||||
<view>联系电话</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-10">{{info.mobile}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
<router-link to="/bundle/pages/shop_setting_edit/shop_setting_edit?type=3">
|
||||
<view class="item line-2">
|
||||
<view>店铺简介</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-10 line-2">{{info.intro}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template v-if="current == 1">
|
||||
<view class="item bb">
|
||||
<view>经营状态</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-20">{{info.is_run?'营业中':'暂停营业'}}</view>
|
||||
<u-switch @change="isRunFunc" size="36" v-model="info.is_run"></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<router-link to="/bundle/pages/run_time/run_time">
|
||||
<view class="item">
|
||||
<view>营业时间</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-10 line-2">{{shopInfo.run_start_time||'-'}}-{{shopInfo.run_end_time||'-'}}
|
||||
</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
<router-link to="/pages/address_edit/address_edit?type=1">
|
||||
<view class="item">
|
||||
<view>商家地址</view>
|
||||
<view class="flex row-right line-1">
|
||||
<view class="m-r-10 line-1">{{shopAddress}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
<router-link to="/pages/address_edit/address_edit?type=2">
|
||||
<view class="item">
|
||||
<view>退货地址</view>
|
||||
<view class="flex row-right line-1">
|
||||
<view class="m-r-10 line-1">{{refundAddress}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</router-link>
|
||||
<view class="item bb">
|
||||
<view>发票开关</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-20">{{info.is_run?'开启':'关闭'}}</view>
|
||||
<u-switch @change="openInvoiceFunc" size="36" v-model="info.open_invoice"></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>经营状态</view>
|
||||
<view class="flex row-right">
|
||||
<view class="m-r-20">{{info.is_run?'支持':'不支持'}}</view>
|
||||
<u-switch @change="specInvoiceFunc" size="36" v-model="info.spec_invoice"></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapGetters,
|
||||
mapActions
|
||||
} from 'vuex'
|
||||
import {
|
||||
apiSetShopInfo
|
||||
} from '@/api/store'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
list: [{
|
||||
name: '基础信息'
|
||||
}, {
|
||||
name: '经营信息'
|
||||
}],
|
||||
|
||||
current: 0,
|
||||
|
||||
info: {
|
||||
is_run: 1,
|
||||
open_invoice: 1,
|
||||
spec_invoice: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
shopAddress() {
|
||||
return this.shopInfo.province_name + " " + this.shopInfo.city_name + " " + this.shopInfo.district_name +
|
||||
" " + this.shopInfo.address;
|
||||
},
|
||||
refundAddress() {
|
||||
return this.shopInfo.refund_address?.province_name + " " + this.shopInfo.refund_address?.city_name + " " +
|
||||
this.shopInfo.refund_address?.district_name +
|
||||
" " + (this.shopInfo.refund_address?.address ?? '')
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getUser().then(res => {
|
||||
// 深度克隆防止vux的数据地址保存连接
|
||||
this.info = JSON.parse(JSON.stringify(res));
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(['getUser']),
|
||||
|
||||
change(index) {
|
||||
this.current = index;
|
||||
},
|
||||
|
||||
async isRunFunc(event) {
|
||||
try {
|
||||
await apiSetShopInfo({
|
||||
is_run: event ? 1 : 0
|
||||
})
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
} catch(err) {
|
||||
this.$refs.uToast.show({
|
||||
title: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async openInvoiceFunc(event) {
|
||||
try {
|
||||
await apiSetShopInfo({
|
||||
open_invoice: event ? 1 : 0
|
||||
})
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
} catch(err) {
|
||||
this.$refs.uToast.show({
|
||||
title: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async specInvoiceFunc(event) {
|
||||
try {
|
||||
await apiSetShopInfo({
|
||||
spec_invoice: event ? 1 : 0
|
||||
})
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
} catch(err) {
|
||||
this.$refs.uToast.show({
|
||||
title: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: $-color-white;
|
||||
justify-content: space-between;
|
||||
|
||||
>view:first-child {
|
||||
width: 180rpx;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
>view:last-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.bb {
|
||||
border-bottom: 1px solid #F8F8F8;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<view class="m-t-20">
|
||||
|
||||
<template v-if="type == 1">
|
||||
<view class="item bb">
|
||||
<view>联系人</view>
|
||||
<view class="flex row-right">
|
||||
<input type="text" placeholder="请输入联系人姓名" v-model="nickname" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-if="type == 2">
|
||||
<view class="item bb">
|
||||
<view>联系电话</view>
|
||||
<view class="flex row-right">
|
||||
<input type="text" placeholder="请输入联系电话" v-model="mobile" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-if="type == 3">
|
||||
<view class="item bb" style="align-items:flex-start">
|
||||
<view>店铺简介</view>
|
||||
<view style="text-align: left;" class="m-t-4">
|
||||
<textarea v-model="intro" placeholder="请输入店铺简介"/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="br60 btn flex row-center white md" @click="onSubmit(type)">
|
||||
保存
|
||||
</view>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiSetShopInfo
|
||||
} from '@/api/store'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
type: 1, //1=联系人 2=联系电话 3=简介
|
||||
|
||||
nickname: '',
|
||||
mobile: '',
|
||||
intro: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try{
|
||||
const type = this.$Route.query.type;
|
||||
if(type) {
|
||||
this.type = type;
|
||||
this.nickname = this.shopInfo.nickname
|
||||
this.mobile = this.shopInfo.mobile
|
||||
this.intro = this.shopInfo.intro
|
||||
}
|
||||
|
||||
}catch(e){
|
||||
//TODO handle the exception
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit(type) {
|
||||
if(type == 1) {
|
||||
const res = await apiSetShopInfo({nickname: this.nickname})
|
||||
} else if(type == 2) {
|
||||
const res = await apiSetShopInfo({mobile: this.mobile})
|
||||
} else if(type == 3) {
|
||||
const res = await apiSetShopInfo({intro: this.intro})
|
||||
}
|
||||
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$Router.back()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: $-color-white;
|
||||
justify-content: space-between;
|
||||
>view:first-child {
|
||||
width: 180rpx;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
font-weight: 500;
|
||||
}
|
||||
>view:last-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
textarea {
|
||||
width: 560rpx;
|
||||
height: 300rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 auto;
|
||||
margin-top: 40rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<view class="goods-detail">
|
||||
|
||||
<view class="m-t-20">
|
||||
<goods-card :data="goodsInfo">
|
||||
</goods-card>
|
||||
</view>
|
||||
|
||||
<view class="muted xs p-l-20 p-b-20 p-t-20 flex row-between">
|
||||
{{goodsInfo.spec_type==1?'规格型号(统一规格)':'规格型号(多规格)'}}
|
||||
<view v-if="goodsInfo.spec_type==2" class="p-r-20" @click="spec_type=!spec_type">{{spec_type?'取消批量':'批量设置'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="primary flex p-b-20" v-show="spec_type">
|
||||
<view class="m-l-20" @click="openSpecEditFunc('price')">价格</view>
|
||||
<view class="m-l-20" @click="openSpecEditFunc('market_price')">市场价</view>
|
||||
<view class="m-l-20" @click="openSpecEditFunc('chengben_price')">成本价</view>
|
||||
<view class="m-l-20" @click="openSpecEditFunc('stock')">库存</view>
|
||||
</view>
|
||||
|
||||
<block v-for="(item, index) in goodsInfo.goods_item" :key="index">
|
||||
<view class="m-b-20 p-t-20 bg-white">
|
||||
<view class="m-b-10 primary nr title">{{item.spec_value_str}}</view>
|
||||
<view class="item bb">
|
||||
<view>价格: </view>
|
||||
<view class="flex row-between">
|
||||
<input type="text" placeholder="请输入价格" v-model="item.price" />
|
||||
元
|
||||
</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>市场价:</view>
|
||||
<view class="flex row-between">
|
||||
<input type="text" placeholder="请输入市场价" v-model="item.market_price" />
|
||||
元
|
||||
</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>成本价:</view>
|
||||
<view class="flex row-between">
|
||||
<input type="text" placeholder="请输入成本价" v-model="item.chengben_price" />
|
||||
元
|
||||
</view>
|
||||
</view>
|
||||
<view class="item bb">
|
||||
<view>库存:</view>
|
||||
<view class="flex row-between">
|
||||
<input type="text" placeholder="请输入库存" v-model="item.stock" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="footer bg-white flex row-between fixed">
|
||||
<button class="btn br60 md white" size="md" hover-class="none" @click="onSubmit">
|
||||
确认
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<modal title="批量设置" height="200rpx" v-model="flag" @confirm="specEditFunc(action)">
|
||||
<template v-if="action == 'price'">
|
||||
<view class="black nr flex" style="height: 200rpx;">
|
||||
价格: <input type="text" class="input" v-model="price" />元
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="action == 'market_price'">
|
||||
<view class="black nr flex" style="height: 200rpx;">
|
||||
市场价: <input type="text" class="input" v-model="market_price" />元
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="action == 'chengben_price'">
|
||||
<view class="black nr flex" style="height: 200rpx;">
|
||||
成本价: <input type="text" class="input" v-model="chengben_price" />元
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="action == 'stock'">
|
||||
<view class="black nr flex" style="height: 200rpx;">
|
||||
库存: <input type="text" class="input" v-model="stock" />
|
||||
</view>
|
||||
</template>
|
||||
</modal>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiGoodsDetail,apiGoodsEdit } from '@/api/goods'
|
||||
import { strToParams } from '@/utils/tools'
|
||||
|
||||
export default {
|
||||
name: 'GoodsDetail',
|
||||
|
||||
data() {
|
||||
return {
|
||||
goodsInfo: {}, // 商品信息
|
||||
|
||||
action: '',
|
||||
spec_type: false, //多规格设置 设置或取消批量多规格
|
||||
flag: false, //多规格设置 设置弹窗
|
||||
price: '',//多规格设置 价格
|
||||
market_price: '', //多规格设置 市场价
|
||||
chengben_price: '',//多规格设置 成本价
|
||||
stock: '' ,//多规格设置 库存
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化商品详情数据
|
||||
initGoodsDetail() {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiGoodsDetail({
|
||||
id: this.goods_id,
|
||||
visit: 1,
|
||||
}).then(data => {
|
||||
this.goodsInfo = data
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
openSpecEditFunc(action) {
|
||||
this.action = action;
|
||||
this.flag = true
|
||||
},
|
||||
|
||||
specEditFunc(action) {
|
||||
this.goodsInfo.goods_item.forEach(item => {
|
||||
item[action] = this[action]
|
||||
})
|
||||
this.goodsInfo.goods_item = [...this.goodsInfo.goods_item]
|
||||
},
|
||||
|
||||
async onSubmit() {
|
||||
let items = [];
|
||||
items = this.goodsInfo.goods_item.map(({id,market_price,price,stock,chengben_price}) => ({id,market_price,price,stock,chengben_price}))
|
||||
await apiGoodsEdit({items})
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
this.$Router.back()
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad() {
|
||||
const options = this.$Route.query
|
||||
|
||||
this.goods_id = options.id
|
||||
|
||||
try {
|
||||
if (!this.goods_id) throw new Error('该商品不存在')
|
||||
// 商品详情数据
|
||||
await this.initGoodsDetail()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-detail {
|
||||
padding-bottom: 200rpx;
|
||||
|
||||
.primary {
|
||||
color: $-color-primary;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 300rpx;
|
||||
padding: 10rpx;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 4rpx;
|
||||
border: 1px solid rgba(219, 219, 219, 100);
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: $-color-white;
|
||||
justify-content: flex-start;
|
||||
|
||||
>view:first-child {
|
||||
width: 180rpx;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
>view:last-child {
|
||||
width: 400rpx;
|
||||
text-align: left;
|
||||
height: 76rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 6rpx;
|
||||
border: 1px solid rgba(219, 219, 219, 100);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $-color-white;
|
||||
display: inline-block;
|
||||
margin: 0 20rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: 110rpx;
|
||||
padding: 0 30rpx;
|
||||
box-sizing: content-box;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<view class="m-t-20">
|
||||
|
||||
<view class="muted nr p-l-30 p-t-20 p-b-28">修改密码</view>
|
||||
|
||||
<view class="item bb">
|
||||
<view>当前密码</view>
|
||||
<view class="flex row-left">
|
||||
<input type="text" placeholder="请输入当前密码" v-model="old_password" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="item bb">
|
||||
<view>新密码</view>
|
||||
<view class="flex row-left">
|
||||
<input type="text" placeholder="请输入新密码" v-model="password" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="item bb">
|
||||
<view>确认密码</view>
|
||||
<view class="flex row-left">
|
||||
<input type="text" placeholder="请再次输入确认密码" v-model="password_confirm" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="br60 btn flex row-center white md" @click="onSubmit(type)">
|
||||
确认修改
|
||||
</view>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiLogout,
|
||||
apiSetPassword
|
||||
} from '@/api/user'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
old_password: '',
|
||||
password: '',
|
||||
password_confirm: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
await apiSetPassword({
|
||||
old_password: this.old_password,
|
||||
password: this.password,
|
||||
password_confirm: this.password_confirm
|
||||
})
|
||||
|
||||
this.$refs.uToast.show({
|
||||
title: '设置成功',
|
||||
type: 'success'
|
||||
})
|
||||
this.logout()
|
||||
},
|
||||
|
||||
logout() {
|
||||
// 退出登录
|
||||
apiLogout().then(res => {
|
||||
this.$store.commit("logout");
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: "/pages/login/login"
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
padding: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: $-color-white;
|
||||
justify-content: space-between;
|
||||
|
||||
>view:first-child {
|
||||
width: 180rpx;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
>view:last-child {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
|
||||
textarea {
|
||||
width: 560rpx;
|
||||
height: 300rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bb {
|
||||
border-bottom: 1px solid #F8F8F8;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 auto;
|
||||
margin-top: 40rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<view class="wallet">
|
||||
|
||||
<!-- Header -->
|
||||
<view class="header white flex row-between">
|
||||
<view class="white">
|
||||
<view class=" xs">可提现金额</view>
|
||||
<view class="header-price m-t-20">{{withdrawInfo.wallet||0}}</view>
|
||||
</view>
|
||||
|
||||
<router-link to="/bundle/pages/user_withdraw/user_withdraw">
|
||||
<view class="header-btn br60 flex bg-white row-center md">
|
||||
去提现
|
||||
</view>
|
||||
</router-link>
|
||||
</view>
|
||||
|
||||
<!-- Section -->
|
||||
<view class="section m-t-40">
|
||||
<view class="title xl normal bold p-l-30">
|
||||
<view class="inline m-t-4 m-r-20"></view>
|
||||
提现记录
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<mescroll-uni ref="mescrollRef" top="0rpx" :height="height+'px'" @init="mescrollInit"
|
||||
@up="upCallback" :up="upOption" @down="downCallback">
|
||||
|
||||
<block v-for="(item, index) in lists" :key="index">
|
||||
<view class="wallet-cell flex row-between">
|
||||
<!-- Left -->
|
||||
<view>
|
||||
<view class="remark md">余额提现</view>
|
||||
<view class="time m-t-10 muted sm">{{item.create_time}}</view>
|
||||
</view>
|
||||
|
||||
<!-- Right -->
|
||||
<view class="black">
|
||||
<view class="money lg text-right">{{item.change_amount}}</view>
|
||||
<template v-if="item.status == 0">
|
||||
<view style="color: #01D739;" class="sm">申请中</view>
|
||||
</template>
|
||||
<template v-if="item.status == 1">
|
||||
<view style="color: #01D739;" class="sm">处理中</view>
|
||||
</template>
|
||||
<template v-if="item.status == 2">
|
||||
<view class="lighter sm">转账成功</view>
|
||||
</template>
|
||||
<template v-if="item.status == 3">
|
||||
<view style="color: #FF4141;" class="sm">转账失败</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
</mescroll-uni>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
|
||||
import {
|
||||
getRect
|
||||
} from '@/utils/tools'
|
||||
import {
|
||||
apiWithdrawLog,
|
||||
apiGetWithdrawInfo
|
||||
} from "@/api/user.js"
|
||||
export default {
|
||||
mixins: [MescrollMixin],
|
||||
data() {
|
||||
return {
|
||||
height: 414,
|
||||
withdrawInfo: {},
|
||||
lists: [],
|
||||
|
||||
upOption: {
|
||||
empty: {
|
||||
icon: '/static/images/empty/money.png',
|
||||
tip: '暂无提现记录!', // 提示
|
||||
fixed: true,
|
||||
top: "200rpx",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.getHeight();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getWithdrawInfoFunc()
|
||||
},
|
||||
methods: {
|
||||
async getHeight() {
|
||||
const content = await getRect('.content')
|
||||
this.height = content.height
|
||||
},
|
||||
|
||||
async getWithdrawInfoFunc() {
|
||||
const res = await apiGetWithdrawInfo();
|
||||
this.withdrawInfo = res;
|
||||
},
|
||||
|
||||
upCallback(page) {
|
||||
const pageNum = page.num
|
||||
const pageSize = page.size
|
||||
|
||||
console.log(page)
|
||||
|
||||
apiWithdrawLog({
|
||||
page_no: pageNum,
|
||||
page_size: pageSize,
|
||||
}).then(({
|
||||
lists,
|
||||
count,
|
||||
}) => {
|
||||
// 如果是第一页或是搜索时需手动置空列表
|
||||
if (pageNum == 1||this.keyword) this.lists = []
|
||||
// 重置列表数据
|
||||
this.lists = [...this.lists, ...lists]
|
||||
this.mescroll.endBySize(10, count)
|
||||
}).catch((err) => {
|
||||
this.mescroll.endErr()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.wallet {
|
||||
.header {
|
||||
margin: 20rpx 30rpx;
|
||||
padding: 50rpx 40rpx;
|
||||
border-radius: 14rpx;
|
||||
background-color: $-color-primary;
|
||||
&-price {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
&-btn {
|
||||
width: 182rpx;
|
||||
height: 72rpx;
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
.title {
|
||||
view {
|
||||
width: 8rpx;
|
||||
height: 30rpx;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
padding-bottom: 30rpx;
|
||||
border-bottom: $-solid-border;
|
||||
}
|
||||
.content {
|
||||
padding: 0 30rpx;
|
||||
height: calc(100vh - 380rpx - env(safe-area-inset-bottom));
|
||||
|
||||
.wallet-cell {
|
||||
padding: 16rpx 30rpx;
|
||||
border-bottom: $-solid-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<view>
|
||||
<view class="withdraw">
|
||||
|
||||
<!-- Header -->
|
||||
<view class="withdraw-header bg-white flex row-between"
|
||||
@click="goPage('/bundle/pages/bank_list/bank_list')">
|
||||
<view class="black">提现账户</view>
|
||||
<view>
|
||||
<template v-if="!bank.id">
|
||||
<view class="muted">请选择<u-icon name="arrow-right" class="m-l-12"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="black">{{bank.name}}({{bank.account.substring(bank.account.length-4)}})<u-icon name="arrow-right" class="m-l-12">
|
||||
</u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Section -->
|
||||
<view class="withdraw-content m-t-20">
|
||||
<view class="black nr bold m-b-20">提现金额</view>
|
||||
|
||||
<view class="input flex">
|
||||
<text class="black bold">¥</text>
|
||||
<input type="text" v-model="money" :placeholder="'最低提现'+ withdrawInfo.min_withdrawal_money+'元'" />
|
||||
</view>
|
||||
|
||||
<view class="flex row-between m-t-30 m-b-30">
|
||||
<view class="normal sm m-t-10">可提现余额¥{{withdrawInfo.wallet||0}}</view>
|
||||
<text class="all sm flex row-right"
|
||||
@click="money = withdrawInfo.wallet">全部提现</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="m-t-30 m-l-24 xs black">
|
||||
提示:
|
||||
</view>
|
||||
|
||||
<view class="m-t-20 m-l-24 xs black">
|
||||
1. 最高可提现:¥{{withdrawInfo.max_withdrawal_money||0}}
|
||||
</view>
|
||||
<view class="m-t-20 m-l-24 xs black">
|
||||
2. 最低可提现:¥{{withdrawInfo.min_withdrawal_money||0}}
|
||||
</view>
|
||||
<view class="m-t-20 m-l-24 xs black" style="margin-bottom: 80rpx;">
|
||||
3. 提现手续费需扣除:{{withdrawInfo.withdrawal_service_charge||0}}%
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="withdraw-btn flex row-center br60 md white" @click="withdrawFunc">
|
||||
确认提现
|
||||
</view>
|
||||
|
||||
<u-toast ref="uToast" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
apiGetWithdrawInfo,
|
||||
apiWithdrawApply
|
||||
} from "@/api/user.js"
|
||||
import {
|
||||
prepay
|
||||
} from "@/api/app.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
money: '',
|
||||
bank: {},
|
||||
withdrawInfo: {}
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getWithdrawInfoFunc();
|
||||
uni.$on('getBank', (res) => {
|
||||
this.bank = res;
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 移除监听事件 优化性能
|
||||
uni.$off('getBank');
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getWithdrawInfoFunc() {
|
||||
const res = await apiGetWithdrawInfo();
|
||||
this.withdrawInfo = res;
|
||||
},
|
||||
|
||||
// 提现
|
||||
async withdrawFunc() {
|
||||
if (!this.bank.id) return this.$toast({
|
||||
title: '请选择提现的账户'
|
||||
})
|
||||
if (this.money == '') return this.$toast({
|
||||
title: '请输入提现金额'
|
||||
})
|
||||
const params = {
|
||||
money: this.money,
|
||||
bank_id: this.bank.id
|
||||
}
|
||||
const res = await apiWithdrawApply({
|
||||
...params
|
||||
})
|
||||
|
||||
this.$refs.uToast.show({
|
||||
title: '提现成功',
|
||||
type: 'success',
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$Router.back()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goPage(url) {
|
||||
this.$Router.push(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// 第一层
|
||||
.withdraw {
|
||||
padding-top: 20rpx;
|
||||
|
||||
&-header {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: 100%;
|
||||
padding: 30rpx;
|
||||
background-color: #FFFFFF;
|
||||
|
||||
.input {
|
||||
font-size: 46rpx;
|
||||
border-bottom: 1rpx solid #E5E5E5;
|
||||
|
||||
input {
|
||||
padding-left: 30rpx;
|
||||
font-size: 40rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.all {
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
width: 690rpx;
|
||||
height: 88rpx;
|
||||
margin: 0 30rpx;
|
||||
margin-top: 40rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 843 B |
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<view
|
||||
:class="['address-card', {'address-card--empty': !hasContent}]"
|
||||
:style="{'border-radius': borderRadius, 'background-color': backgroundColor}"
|
||||
>
|
||||
<!-- Sign -->
|
||||
<u-icon name="map" size="48" />
|
||||
|
||||
<!-- Content -->
|
||||
<view class="content">
|
||||
<!-- 地址信息 -->
|
||||
<view v-if="hasContent">
|
||||
<view class="bold">
|
||||
<slot name="header"></slot>
|
||||
</view>
|
||||
<view class="m-t-10 lighter">
|
||||
<slot name="main"></slot>
|
||||
</view>
|
||||
<view class="m-t-10 xs muted">
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 不存在地址 -->
|
||||
<view v-else>{{ placeholder }}</view>
|
||||
</view>
|
||||
|
||||
<!-- Action Sign -->
|
||||
<u-icon v-if="action" name="arrow-right" size="30" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
/**
|
||||
* @description 地址卡片
|
||||
* @property {Boolean} name 名称
|
||||
* @property {Boolean} action suffix图标 (默认值:true)
|
||||
* @property {Boolean} hasContent 是否存在内容 (默认值:false)
|
||||
* @property {String} placeholder 提示文字 (默认值:设置收货地址)
|
||||
* @property {String} borderRadius 圆角
|
||||
* @property {String} backgroundColor 背景色 (默认值:#FFFFFF)
|
||||
* @example <address-card :has-content="isEmpty" :action="true" />
|
||||
*/
|
||||
export default {
|
||||
name: 'AddressCard',
|
||||
|
||||
props: {
|
||||
// suffix图标
|
||||
action: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 是否存在内容
|
||||
hasContent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 提示文字
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '设置收货地址'
|
||||
},
|
||||
|
||||
// 圆角
|
||||
borderRadius: {
|
||||
type: String,
|
||||
default: 'unset',
|
||||
},
|
||||
|
||||
// 背景色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#FFFFFF'
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 164rpx;
|
||||
padding: 20rpx;
|
||||
|
||||
&--empty {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 20rpx;
|
||||
font-size: $-font-size-md;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<view class="switch" @tap="onClick" :style="[switchStyle]">
|
||||
|
||||
<!-- 背景 -->
|
||||
<view class="switch--bg flex br60 row-around">
|
||||
<!-- 文字 -->
|
||||
<view class="switch--text br60 sm flex row-left p-l-20">
|
||||
<view :class="value == 1 ? 'ani2':'ani1'" class="primary">{{firstText}}</view>
|
||||
<view :class="value == 0 ? 'ani2':'ani1'" class="lighter">{{lastText}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 滑块 -->
|
||||
<view class="switch--slider br60" :class="value == 1 ? 'open':'close'"></view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* switch 开关选择器
|
||||
*/
|
||||
export default {
|
||||
name: "u-switch",
|
||||
props: {
|
||||
// 是否为加载中状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
firstText: {
|
||||
type: [Number, String],
|
||||
default: '营业中'
|
||||
},
|
||||
lastText: {
|
||||
type: [Number, String],
|
||||
default: '暂停营业'
|
||||
},
|
||||
// 是否为禁用装填
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 打开时的背景颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#f2f2f2'
|
||||
},
|
||||
// 关闭时的背景颜色
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
// 通过v-model双向绑定的值
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
// 是否使手机发生短促震动,目前只在iOS的微信小程序有效(2020-05-06)
|
||||
vibrateShort: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 打开选择器时的值
|
||||
activeValue: {
|
||||
type: [Number, String, Boolean],
|
||||
default: true
|
||||
},
|
||||
// 关闭选择器时的值
|
||||
inactiveValue: {
|
||||
type: [Number, String, Boolean],
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
switchStyle() {
|
||||
let style = {};
|
||||
style.fontSize = this.size + 'rpx';
|
||||
style.backgroundColor = this.value ? this.activeColor : this.inactiveColor;
|
||||
return style;
|
||||
},
|
||||
loadingColor() {
|
||||
return this.value ? this.activeColor : null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
if (!this.disabled && !this.loading) {
|
||||
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
|
||||
if (this.vibrateShort) uni.vibrateShort();
|
||||
this.$emit('input', this.value == 1 ? 0 : 1);
|
||||
// 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
|
||||
this.$nextTick(() => {
|
||||
this.$emit('change', {
|
||||
value: this.value == 1 ? this.activeValue : this.inactiveValue,
|
||||
text: this.value
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch {
|
||||
width: 150rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 60rpx;
|
||||
position: relative;
|
||||
border-radius: 40rpx;
|
||||
|
||||
&--slider {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
right: 12rpx;
|
||||
top: 50%;
|
||||
z-index: 101;
|
||||
transform: translateY(-50%);
|
||||
transition: .2s linear;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.open {
|
||||
background-color: #01D739;
|
||||
}
|
||||
|
||||
.close {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.ani1 {
|
||||
transition: all .3s;
|
||||
position: absolute;
|
||||
left: -20rpx;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ani2 {
|
||||
transition: all .5s;
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&--bg {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
background-color: $-color-white;
|
||||
}
|
||||
|
||||
&--text {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1002;
|
||||
font-size: 22rpx;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
|
||||
.primary {
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<view class="charts-box">
|
||||
<qiun-data-charts
|
||||
type="area"
|
||||
:chartData="chartData"
|
||||
background="none"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
ids: {
|
||||
type: String,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '300rpx'
|
||||
},
|
||||
chartData: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style class="scss">
|
||||
.charts-box {
|
||||
width: 676rpx;
|
||||
height: 544rpx;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<view class="goods bg-white">
|
||||
|
||||
<!-- Stction -->
|
||||
<view class="goods-wrap flex" @click="toDetail(data.id)">
|
||||
<view class="image">
|
||||
<u-image :src="data.image" width="160" height="160"></u-image>
|
||||
</view>
|
||||
|
||||
<view class="m-l-16 line-1">
|
||||
<!-- 商品名称 -->
|
||||
<view class="goods-name line-1 m-t-10">{{data.name}}</view>
|
||||
<!-- 商品价格 -->
|
||||
<view class="goods-price primary m-t-10">¥{{data.min_price}}</view>
|
||||
<!-- 商品库存销量 -->
|
||||
<view class="muted flex row-between xs m-t-10">
|
||||
<view>总库存: {{data.stock}}</view>
|
||||
<view>总销量: {{data.sales_actual}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Footer -->
|
||||
<view class="goods-footer flex row-right">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
/**
|
||||
* @description 商品管理卡片
|
||||
*
|
||||
* @example <goods-card :data="goods" />
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'GoodsCard',
|
||||
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toDetail(id) {
|
||||
this.$Router.push({
|
||||
path: '/pages/goods_detail/goods_detail',
|
||||
query: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
&-wrap {
|
||||
width: 100%;
|
||||
.goods-name {
|
||||
color: #101010;
|
||||
font-size: $-font-size-nr;
|
||||
}
|
||||
.goods-price {
|
||||
color: #FF0000;
|
||||
font-size: $-font-size-nr;
|
||||
}
|
||||
>view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
&-footer {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,277 @@
|
|||
<template>
|
||||
<u-popup v-model="showPop" height="70vh" mode="bottom" border-radius="14" :closeable="true" @close="onClose"
|
||||
:safe-area-inset-bottom="true">
|
||||
<view class="bg-white spec-contain">
|
||||
<view class="spec-header flex">
|
||||
<u-image class="m-r-20" width="180rpx" height="180rpx" border-radius="10rpx" @tap="previewImage(checkedGoods.image)"
|
||||
:src="checkedGoods.image"></u-image>
|
||||
<view class="goods-info">
|
||||
<view class="primary flex">
|
||||
<price :content="checkedGoods.price" main-size="46rpx" minor-size="38rpx" color="#FF2C3C">
|
||||
</price>
|
||||
</view>
|
||||
<view class="sm m-t-120">
|
||||
库存:{{stock}}件
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 规格 -->
|
||||
<view class="spec-main">
|
||||
<scroll-view style="min-height: 600rpx;" scroll-y="true">
|
||||
<view class="spec-list">
|
||||
<view v-for="(item, index) in specList" :key="index" class="spec">
|
||||
<view class="flex row-between">
|
||||
<view class="name m-b-30">{{ item.name }}</view>
|
||||
</view>
|
||||
<view class="flex wrap">
|
||||
<view v-for="(specitem, index2) in item.spec_value" :key="index2"
|
||||
:class="'spec-item sm ' + ( specitem.checked ? 'checked' : '' )"
|
||||
@tap="choseSpecItem(item.id, specitem.id)">
|
||||
{{ specitem.value }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
stock: 0,//库存
|
||||
checkedGoods: {},
|
||||
specList: [],
|
||||
showPop: false,
|
||||
isSelectedAll: true, //是否全选
|
||||
selectStr: '', // 已选规格
|
||||
specListAll: [], // 是否全选
|
||||
};
|
||||
},
|
||||
|
||||
components: {},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean
|
||||
},
|
||||
goods: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
watch: {
|
||||
// 监听商品
|
||||
goods(value) {
|
||||
this.stock = value.stock
|
||||
let specList = value.goods_spec || [];
|
||||
let goodsItem = value.goods_item || [];
|
||||
specList.forEach(item => {
|
||||
if (item.spec_value) {
|
||||
item.spec_value.forEach((specitem, specindex) => {
|
||||
specitem.checked = 1;
|
||||
});
|
||||
}
|
||||
this.specListAll[item.id] = true
|
||||
});
|
||||
this.specList = specList
|
||||
this.checkedGoods = goodsItem.length ? goodsItem[0] : {};
|
||||
},
|
||||
// 监听规格
|
||||
specList(value) {
|
||||
const {
|
||||
goods_item
|
||||
} = this.goods;
|
||||
let keyArr = [];
|
||||
let specArr = [];
|
||||
let specArrCM = []; // 个规格选中的id
|
||||
value.forEach(item => {
|
||||
if (item.spec_value) {
|
||||
item.spec_value.forEach((specitem, indexS) => {
|
||||
if (specitem.checked) {
|
||||
keyArr.push(specitem.id);
|
||||
specArr.push(specitem.value)
|
||||
specArrCM.push(specitem.id)
|
||||
}
|
||||
if (indexS == item.spec_value.length - 1) {
|
||||
if (specArrCM.length == item.spec_value.length) {
|
||||
this.specListAll[item.id] = true
|
||||
} else {
|
||||
this.specListAll[item.id] = false
|
||||
}
|
||||
specArrCM = []
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.selectStr = specArr.join('、')
|
||||
// hsz
|
||||
if (!keyArr.length) return;
|
||||
let key = keyArr[0];
|
||||
let index = goods_item.findIndex(item => {
|
||||
return item.spec_value_ids == key;
|
||||
});
|
||||
if (index == -1) return;
|
||||
this.checkedGoods = goods_item[index]
|
||||
this.$emit('change', {
|
||||
detail: goods_item[index]
|
||||
});
|
||||
},
|
||||
show(val) {
|
||||
this.showPop = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
// 点击选中规格。
|
||||
choseSpecItem(id, specid) {
|
||||
this.specList.forEach(item => {
|
||||
if (item.spec_value && item.id == id) {
|
||||
item.spec_value.forEach(specitem => {
|
||||
if (specitem.id == specid) {
|
||||
if (specitem.checked == 1) {
|
||||
specitem.checked = 0;
|
||||
} else {
|
||||
specitem.checked = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.specList = [...this.specList]
|
||||
this.getStock();
|
||||
},
|
||||
|
||||
getStock() {
|
||||
let sku = [];
|
||||
// 遍历得到sku = [[1],[157,158]]数组
|
||||
this.specList.forEach((item, index) => {
|
||||
sku.push([])
|
||||
item.spec_value.forEach(specitem => {
|
||||
if (specitem.checked) {
|
||||
sku[index].push(specitem.id)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 算出数组多种规格组合
|
||||
let keyArr = this.dkej(sku)
|
||||
let stock = 0
|
||||
keyArr.forEach((item) => {
|
||||
const a = item.join(',')
|
||||
let index = this.goods.goods_item.findIndex(item => {
|
||||
return item.spec_value_ids == a;
|
||||
});
|
||||
if (index == -1) return;
|
||||
stock += this.goods.goods_item[index].stock
|
||||
})
|
||||
this.stock = stock
|
||||
},
|
||||
|
||||
dkej(arr) {
|
||||
let accArr = []
|
||||
for (var arrar in arr) {
|
||||
arrar = arr[arrar]
|
||||
let result = []
|
||||
for (var i in arrar) {
|
||||
if (accArr.length) {
|
||||
for (var x in accArr) {
|
||||
result.push(accArr[x].concat(arrar[i]))
|
||||
}
|
||||
} else {
|
||||
result.push([arrar[i]])
|
||||
}
|
||||
}
|
||||
accArr = result
|
||||
}
|
||||
return accArr
|
||||
},
|
||||
|
||||
previewImage(current) {
|
||||
uni.previewImage({
|
||||
current,
|
||||
// 当前显示图片的http链接
|
||||
urls: [current] // 需要预览的图片http链接列表
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.spec-contain {
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 10rpx;
|
||||
top: 10rpx;
|
||||
}
|
||||
|
||||
.spec-header {
|
||||
padding: 30rpx;
|
||||
padding-right: 70rpx;
|
||||
align-items: flex-start;
|
||||
border: $-solid-border;
|
||||
|
||||
.vip-price {
|
||||
margin: 0 24rpx;
|
||||
background-color: #FFE9BA;
|
||||
line-height: 36rpx;
|
||||
border-radius: 6rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.price-name {
|
||||
background-color: #101010;
|
||||
padding: 3rpx 12rpx;
|
||||
color: #FFD4B7;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
position: absolute;
|
||||
right: -15rpx;
|
||||
background-color: #FFE9BA;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spec-main {
|
||||
.spec-list {
|
||||
padding: 30rpx 20rpx;
|
||||
|
||||
.spec-item {
|
||||
line-height: 52rpx;
|
||||
padding: 0 30rpx;
|
||||
background-color: #F6F6F6;
|
||||
border-radius: 30rpx;
|
||||
margin: 0 20rpx 20rpx 0;
|
||||
border: 1rpx solid #F6F6F6;
|
||||
|
||||
&.checked {
|
||||
font-weight: 500;
|
||||
border: 1rpx solid $-color-primary;
|
||||
color: #fff;
|
||||
background-color: $-color-primary;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<view class="swiper-wrap">
|
||||
<swiper
|
||||
class="swiper"
|
||||
ref="swiper"
|
||||
:autoplay="autoplay"
|
||||
:circular="circular"
|
||||
:interval="interval"
|
||||
:duration="duration"
|
||||
@change="changeSwiper"
|
||||
>
|
||||
<block v-for="(item, index) in urls" :key="index">
|
||||
<swiper-item @tap="previewSwiperItem(index)">
|
||||
<view v-if="item['type'] === 'video'" class="video-wrap">
|
||||
<j-video
|
||||
width="750rpx"
|
||||
height="750rpx"
|
||||
:url="item.url"
|
||||
:poster="videoCover"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<u-image width="750" height="750" :src="item.uri" mode="scaleToFill" />
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
|
||||
<view class="dots black sm bg-white br60">{{ currentSwiperIndex + 1 }}/{{ urls.length || 0 }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentSwiperIndex: 0,
|
||||
showPlay: true,
|
||||
showControls: false,
|
||||
autoplay: true,
|
||||
videoContext: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
props: {
|
||||
// 图片链接列表
|
||||
images: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
// 视频链接
|
||||
video: {
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
},
|
||||
|
||||
// 视频封面
|
||||
videoCover: {
|
||||
type: [String, Boolean],
|
||||
default:false
|
||||
},
|
||||
|
||||
// 衔接滑动,即播放到末尾后重新回到开头
|
||||
circular: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
// 自动切换时间间隔
|
||||
interval: {
|
||||
type: Number,
|
||||
default: 3 * 1000,
|
||||
},
|
||||
|
||||
// 滑动动画时长
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
urls() {
|
||||
const images = this.images.map(item => ({ uri: item.uri, type: 'image' }))
|
||||
|
||||
return this.video
|
||||
? [{ uri: this.video, type: 'video' }, ...images]
|
||||
: images
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted() {
|
||||
this.videoContext = uni.createVideoContext('video', this)
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
// 切换Swiper项
|
||||
changeSwiper(e) {
|
||||
this.currentSwiperIndex = e.detail.current
|
||||
},
|
||||
|
||||
// 视频异常
|
||||
videoErrorCallback(err) {
|
||||
throw new Error(err)
|
||||
},
|
||||
|
||||
// 查看Swiper项
|
||||
previewSwiperItem(current) {
|
||||
const type = this.urls[current].type
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.previewMedia({
|
||||
current,
|
||||
sources: this.urls,
|
||||
})
|
||||
//#endif
|
||||
|
||||
// #ifdef H5 || APP-PLUS
|
||||
const index = this.video ? current - 1 : current
|
||||
type === "video"
|
||||
? this.videoContext.requestFullScreen()
|
||||
: uni.previewImage({ index, urls: this.images })
|
||||
//#endif
|
||||
},
|
||||
|
||||
// 视频播放
|
||||
playVideo() {
|
||||
// this.videoContext.play()
|
||||
},
|
||||
|
||||
// 当视频进入和退出全屏时触发
|
||||
fullscreenchange(e) {
|
||||
const { fullScreen } = e.detail
|
||||
this.showPlay = !fullScreen
|
||||
this.showControls = !!fullScreen
|
||||
console.log(fullScreen)
|
||||
!!fullScreen && this.videoContext.play()
|
||||
!fullScreen && this.videoContext.pause()
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.swiper-wrap {
|
||||
width: 100%;
|
||||
height: 750rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.swiper-wrap .swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.swiper-wrap .swiper .slide-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-wrap .dots {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 24rpx;
|
||||
display: flex;
|
||||
height: 34rpx;
|
||||
padding: 0 15rpx;
|
||||
}
|
||||
|
||||
.swiper-wrap .video-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.swiper-wrap .video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-wrap .icon-play {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<view
|
||||
class="root"
|
||||
:style="{width,height}"
|
||||
>
|
||||
<image
|
||||
:style="{width,height}"
|
||||
class="posterImg"
|
||||
:src="posterUrl"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view
|
||||
class="box"
|
||||
:style="{width,height}"
|
||||
@click="state=!state"
|
||||
>
|
||||
<image
|
||||
class="playIcon"
|
||||
src="/static/images/icon_play.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
<video
|
||||
:id="videoId"
|
||||
:style="{height,width:state?'750rpx':'1rpx'}"
|
||||
:autoplay="false"
|
||||
class="video"
|
||||
:src="url"
|
||||
@timeupdate="timeupdate"
|
||||
@fullscreenchange="fullscreenchange"
|
||||
@pause="state=0"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
poster: {
|
||||
type: [String, Boolean],
|
||||
default: '',
|
||||
},
|
||||
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
direction: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
width: {
|
||||
type: String,
|
||||
default: '750rpx',
|
||||
},
|
||||
|
||||
height: {
|
||||
type: String,
|
||||
default: '450rpx',
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
VideoContext: {},
|
||||
state: false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
videoId: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
posterUrl() {
|
||||
if (this.poster) return this.poster
|
||||
return this.url + '?x-oss-process=video/snapshot,t_' + (parseInt(this.currentTime * 1000)) +
|
||||
',f_jpg,w_800,m_fast'
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fullscreenchange(e) {
|
||||
this.state = e.detail.fullScreen
|
||||
},
|
||||
timeupdate(e) {
|
||||
this.duration = e.detail.duration
|
||||
this.currentTime = e.detail.currentTime
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.videoId = Date.now() + Math.ceil(Math.random() * 10000000) + "";
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.VideoContext = uni.createVideoContext(this.videoId)
|
||||
},
|
||||
|
||||
watch: {
|
||||
state(state, oldValue) {
|
||||
//console.log(state,'state');
|
||||
if (!state) {
|
||||
this.VideoContext.pause()
|
||||
} else {
|
||||
this.VideoContext.play()
|
||||
setTimeout(() => {
|
||||
this.VideoContext.requestFullScreen({
|
||||
direction: this.direction
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.root {
|
||||
position: relative;
|
||||
width: 750rpx;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.posterImg,
|
||||
.video,
|
||||
.box {
|
||||
display: flex;
|
||||
width: 750rpx;
|
||||
height: 300px;
|
||||
//border: solid 1px red;absolute
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.video {
|
||||
margin-left: -2000px;
|
||||
}
|
||||
|
||||
.posterImg {
|
||||
//border: solid red 1px;
|
||||
}
|
||||
|
||||
.box {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.playIcon {
|
||||
width: 100rpx;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<view class="loading" :style="{'background-color': bgColor}">
|
||||
<u-loading :size="size" mode="flower"></u-loading>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 60
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
/* 下拉刷新区域 */
|
||||
.mescroll-downwarp {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 下拉刷新--内容区,定位于区域底部 */
|
||||
.mescroll-downwarp .downwarp-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
min-height: 60rpx;
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 下拉刷新--提示文本 */
|
||||
.mescroll-downwarp .downwarp-tip {
|
||||
display: inline-block;
|
||||
font-size: 28rpx;
|
||||
vertical-align: middle;
|
||||
margin-left: 16rpx;
|
||||
/* color: gray; 已在style设置color,此处删去*/
|
||||
}
|
||||
|
||||
/* 下拉刷新--旋转进度条 */
|
||||
.mescroll-downwarp .downwarp-progress {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid gray;
|
||||
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.mescroll-downwarp .mescroll-rotate {
|
||||
animation: mescrollDownRotate 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mescrollDownRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<!-- 下拉刷新区域 -->
|
||||
<template>
|
||||
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
option: Object , // down的配置项
|
||||
type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
|
||||
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption(){
|
||||
return this.option || {}
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.type === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.rate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
switch (this.type){
|
||||
case 1: return this.mOption.textInOffset;
|
||||
case 2: return this.mOption.textOutOffset;
|
||||
case 3: return this.mOption.textLoading;
|
||||
case 4: return this.mOption.textLoading;
|
||||
default: return this.mOption.textInOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./mescroll-down.css";
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
<!--空布局
|
||||
|
||||
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
|
||||
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
|
||||
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
-->
|
||||
<template>
|
||||
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
|
||||
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
|
||||
<view v-if="tip" class="empty-tip">{{ tip }}</view>
|
||||
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 引入全局配置
|
||||
import GlobalOption from './../mescroll-uni-option.js';
|
||||
export default {
|
||||
props: {
|
||||
// empty的配置项: 默认为GlobalOption.up.empty
|
||||
option: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
// 使用computed获取配置,用于支持option的动态配置
|
||||
computed: {
|
||||
// 图标
|
||||
icon() {
|
||||
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
|
||||
},
|
||||
// 文本提示
|
||||
tip() {
|
||||
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击按钮
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 无任何数据的空布局 */
|
||||
.mescroll-empty {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 200rpx 50rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mescroll-empty.empty-fixed {
|
||||
z-index: 99;
|
||||
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
|
||||
top: 100rpx;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-icon {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-tip {
|
||||
margin-top: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-btn {
|
||||
display: inline-block;
|
||||
margin-top: 40rpx;
|
||||
min-width: 200rpx;
|
||||
padding: 18rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1rpx solid #e04b28;
|
||||
border-radius: 60rpx;
|
||||
color: #e04b28;
|
||||
}
|
||||
|
||||
.mescroll-empty .empty-btn:active {
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<!-- 回到顶部的按钮 -->
|
||||
<template>
|
||||
<image
|
||||
v-if="mOption.src"
|
||||
class="mescroll-totop"
|
||||
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
|
||||
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
|
||||
:src="mOption.src"
|
||||
mode="widthFix"
|
||||
@click="toTopClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// up.toTop的配置项
|
||||
option: Object,
|
||||
// 是否显示
|
||||
value: false
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption(){
|
||||
return this.option || {}
|
||||
},
|
||||
// 优先显示左边
|
||||
left(){
|
||||
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
|
||||
},
|
||||
// 右边距离 (优先显示左边)
|
||||
right() {
|
||||
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addUnit(num){
|
||||
if(!num) return 0;
|
||||
if(typeof num === 'number') return num + 'rpx';
|
||||
return num
|
||||
},
|
||||
toTopClick() {
|
||||
this.$emit('input', false); // 使v-model生效
|
||||
this.$emit('click'); // 派发点击事件
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 回到顶部的按钮 */
|
||||
.mescroll-totop {
|
||||
z-index: 9990;
|
||||
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
|
||||
right: 20rpx;
|
||||
bottom: 120rpx;
|
||||
width: 84rpx;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s; /* 过渡 */
|
||||
margin-bottom: var(--window-bottom); /* css变量 */
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-totop-safearea {
|
||||
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
|
||||
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示 -- 淡入 */
|
||||
.mescroll-totop-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 隐藏 -- 淡出且不接收事件*/
|
||||
.mescroll-totop-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
/* 上拉加载区域 */
|
||||
.mescroll-upwarp {
|
||||
box-sizing: border-box;
|
||||
min-height: 110rpx;
|
||||
padding: 30rpx 0;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/*提示文本 */
|
||||
.mescroll-upwarp .upwarp-tip,
|
||||
.mescroll-upwarp .upwarp-nodata {
|
||||
display: inline-block;
|
||||
font-size: 28rpx;
|
||||
vertical-align: middle;
|
||||
/* color: gray; 已在style设置color,此处删去*/
|
||||
}
|
||||
|
||||
.mescroll-upwarp .upwarp-tip {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/*旋转进度条 */
|
||||
.mescroll-upwarp .upwarp-progress {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid gray;
|
||||
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.mescroll-upwarp .mescroll-rotate {
|
||||
animation: mescrollUpRotate 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mescrollUpRotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<!-- 上拉加载区域 -->
|
||||
<template>
|
||||
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="isUpLoading">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
option: Object, // up的配置项
|
||||
type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
|
||||
},
|
||||
computed: {
|
||||
// 支付宝小程序需写成计算属性,prop定义default仍报错
|
||||
mOption() {
|
||||
return this.option || {};
|
||||
},
|
||||
// 加载中
|
||||
isUpLoading() {
|
||||
return this.type === 1;
|
||||
},
|
||||
// 没有更多了
|
||||
isUpNoMore() {
|
||||
return this.type === 2;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './mescroll-up.css';
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
.mescroll-body {
|
||||
position: relative; /* 下拉刷新区域相对自身定位 */
|
||||
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
|
||||
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
|
||||
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
|
||||
}
|
||||
.mescroll-body .downwarp-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
|
||||
.mescroll-body.mescorll-sticky{
|
||||
overflow: unset !important
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-safearea {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
<template>
|
||||
<view
|
||||
class="mescroll-body mescroll-render-touch"
|
||||
:class="{'mescorll-sticky': sticky}"
|
||||
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
|
||||
@touchstart="wxsBiz.touchstartEvent"
|
||||
@touchmove="wxsBiz.touchmoveEvent"
|
||||
@touchend="wxsBiz.touchendEvent"
|
||||
@touchcancel="wxsBiz.touchendEvent"
|
||||
:change:prop="wxsBiz.propObserver"
|
||||
:prop="wxsProp"
|
||||
>
|
||||
<!-- 状态栏 -->
|
||||
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
|
||||
|
||||
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
|
||||
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
|
||||
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
|
||||
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 空布局 -->
|
||||
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
|
||||
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
|
||||
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="upLoadType===1">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 适配iPhoneX -->
|
||||
<view v-if="safearea" class="mescroll-safearea"></view>
|
||||
|
||||
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
|
||||
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
|
||||
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- app, h5使用renderjs -->
|
||||
<!-- #ifdef APP-PLUS || H5 -->
|
||||
<script module="renderBiz" lang="renderjs">
|
||||
import renderBiz from './wxs/renderjs.js';
|
||||
export default {
|
||||
mixins: [renderBiz]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
|
||||
<script>
|
||||
// 引入mescroll-uni.js,处理核心逻辑
|
||||
import MeScroll from './mescroll-uni.js';
|
||||
// 引入全局配置
|
||||
import GlobalOption from './mescroll-uni-option.js';
|
||||
// 引入空布局组件
|
||||
import MescrollEmpty from './components/mescroll-empty.vue';
|
||||
// 引入回到顶部组件
|
||||
import MescrollTop from './components/mescroll-top.vue';
|
||||
// 引入兼容wxs(含renderjs)写法的mixins
|
||||
import WxsMixin from './wxs/mixins.js';
|
||||
|
||||
export default {
|
||||
mixins: [WxsMixin],
|
||||
components: {
|
||||
MescrollEmpty,
|
||||
MescrollTop
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mescroll: {optDown:{},optUp:{}}, // mescroll实例
|
||||
downHight: 0, //下拉刷新: 容器高度
|
||||
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
|
||||
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
|
||||
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
|
||||
isShowEmpty: false, // 是否显示空布局
|
||||
isShowToTop: false, // 是否显示回到顶部按钮
|
||||
windowHeight: 0, // 可使用窗口的高度
|
||||
windowBottom: 0, // 可使用窗口的底部位置
|
||||
statusBarHeight: 0 // 状态栏高度
|
||||
};
|
||||
},
|
||||
props: {
|
||||
down: Object, // 下拉刷新的参数配置
|
||||
up: Object, // 上拉加载的参数配置
|
||||
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
|
||||
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
|
||||
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
|
||||
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
|
||||
},
|
||||
watch:{
|
||||
downLoadType(val) {
|
||||
this.$emit('changedownloding', val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
|
||||
minHeight(){
|
||||
return typeof this.height == 'string' ? this.height : this.toPx(this.height || '100%') + 'px'
|
||||
},
|
||||
// 下拉布局往下偏移的距离 (px)
|
||||
numTop() {
|
||||
return this.toPx(this.top)
|
||||
},
|
||||
padTop() {
|
||||
return this.numTop + 'px';
|
||||
},
|
||||
// 上拉布局往上偏移 (px)
|
||||
numBottom() {
|
||||
return this.toPx(this.bottom);
|
||||
},
|
||||
padBottom() {
|
||||
return this.numBottom + 'px';
|
||||
},
|
||||
// 是否为重置下拉的状态
|
||||
isDownReset() {
|
||||
return this.downLoadType === 3 || this.downLoadType === 4;
|
||||
},
|
||||
// 过渡
|
||||
transition() {
|
||||
return this.isDownReset ? 'transform 300ms' : '';
|
||||
},
|
||||
translateY() {
|
||||
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.downLoadType === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.downRate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
|
||||
switch (this.downLoadType){
|
||||
case 1: return this.mescroll.optDown.textInOffset;
|
||||
case 2: return this.mescroll.optDown.textOutOffset;
|
||||
case 3: return this.mescroll.optDown.textLoading;
|
||||
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
|
||||
default: return this.mescroll.optDown.textInOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//number,rpx,upx,px,% --> px的数值
|
||||
toPx(num) {
|
||||
if (typeof num === 'string') {
|
||||
if (num.indexOf('px') !== -1) {
|
||||
if (num.indexOf('rpx') !== -1) {
|
||||
// "10rpx"
|
||||
num = num.replace('rpx', '');
|
||||
} else if (num.indexOf('upx') !== -1) {
|
||||
// "10upx"
|
||||
num = num.replace('upx', '');
|
||||
} else {
|
||||
// "10px"
|
||||
return Number(num.replace('px', ''));
|
||||
}
|
||||
} else if (num.indexOf('%') !== -1) {
|
||||
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
|
||||
let rate = Number(num.replace('%', '')) / 100;
|
||||
return this.windowHeight * rate;
|
||||
}
|
||||
}
|
||||
return num ? uni.upx2px(Number(num)) : 0;
|
||||
},
|
||||
// 点击空布局的按钮回调
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick', this.mescroll);
|
||||
},
|
||||
// 点击回到顶部的按钮回调
|
||||
toTopClick() {
|
||||
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
|
||||
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
created() {
|
||||
let vm = this;
|
||||
|
||||
let diyOption = {
|
||||
// 下拉刷新的配置
|
||||
down: {
|
||||
inOffset() {
|
||||
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
outOffset() {
|
||||
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
onMoving(mescroll, rate, downHight) {
|
||||
// 下拉过程中的回调,滑动过程一直在执行;
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
showLoading(mescroll, downHight) {
|
||||
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
beforeEndDownScroll(mescroll){
|
||||
vm.downLoadType = 4;
|
||||
return mescroll.optDown.beforeEndDelay // 延时结束的时长
|
||||
},
|
||||
endDownScroll() {
|
||||
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
|
||||
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
|
||||
if(vm.downLoadType === 4) vm.downLoadType = 0
|
||||
},300)
|
||||
},
|
||||
// 派发下拉刷新的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('down', mescroll);
|
||||
}
|
||||
},
|
||||
// 上拉加载的配置
|
||||
up: {
|
||||
// 显示加载中的回调
|
||||
showLoading() {
|
||||
vm.upLoadType = 1;
|
||||
},
|
||||
// 显示无更多数据的回调
|
||||
showNoMore() {
|
||||
vm.$nextTick(() => {
|
||||
vm.upLoadType = 2;
|
||||
})
|
||||
},
|
||||
// 隐藏上拉加载的回调
|
||||
hideUpScroll(mescroll) {
|
||||
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
|
||||
},
|
||||
// 空布局
|
||||
empty: {
|
||||
onShow(isShow) {
|
||||
// 显示隐藏的回调
|
||||
vm.isShowEmpty = isShow;
|
||||
}
|
||||
},
|
||||
// 回到顶部
|
||||
toTop: {
|
||||
onShow(isShow) {
|
||||
// 显示隐藏的回调
|
||||
vm.isShowToTop = isShow;
|
||||
}
|
||||
},
|
||||
// 派发上拉加载的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('up', mescroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
|
||||
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
|
||||
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
|
||||
|
||||
// 初始化MeScroll对象
|
||||
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
|
||||
// init回调mescroll对象
|
||||
vm.$emit('init', vm.mescroll);
|
||||
|
||||
// 设置高度
|
||||
const sys = uni.getSystemInfoSync();
|
||||
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
|
||||
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
|
||||
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是page的scroll,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
if(typeof y === 'string'){
|
||||
// 滚动到指定view (y为css选择器)
|
||||
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
|
||||
let selector;
|
||||
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
|
||||
selector = '#'+y // 不带#和. 则默认为id选择器
|
||||
}else{
|
||||
selector = y
|
||||
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
|
||||
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
|
||||
selector = y.split('>>>')[1].trim()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
|
||||
if (rect) {
|
||||
let top = rect.top
|
||||
top += vm.mescroll.getScrollTop()
|
||||
uni.pageScrollTo({
|
||||
scrollTop: top,
|
||||
duration: t
|
||||
})
|
||||
} else{
|
||||
console.error(selector + ' does not exist');
|
||||
}
|
||||
}).exec()
|
||||
},30)
|
||||
} else{
|
||||
// 滚动到指定位置 (y必须为数字)
|
||||
uni.pageScrollTo({
|
||||
scrollTop: y,
|
||||
duration: t
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
|
||||
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
|
||||
vm.mescroll.optUp.toTop.safearea = vm.safearea;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./mescroll-body.css";
|
||||
@import "./components/mescroll-down.css";
|
||||
@import './components/mescroll-up.css';
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
// mescroll-body 和 mescroll-uni 通用
|
||||
|
||||
// import MescrollUni from "./mescroll-uni.vue";
|
||||
// import MescrollBody from "./mescroll-body.vue";
|
||||
|
||||
const MescrollMixin = {
|
||||
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
|
||||
// MescrollUni,
|
||||
// MescrollBody
|
||||
// },
|
||||
data() {
|
||||
return {
|
||||
mescroll: null //mescroll实例对象
|
||||
}
|
||||
},
|
||||
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
onPullDownRefresh(){
|
||||
this.mescroll && this.mescroll.onPullDownRefresh();
|
||||
},
|
||||
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
onPageScroll(e) {
|
||||
this.mescroll && this.mescroll.onPageScroll(e);
|
||||
},
|
||||
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
onReachBottom() {
|
||||
this.mescroll && this.mescroll.onReachBottom();
|
||||
},
|
||||
methods: {
|
||||
// mescroll组件初始化的回调,可获取到mescroll对象
|
||||
mescrollInit(mescroll) {
|
||||
console.log(mescroll)
|
||||
this.mescroll = mescroll;
|
||||
this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
},
|
||||
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
mescrollInitByRef() {
|
||||
if(!this.mescroll || !this.mescroll.resetUpScroll){
|
||||
let mescrollRef = this.$refs.mescrollRef;
|
||||
if(mescrollRef) this.mescroll = mescrollRef.mescroll
|
||||
}
|
||||
},
|
||||
// 下拉刷新的回调 (mixin默认resetUpScroll)
|
||||
downCallback() {
|
||||
if(this.mescroll.optUp.use){
|
||||
this.mescroll.resetUpScroll()
|
||||
}else{
|
||||
setTimeout(()=>{
|
||||
this.mescroll.endSuccess();
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
// 上拉加载的回调
|
||||
upCallback() {
|
||||
// mixin默认延时500自动结束加载
|
||||
setTimeout(()=>{
|
||||
this.mescroll.endErr();
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MescrollMixin;
|
|
@ -0,0 +1,36 @@
|
|||
// 全局配置
|
||||
// mescroll-body 和 mescroll-uni 通用
|
||||
const GlobalOption = {
|
||||
down: {
|
||||
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textSuccess: '加载成功', // 加载成功的文本
|
||||
textErr: '加载失败', // 加载失败的文本
|
||||
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
},
|
||||
up: {
|
||||
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textNoMore: '没有更多了~', // 没有更多数据的提示文本
|
||||
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
toTop: {
|
||||
// 回到顶部按钮,需配置src才显示
|
||||
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
bottom: 100, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
width: 84 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
},
|
||||
empty: {
|
||||
use: true, // 是否显示空布局
|
||||
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||
tip: '空空如也' // 提示
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalOption
|
|
@ -0,0 +1,38 @@
|
|||
.mescroll-uni-warp{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mescroll-uni-content{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mescroll-uni {
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200rpx;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
|
||||
}
|
||||
|
||||
/* 定位的方式固定高度 */
|
||||
.mescroll-uni-fixed{
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: auto; /* 使right生效 */
|
||||
height: auto; /* 使bottom生效 */
|
||||
}
|
||||
|
||||
/* 适配 iPhoneX */
|
||||
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
|
||||
.mescroll-safearea {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,799 @@
|
|||
/* mescroll
|
||||
* version 1.3.3
|
||||
* 2020-09-15 wenju
|
||||
* https://www.mescroll.com
|
||||
*/
|
||||
|
||||
export default function MeScroll(options, isScrollBody) {
|
||||
let me = this;
|
||||
me.version = '1.3.3'; // mescroll版本号
|
||||
me.options = options || {}; // 配置
|
||||
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
|
||||
|
||||
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
|
||||
me.isUpScrolling = false; // 是否在执行上拉加载的回调
|
||||
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
|
||||
|
||||
// 初始化下拉刷新
|
||||
me.initDownScroll();
|
||||
// 初始化上拉加载,则初始化
|
||||
me.initUpScroll();
|
||||
|
||||
// 自动加载
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
|
||||
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
|
||||
if (me.optDown.autoShowLoading) {
|
||||
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
|
||||
} else {
|
||||
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
|
||||
}
|
||||
}
|
||||
// 自动触发上拉加载
|
||||
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
|
||||
setTimeout(function(){
|
||||
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
|
||||
},100)
|
||||
}
|
||||
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
|
||||
}
|
||||
|
||||
/* 配置参数:下拉刷新 */
|
||||
MeScroll.prototype.extendDownScroll = function(optDown) {
|
||||
// 下拉刷新的配置
|
||||
MeScroll.extend(optDown, {
|
||||
use: true, // 是否启用下拉刷新; 默认true
|
||||
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
|
||||
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
|
||||
isLock: false, // 是否锁定下拉刷新,默认false;
|
||||
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
|
||||
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
|
||||
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
|
||||
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textSuccess: '加载成功', // 加载成功的文本
|
||||
textErr: '加载失败', // 加载失败的文本
|
||||
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
|
||||
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
inited: null, // 下拉刷新初始化完毕的回调
|
||||
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
|
||||
outOffset: null, // 下拉的距离大于offset那一刻的回调
|
||||
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
|
||||
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
|
||||
showLoading: null, // 显示下拉刷新进度的回调
|
||||
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
|
||||
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
|
||||
endDownScroll: null, // 结束下拉刷新的回调
|
||||
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
|
||||
callback: function(mescroll) {
|
||||
// 下拉刷新的回调;默认重置上拉加载列表为第一页
|
||||
mescroll.resetUpScroll();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* 配置参数:上拉加载 */
|
||||
MeScroll.prototype.extendUpScroll = function(optUp) {
|
||||
// 上拉加载的配置
|
||||
MeScroll.extend(optUp, {
|
||||
use: true, // 是否启用上拉加载; 默认true
|
||||
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
|
||||
isLock: false, // 是否锁定上拉加载,默认false;
|
||||
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
|
||||
callback: null, // 上拉加载的回调;function(page,mescroll){ }
|
||||
page: {
|
||||
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
|
||||
size: 10, // 每页数据的数量
|
||||
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
|
||||
},
|
||||
noMoreSize: 4, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
|
||||
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
|
||||
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
inited: null, // 初始化完毕的回调
|
||||
showLoading: null, // 显示加载中的回调
|
||||
showNoMore: null, // 显示无更多数据的回调
|
||||
hideUpScroll: null, // 隐藏上拉加载的回调
|
||||
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
|
||||
toTop: {
|
||||
// 回到顶部按钮,需配置src才显示
|
||||
src: null, // 图片路径,默认null (绝对路径或网络图)
|
||||
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
|
||||
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
|
||||
btnClick: null, // 点击按钮的回调
|
||||
onShow: null, // 是否显示的回调
|
||||
zIndex: 9990, // fixed定位z-index值
|
||||
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
|
||||
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
},
|
||||
empty: {
|
||||
use: true, // 是否显示空布局
|
||||
icon: null, // 图标路径
|
||||
tip: '~ 暂无相关数据 ~', // 提示
|
||||
btnText: '', // 按钮
|
||||
btnClick: null, // 点击按钮的回调
|
||||
onShow: null, // 是否显示的回调
|
||||
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
|
||||
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
|
||||
zIndex: 99 // fixed定位z-index值
|
||||
},
|
||||
onScroll: false // 是否监听滚动事件
|
||||
})
|
||||
}
|
||||
|
||||
/* 配置参数 */
|
||||
MeScroll.extend = function(userOption, defaultOption) {
|
||||
if (!userOption) return defaultOption;
|
||||
for (let key in defaultOption) {
|
||||
if (userOption[key] == null) {
|
||||
let def = defaultOption[key];
|
||||
if (def != null && typeof def === 'object') {
|
||||
userOption[key] = MeScroll.extend({}, def); // 深度匹配
|
||||
} else {
|
||||
userOption[key] = def;
|
||||
}
|
||||
} else if (typeof userOption[key] === 'object') {
|
||||
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
|
||||
}
|
||||
}
|
||||
return userOption;
|
||||
}
|
||||
|
||||
/* 简单判断是否配置了颜色 (非透明,非白色) */
|
||||
MeScroll.prototype.hasColor = function(color) {
|
||||
if(!color) return false;
|
||||
let c = color.toLowerCase();
|
||||
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
|
||||
}
|
||||
|
||||
/* -------初始化下拉刷新------- */
|
||||
MeScroll.prototype.initDownScroll = function() {
|
||||
let me = this;
|
||||
// 配置参数
|
||||
me.optDown = me.options.down || {};
|
||||
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
me.extendDownScroll(me.optDown);
|
||||
|
||||
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
|
||||
if(me.isScrollBody && me.optDown.native){
|
||||
me.optDown.use = false
|
||||
}else{
|
||||
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
|
||||
}
|
||||
|
||||
me.downHight = 0; // 下拉区域的高度
|
||||
|
||||
// 在页面中加入下拉布局
|
||||
if (me.optDown.use && me.optDown.inited) {
|
||||
// 初始化完毕的回调
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
me.optDown.inited(me);
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表touchstart事件 */
|
||||
MeScroll.prototype.touchstartEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
|
||||
this.startPoint = this.getPoint(e); // 记录起点
|
||||
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
|
||||
this.startAngle = 0; // 初始角度
|
||||
this.lastPoint = this.startPoint; // 重置上次move的点
|
||||
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||
this.inTouchend = false; // 标记不是touchend
|
||||
}
|
||||
|
||||
/* 列表touchmove事件 */
|
||||
MeScroll.prototype.touchmoveEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
let me = this;
|
||||
|
||||
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||
let curPoint = me.getPoint(e); // 当前点
|
||||
|
||||
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
||||
// 向下拉 && 在顶部
|
||||
// mescroll-body,直接判定在顶部即可
|
||||
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||
if (moveY > 0 && (
|
||||
(me.isScrollBody && scrollTop <= 0)
|
||||
||
|
||||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
|
||||
)) {
|
||||
// 可下拉的条件
|
||||
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
|
||||
me.optUp.isBoth))) {
|
||||
|
||||
// 下拉的初始角度是否在配置的范围内
|
||||
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||
|
||||
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
|
||||
me.inTouchend = true; // 标记执行touchend
|
||||
me.touchendEvent(); // 提前触发touchend
|
||||
return;
|
||||
}
|
||||
|
||||
me.preventDefault(e); // 阻止默认事件
|
||||
|
||||
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||
|
||||
// 下拉距离 < 指定距离
|
||||
if (me.downHight < me.optDown.offset) {
|
||||
if (me.movetype !== 1) {
|
||||
me.movetype = 1; // 加入标记,保证只执行一次
|
||||
me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
|
||||
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||
|
||||
// 指定距离 <= 下拉距离
|
||||
} else {
|
||||
if (me.movetype !== 2) {
|
||||
me.movetype = 2; // 加入标记,保证只执行一次
|
||||
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
if (diff > 0) { // 向下拉
|
||||
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||
} else { // 向上收
|
||||
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||
}
|
||||
}
|
||||
|
||||
me.downHight = Math.round(me.downHight) // 取整
|
||||
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||
}
|
||||
}
|
||||
|
||||
me.lastPoint = curPoint; // 记录本次移动的点
|
||||
}
|
||||
|
||||
/* 列表touchend事件 */
|
||||
MeScroll.prototype.touchendEvent = function(e) {
|
||||
if (!this.optDown.use) return;
|
||||
// 如果下拉区域高度已改变,则需重置回来
|
||||
if (this.isMoveDown) {
|
||||
if (this.downHight >= this.optDown.offset) {
|
||||
// 符合触发刷新的条件
|
||||
this.triggerDownScroll();
|
||||
} else {
|
||||
// 不符合的话 则重置
|
||||
this.downHight = 0;
|
||||
this.endDownScrollCall(this);
|
||||
}
|
||||
this.movetype = 0;
|
||||
this.isMoveDown = false;
|
||||
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 上滑
|
||||
if (isScrollUp) {
|
||||
// 需检查滑动的角度
|
||||
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (angle > 80) {
|
||||
// 检查并触发上拉
|
||||
this.triggerUpScroll(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
MeScroll.prototype.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {
|
||||
x: e.touches[0].pageX,
|
||||
y: e.touches[0].pageY
|
||||
}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {
|
||||
x: e.changedTouches[0].pageX,
|
||||
y: e.changedTouches[0].pageY
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算两点之间的角度: 区间 [0,90]*/
|
||||
MeScroll.prototype.getAngle = function(p1, p2) {
|
||||
let x = Math.abs(p1.x - p2.x);
|
||||
let y = Math.abs(p1.y - p2.y);
|
||||
let z = Math.sqrt(x * x + y * y);
|
||||
let angle = 0;
|
||||
if (z !== 0) {
|
||||
angle = Math.asin(y / z) / Math.PI * 180;
|
||||
}
|
||||
return angle
|
||||
}
|
||||
|
||||
/* 触发下拉刷新 */
|
||||
MeScroll.prototype.triggerDownScroll = function() {
|
||||
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
|
||||
//return true则处于完全自定义状态
|
||||
} else {
|
||||
this.showDownScroll(); // 下拉刷新中...
|
||||
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示下拉进度布局 */
|
||||
MeScroll.prototype.showDownScroll = function() {
|
||||
this.isDownScrolling = true; // 标记下拉中
|
||||
if (this.optDown.native) {
|
||||
uni.startPullDownRefresh(); // 系统自带的下拉刷新
|
||||
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
} else{
|
||||
this.downHight = this.optDown.offset; // 更新下拉区域高度
|
||||
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.prototype.showDownLoadingCall = function(downHight) {
|
||||
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
|
||||
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
|
||||
}
|
||||
|
||||
/* 显示系统自带的下拉刷新时需要处理的业务 */
|
||||
MeScroll.prototype.onPullDownRefresh = function() {
|
||||
this.isDownScrolling = true; // 标记下拉中
|
||||
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
|
||||
/* 结束下拉刷新 */
|
||||
MeScroll.prototype.endDownScroll = function() {
|
||||
if (this.optDown.native) { // 结束原生下拉刷新
|
||||
this.isDownScrolling = false;
|
||||
this.endDownScrollCall(this);
|
||||
uni.stopPullDownRefresh();
|
||||
return
|
||||
}
|
||||
let me = this;
|
||||
// 结束下拉刷新的方法
|
||||
let endScroll = function() {
|
||||
me.downHight = 0;
|
||||
me.isDownScrolling = false;
|
||||
me.endDownScrollCall(me);
|
||||
if(!me.isScrollBody){
|
||||
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||||
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
|
||||
}
|
||||
}
|
||||
// 结束下拉刷新时的回调
|
||||
let delay = 0;
|
||||
if (me.optDown.beforeEndDownScroll) {
|
||||
delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
|
||||
if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
|
||||
}
|
||||
if (typeof delay === 'number' && delay > 0) {
|
||||
setTimeout(endScroll, delay);
|
||||
} else {
|
||||
endScroll();
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.prototype.endDownScrollCall = function() {
|
||||
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
|
||||
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
|
||||
}
|
||||
|
||||
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
|
||||
MeScroll.prototype.lockDownScroll = function(isLock) {
|
||||
if (isLock == null) isLock = true;
|
||||
this.optDown.isLock = isLock;
|
||||
}
|
||||
|
||||
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
|
||||
MeScroll.prototype.lockUpScroll = function(isLock) {
|
||||
if (isLock == null) isLock = true;
|
||||
this.optUp.isLock = isLock;
|
||||
}
|
||||
|
||||
/* -------初始化上拉加载------- */
|
||||
MeScroll.prototype.initUpScroll = function() {
|
||||
let me = this;
|
||||
// 配置参数
|
||||
me.optUp = me.options.up || {use: false}
|
||||
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
me.extendUpScroll(me.optUp);
|
||||
|
||||
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
|
||||
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
|
||||
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
|
||||
|
||||
// 初始化完毕的回调
|
||||
if (me.optUp.inited) {
|
||||
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
me.optUp.inited(me);
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/*滚动到底部的事件 (仅mescroll-body生效)*/
|
||||
MeScroll.prototype.onReachBottom = function() {
|
||||
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
|
||||
if (!this.optUp.isLock && this.optUp.hasNext) {
|
||||
this.triggerUpScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*列表滚动事件 (仅mescroll-body生效)*/
|
||||
MeScroll.prototype.onPageScroll = function(e) {
|
||||
if (!this.isScrollBody) return;
|
||||
|
||||
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
|
||||
this.setScrollTop(e.scrollTop);
|
||||
|
||||
// 顶部按钮的显示隐藏
|
||||
if (e.scrollTop >= this.optUp.toTop.offset) {
|
||||
this.showTopBtn();
|
||||
} else {
|
||||
this.hideTopBtn();
|
||||
}
|
||||
}
|
||||
|
||||
/*列表滚动事件*/
|
||||
MeScroll.prototype.scroll = function(e, onScroll) {
|
||||
// 更新滚动条的位置
|
||||
this.setScrollTop(e.scrollTop);
|
||||
// 更新滚动内容高度
|
||||
this.setScrollHeight(e.scrollHeight);
|
||||
|
||||
// 向上滑还是向下滑动
|
||||
if (this.preScrollY == null) this.preScrollY = 0;
|
||||
this.isScrollUp = e.scrollTop - this.preScrollY > 0;
|
||||
this.preScrollY = e.scrollTop;
|
||||
|
||||
// 上滑 && 检查并触发上拉
|
||||
this.isScrollUp && this.triggerUpScroll(true);
|
||||
|
||||
// 顶部按钮的显示隐藏
|
||||
if (e.scrollTop >= this.optUp.toTop.offset) {
|
||||
this.showTopBtn();
|
||||
} else {
|
||||
this.hideTopBtn();
|
||||
}
|
||||
|
||||
// 滑动监听
|
||||
this.optUp.onScroll && onScroll && onScroll()
|
||||
}
|
||||
|
||||
/* 触发上拉加载 */
|
||||
MeScroll.prototype.triggerUpScroll = function(isCheck) {
|
||||
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
|
||||
// 是否校验在底部; 默认不校验
|
||||
if (isCheck === true) {
|
||||
let canUp = false;
|
||||
// 还有下一页 && 没有锁定 && 不在下拉中
|
||||
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
|
||||
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
|
||||
canUp = true; // 标记可上拉
|
||||
}
|
||||
}
|
||||
if (canUp === false) return;
|
||||
}
|
||||
this.showUpScroll(); // 上拉加载中...
|
||||
this.optUp.page.num++; // 预先加一页,如果失败则减回
|
||||
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.optUp.callback(this); // 执行回调,联网加载数据
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示上拉加载中 */
|
||||
MeScroll.prototype.showUpScroll = function() {
|
||||
this.isUpScrolling = true; // 标记上拉加载中
|
||||
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
|
||||
}
|
||||
|
||||
/* 显示上拉无更多数据 */
|
||||
MeScroll.prototype.showNoMore = function() {
|
||||
this.optUp.hasNext = false; // 标记无更多数据
|
||||
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
|
||||
}
|
||||
|
||||
/* 隐藏上拉区域**/
|
||||
MeScroll.prototype.hideUpScroll = function() {
|
||||
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
|
||||
}
|
||||
|
||||
/* 结束上拉加载 */
|
||||
MeScroll.prototype.endUpScroll = function(isShowNoMore) {
|
||||
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
|
||||
if (isShowNoMore) {
|
||||
this.showNoMore(); // isShowNoMore=true,显示无更多数据
|
||||
} else {
|
||||
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
|
||||
}
|
||||
}
|
||||
this.isUpScrolling = false; // 标记结束上拉加载
|
||||
}
|
||||
|
||||
/* 重置上拉加载列表为第一页
|
||||
*isShowLoading 是否显示进度布局;
|
||||
* 1.默认null,不传参,则显示上拉加载的进度布局
|
||||
* 2.传参true, 则显示下拉刷新的进度布局
|
||||
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
|
||||
*/
|
||||
MeScroll.prototype.resetUpScroll = function(isShowLoading) {
|
||||
if (this.optUp && this.optUp.use) {
|
||||
let page = this.optUp.page;
|
||||
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
|
||||
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
|
||||
page.num = this.startNum; // 重置为第一页
|
||||
page.time = null; // 重置时间为空
|
||||
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
|
||||
if (isShowLoading == null) {
|
||||
this.removeEmpty(); // 移除空布局
|
||||
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
|
||||
} else {
|
||||
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
|
||||
}
|
||||
}
|
||||
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置page.num的值 */
|
||||
MeScroll.prototype.setPageNum = function(num) {
|
||||
this.optUp.page.num = num - 1;
|
||||
}
|
||||
|
||||
/* 设置page.size的值 */
|
||||
MeScroll.prototype.setPageSize = function(size) {
|
||||
this.optUp.page.size = size;
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据量(必传)
|
||||
* totalPage: 总页数(必传)
|
||||
* systime: 服务器时间 (可空)
|
||||
*/
|
||||
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
|
||||
let hasNext;
|
||||
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
|
||||
this.endSuccess(dataSize, hasNext, systime);
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据量(必传)
|
||||
* totalSize: 列表所有数据总数量(必传)
|
||||
* systime: 服务器时间 (可空)
|
||||
*/
|
||||
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
|
||||
let hasNext;
|
||||
if (this.optUp.use && totalSize != null) {
|
||||
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
|
||||
hasNext = loadSize < totalSize; // 是否还有下一页
|
||||
}
|
||||
this.endSuccess(dataSize, hasNext, systime);
|
||||
}
|
||||
|
||||
/* 联网回调成功,结束下拉刷新和上拉加载
|
||||
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
|
||||
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
|
||||
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
|
||||
*/
|
||||
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
|
||||
let me = this;
|
||||
// 结束下拉刷新
|
||||
if (me.isDownScrolling) {
|
||||
me.isDownEndSuccess = true
|
||||
me.endDownScroll();
|
||||
}
|
||||
|
||||
// 结束上拉加载
|
||||
if (me.optUp.use) {
|
||||
let isShowNoMore; // 是否已无更多数据
|
||||
if (dataSize != null) {
|
||||
let pageNum = me.optUp.page.num; // 当前页码
|
||||
let pageSize = me.optUp.page.size; // 每页长度
|
||||
// 如果是第一页
|
||||
if (pageNum === 1) {
|
||||
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
|
||||
}
|
||||
if (dataSize < pageSize || hasNext === false) {
|
||||
// 返回的数据不满一页时,则说明已无更多数据
|
||||
me.optUp.hasNext = false;
|
||||
if (dataSize === 0 && pageNum === 1) {
|
||||
// 如果第一页无任何数据且配置了空布局
|
||||
isShowNoMore = false;
|
||||
me.showEmpty();
|
||||
} else {
|
||||
// 总列表数少于配置的数量,则不显示无更多数据
|
||||
let allDataSize = (pageNum - 1) * pageSize + dataSize;
|
||||
if (allDataSize < me.optUp.noMoreSize) {
|
||||
isShowNoMore = false;
|
||||
} else {
|
||||
isShowNoMore = true;
|
||||
}
|
||||
me.removeEmpty(); // 移除空布局
|
||||
}
|
||||
} else {
|
||||
// 还有下一页
|
||||
isShowNoMore = false;
|
||||
me.optUp.hasNext = true;
|
||||
me.removeEmpty(); // 移除空布局
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏上拉
|
||||
me.endUpScroll(isShowNoMore);
|
||||
}
|
||||
}
|
||||
|
||||
/* 回调失败,结束下拉刷新和上拉加载 */
|
||||
MeScroll.prototype.endErr = function(errDistance) {
|
||||
// 结束下拉,回调失败重置回原来的页码和时间
|
||||
if (this.isDownScrolling) {
|
||||
this.isDownEndSuccess = false
|
||||
let page = this.optUp.page;
|
||||
if (page && this.prePageNum) {
|
||||
page.num = this.prePageNum;
|
||||
page.time = this.prePageTime;
|
||||
}
|
||||
this.endDownScroll();
|
||||
}
|
||||
// 结束上拉,回调失败重置回原来的页码
|
||||
if (this.isUpScrolling) {
|
||||
this.optUp.page.num--;
|
||||
this.endUpScroll(false);
|
||||
// 如果是mescroll-body,则需往回滚一定距离
|
||||
if(this.isScrollBody && errDistance !== 0){ // 不处理0
|
||||
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
|
||||
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示空布局 */
|
||||
MeScroll.prototype.showEmpty = function() {
|
||||
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
|
||||
}
|
||||
|
||||
/* 移除空布局 */
|
||||
MeScroll.prototype.removeEmpty = function() {
|
||||
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
|
||||
}
|
||||
|
||||
/* 显示回到顶部的按钮 */
|
||||
MeScroll.prototype.showTopBtn = function() {
|
||||
if (!this.topBtnShow) {
|
||||
this.topBtnShow = true;
|
||||
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏回到顶部的按钮 */
|
||||
MeScroll.prototype.hideTopBtn = function() {
|
||||
if (this.topBtnShow) {
|
||||
this.topBtnShow = false;
|
||||
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
MeScroll.prototype.getScrollTop = function() {
|
||||
return this.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 记录滚动条的位置 */
|
||||
MeScroll.prototype.setScrollTop = function(y) {
|
||||
this.scrollTop = y;
|
||||
}
|
||||
|
||||
/* 滚动到指定位置 */
|
||||
MeScroll.prototype.scrollTo = function(y, t) {
|
||||
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
|
||||
}
|
||||
|
||||
/* 自定义scrollTo */
|
||||
MeScroll.prototype.resetScrollTo = function(myScrollTo) {
|
||||
this.myScrollTo = myScrollTo
|
||||
}
|
||||
|
||||
/* 滚动条到底部的距离 */
|
||||
MeScroll.prototype.getScrollBottom = function() {
|
||||
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
|
||||
}
|
||||
|
||||
/* 计步器
|
||||
star: 开始值
|
||||
end: 结束值
|
||||
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
|
||||
t: 计步时长,传0则直接回调end值;不传则默认300ms
|
||||
rate: 周期;不传则默认30ms计步一次
|
||||
* */
|
||||
MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
|
||||
let diff = end - star; // 差值
|
||||
if (t === 0 || diff === 0) {
|
||||
callback && callback(end);
|
||||
return;
|
||||
}
|
||||
t = t || 300; // 时长 300ms
|
||||
rate = rate || 30; // 周期 30ms
|
||||
let count = t / rate; // 次数
|
||||
let step = diff / count; // 步长
|
||||
let i = 0; // 计数
|
||||
let timer = setInterval(function() {
|
||||
if (i < count - 1) {
|
||||
star += step;
|
||||
callback && callback(star, timer);
|
||||
i++;
|
||||
} else {
|
||||
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, rate);
|
||||
}
|
||||
|
||||
/* 滚动容器的高度 */
|
||||
MeScroll.prototype.getClientHeight = function(isReal) {
|
||||
let h = this.clientHeight || 0
|
||||
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
|
||||
h = this.getBodyHeight()
|
||||
}
|
||||
return h
|
||||
}
|
||||
MeScroll.prototype.setClientHeight = function(h) {
|
||||
this.clientHeight = h;
|
||||
}
|
||||
|
||||
/* 滚动内容的高度 */
|
||||
MeScroll.prototype.getScrollHeight = function() {
|
||||
return this.scrollHeight || 0;
|
||||
}
|
||||
MeScroll.prototype.setScrollHeight = function(h) {
|
||||
this.scrollHeight = h;
|
||||
}
|
||||
|
||||
/* body的高度 */
|
||||
MeScroll.prototype.getBodyHeight = function() {
|
||||
return this.bodyHeight || 0;
|
||||
}
|
||||
MeScroll.prototype.setBodyHeight = function(h) {
|
||||
this.bodyHeight = h;
|
||||
}
|
||||
|
||||
/* 阻止浏览器默认滚动事件 */
|
||||
MeScroll.prototype.preventDefault = function(e) {
|
||||
// 小程序不支持e.preventDefault, 已在wxs中禁止
|
||||
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
|
||||
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
|
||||
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
|
||||
}
|
|
@ -0,0 +1,430 @@
|
|||
<template>
|
||||
<view class="mescroll-uni-warp">
|
||||
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom, backgroundColor: bgColor}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
|
||||
<view class="mescroll-uni-content mescroll-render-touch"
|
||||
@touchstart="wxsBiz.touchstartEvent"
|
||||
@touchmove="wxsBiz.touchmoveEvent"
|
||||
@touchend="wxsBiz.touchendEvent"
|
||||
@touchcancel="wxsBiz.touchendEvent"
|
||||
:change:prop="wxsBiz.propObserver"
|
||||
:prop="wxsProp">
|
||||
<!-- 状态栏 -->
|
||||
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
|
||||
|
||||
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
|
||||
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
|
||||
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
|
||||
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
|
||||
<view class="downwarp-content">
|
||||
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
|
||||
<view class="downwarp-tip">{{downText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 空布局 -->
|
||||
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
|
||||
|
||||
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
|
||||
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
|
||||
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
|
||||
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
|
||||
<view v-show="upLoadType===1">
|
||||
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
|
||||
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
|
||||
</view>
|
||||
<!-- 无数据 -->
|
||||
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 适配iPhoneX -->
|
||||
<view v-if="safearea" class="mescroll-safearea"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
|
||||
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
|
||||
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
|
||||
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- app, h5使用renderjs -->
|
||||
<!-- #ifdef APP-PLUS || H5 -->
|
||||
<script module="renderBiz" lang="renderjs">
|
||||
import renderBiz from './wxs/renderjs.js';
|
||||
export default {
|
||||
mixins:[renderBiz]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
|
||||
<script>
|
||||
// 引入mescroll-uni.js,处理核心逻辑
|
||||
import MeScroll from './mescroll-uni.js';
|
||||
// 引入全局配置
|
||||
import GlobalOption from './mescroll-uni-option.js';
|
||||
// 引入空布局组件
|
||||
import MescrollEmpty from './components/mescroll-empty.vue';
|
||||
// 引入回到顶部组件
|
||||
import MescrollTop from './components/mescroll-top.vue';
|
||||
// 引入兼容wxs(含renderjs)写法的mixins
|
||||
import WxsMixin from './wxs/mixins.js';
|
||||
|
||||
export default {
|
||||
mixins: [WxsMixin],
|
||||
components: {
|
||||
MescrollEmpty,
|
||||
MescrollTop
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mescroll: {optDown:{},optUp:{}}, // mescroll实例
|
||||
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
|
||||
downHight: 0, //下拉刷新: 容器高度
|
||||
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
|
||||
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
|
||||
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
|
||||
isShowEmpty: false, // 是否显示空布局
|
||||
isShowToTop: false, // 是否显示回到顶部按钮
|
||||
scrollTop: 0, // 滚动条的位置
|
||||
scrollAnim: false, // 是否开启滚动动画
|
||||
windowTop: 0, // 可使用窗口的顶部位置
|
||||
windowBottom: 0, // 可使用窗口的底部位置
|
||||
windowHeight: 0, // 可使用窗口的高度
|
||||
statusBarHeight: 0 // 状态栏高度
|
||||
}
|
||||
},
|
||||
props: {
|
||||
down: Object, // 下拉刷新的参数配置
|
||||
up: Object, // 上拉加载的参数配置
|
||||
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
|
||||
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
|
||||
fixed: { // 是否通过fixed固定mescroll的高度, 默认true
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
|
||||
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 是否使用fixed定位 (当height有值,则不使用)
|
||||
isFixed(){
|
||||
return !this.height && this.fixed
|
||||
},
|
||||
// mescroll的高度
|
||||
scrollHeight(){
|
||||
if (this.isFixed) {
|
||||
return "auto"
|
||||
} else if(this.height){
|
||||
return this.toPx(this.height) + 'px'
|
||||
}else{
|
||||
return "100%"
|
||||
}
|
||||
},
|
||||
// 下拉布局往下偏移的距离 (px)
|
||||
numTop() {
|
||||
return this.toPx(this.top)
|
||||
},
|
||||
fixedTop() {
|
||||
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
|
||||
},
|
||||
padTop() {
|
||||
return !this.isFixed ? this.numTop + 'px' : 0
|
||||
},
|
||||
// 上拉布局往上偏移 (px)
|
||||
numBottom() {
|
||||
return this.toPx(this.bottom)
|
||||
},
|
||||
fixedBottom() {
|
||||
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
|
||||
},
|
||||
padBottom() {
|
||||
return !this.isFixed ? this.numBottom + 'px' : 0
|
||||
},
|
||||
// 是否为重置下拉的状态
|
||||
isDownReset(){
|
||||
return this.downLoadType===3 || this.downLoadType===4
|
||||
},
|
||||
// 过渡
|
||||
transition() {
|
||||
return this.isDownReset ? 'transform 300ms' : '';
|
||||
},
|
||||
translateY() {
|
||||
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
|
||||
},
|
||||
// 列表是否可滑动
|
||||
scrollable(){
|
||||
return this.downLoadType===0 || this.isDownReset
|
||||
},
|
||||
// 是否在加载中
|
||||
isDownLoading(){
|
||||
return this.downLoadType === 3
|
||||
},
|
||||
// 旋转的角度
|
||||
downRotate(){
|
||||
return 'rotate(' + 360 * this.downRate + 'deg)'
|
||||
},
|
||||
// 文本提示
|
||||
downText(){
|
||||
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
|
||||
switch (this.downLoadType){
|
||||
case 1: return this.mescroll.optDown.textInOffset;
|
||||
case 2: return this.mescroll.optDown.textOutOffset;
|
||||
case 3: return this.mescroll.optDown.textLoading;
|
||||
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
|
||||
default: return this.mescroll.optDown.textInOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//number,rpx,upx,px,% --> px的数值
|
||||
toPx(num){
|
||||
if(typeof num === "string"){
|
||||
if (num.indexOf('px') !== -1) {
|
||||
if(num.indexOf('rpx') !== -1) { // "10rpx"
|
||||
num = num.replace('rpx', '');
|
||||
} else if(num.indexOf('upx') !== -1) { // "10upx"
|
||||
num = num.replace('upx', '');
|
||||
} else { // "10px"
|
||||
return Number(num.replace('px', ''))
|
||||
}
|
||||
}else if (num.indexOf('%') !== -1){
|
||||
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
|
||||
let rate = Number(num.replace("%","")) / 100
|
||||
return this.windowHeight * rate
|
||||
}
|
||||
}
|
||||
return num ? uni.upx2px(Number(num)) : 0
|
||||
},
|
||||
//注册列表滚动事件,用于下拉刷新和上拉加载
|
||||
scroll(e) {
|
||||
this.mescroll.scroll(e.detail, () => {
|
||||
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
|
||||
})
|
||||
},
|
||||
// 点击空布局的按钮回调
|
||||
emptyClick() {
|
||||
this.$emit('emptyclick', this.mescroll)
|
||||
},
|
||||
// 点击回到顶部的按钮回调
|
||||
toTopClick() {
|
||||
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
|
||||
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
|
||||
},
|
||||
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
|
||||
setClientHeight() {
|
||||
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
|
||||
this.isExec = true; // 避免多次获取
|
||||
this.$nextTick(() => { // 确保dom已渲染
|
||||
this.getClientInfo(data=>{
|
||||
this.isExec = false;
|
||||
if (data) {
|
||||
this.mescroll.setClientHeight(data.height);
|
||||
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
|
||||
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
|
||||
setTimeout(() => {
|
||||
this.setClientHeight()
|
||||
}, this.clientNum * 100)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
// 获取滚动区域的信息
|
||||
getClientInfo(success){
|
||||
let query = uni.createSelectorQuery();
|
||||
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
|
||||
// #endif
|
||||
let view = query.select('#' + this.viewId);
|
||||
view.boundingClientRect(data => {
|
||||
success(data)
|
||||
}).exec();
|
||||
}
|
||||
},
|
||||
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
|
||||
created() {
|
||||
let vm = this;
|
||||
|
||||
let diyOption = {
|
||||
// 下拉刷新的配置
|
||||
down: {
|
||||
inOffset() {
|
||||
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
outOffset() {
|
||||
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
onMoving(mescroll, rate, downHight) {
|
||||
// 下拉过程中的回调,滑动过程一直在执行;
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
|
||||
},
|
||||
showLoading(mescroll, downHight) {
|
||||
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
},
|
||||
beforeEndDownScroll(mescroll){
|
||||
vm.downLoadType = 4;
|
||||
return mescroll.optDown.beforeEndDelay // 延时结束的时长
|
||||
},
|
||||
endDownScroll() {
|
||||
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
|
||||
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
|
||||
vm.downResetTimer && clearTimeout(vm.downResetTimer)
|
||||
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
|
||||
if(vm.downLoadType===4) vm.downLoadType = 0
|
||||
},300)
|
||||
},
|
||||
// 派发下拉刷新的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('down', mescroll)
|
||||
}
|
||||
},
|
||||
// 上拉加载的配置
|
||||
up: {
|
||||
// 显示加载中的回调
|
||||
showLoading() {
|
||||
vm.upLoadType = 1;
|
||||
},
|
||||
// 显示无更多数据的回调
|
||||
showNoMore() {
|
||||
vm.$nextTick(() => {
|
||||
vm.upLoadType = 2;
|
||||
})
|
||||
},
|
||||
// 隐藏上拉加载的回调
|
||||
hideUpScroll(mescroll) {
|
||||
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
|
||||
},
|
||||
// 空布局
|
||||
empty: {
|
||||
onShow(isShow) { // 显示隐藏的回调
|
||||
vm.isShowEmpty = isShow;
|
||||
}
|
||||
},
|
||||
// 回到顶部
|
||||
toTop: {
|
||||
onShow(isShow) { // 显示隐藏的回调
|
||||
vm.isShowToTop = isShow;
|
||||
}
|
||||
},
|
||||
// 派发上拉加载的回调
|
||||
callback: function(mescroll) {
|
||||
vm.$emit('up', mescroll);
|
||||
// 更新容器的高度 (多mescroll的情况)
|
||||
vm.setClientHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
|
||||
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
|
||||
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
|
||||
|
||||
// 初始化MeScroll对象
|
||||
vm.mescroll = new MeScroll(myOption);
|
||||
vm.mescroll.viewId = vm.viewId; // 附带id
|
||||
// init回调mescroll对象
|
||||
vm.$emit('init', vm.mescroll);
|
||||
|
||||
// 设置高度
|
||||
const sys = uni.getSystemInfoSync();
|
||||
if(sys.windowTop) vm.windowTop = sys.windowTop;
|
||||
if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
|
||||
if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
|
||||
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
|
||||
// 使down的bottomOffset生效
|
||||
vm.mescroll.setBodyHeight(sys.windowHeight);
|
||||
|
||||
// 因为使用的是scrollview,这里需自定义scrollTo
|
||||
vm.mescroll.resetScrollTo((y, t) => {
|
||||
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
|
||||
if(typeof y === 'string'){
|
||||
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
|
||||
vm.getClientInfo(function(rect){
|
||||
let mescrollTop = rect.top // mescroll到顶部的距离
|
||||
let selector;
|
||||
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
|
||||
selector = '#'+y // 不带#和. 则默认为id选择器
|
||||
}else{
|
||||
selector = y
|
||||
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
|
||||
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
|
||||
selector = y.split('>>>')[1].trim()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
|
||||
if (rect) {
|
||||
let curY = vm.mescroll.getScrollTop()
|
||||
let top = rect.top - mescrollTop
|
||||
top += curY
|
||||
if(!vm.isFixed) top -= vm.numTop
|
||||
vm.scrollTop = curY;
|
||||
vm.$nextTick(function() {
|
||||
vm.scrollTop = top
|
||||
})
|
||||
} else{
|
||||
console.error(selector + ' does not exist');
|
||||
}
|
||||
}).exec()
|
||||
})
|
||||
return;
|
||||
}
|
||||
let curY = vm.mescroll.getScrollTop()
|
||||
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
|
||||
vm.scrollTop = curY;
|
||||
vm.$nextTick(function() {
|
||||
vm.scrollTop = y
|
||||
})
|
||||
} else {
|
||||
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
|
||||
vm.scrollTop = step
|
||||
}, t)
|
||||
}
|
||||
})
|
||||
|
||||
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
|
||||
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
|
||||
vm.mescroll.optUp.toTop.safearea = vm.safearea;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 设置容器的高度
|
||||
this.setClientHeight()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "./mescroll-uni.css";
|
||||
@import "./components/mescroll-down.css";
|
||||
@import './components/mescroll-up.css';
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
|
||||
*/
|
||||
const MescrollCompMixin = {
|
||||
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
|
||||
onPageScroll(e) {
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom() {
|
||||
this.handleReachBottom()
|
||||
},
|
||||
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
onPullDownRefresh(){
|
||||
this.handlePullDownRefresh()
|
||||
},
|
||||
// mescroll-body写在子子子...组件的情况 (多级)
|
||||
data() {
|
||||
return {
|
||||
mescroll: {
|
||||
onPageScroll: e=>{
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom: ()=>{
|
||||
this.handleReachBottom()
|
||||
},
|
||||
onPullDownRefresh: ()=>{
|
||||
this.handlePullDownRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
handlePageScroll(e){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onPageScroll(e);
|
||||
},
|
||||
handleReachBottom(){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onReachBottom();
|
||||
},
|
||||
handlePullDownRefresh(){
|
||||
let item = this.$refs["mescrollItem"];
|
||||
if(item && item.mescroll) item.mescroll.onPullDownRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollCompMixin;
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
|
||||
*/
|
||||
const MescrollMoreItemMixin = {
|
||||
// 支付宝小程序不支持props的mixin,需写在具体的页面中
|
||||
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||
props:{
|
||||
i: Number, // 每个tab页的专属下标
|
||||
index: { // 当前tab的下标
|
||||
type: Number,
|
||||
default(){
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
data() {
|
||||
return {
|
||||
downOption:{
|
||||
auto:false // 不自动加载
|
||||
},
|
||||
upOption:{
|
||||
auto:false // 不自动加载
|
||||
},
|
||||
isInit: false // 当前tab是否已初始化
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
// 监听下标的变化
|
||||
index(val){
|
||||
if (this.i === val && !this.isInit) {
|
||||
this.isInit = true; // 标记为true
|
||||
this.mescroll && this.mescroll.triggerDownScroll();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
mescrollInitByRef() {
|
||||
if(!this.mescroll || !this.mescroll.resetUpScroll){
|
||||
// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
|
||||
let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
|
||||
if(mescrollRef) this.mescroll = mescrollRef.mescroll
|
||||
}
|
||||
},
|
||||
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
|
||||
mescrollInit(mescroll) {
|
||||
this.mescroll = mescroll;
|
||||
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
// 自动加载当前tab的数据
|
||||
if(this.i === this.index){
|
||||
this.isInit = true; // 标记为true
|
||||
this.mescroll.triggerDownScroll();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollMoreItemMixin;
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
|
||||
*/
|
||||
const MescrollMoreMixin = {
|
||||
data() {
|
||||
return {
|
||||
tabIndex: 0, // 当前tab下标
|
||||
mescroll: {
|
||||
onPageScroll: e=>{
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom: ()=>{
|
||||
this.handleReachBottom()
|
||||
},
|
||||
onPullDownRefresh: ()=>{
|
||||
this.handlePullDownRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
|
||||
onPageScroll(e) {
|
||||
this.handlePageScroll(e)
|
||||
},
|
||||
onReachBottom() {
|
||||
this.handleReachBottom()
|
||||
},
|
||||
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
onPullDownRefresh(){
|
||||
this.handlePullDownRefresh()
|
||||
},
|
||||
methods:{
|
||||
handlePageScroll(e){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onPageScroll(e);
|
||||
},
|
||||
handleReachBottom(){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onReachBottom();
|
||||
},
|
||||
handlePullDownRefresh(){
|
||||
let mescroll = this.getMescroll(this.tabIndex);
|
||||
mescroll && mescroll.onPullDownRefresh();
|
||||
},
|
||||
// 根据下标获取对应子组件的mescroll
|
||||
getMescroll(i){
|
||||
if(!this.mescrollItems) this.mescrollItems = [];
|
||||
if(!this.mescrollItems[i]) {
|
||||
// v-for中的refs
|
||||
let vForItem = this.$refs["mescrollItem"];
|
||||
if(vForItem){
|
||||
this.mescrollItems[i] = vForItem[i]
|
||||
}else{
|
||||
// 普通的refs,不可重复
|
||||
this.mescrollItems[i] = this.$refs["mescrollItem"+i];
|
||||
}
|
||||
}
|
||||
let item = this.mescrollItems[i]
|
||||
return item ? item.mescroll : null
|
||||
},
|
||||
// 切换tab,恢复滚动条位置
|
||||
tabChange(i){
|
||||
let mescroll = this.getMescroll(i);
|
||||
if(mescroll){
|
||||
// 延时(比$nextTick靠谱一些),确保元素已渲染
|
||||
setTimeout(()=>{
|
||||
mescroll.scrollTo(mescroll.getScrollTop(),0)
|
||||
},30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MescrollMoreMixin;
|
|
@ -0,0 +1,109 @@
|
|||
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
|
||||
const WxsMixin = {
|
||||
data() {
|
||||
return {
|
||||
// 传入wxs视图层的数据 (响应式)
|
||||
wxsProp: {
|
||||
optDown:{}, // 下拉刷新的配置
|
||||
scrollTop:0, // 滚动条的距离
|
||||
bodyHeight:0, // body的高度
|
||||
isDownScrolling:false, // 是否正在下拉刷新中
|
||||
isUpScrolling:false, // 是否正在上拉加载中
|
||||
isScrollBody:true, // 是否为mescroll-body滚动
|
||||
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
|
||||
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
},
|
||||
|
||||
// 标记调用wxs视图层的方法
|
||||
callProp: {
|
||||
callType: '', // 方法名
|
||||
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
},
|
||||
|
||||
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
|
||||
// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
wxsBiz: {
|
||||
//注册列表touchstart事件,用于下拉刷新
|
||||
touchstartEvent: e=> {
|
||||
this.mescroll.touchstartEvent(e);
|
||||
},
|
||||
//注册列表touchmove事件,用于下拉刷新
|
||||
touchmoveEvent: e=> {
|
||||
this.mescroll.touchmoveEvent(e);
|
||||
},
|
||||
//注册列表touchend事件,用于下拉刷新
|
||||
touchendEvent: e=> {
|
||||
this.mescroll.touchendEvent(e);
|
||||
},
|
||||
propObserver(){}, // 抹平wxs的写法
|
||||
callObserver(){} // 抹平wxs的写法
|
||||
},
|
||||
// #endif
|
||||
|
||||
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
|
||||
// #ifndef APP-PLUS || H5
|
||||
renderBiz: {
|
||||
propObserver(){} // 抹平renderjs的写法
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// wxs视图层调用逻辑层的回调
|
||||
wxsCall(msg){
|
||||
if(msg.type === 'setWxsProp'){
|
||||
// 更新wxsProp数据 (值改变才触发更新)
|
||||
this.wxsProp = {
|
||||
optDown: this.mescroll.optDown,
|
||||
scrollTop: this.mescroll.getScrollTop(),
|
||||
bodyHeight: this.mescroll.getBodyHeight(),
|
||||
isDownScrolling: this.mescroll.isDownScrolling,
|
||||
isUpScrolling: this.mescroll.isUpScrolling,
|
||||
isUpBoth: this.mescroll.optUp.isBoth,
|
||||
isScrollBody:this.mescroll.isScrollBody,
|
||||
t: Date.now()
|
||||
}
|
||||
}else if(msg.type === 'setLoadType'){
|
||||
// 设置inOffset,outOffset的状态
|
||||
this.downLoadType = msg.downLoadType
|
||||
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
|
||||
// 重置是否加载成功的状态
|
||||
this.$set(this.mescroll, 'isDownEndSuccess', null)
|
||||
}else if(msg.type === 'triggerDownScroll'){
|
||||
// 主动触发下拉刷新
|
||||
this.mescroll.triggerDownScroll();
|
||||
}else if(msg.type === 'endDownScroll'){
|
||||
// 结束下拉刷新
|
||||
this.mescroll.endDownScroll();
|
||||
}else if(msg.type === 'triggerUpScroll'){
|
||||
// 主动触发上拉加载
|
||||
this.mescroll.triggerUpScroll(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
// 配置主动触发wxs显示加载进度的回调
|
||||
this.mescroll.optDown.afterLoading = ()=>{
|
||||
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
}
|
||||
// 配置主动触发wxs隐藏加载进度的回调
|
||||
this.mescroll.optDown.afterEndDownScroll = ()=>{
|
||||
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
|
||||
setTimeout(()=>{
|
||||
if(this.downLoadType === 4 || this.downLoadType === 0){
|
||||
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
}
|
||||
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
this.$set(this.mescroll, 'downLoadType', this.downLoadType)
|
||||
}, delay)
|
||||
}
|
||||
// 初始化wxs的数据
|
||||
this.wxsCall({type: 'setWxsProp'})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
export default WxsMixin;
|
|
@ -0,0 +1,92 @@
|
|||
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
|
||||
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
|
||||
// https://uniapp.dcloud.io/frame?id=renderjs
|
||||
|
||||
// 与wxs的me实例一致
|
||||
var me = {}
|
||||
|
||||
// 初始化window对象的touch事件 (仅初始化一次)
|
||||
if(window && !window.$mescrollRenderInit){
|
||||
window.$mescrollRenderInit = true
|
||||
|
||||
|
||||
window.addEventListener('touchstart', function(e){
|
||||
if (me.disabled()) return;
|
||||
me.startPoint = me.getPoint(e); // 记录起点
|
||||
}, {passive: true})
|
||||
|
||||
|
||||
window.addEventListener('touchmove', function(e){
|
||||
if (me.disabled()) return;
|
||||
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
|
||||
|
||||
var curPoint = me.getPoint(e); // 当前点
|
||||
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 向下拉
|
||||
if (moveY > 0) {
|
||||
// 可下拉的条件
|
||||
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
|
||||
|
||||
// 只有touch在mescroll的view上面,才禁止bounce
|
||||
var el = e.target;
|
||||
var isMescrollTouch = false;
|
||||
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
|
||||
var cls = el.classList;
|
||||
if (cls && cls.contains('mescroll-render-touch')) {
|
||||
isMescrollTouch = true
|
||||
break;
|
||||
}
|
||||
el = el.parentNode; // 继续检查其父元素
|
||||
}
|
||||
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
|
||||
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, {passive: false})
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
me.getScrollTop = function() {
|
||||
return me.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 是否禁用下拉刷新 */
|
||||
me.disabled = function(){
|
||||
return !me.optDown || !me.optDown.use || me.optDown.native
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
me.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {x: 0,y: 0}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
|
||||
} else {
|
||||
return {x: e.clientX,y: e.clientY}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (实时更新数据)
|
||||
*/
|
||||
function propObserver(wxsProp) {
|
||||
me.optDown = wxsProp.optDown
|
||||
me.scrollTop = wxsProp.scrollTop
|
||||
me.isDownScrolling = wxsProp.isDownScrolling
|
||||
me.isUpScrolling = wxsProp.isUpScrolling
|
||||
me.isUpBoth = wxsProp.isUpBoth
|
||||
}
|
||||
|
||||
/* 导出模块 */
|
||||
const renderBiz = {
|
||||
data() {
|
||||
return {
|
||||
propObserver: propObserver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default renderBiz;
|
|
@ -0,0 +1,268 @@
|
|||
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
|
||||
// https://uniapp.dcloud.io/frame?id=wxs
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
|
||||
|
||||
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
|
||||
var me = {}
|
||||
|
||||
// ------ 自定义下拉刷新动画 start ------
|
||||
|
||||
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
|
||||
me.onMoving = function (ins, rate, downHight){
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
|
||||
'transform': 'translateY(' + downHight + 'px)',
|
||||
'transition': ''
|
||||
})
|
||||
// 环形进度条
|
||||
var progress = ins.selectComponent('.mescroll-wxs-progress')
|
||||
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
|
||||
})
|
||||
}
|
||||
|
||||
/* 显示下拉刷新进度 */
|
||||
me.showLoading = function (ins){
|
||||
me.downHight = me.optDown.offset
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'auto',
|
||||
'transform': 'translateY(' + me.downHight + 'px)',
|
||||
'transition': 'transform 300ms'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* 结束下拉 */
|
||||
me.endDownScroll = function (ins){
|
||||
me.downHight = 0;
|
||||
me.isDownScrolling = false;
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': 'auto',
|
||||
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
|
||||
'transition': 'transform 300ms'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
|
||||
me.clearTransform = function (ins){
|
||||
ins.requestAnimationFrame(function () {
|
||||
ins.selectComponent('.mescroll-wxs-content').setStyle({
|
||||
'will-change': '',
|
||||
'transform': '',
|
||||
'transition': ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ------ 自定义下拉刷新动画 end ------
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (实时更新数据)
|
||||
*/
|
||||
function propObserver(wxsProp) {
|
||||
me.optDown = wxsProp.optDown
|
||||
me.scrollTop = wxsProp.scrollTop
|
||||
me.bodyHeight = wxsProp.bodyHeight
|
||||
me.isDownScrolling = wxsProp.isDownScrolling
|
||||
me.isUpScrolling = wxsProp.isUpScrolling
|
||||
me.isUpBoth = wxsProp.isUpBoth
|
||||
me.isScrollBody = wxsProp.isScrollBody
|
||||
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听逻辑层数据的变化 (调用wxs的方法)
|
||||
*/
|
||||
function callObserver(callProp, oldValue, ins) {
|
||||
if (me.disabled()) return;
|
||||
if(callProp.callType){
|
||||
// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
|
||||
if(callProp.callType === 'showLoading'){
|
||||
me.showLoading(ins)
|
||||
}else if(callProp.callType === 'endDownScroll'){
|
||||
me.endDownScroll(ins)
|
||||
}else if(callProp.callType === 'clearTransform'){
|
||||
me.clearTransform(ins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* touch事件
|
||||
*/
|
||||
function touchstartEvent(e, ins) {
|
||||
me.downHight = 0; // 下拉的距离
|
||||
me.startPoint = me.getPoint(e); // 记录起点
|
||||
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
|
||||
me.startAngle = 0; // 初始角度
|
||||
me.lastPoint = me.startPoint; // 重置上次move的点
|
||||
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||
me.inTouchend = false; // 标记不是touchend
|
||||
|
||||
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
|
||||
}
|
||||
|
||||
function touchmoveEvent(e, ins) {
|
||||
var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
|
||||
|
||||
if (me.disabled()) return isPrevent;
|
||||
|
||||
var scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||
var curPoint = me.getPoint(e); // 当前点
|
||||
|
||||
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
||||
// 向下拉 && 在顶部
|
||||
// mescroll-body,直接判定在顶部即可
|
||||
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||
if (moveY > 0 && (
|
||||
(me.isScrollBody && scrollTop <= 0)
|
||||
||
|
||||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
|
||||
)) {
|
||||
// 可下拉的条件
|
||||
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
|
||||
me.isUpBoth))) {
|
||||
|
||||
// 下拉的角度是否在配置的范围内
|
||||
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||
|
||||
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
|
||||
me.inTouchend = true; // 标记执行touchend
|
||||
touchendEvent(e, ins); // 提前触发touchend
|
||||
return isPrevent;
|
||||
}
|
||||
|
||||
isPrevent = false // 小程序是return false
|
||||
|
||||
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||
|
||||
// 下拉距离 < 指定距离
|
||||
if (me.downHight < me.optDown.offset) {
|
||||
if (me.movetype !== 1) {
|
||||
me.movetype = 1; // 加入标记,保证只执行一次
|
||||
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||
|
||||
// 指定距离 <= 下拉距离
|
||||
} else {
|
||||
if (me.movetype !== 2) {
|
||||
me.movetype = 2; // 加入标记,保证只执行一次
|
||||
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
|
||||
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
}
|
||||
if (diff > 0) { // 向下拉
|
||||
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||
} else { // 向上收
|
||||
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||
}
|
||||
}
|
||||
|
||||
me.downHight = Math.round(me.downHight) // 取整
|
||||
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||
me.onMoving(ins, rate, me.downHight)
|
||||
}
|
||||
}
|
||||
|
||||
me.lastPoint = curPoint; // 记录本次移动的点
|
||||
|
||||
return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
|
||||
}
|
||||
|
||||
function touchendEvent(e, ins) {
|
||||
// 如果下拉区域高度已改变,则需重置回来
|
||||
if (me.isMoveDown) {
|
||||
if (me.downHight >= me.optDown.offset) {
|
||||
// 符合触发刷新的条件
|
||||
me.downHight = me.optDown.offset; // 更新下拉区域高度
|
||||
// me.triggerDownScroll();
|
||||
me.callMethod(ins, {type: 'triggerDownScroll'})
|
||||
} else {
|
||||
// 不符合的话 则重置
|
||||
me.downHight = 0;
|
||||
// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
|
||||
me.callMethod(ins, {type: 'endDownScroll'})
|
||||
}
|
||||
me.movetype = 0;
|
||||
me.isMoveDown = false;
|
||||
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
// 上滑
|
||||
if (isScrollUp) {
|
||||
// 需检查滑动的角度
|
||||
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
|
||||
if (angle > 80) {
|
||||
// 检查并触发上拉
|
||||
// me.triggerUpScroll(true);
|
||||
me.callMethod(ins, {type: 'triggerUpScroll'})
|
||||
}
|
||||
}
|
||||
}
|
||||
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
|
||||
}
|
||||
|
||||
/* 是否禁用下拉刷新 */
|
||||
me.disabled = function(){
|
||||
return !me.optDown || !me.optDown.use || me.optDown.native
|
||||
}
|
||||
|
||||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||||
me.getPoint = function(e) {
|
||||
if (!e) {
|
||||
return {x: 0,y: 0}
|
||||
}
|
||||
if (e.touches && e.touches[0]) {
|
||||
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
|
||||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||||
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
|
||||
} else {
|
||||
return {x: e.clientX,y: e.clientY}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算两点之间的角度: 区间 [0,90]*/
|
||||
me.getAngle = function (p1, p2) {
|
||||
var x = Math.abs(p1.x - p2.x);
|
||||
var y = Math.abs(p1.y - p2.y);
|
||||
var z = Math.sqrt(x * x + y * y);
|
||||
var angle = 0;
|
||||
if (z !== 0) {
|
||||
angle = Math.asin(y / z) / Math.PI * 180;
|
||||
}
|
||||
return angle
|
||||
}
|
||||
|
||||
/* 获取滚动条的位置 */
|
||||
me.getScrollTop = function() {
|
||||
return me.scrollTop || 0
|
||||
}
|
||||
|
||||
/* 获取body的高度 */
|
||||
me.getBodyHeight = function() {
|
||||
return me.bodyHeight || 0;
|
||||
}
|
||||
|
||||
/* 调用逻辑层的方法 */
|
||||
me.callMethod = function(ins, param) {
|
||||
if(ins) ins.callMethod('wxsCall', param)
|
||||
}
|
||||
|
||||
/* 导出模块 */
|
||||
module.exports = {
|
||||
propObserver: propObserver,
|
||||
callObserver: callObserver,
|
||||
touchstartEvent: touchstartEvent,
|
||||
touchmoveEvent: touchmoveEvent,
|
||||
touchendEvent: touchendEvent
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<view>
|
||||
<u-popup v-model="inputValue" :zIndex="999999" mode="center">
|
||||
<view class="bg-white popup">
|
||||
<view class="title">{{title}}</view>
|
||||
<view class="content line-3" :style="{'height': height}">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="footer flex row-around">
|
||||
<button v-if="cancelShow" class="btn hollow flex row-center normal" @click="cancel">{{cancelText}}</button>
|
||||
<button :style="{'width': cancelShow==false?'100%':'220rpx'}" class="btn solid flex row-center normal" @click="confirm">{{confirmText}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cancelShow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '提示'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '310rpx'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确认'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val1, val2) {
|
||||
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误
|
||||
if (!this.changeFromInner) {
|
||||
if (this.inputValue == val1) return
|
||||
this.inputValue = val1;
|
||||
// 进行this.$nextTick延时
|
||||
this.$nextTick(function() {
|
||||
this.changeFromInner = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
inputValue(val1, v2) {
|
||||
if(this.isFistVal) return
|
||||
this.changeFromInner = true;
|
||||
// 一定时间内,清除changeFromInner标记,否则内部值改变后
|
||||
// 外部通过程序修改value值,将会无效
|
||||
this.innerChangeTimer = setTimeout(() => {
|
||||
this.changeFromInner = false;
|
||||
}, 150);
|
||||
this.$emit('input', val1)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 弹窗Popup显示状态
|
||||
show: {
|
||||
get: function() {
|
||||
return this.value
|
||||
},
|
||||
set: function(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputValue: false,
|
||||
changeFromInner: false, // 值发生变化,是来自内部还是外部
|
||||
isFistVal: true
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.inputValue = this.value;
|
||||
this.$nextTick(function() {
|
||||
this.isFistVal = false
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
onTrigger() {
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.inputValue = false;
|
||||
this.$emit('cancel')
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.inputValue = false;
|
||||
this.$emit('confirm')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup {
|
||||
width: 600rpx;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.title {
|
||||
padding: 30rpx 0;
|
||||
font-size: $-font-size-md;
|
||||
text-align: center;
|
||||
color: #101010;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 600rpx;
|
||||
word-wrap:break-word;
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 24rpx;
|
||||
.btn {
|
||||
width: 220rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: $-font-size-sm;
|
||||
}
|
||||
|
||||
.solid {
|
||||
color: $-color-white;
|
||||
background-color: #40AFFA;
|
||||
}
|
||||
|
||||
.hollow {
|
||||
color: $-color-lighter;
|
||||
border: 1px solid #DBDBDB;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<view class="order bg-white m-b-20">
|
||||
<!-- Header -->
|
||||
<view class="order-header flex row-between">
|
||||
<view class="normal nr">
|
||||
订单编号: {{data.order_sn}}
|
||||
</view>
|
||||
<view class="order-status nr m-l-20">
|
||||
{{data.order_status_text}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Stction -->
|
||||
<block v-for="(item, index) in data.order_goods">
|
||||
<view class="order-section flex m-b-20 col-top" @click="toDetail(data.id)">
|
||||
<view class="image">
|
||||
<u-image :src="item.image" width="160rpx" height="160rpx"></u-image>
|
||||
</view>
|
||||
|
||||
<view class="m-l-16 line-2">
|
||||
<!-- 订单名称 -->
|
||||
<view class="m-t-10">
|
||||
<view class="order-name line-2">
|
||||
{{item.goods_name}}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 商品规格 -->
|
||||
<view class="order-str m-t-10">{{item.spec_value}}</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class=" flex row-right">
|
||||
<!-- 实付款金额 -->
|
||||
<view class="muted flex sm m-t-10">
|
||||
<view>共{{data.order_goods.length}}件,实付款: </view>
|
||||
<price class="header-content-price" :content="data.order_amount" main-size="30rpx" minor-size="22rpx"
|
||||
color="#FF4141" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Footer -->
|
||||
<view class="order-footer flex row-right">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @description 订单管理卡片
|
||||
*
|
||||
* @example <order-card :data="order" />
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'GoodsCard',
|
||||
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toDetail(id) {
|
||||
this.$Router.push({
|
||||
path: '/pages/order_detail/order_detail',
|
||||
query: {
|
||||
id
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&-header {
|
||||
padding-bottom: 20rpx;
|
||||
.order-status {
|
||||
color: $-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&-section {
|
||||
width: 100%;
|
||||
|
||||
.order-name {
|
||||
width: 100%;
|
||||
color: $-color-black;
|
||||
font-size: $-font-size-nr;
|
||||
}
|
||||
|
||||
.order-str {
|
||||
color: $-color-muted;
|
||||
font-size: $-font-size-xs;
|
||||
}
|
||||
|
||||
>view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<view class="price-container">
|
||||
<view :class="['price-wrap', {'price-wrap--disabled': lineThrough}]" :style="{color: color }">
|
||||
<!-- Prefix -->
|
||||
<view class="fix-pre" :style="{fontSize: minorSize}">
|
||||
<slot name="prefix">{{ prefix }}</slot>
|
||||
</view>
|
||||
|
||||
<!-- Content -->
|
||||
<view class="content" :style="{'font-weight': fontWeight}">
|
||||
<!-- Integer -->
|
||||
<text class="integer" :style="{fontSize: mainSize}">{{ integer }}</text>
|
||||
<!-- Decimals -->
|
||||
<text class="decimals" :style="{fontSize: minorSize}">{{ decimals }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Suffix -->
|
||||
<view class="fix-suf" :style="{fontSize: minorSize}">
|
||||
<slot name="suffix">{{ suffix }}</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* @description 价格展示,适用于有前后缀,小数样式不一
|
||||
* @property {String|Number} content 价格 (必填项)
|
||||
* @property {Number} prec 小数位 (默认: 2)
|
||||
* @property {Boolean} autoPrec 自动小数位【注:以prec为最大小数位】 (默认: true)
|
||||
* @property {String} color 颜色 (默认: 'unset')
|
||||
* @property {String} mainSize 主要内容字体大小 (默认: 46rpx)
|
||||
* @property {String} minorSize 主要内容字体大小 (默认: 32rpx)
|
||||
* @property {Boolean} lineThrough 贯穿线 (默认: false)
|
||||
* @property {String|Number} fontWeight 字重 (默认: normal)
|
||||
* @property {String} prefix 前缀 (默认: ¥)
|
||||
* @property {String} suffix 后缀
|
||||
* @example <price content="100" suffix="\/元" />
|
||||
*/
|
||||
|
||||
import { formatPrice } from '@/utils/tools'
|
||||
|
||||
export default {
|
||||
name: 'Price',
|
||||
|
||||
props: {
|
||||
// 价格
|
||||
content: {
|
||||
type: String | Number,
|
||||
required: true
|
||||
},
|
||||
|
||||
// 小数位
|
||||
prec: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
|
||||
// 动态小数
|
||||
autoPrec: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
},
|
||||
|
||||
// 主要内容字体大小
|
||||
mainSize: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
},
|
||||
|
||||
// 次要内容字体大小
|
||||
minorSize: {
|
||||
type: String,
|
||||
default: 'inherit'
|
||||
},
|
||||
|
||||
// 贯穿线
|
||||
lineThrough: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 字重
|
||||
fontWeight: {
|
||||
type: String | Number,
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
// 前缀
|
||||
prefix: {
|
||||
type: String,
|
||||
default: '¥'
|
||||
},
|
||||
|
||||
// 后缀
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 金额整数部分
|
||||
integer() {
|
||||
return formatPrice({
|
||||
price: this.content,
|
||||
take: 'int'
|
||||
});
|
||||
},
|
||||
|
||||
// 金额小数部分
|
||||
decimals() {
|
||||
const decimals = formatPrice({
|
||||
price: this.content,
|
||||
take: 'dec',
|
||||
prec: this.prec
|
||||
})
|
||||
|
||||
return this.autoPrec
|
||||
? (decimals * 1 ? ('.' + decimals) : '')
|
||||
: (this.prec ? ('.' + decimals) : '')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.price-container {
|
||||
display: inline-block;
|
||||
line-height: 1em;
|
||||
// @include font_color()
|
||||
}
|
||||
|
||||
.price-wrap {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&--disabled {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
display: block;
|
||||
content: "";
|
||||
height: .05em;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
// font-weight: 500;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 www.uviewui.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,106 @@
|
|||
<p align="center">
|
||||
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
|
||||
</p>
|
||||
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
|
||||
<h3 align="center">多平台快速开发的UI框架</h3>
|
||||
|
||||
|
||||
## 说明
|
||||
|
||||
uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
|
||||
|
||||
## 特性
|
||||
|
||||
- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
|
||||
- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
|
||||
- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
|
||||
- 众多的常用页面和布局,让您专注逻辑,事半功倍
|
||||
- 详尽的文档支持,现代化的演示效果
|
||||
- 按需引入,精简打包体积
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# npm方式安装
|
||||
npm i uview-ui
|
||||
```
|
||||
|
||||
## 快速上手
|
||||
|
||||
1. `main.js`引入uView库
|
||||
```js
|
||||
// main.js
|
||||
import uView from 'uview-ui';
|
||||
Vue.use(uView);
|
||||
```
|
||||
|
||||
2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
|
||||
```css
|
||||
/* App.vue */
|
||||
<style lang="scss">
|
||||
@import "uview-ui/index.scss";
|
||||
</style>
|
||||
```
|
||||
|
||||
3. `uni.scss`引入全局scss变量文件
|
||||
```css
|
||||
/* uni.scss */
|
||||
@import "uview-ui/theme.scss";
|
||||
```
|
||||
|
||||
4. `pages.json`配置easycom规则(按需引入)
|
||||
|
||||
```js
|
||||
// pages.json
|
||||
{
|
||||
"easycom": {
|
||||
// npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
|
||||
// npm安装方式
|
||||
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
|
||||
// 下载安装方式
|
||||
// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
|
||||
},
|
||||
// 此为本身已有的内容
|
||||
"pages": [
|
||||
// ......
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
|
||||
|
||||
## 使用方法
|
||||
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<u-button>按钮</u-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
|
||||
|
||||
## 链接
|
||||
|
||||
- [官方文档](https://uviewui.com/)
|
||||
- [更新日志](https://uviewui.com/components/changelog.html)
|
||||
- [升级指南](https://uviewui.com/components/changelog.html)
|
||||
- [关于我们](https://uviewui.com/cooperation/about.html)
|
||||
|
||||
## 预览
|
||||
|
||||
您可以通过**微信**扫码,查看最佳的演示效果。
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
|
||||
|
||||
<!-- ## 捐赠uView的研发
|
||||
|
||||
uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
|
||||
|
||||
<img src="https://uviewui.com/common/wechat.png" width="220" >
|
||||
<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
|
||||
-->
|
||||
## 版权信息
|
||||
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
|
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
|
||||
length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
|
||||
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
|
||||
{{tips.text}}
|
||||
</view>
|
||||
<block v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
@touchmove.stop.prevent
|
||||
@tap="itemClick(index)"
|
||||
:style="[itemStyle(index)]"
|
||||
class="u-action-sheet-item u-line-1"
|
||||
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<text>{{item.text}}</text>
|
||||
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class="u-gab" v-if="cancelBtn">
|
||||
</view>
|
||||
<view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
|
||||
:hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* actionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
* @tutorial https://www.uviewui.com/components/actionSheet.html
|
||||
* @property {Array<Object>} list 按钮的文字数组,见官方文档示例
|
||||
* @property {Object} tips 顶部的提示文字,见官方文档示例
|
||||
* @property {String} cancel-text 取消按钮的提示文字
|
||||
* @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
|
||||
* @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
|
||||
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
|
||||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
|
||||
* @property {Number String} z-index z-index值(默认1075)
|
||||
* @property {String} cancel-text 取消按钮的提示文字
|
||||
* @event {Function} click 点击ActionSheet列表项时触发
|
||||
* @event {Function} close 点击取消按钮时触发
|
||||
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
|
||||
*/
|
||||
export default {
|
||||
name: "u-action-sheet",
|
||||
props: {
|
||||
// 点击遮罩是否可以关闭actionsheet
|
||||
maskCloseAble: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
// 如下
|
||||
// return [{
|
||||
// text: '确定',
|
||||
// color: '',
|
||||
// fontSize: ''
|
||||
// }]
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 顶部的提示文字
|
||||
tips: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
text: '',
|
||||
color: '',
|
||||
fontSize: '26'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 底部的取消按钮
|
||||
cancelBtn: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 通过双向绑定控制组件的弹出与收起
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 弹出的顶部圆角值
|
||||
borderRadius: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 弹出的z-index值
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 取消按钮的文字提示
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 顶部提示的样式
|
||||
tipsStyle() {
|
||||
let style = {};
|
||||
if (this.tips.color) style.color = this.tips.color;
|
||||
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
|
||||
return style;
|
||||
},
|
||||
// 操作项目的样式
|
||||
itemStyle() {
|
||||
return (index) => {
|
||||
let style = {};
|
||||
if (this.list[index].color) style.color = this.list[index].color;
|
||||
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
|
||||
// 选项被禁用的样式
|
||||
if (this.list[index].disabled) style.color = '#c0c4cc';
|
||||
return style;
|
||||
}
|
||||
},
|
||||
uZIndex() {
|
||||
// 如果用户有传递z-index值,优先使用
|
||||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击取消按钮
|
||||
close() {
|
||||
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
|
||||
// 这是一个vue发送事件的特殊用法
|
||||
this.popupClose();
|
||||
this.$emit('close');
|
||||
},
|
||||
// 弹窗关闭
|
||||
popupClose() {
|
||||
this.$emit('input', false);
|
||||
},
|
||||
// 点击某一个item
|
||||
itemClick(index) {
|
||||
// disabled的项禁止点击
|
||||
if(this.list[index].disabled) return;
|
||||
this.$emit('click', index);
|
||||
this.$emit('input', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-tips {
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
padding: 34rpx 0;
|
||||
line-height: 1;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.u-action-sheet-item {
|
||||
@include vue-flex;;
|
||||
line-height: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
padding: 34rpx 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.u-action-sheet-item__subtext {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.u-gab {
|
||||
height: 12rpx;
|
||||
background-color: rgb(234, 234, 236);
|
||||
}
|
||||
|
||||
.u-actionsheet-cancel {
|
||||
color: $u-main-color;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<view class="u-alert-tips" v-if="show" :class="[
|
||||
!show ? 'u-close-alert-tips': '',
|
||||
type ? 'u-alert-tips--bg--' + type + '-light' : '',
|
||||
type ? 'u-alert-tips--border--' + type + '-disabled' : '',
|
||||
]" :style="{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor
|
||||
}">
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
|
||||
</view>
|
||||
<view class="u-alert-content" @tap.stop="click">
|
||||
<view class="u-alert-title" :style="[uTitleStyle]">
|
||||
{{title}}
|
||||
</view>
|
||||
<view v-if="description" class="u-alert-desc" :style="[descStyle]">
|
||||
{{description}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
|
||||
:size="22" class="u-close-icon" :style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}"></u-icon>
|
||||
</view>
|
||||
<text v-if="closeAble && closeText" class="u-close-text" :style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}">{{closeText}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* alertTips 警告提示
|
||||
* @description 警告提示,展现需要关注的信息
|
||||
* @tutorial https://uviewui.com/components/alertTips.html
|
||||
* @property {String} title 显示的标题文字
|
||||
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
|
||||
* @property {String} type 关闭按钮(默认为叉号icon图标)
|
||||
* @property {String} icon 图标名称
|
||||
* @property {Object} icon-style 图标的样式,对象形式
|
||||
* @property {Object} title-style 标题的样式,对象形式
|
||||
* @property {Object} desc-style 描述的样式,对象形式
|
||||
* @property {String} close-able 用文字替代关闭图标,close-able为true时有效
|
||||
* @property {Boolean} show-icon 是否显示左边的辅助图标
|
||||
* @property {Boolean} show 显示或隐藏组件
|
||||
* @event {Function} click 点击组件时触发
|
||||
* @event {Function} close 点击关闭按钮时触发
|
||||
*/
|
||||
export default {
|
||||
name: 'u-alert-tips',
|
||||
props: {
|
||||
// 显示文字
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 主题,success/warning/info/error
|
||||
type: {
|
||||
type: String,
|
||||
default: 'warning'
|
||||
},
|
||||
// 辅助性文字
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否可关闭
|
||||
closeAble: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 关闭按钮自定义文本
|
||||
closeText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 文字颜色,如果定义了color值,icon会失效
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 边框颜色
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 左边显示的icon
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// icon的样式
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 标题的样式
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 描述文字的样式
|
||||
descStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uTitleStyle() {
|
||||
let style = {};
|
||||
// 如果有描述文字的话,标题进行加粗
|
||||
style.fontWeight = this.description ? 500 : 'normal';
|
||||
// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
|
||||
return this.$u.deepMerge(style, this.titleStyle);
|
||||
},
|
||||
uIcon() {
|
||||
// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
|
||||
return this.icon ? this.icon : this.$u.type2icon(this.type);
|
||||
},
|
||||
uIconType() {
|
||||
// 如果有设置图标的样式,优先使用,没有的话,则用type的样式
|
||||
return Object.keys(this.iconStyle).length ? '' : this.type;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击内容
|
||||
click() {
|
||||
this.$emit('click');
|
||||
},
|
||||
// 点击关闭按钮
|
||||
close() {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-alert-tips {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 30rpx;
|
||||
border-radius: 8rpx;
|
||||
position: relative;
|
||||
transition: all 0.3s linear;
|
||||
border: 1px solid #fff;
|
||||
|
||||
&--bg--primary-light {
|
||||
background-color: $u-type-primary-light;
|
||||
}
|
||||
|
||||
&--bg--info-light {
|
||||
background-color: $u-type-info-light;
|
||||
}
|
||||
|
||||
&--bg--success-light {
|
||||
background-color: $u-type-success-light;
|
||||
}
|
||||
|
||||
&--bg--warning-light {
|
||||
background-color: $u-type-warning-light;
|
||||
}
|
||||
|
||||
&--bg--error-light {
|
||||
background-color: $u-type-error-light;
|
||||
}
|
||||
|
||||
&--border--primary-disabled {
|
||||
border-color: $u-type-primary-disabled;
|
||||
}
|
||||
|
||||
&--border--success-disabled {
|
||||
border-color: $u-type-success-disabled;
|
||||
}
|
||||
|
||||
&--border--error-disabled {
|
||||
border-color: $u-type-error-disabled;
|
||||
}
|
||||
|
||||
&--border--warning-disabled {
|
||||
border-color: $u-type-warning-disabled;
|
||||
}
|
||||
|
||||
&--border--info-disabled {
|
||||
border-color: $u-type-info-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.u-close-alert-tips {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.u-icon {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.u-alert-title {
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.u-alert-desc {
|
||||
font-size: 26rpx;
|
||||
text-align: left;
|
||||
color: $u-content-color;
|
||||
}
|
||||
|
||||
.u-close-icon {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
|
||||
.u-close-hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.u-close-text {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,290 @@
|
|||
<template>
|
||||
<view class="content">
|
||||
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
|
||||
<canvas
|
||||
class="cropper"
|
||||
:disable-scroll="true"
|
||||
@touchstart="touchStart"
|
||||
@touchmove="touchMove"
|
||||
@touchend="touchEnd"
|
||||
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
|
||||
canvas-id="cropper"
|
||||
id="cropper"
|
||||
></canvas>
|
||||
<canvas
|
||||
class="cropper"
|
||||
:disable-scroll="true"
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
|
||||
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
|
||||
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
|
||||
height: `${cropperOpt.height * cropperOpt.pixelRatio}`
|
||||
}"
|
||||
canvas-id="targetId"
|
||||
id="targetId"
|
||||
></canvas>
|
||||
</view>
|
||||
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="upload" @tap="uploadTap">选择图片</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="upload" @tap="uploadTap">重新选择</view>
|
||||
<!-- #endif -->
|
||||
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WeCropper from './weCropper.js';
|
||||
export default {
|
||||
props: {
|
||||
// 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
|
||||
// mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
|
||||
boundStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
lineWidth: 4,
|
||||
borderColor: 'rgb(245, 245, 245)',
|
||||
mask: 'rgba(0, 0, 0, 0.35)'
|
||||
};
|
||||
}
|
||||
}
|
||||
// // 裁剪框宽度,单位rpx
|
||||
// rectWidth: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 裁剪框高度,单位rpx
|
||||
// rectHeight: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出图片宽度,单位rpx
|
||||
// destWidth: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出图片高度,单位rpx
|
||||
// destHeight: {
|
||||
// type: [String, Number],
|
||||
// default: 400
|
||||
// },
|
||||
// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
|
||||
// fileType: {
|
||||
// type: String,
|
||||
// default: 'jpg',
|
||||
// },
|
||||
// // 生成的图片质量
|
||||
// // H5上无效,目前不考虑使用此参数
|
||||
// quality: {
|
||||
// type: [Number, String],
|
||||
// default: 1
|
||||
// }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 底部导航的高度
|
||||
bottomNavHeight: 50,
|
||||
originWidth: 200,
|
||||
width: 0,
|
||||
height: 0,
|
||||
cropperOpt: {
|
||||
id: 'cropper',
|
||||
targetId: 'targetCropper',
|
||||
pixelRatio: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scale: 2.5,
|
||||
zoom: 8,
|
||||
cut: {
|
||||
x: (this.width - this.originWidth) / 2,
|
||||
y: (this.height - this.originWidth) / 2,
|
||||
width: this.originWidth,
|
||||
height: this.originWidth
|
||||
},
|
||||
boundStyle: {
|
||||
lineWidth: uni.upx2px(this.boundStyle.lineWidth),
|
||||
mask: this.boundStyle.mask,
|
||||
color: this.boundStyle.borderColor
|
||||
}
|
||||
},
|
||||
// 裁剪框和输出图片的尺寸,高度默认等于宽度
|
||||
// 输出图片宽度,单位px
|
||||
destWidth: 200,
|
||||
// 裁剪框宽度,单位px
|
||||
rectWidth: 200,
|
||||
// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
|
||||
fileType: 'jpg',
|
||||
src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
let rectInfo = uni.getSystemInfoSync();
|
||||
this.width = rectInfo.windowWidth;
|
||||
this.height = rectInfo.windowHeight - this.bottomNavHeight;
|
||||
this.cropperOpt.width = this.width;
|
||||
this.cropperOpt.height = this.height;
|
||||
this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
|
||||
|
||||
if (option.destWidth) this.destWidth = option.destWidth;
|
||||
if (option.rectWidth) {
|
||||
let rectWidth = Number(option.rectWidth);
|
||||
this.cropperOpt.cut = {
|
||||
x: (this.width - rectWidth) / 2,
|
||||
y: (this.height - rectWidth) / 2,
|
||||
width: rectWidth,
|
||||
height: rectWidth
|
||||
};
|
||||
}
|
||||
this.rectWidth = option.rectWidth;
|
||||
if (option.fileType) this.fileType = option.fileType;
|
||||
// 初始化
|
||||
this.cropper = new WeCropper(this.cropperOpt)
|
||||
.on('ready', ctx => {
|
||||
// wecropper is ready for work!
|
||||
})
|
||||
.on('beforeImageLoad', ctx => {
|
||||
// before picture loaded, i can do something
|
||||
})
|
||||
.on('imageLoad', ctx => {
|
||||
// picture loaded
|
||||
})
|
||||
.on('beforeDraw', (ctx, instance) => {
|
||||
// before canvas draw,i can do something
|
||||
});
|
||||
// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: '#ffffff',
|
||||
backgroundColor: '#000000'
|
||||
});
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: res => {
|
||||
this.src = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
this.cropper.pushOrign(this.src);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
touchStart(e) {
|
||||
this.cropper.touchStart(e);
|
||||
},
|
||||
touchMove(e) {
|
||||
this.cropper.touchMove(e);
|
||||
},
|
||||
touchEnd(e) {
|
||||
this.cropper.touchEnd(e);
|
||||
},
|
||||
getCropperImage(isPre = false) {
|
||||
if(!this.src) return this.$u.toast('请先选择图片再裁剪');
|
||||
|
||||
let cropper_opt = {
|
||||
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
|
||||
destWidth: Number(this.destWidth),
|
||||
fileType: this.fileType
|
||||
};
|
||||
this.cropper.getCropperImage(cropper_opt, (path, err) => {
|
||||
if (err) {
|
||||
uni.showModal({
|
||||
title: '温馨提示',
|
||||
content: err.message
|
||||
});
|
||||
} else {
|
||||
if (isPre) {
|
||||
uni.previewImage({
|
||||
current: '', // 当前显示图片的 http 链接
|
||||
urls: [path] // 需要预览的图片 http 链接列表
|
||||
});
|
||||
} else {
|
||||
uni.$emit('uAvatarCropper', path);
|
||||
this.$u.route({
|
||||
type: 'back'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
uploadTap() {
|
||||
const self = this;
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: (res) => {
|
||||
self.src = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
|
||||
self.cropper.pushOrign(this.src);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.content {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.cropper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.cropper-buttons {
|
||||
background-color: #000000;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.cropper-wrapper {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.cropper-buttons {
|
||||
width: 100vw;
|
||||
@include vue-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.cropper-buttons .upload,
|
||||
.cropper-buttons .getCropperImage {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cropper-buttons .upload {
|
||||
text-align: left;
|
||||
padding-left: 50rpx;
|
||||
}
|
||||
|
||||
.cropper-buttons .getCropperImage {
|
||||
text-align: right;
|
||||
padding-right: 50rpx;
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,244 @@
|
|||
<template>
|
||||
<view class="u-avatar" :style="[wrapStyle]" @tap="click">
|
||||
<image
|
||||
@error="loadError"
|
||||
:style="[imgStyle]"
|
||||
class="u-avatar__img"
|
||||
v-if="!uText && avatar"
|
||||
:src="avatar"
|
||||
:mode="imgMode"
|
||||
></image>
|
||||
<text class="u-line-1" v-else-if="uText" :style="{
|
||||
fontSize: '38rpx'
|
||||
}">{{uText}}</text>
|
||||
<slot v-else></slot>
|
||||
<view class="u-avatar__sex" v-if="showSex" :class="['u-avatar__sex--' + sexIcon]" :style="[uSexStyle]">
|
||||
<u-icon :name="sexIcon" size="20"></u-icon>
|
||||
</view>
|
||||
<view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
|
||||
<u-icon :name="levelIcon" size="20"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let base64Avatar = "data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
|
||||
/**
|
||||
* avatar 头像
|
||||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
|
||||
* @tutorial https://www.uviewui.com/components/avatar.html
|
||||
* @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff)
|
||||
* @property {String} src 头像路径,如加载失败,将会显示默认头像
|
||||
* @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值,单位rpx(默认default)
|
||||
* @property {String} mode 显示类型,见上方说明(默认circle)
|
||||
* @property {String} sex-icon 性别图标,man-男,woman-女(默认man)
|
||||
* @property {String} level-icon 等级图标(默认level)
|
||||
* @property {String} sex-bg-color 性别图标背景颜色
|
||||
* @property {String} level-bg-color 等级图标背景颜色
|
||||
* @property {String} show-sex 是否显示性别图标(默认false)
|
||||
* @property {String} show-level 是否显示等级图标(默认false)
|
||||
* @property {String} img-mode 头像图片的裁剪类型,与uni的image组件的mode参数一致,如效果达不到需求,可尝试传widthFix值(默认aspectFill)
|
||||
* @property {String} index 用户传递的标识符值,如果是列表循环,可穿v-for的index值
|
||||
* @event {Function} click 头像被点击
|
||||
* @example <u-avatar :src="src"></u-avatar>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-avatar',
|
||||
props: {
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent'
|
||||
},
|
||||
// 头像路径
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 尺寸,large-大,default-中等,mini-小,如果为数值,则单位为rpx
|
||||
// 宽度等于高度
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 'default'
|
||||
},
|
||||
// 头像模型,square-带圆角方形,circle-圆形
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'circle'
|
||||
},
|
||||
// 文字内容
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图片的裁剪模型
|
||||
imgMode: {
|
||||
type: String,
|
||||
default: 'aspectFill'
|
||||
},
|
||||
// 标识符
|
||||
index: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 右上角性别角标,man-男,woman-女
|
||||
sexIcon: {
|
||||
type: String,
|
||||
default: 'man'
|
||||
},
|
||||
// 右下角的等级图标
|
||||
levelIcon: {
|
||||
type: String,
|
||||
default: 'level'
|
||||
},
|
||||
// 右下角等级图标背景颜色
|
||||
levelBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 右上角性别图标的背景颜色
|
||||
sexBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示性别图标
|
||||
showSex: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示等级图标
|
||||
showLevel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
// 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值
|
||||
avatar: this.src ? this.src : base64Avatar,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src(n) {
|
||||
// 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
|
||||
if(!n) {
|
||||
// 如果传入null或者'',或者undefined,显示默认头像
|
||||
this.avatar = base64Avatar;
|
||||
this.error = true;
|
||||
} else {
|
||||
this.avatar = n;
|
||||
this.error = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wrapStyle() {
|
||||
let style = {};
|
||||
style.height = this.size == 'large' ? '120rpx' : this.size == 'default' ?
|
||||
'90rpx' : this.size == 'mini' ? '70rpx' : this.size + 'rpx';
|
||||
style.width = style.height;
|
||||
style.flex = `0 0 ${style.height}`;
|
||||
style.backgroundColor = this.bgColor;
|
||||
style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
|
||||
if(this.text) style.padding = `0 6rpx`;
|
||||
return style;
|
||||
},
|
||||
imgStyle() {
|
||||
let style = {};
|
||||
style.borderRadius = this.mode == 'circle' ? '500px' : '5px';
|
||||
return style;
|
||||
},
|
||||
// 取字符串的第一个字符
|
||||
uText() {
|
||||
return String(this.text)[0];
|
||||
},
|
||||
// 性别图标的自定义样式
|
||||
uSexStyle() {
|
||||
let style = {};
|
||||
if(this.sexBgColor) style.backgroundColor = this.sexBgColor;
|
||||
return style;
|
||||
},
|
||||
// 等级图标的自定义样式
|
||||
uLevelStyle() {
|
||||
let style = {};
|
||||
if(this.levelBgColor) style.backgroundColor = this.levelBgColor;
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 图片加载错误时,显示默认头像
|
||||
loadError() {
|
||||
this.error = true;
|
||||
this.avatar = base64Avatar;
|
||||
},
|
||||
click() {
|
||||
this.$emit('click', this.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-avatar {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: $u-content-color;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__sex {
|
||||
position: absolute;
|
||||
width: 32rpx;
|
||||
color: #ffffff;
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
top: 5%;
|
||||
z-index: 1;
|
||||
right: -7%;
|
||||
border: 1px #ffffff solid;
|
||||
|
||||
&--man {
|
||||
background-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--woman {
|
||||
background-color: $u-type-error;
|
||||
}
|
||||
|
||||
&--none {
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
|
||||
&__level {
|
||||
position: absolute;
|
||||
width: 32rpx;
|
||||
color: #ffffff;
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
bottom: 5%;
|
||||
z-index: 1;
|
||||
right: -7%;
|
||||
border: 1px #ffffff solid;
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
|
||||
bottom: bottom + 'rpx',
|
||||
right: right + 'rpx',
|
||||
borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
|
||||
zIndex: uZIndex,
|
||||
opacity: opacity
|
||||
}, customStyle]">
|
||||
<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
|
||||
<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
|
||||
<view class="u-back-top__content__tips">
|
||||
{{tips}}
|
||||
</view>
|
||||
</view>
|
||||
<slot v-else />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'u-back-top',
|
||||
props: {
|
||||
// 返回顶部的形状,circle-圆形,square-方形
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'circle'
|
||||
},
|
||||
// 自定义图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'arrow-upward'
|
||||
},
|
||||
// 提示文字
|
||||
tips: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 返回顶部滚动时间
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 100
|
||||
},
|
||||
// 滚动距离
|
||||
scrollTop: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 距离顶部多少距离显示,单位rpx
|
||||
top: {
|
||||
type: [Number, String],
|
||||
default: 400
|
||||
},
|
||||
// 返回顶部按钮到底部的距离,单位rpx
|
||||
bottom: {
|
||||
type: [Number, String],
|
||||
default: 200
|
||||
},
|
||||
// 返回顶部按钮到右边的距离,单位rpx
|
||||
right: {
|
||||
type: [Number, String],
|
||||
default: 40
|
||||
},
|
||||
// 层级
|
||||
zIndex: {
|
||||
type: [Number, String],
|
||||
default: '9'
|
||||
},
|
||||
// 图标的样式,对象形式
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
color: '#909399',
|
||||
fontSize: '38rpx'
|
||||
}
|
||||
}
|
||||
},
|
||||
// 整个组件的样式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showBackTop(nVal, oVal) {
|
||||
// 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
|
||||
// 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
|
||||
if(nVal) {
|
||||
this.uZIndex = this.zIndex;
|
||||
this.opacity = 1;
|
||||
} else {
|
||||
this.uZIndex = -1;
|
||||
this.opacity = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showBackTop() {
|
||||
// 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
|
||||
// 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
|
||||
return this.scrollTop > uni.upx2px(this.top);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 不透明度,为了让组件有一个显示和隐藏的过渡动画
|
||||
opacity: 0,
|
||||
// 组件的z-index值,隐藏时设置为-1,就会看不到
|
||||
uZIndex: -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
backToTop() {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: this.duration
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-back-top {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
position: fixed;
|
||||
z-index: 9;
|
||||
@include vue-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: #E1E1E1;
|
||||
color: $u-content-color;
|
||||
align-items: center;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
&__content {
|
||||
@include vue-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__tips {
|
||||
font-size: 24rpx;
|
||||
transform: scale(0.8);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<view v-if="show" class="u-badge" :class="[
|
||||
isDot ? 'u-badge-dot' : '',
|
||||
size == 'mini' ? 'u-badge-mini' : '',
|
||||
type ? 'u-badge--bg--' + type : ''
|
||||
]" :style="[{
|
||||
top: offset[0] + 'rpx',
|
||||
right: offset[1] + 'rpx',
|
||||
fontSize: fontSize + 'rpx',
|
||||
position: absolute ? 'absolute' : 'static',
|
||||
color: color,
|
||||
backgroundColor: bgColor
|
||||
}, boxStyle]"
|
||||
>
|
||||
{{showText}}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* badge 角标
|
||||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
|
||||
* @tutorial https://www.uviewui.com/components/badge.html
|
||||
* @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
|
||||
* @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
|
||||
* @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
|
||||
* @property {String Number} overflow-count 展示封顶的数字值(默认99)
|
||||
* @property {String} type 使用预设的背景颜色(默认error)
|
||||
* @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
|
||||
* @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
|
||||
* @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
|
||||
* @property {String} color 字体颜色(默认#ffffff)
|
||||
* @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
|
||||
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
|
||||
* @example <u-badge type="error" count="7"></u-badge>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-badge',
|
||||
props: {
|
||||
// primary,warning,success,error,info
|
||||
type: {
|
||||
type: String,
|
||||
default: 'error'
|
||||
},
|
||||
// default, mini
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
//是否是圆点
|
||||
isDot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 显示的数值内容
|
||||
count: {
|
||||
type: [Number, String],
|
||||
},
|
||||
// 展示封顶的数字值
|
||||
overflowCount: {
|
||||
type: Number,
|
||||
default: 99
|
||||
},
|
||||
// 当数值为 0 时,是否展示 Badge
|
||||
showZero: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 位置偏移
|
||||
offset: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [20, 20]
|
||||
}
|
||||
},
|
||||
// 是否开启绝对定位,开启了offset才会起作用
|
||||
absolute: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 字体大小
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: '24'
|
||||
},
|
||||
// 字体演示
|
||||
color: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
// badge的背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
|
||||
isCenter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 是否将badge中心与父组件右上角重合
|
||||
boxStyle() {
|
||||
let style = {};
|
||||
if(this.isCenter) {
|
||||
style.top = 0;
|
||||
style.right = 0;
|
||||
// Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
|
||||
style.transform = "translateY(-50%) translateX(50%)";
|
||||
} else {
|
||||
style.top = this.offset[0] + 'rpx';
|
||||
style.right = this.offset[1] + 'rpx';
|
||||
style.transform = "translateY(0) translateX(0)";
|
||||
}
|
||||
// 如果尺寸为mini,后接上scal()
|
||||
if(this.size == 'mini') {
|
||||
style.transform = style.transform + " scale(0.8)";
|
||||
}
|
||||
return style;
|
||||
},
|
||||
// isDot类型时,不显示文字
|
||||
showText() {
|
||||
if(this.isDot) return '';
|
||||
else {
|
||||
if(this.count > this.overflowCount) return `${this.overflowCount}+`;
|
||||
else return this.count;
|
||||
}
|
||||
},
|
||||
// 是否显示组件
|
||||
show() {
|
||||
// 如果count的值为0,并且showZero设置为false,不显示组件
|
||||
if(this.count == 0 && this.showZero == false) return false;
|
||||
else return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-badge {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 24rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 100rpx;
|
||||
z-index: 9;
|
||||
min-width: 32rpx;
|
||||
&--bg--primary {
|
||||
background-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--bg--error {
|
||||
background-color: $u-type-error;
|
||||
}
|
||||
|
||||
&--bg--success {
|
||||
background-color: $u-type-success;
|
||||
}
|
||||
|
||||
&--bg--info {
|
||||
background-color: $u-type-info;
|
||||
}
|
||||
|
||||
&--bg--warning {
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
|
||||
.u-badge-dot {
|
||||
height: 16rpx;
|
||||
width: 16rpx;
|
||||
border-radius: 100rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.u-badge-mini {
|
||||
transform: scale(0.8);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
// .u-primary {
|
||||
// background: $u-type-primary;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// .u-error {
|
||||
// background: $u-type-error;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// .u-warning {
|
||||
// background: $u-type-warning;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// .u-success {
|
||||
// background: $u-type-success;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
// .u-black {
|
||||
// background: #585858;
|
||||
// color: #fff;
|
||||
// }
|
||||
|
||||
.u-info {
|
||||
background-color: $u-type-info;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,596 @@
|
|||
<template>
|
||||
<button
|
||||
id="u-wave-btn"
|
||||
class="u-btn u-line-1 u-fix-ios-appearance"
|
||||
:class="[
|
||||
'u-size-' + size,
|
||||
plain ? 'u-btn--' + type + '--plain' : '',
|
||||
loading ? 'u-loading' : '',
|
||||
shape == 'circle' ? 'u-round-circle' : '',
|
||||
hairLine ? showHairLineBorder : 'u-btn--bold-border',
|
||||
'u-btn--' + type,
|
||||
disabled ? `u-btn--${type}--disabled` : '',
|
||||
]"
|
||||
:hover-start-time="Number(hoverStartTime)"
|
||||
:hover-stay-time="Number(hoverStayTime)"
|
||||
:disabled="disabled"
|
||||
:form-type="formType"
|
||||
:open-type="openType"
|
||||
:app-parameter="appParameter"
|
||||
:hover-stop-propagation="hoverStopPropagation"
|
||||
:send-message-title="sendMessageTitle"
|
||||
send-message-path="sendMessagePath"
|
||||
:lang="lang"
|
||||
:data-name="dataName"
|
||||
:session-from="sessionFrom"
|
||||
:send-message-img="sendMessageImg"
|
||||
:show-message-card="showMessageCard"
|
||||
@getphonenumber="getphonenumber"
|
||||
@getuserinfo="getuserinfo"
|
||||
@error="error"
|
||||
@opensetting="opensetting"
|
||||
@launchapp="launchapp"
|
||||
:style="[customStyle, {
|
||||
overflow: ripple ? 'hidden' : 'visible'
|
||||
}]"
|
||||
@tap.stop="click($event)"
|
||||
:hover-class="getHoverClass"
|
||||
:loading="loading"
|
||||
>
|
||||
<slot></slot>
|
||||
<view
|
||||
v-if="ripple"
|
||||
class="u-wave-ripple"
|
||||
:class="[waveActive ? 'u-wave-active' : '']"
|
||||
:style="{
|
||||
top: rippleTop + 'px',
|
||||
left: rippleLeft + 'px',
|
||||
width: fields.targetWidth + 'px',
|
||||
height: fields.targetWidth + 'px',
|
||||
'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
|
||||
}"
|
||||
></view>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* button 按钮
|
||||
* @description Button 按钮
|
||||
* @tutorial https://www.uviewui.com/components/button.html
|
||||
* @property {String} size 按钮的大小
|
||||
* @property {Boolean} ripple 是否开启点击水波纹效果
|
||||
* @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
|
||||
* @property {String} type 按钮的样式类型
|
||||
* @property {Boolean} plain 按钮是否镂空,背景色透明
|
||||
* @property {Boolean} disabled 是否禁用
|
||||
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
|
||||
* @property {Boolean} shape 按钮外观形状,见文档说明
|
||||
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
|
||||
* @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
|
||||
* @property {String} open-type 开放能力
|
||||
* @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
|
||||
* @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
|
||||
* @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
|
||||
* @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
|
||||
* @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
|
||||
* @event {Function} click 按钮点击
|
||||
* @event {Function} getphonenumber open-type="getPhoneNumber"时有效
|
||||
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
|
||||
* @event {Function} error 当使用开放能力时,发生错误的回调
|
||||
* @event {Function} opensetting 在打开授权设置页并关闭后回调
|
||||
* @event {Function} launchapp 打开 APP 成功的回调
|
||||
* @example <u-button>月落</u-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-button',
|
||||
props: {
|
||||
// 是否细边框
|
||||
hairLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 按钮的预置样式,default,primary,error,warning,success
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
// 按钮尺寸,default,medium,mini
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
// 按钮形状,circle(两边为半圆),square(带圆角)
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'square'
|
||||
},
|
||||
// 按钮是否镂空
|
||||
plain: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否禁止状态
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否加载中
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 开放能力,具体请看uniapp稳定关于button组件部分说明
|
||||
// https://uniapp.dcloud.io/component/button
|
||||
openType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
|
||||
// 取值为submit(提交表单),reset(重置表单)
|
||||
formType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
|
||||
// 只微信小程序、QQ小程序有效
|
||||
appParameter: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
|
||||
hoverStopPropagation: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
|
||||
lang: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
// 会话来源,open-type="contact"时有效。只微信小程序有效
|
||||
sessionFrom: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 会话内消息卡片标题,open-type="contact"时有效
|
||||
// 默认当前标题,只微信小程序有效
|
||||
sendMessageTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
|
||||
// 默认当前分享路径,只微信小程序有效
|
||||
sendMessagePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 会话内消息卡片图片,open-type="contact"时有效
|
||||
// 默认当前页面截图,只微信小程序有效
|
||||
sendMessageImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
|
||||
// 用户点击后可以快速发送小程序消息,open-type="contact"时有效
|
||||
showMessageCard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 手指按(触摸)按钮时按钮时的背景颜色
|
||||
hoverBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 水波纹的背景颜色
|
||||
rippleBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否开启水波纹效果
|
||||
ripple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 按下的类名
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 自定义样式,对象形式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
|
||||
dataName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 节流,一定时间内只能触发一次
|
||||
throttleTime: {
|
||||
type: [String, Number],
|
||||
default: 1000
|
||||
},
|
||||
// 按住后多久出现点击态,单位毫秒
|
||||
hoverStartTime: {
|
||||
type: [String, Number],
|
||||
default: 20
|
||||
},
|
||||
// 手指松开后点击态保留时间,单位毫秒
|
||||
hoverStayTime: {
|
||||
type: [String, Number],
|
||||
default: 150
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// 当没有传bgColor变量时,按钮按下去的颜色类名
|
||||
getHoverClass() {
|
||||
// 如果开启水波纹效果,则不启用hover-class效果
|
||||
if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
|
||||
let hoverClass = '';
|
||||
hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
|
||||
return hoverClass;
|
||||
},
|
||||
// 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
|
||||
showHairLineBorder() {
|
||||
if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
|
||||
return '';
|
||||
} else {
|
||||
return 'u-hairline-border';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
|
||||
rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
|
||||
fields: {}, // 波纹按钮节点信息
|
||||
waveActive: false // 激活水波纹
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 按钮点击
|
||||
click(e) {
|
||||
// 进行节流控制,每this.throttle毫秒内,只在开始处执行
|
||||
this.$u.throttle(() => {
|
||||
// 如果按钮时disabled和loading状态,不触发水波纹效果
|
||||
if (this.loading === true || this.disabled === true) return;
|
||||
// 是否开启水波纹效果
|
||||
if (this.ripple) {
|
||||
// 每次点击时,移除上一次的类,再次添加,才能触发动画效果
|
||||
this.waveActive = false;
|
||||
this.$nextTick(function() {
|
||||
this.getWaveQuery(e);
|
||||
});
|
||||
}
|
||||
this.$emit('click', e);
|
||||
}, this.throttleTime);
|
||||
},
|
||||
// 查询按钮的节点信息
|
||||
getWaveQuery(e) {
|
||||
this.getElQuery().then(res => {
|
||||
// 查询返回的是一个数组节点
|
||||
let data = res[0];
|
||||
// 查询不到节点信息,不操作
|
||||
if (!data.width || !data.width) return;
|
||||
// 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
|
||||
// 最终的方形(变换后的圆形)才能覆盖整个按钮
|
||||
data.targetWidth = data.height > data.width ? data.height : data.width;
|
||||
if (!data.targetWidth) return;
|
||||
this.fields = data;
|
||||
let touchesX = '',
|
||||
touchesY = '';
|
||||
// #ifdef MP-BAIDU
|
||||
touchesX = e.changedTouches[0].clientX;
|
||||
touchesY = e.changedTouches[0].clientY;
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
touchesX = e.detail.clientX;
|
||||
touchesY = e.detail.clientY;
|
||||
// #endif
|
||||
// #ifndef MP-BAIDU || MP-ALIPAY
|
||||
touchesX = e.touches[0].clientX;
|
||||
touchesY = e.touches[0].clientY;
|
||||
// #endif
|
||||
// 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
|
||||
// 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
|
||||
// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
|
||||
this.rippleTop = touchesY - data.top - data.targetWidth / 2;
|
||||
this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
|
||||
this.$nextTick(() => {
|
||||
this.waveActive = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取节点信息
|
||||
getElQuery() {
|
||||
return new Promise(resolve => {
|
||||
let queryInfo = '';
|
||||
// 获取元素节点信息,请查看uniapp相关文档
|
||||
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
|
||||
queryInfo = uni.createSelectorQuery().in(this);
|
||||
//#ifdef MP-ALIPAY
|
||||
queryInfo = uni.createSelectorQuery();
|
||||
//#endif
|
||||
queryInfo.select('.u-btn').boundingClientRect();
|
||||
queryInfo.exec(data => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 下面为对接uniapp官方按钮开放能力事件回调的对接
|
||||
getphonenumber(res) {
|
||||
this.$emit('getphonenumber', res);
|
||||
},
|
||||
getuserinfo(res) {
|
||||
this.$emit('getuserinfo', res);
|
||||
},
|
||||
error(res) {
|
||||
this.$emit('error', res);
|
||||
},
|
||||
opensetting(res) {
|
||||
this.$emit('opensetting', res);
|
||||
},
|
||||
launchapp(res) {
|
||||
this.$emit('launchapp', res);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
.u-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.u-btn {
|
||||
position: relative;
|
||||
border: 0;
|
||||
//border-radius: 10rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
// 避免边框某些场景可能被“裁剪”,不能设置为hidden
|
||||
overflow: visible;
|
||||
line-height: 1;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0 40rpx;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.15s;
|
||||
|
||||
&--bold-border {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
&--default {
|
||||
color: $u-content-color;
|
||||
border-color: #c0c4cc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
color: #ffffff;
|
||||
border-color: $u-type-primary;
|
||||
background-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: #ffffff;
|
||||
border-color: $u-type-success;
|
||||
background-color: $u-type-success;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: #ffffff;
|
||||
border-color: $u-type-error;
|
||||
background-color: $u-type-error;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: #ffffff;
|
||||
border-color: $u-type-warning;
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
|
||||
&--default--disabled {
|
||||
color: #ffffff;
|
||||
border-color: #e4e7ed;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&--primary--disabled {
|
||||
color: #ffffff!important;
|
||||
border-color: $u-type-primary-disabled!important;
|
||||
background-color: $u-type-primary-disabled!important;
|
||||
}
|
||||
|
||||
&--success--disabled {
|
||||
color: #ffffff!important;
|
||||
border-color: $u-type-success-disabled!important;
|
||||
background-color: $u-type-success-disabled!important;
|
||||
}
|
||||
|
||||
&--error--disabled {
|
||||
color: #ffffff!important;
|
||||
border-color: $u-type-error-disabled!important;
|
||||
background-color: $u-type-error-disabled!important;
|
||||
}
|
||||
|
||||
&--warning--disabled {
|
||||
color: #ffffff!important;
|
||||
border-color: $u-type-warning-disabled!important;
|
||||
background-color: $u-type-warning-disabled!important;
|
||||
}
|
||||
|
||||
&--primary--plain {
|
||||
color: $u-type-primary!important;
|
||||
border-color: $u-type-primary-disabled!important;
|
||||
background-color: $u-type-primary-light!important;
|
||||
}
|
||||
|
||||
&--success--plain {
|
||||
color: $u-type-success!important;
|
||||
border-color: $u-type-success-disabled!important;
|
||||
background-color: $u-type-success-light!important;
|
||||
}
|
||||
|
||||
&--error--plain {
|
||||
color: $u-type-error!important;
|
||||
border-color: $u-type-error-disabled!important;
|
||||
background-color: $u-type-error-light!important;
|
||||
}
|
||||
|
||||
&--warning--plain {
|
||||
color: $u-type-warning!important;
|
||||
border-color: $u-type-warning-disabled!important;
|
||||
background-color: $u-type-warning-light!important;
|
||||
}
|
||||
}
|
||||
|
||||
.u-hairline-border:after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
// 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
|
||||
box-sizing: border-box;
|
||||
// 中心点作为变形(scale())的原点
|
||||
-webkit-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 199.8%;
|
||||
height: 199.7%;
|
||||
-webkit-transform: scale(0.5, 0.5);
|
||||
transform: scale(0.5, 0.5);
|
||||
border: 1px solid currentColor;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.u-wave-ripple {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
background-clip: padding-box;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.u-wave-ripple.u-wave-active {
|
||||
opacity: 0;
|
||||
transform: scale(2);
|
||||
transition: opacity 1s linear, transform 0.4s linear;
|
||||
}
|
||||
|
||||
.u-round-circle {
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
.u-round-circle::after {
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
.u-loading::after {
|
||||
background-color: hsla(0, 0%, 100%, 0.35);
|
||||
}
|
||||
|
||||
.u-size-default {
|
||||
font-size: 30rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
|
||||
.u-size-medium {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
width: auto;
|
||||
font-size: 26rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
padding: 0 80rpx;
|
||||
}
|
||||
|
||||
.u-size-mini {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
width: auto;
|
||||
font-size: 22rpx;
|
||||
padding-top: 1px;
|
||||
height: 50rpx;
|
||||
line-height: 50rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.u-primary-plain-hover {
|
||||
color: #ffffff !important;
|
||||
background: $u-type-primary-dark !important;
|
||||
}
|
||||
|
||||
.u-default-plain-hover {
|
||||
color: $u-type-primary-dark !important;
|
||||
background: $u-type-primary-light !important;
|
||||
}
|
||||
|
||||
.u-success-plain-hover {
|
||||
color: #ffffff !important;
|
||||
background: $u-type-success-dark !important;
|
||||
}
|
||||
|
||||
.u-warning-plain-hover {
|
||||
color: #ffffff !important;
|
||||
background: $u-type-warning-dark !important;
|
||||
}
|
||||
|
||||
.u-error-plain-hover {
|
||||
color: #ffffff !important;
|
||||
background: $u-type-error-dark !important;
|
||||
}
|
||||
|
||||
.u-info-plain-hover {
|
||||
color: #ffffff !important;
|
||||
background: $u-type-info-dark !important;
|
||||
}
|
||||
|
||||
.u-default-hover {
|
||||
color: $u-type-primary-dark !important;
|
||||
border-color: $u-type-primary-dark !important;
|
||||
background-color: $u-type-primary-light !important;
|
||||
}
|
||||
|
||||
.u-primary-hover {
|
||||
background: $u-type-primary-dark !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-success-hover {
|
||||
background: $u-type-success-dark !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-info-hover {
|
||||
background: $u-type-info-dark !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-warning-hover {
|
||||
background: $u-type-warning-dark !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.u-error-hover {
|
||||
background: $u-type-error-dark !important;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,639 @@
|
|||
<template>
|
||||
<u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
|
||||
<view class="u-calendar">
|
||||
<view class="u-calendar__header">
|
||||
<view class="u-calendar__header__text" v-if="!$slots['tooltip']">
|
||||
{{toolTip}}
|
||||
</view>
|
||||
<slot v-else name="tooltip" />
|
||||
</view>
|
||||
<view class="u-calendar__action u-flex u-row-center">
|
||||
<view class="u-calendar__action__icon">
|
||||
<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
|
||||
</view>
|
||||
<view class="u-calendar__action__icon">
|
||||
<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
|
||||
</view>
|
||||
<view class="u-calendar__action__text">{{ showTitle }}</view>
|
||||
<view class="u-calendar__action__icon">
|
||||
<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
|
||||
</view>
|
||||
<view class="u-calendar__action__icon">
|
||||
<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-calendar__week-day">
|
||||
<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
|
||||
</view>
|
||||
<view class="u-calendar__content">
|
||||
<!-- 前置空白部分 -->
|
||||
<block v-for="(item, index) in weekdayArr" :key="index">
|
||||
<view class="u-calendar__content__item"></view>
|
||||
</block>
|
||||
<view class="u-calendar__content__item" :class="{
|
||||
'u-hover-class':openDisAbled(year,month,index+1),
|
||||
'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
|
||||
'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
|
||||
}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
|
||||
@tap="dateClick(index)">
|
||||
<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
|
||||
<view>{{ index + 1 }}</view>
|
||||
</view>
|
||||
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
|
||||
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
|
||||
</view>
|
||||
<view class="u-calendar__content__bg-month">{{month}}</view>
|
||||
</view>
|
||||
<view class="u-calendar__bottom">
|
||||
<view class="u-calendar__bottom__choose">
|
||||
<text>{{mode == 'date' ? activeDate : startDate}}</text>
|
||||
<text v-if="endDate">至{{endDate}}</text>
|
||||
</view>
|
||||
<view class="u-calendar__bottom__btn">
|
||||
<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* calendar 日历
|
||||
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
|
||||
* @tutorial http://uviewui.com/components/calendar.html
|
||||
* @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
|
||||
* @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
|
||||
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
|
||||
* @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
|
||||
* @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
|
||||
* @property {String Number} max-year 可切换的最大年份(默认2050)
|
||||
* @property {String Number} min-year 可切换的最小年份(默认1950)
|
||||
* @property {String Number} min-date 最小可选日期(默认1950-01-01)
|
||||
* @property {String Number} max-date 最大可选日期(默认当前日期)
|
||||
* @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
|
||||
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
|
||||
* @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
|
||||
* @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
|
||||
* @property {String} color 日期字体的默认颜色(默认#303133)
|
||||
* @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
|
||||
* @property {String Number} z-index 弹出时的z-index值(默认10075)
|
||||
* @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
|
||||
* @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
|
||||
* @property {String} range-color 选择范围内字体颜色(默认#2979ff)
|
||||
* @property {String} start-text 起始日期底部的提示文字(默认 '开始')
|
||||
* @property {String} end-text 结束日期底部的提示文字(默认 '结束')
|
||||
* @property {String} btn-type 底部确定按钮的主题(默认 'primary')
|
||||
* @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
|
||||
* @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
|
||||
* @example <u-calendar v-model="show" :mode="mode"></u-calendar>
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'u-calendar',
|
||||
props: {
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否允许通过点击遮罩关闭Picker
|
||||
maskCloseAble: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 通过双向绑定控制组件的弹出与收起
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 弹出的z-index值
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 是否允许切换年份
|
||||
changeYear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否允许切换月份
|
||||
changeMonth: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// date-单个日期选择,range-开始日期+结束日期选择
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'date'
|
||||
},
|
||||
// 可切换的最大年份
|
||||
maxYear: {
|
||||
type: [Number, String],
|
||||
default: 2050
|
||||
},
|
||||
// 可切换的最小年份
|
||||
minYear: {
|
||||
type: [Number, String],
|
||||
default: 1950
|
||||
},
|
||||
// 最小可选日期(不在范围内日期禁用不可选)
|
||||
minDate: {
|
||||
type: [Number, String],
|
||||
default: '1950-01-01'
|
||||
},
|
||||
/**
|
||||
* 最大可选日期
|
||||
* 默认最大值为今天,之后的日期不可选
|
||||
* 2030-12-31
|
||||
* */
|
||||
maxDate: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 弹窗顶部左右两边的圆角值
|
||||
borderRadius: {
|
||||
type: [String, Number],
|
||||
default: 20
|
||||
},
|
||||
// 月份切换按钮箭头颜色
|
||||
monthArrowColor: {
|
||||
type: String,
|
||||
default: '#606266'
|
||||
},
|
||||
// 年份切换按钮箭头颜色
|
||||
yearArrowColor: {
|
||||
type: String,
|
||||
default: '#909399'
|
||||
},
|
||||
// 默认日期字体颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#303133'
|
||||
},
|
||||
// 选中|起始结束日期背景色
|
||||
activeBgColor: {
|
||||
type: String,
|
||||
default: '#2979ff'
|
||||
},
|
||||
// 选中|起始结束日期字体颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
// 范围内日期背景色
|
||||
rangeBgColor: {
|
||||
type: String,
|
||||
default: 'rgba(41,121,255,0.13)'
|
||||
},
|
||||
// 范围内日期字体颜色
|
||||
rangeColor: {
|
||||
type: String,
|
||||
default: '#2979ff'
|
||||
},
|
||||
// mode=range时生效,起始日期自定义文案
|
||||
startText: {
|
||||
type: String,
|
||||
default: '开始'
|
||||
},
|
||||
// mode=range时生效,结束日期自定义文案
|
||||
endText: {
|
||||
type: String,
|
||||
default: '结束'
|
||||
},
|
||||
//按钮样式类型
|
||||
btnType: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
// 当前选中日期带选中效果
|
||||
isActiveCurrent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 切换年月是否触发事件 mode=date时生效
|
||||
isChange: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示右上角的关闭图标
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 顶部的提示文字
|
||||
toolTip: {
|
||||
type: String,
|
||||
default: '选择日期'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 星期几,值为1-7
|
||||
weekday: 1,
|
||||
weekdayArr:[],
|
||||
// 当前月有多少天
|
||||
days: 0,
|
||||
daysArr:[],
|
||||
showTitle: '',
|
||||
year: 2020,
|
||||
month: 0,
|
||||
day: 0,
|
||||
startYear: 0,
|
||||
startMonth: 0,
|
||||
startDay: 0,
|
||||
endYear: 0,
|
||||
endMonth: 0,
|
||||
endDay: 0,
|
||||
today: '',
|
||||
activeDate: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
isStart: true,
|
||||
min: null,
|
||||
max: null,
|
||||
weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dataChange() {
|
||||
return `${this.mode}-${this.minDate}-${this.maxDate}`;
|
||||
},
|
||||
uZIndex() {
|
||||
// 如果用户有传递z-index值,优先使用
|
||||
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dataChange(val) {
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
getColor(index, type) {
|
||||
let color = type == 1 ? '' : this.color;
|
||||
let day = index + 1
|
||||
let date = `${this.year}-${this.month}-${day}`
|
||||
let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
|
||||
let start = this.startDate.replace(/\-/g, '/')
|
||||
let end = this.endDate.replace(/\-/g, '/')
|
||||
if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
|
||||
color = type == 1 ? this.activeBgColor : this.activeColor;
|
||||
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
|
||||
color = type == 1 ? this.rangeBgColor : this.rangeColor;
|
||||
}
|
||||
return color;
|
||||
},
|
||||
init() {
|
||||
let now = new Date();
|
||||
this.year = now.getFullYear();
|
||||
this.month = now.getMonth() + 1;
|
||||
this.day = now.getDate();
|
||||
this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
|
||||
this.activeDate = this.today;
|
||||
this.min = this.initDate(this.minDate);
|
||||
this.max = this.initDate(this.maxDate || this.today);
|
||||
this.startDate = "";
|
||||
this.startYear = 0;
|
||||
this.startMonth = 0;
|
||||
this.startDay = 0;
|
||||
this.endYear = 0;
|
||||
this.endMonth = 0;
|
||||
this.endDay = 0;
|
||||
this.endDate = "";
|
||||
this.isStart = true;
|
||||
this.changeData();
|
||||
},
|
||||
//日期处理
|
||||
initDate(date) {
|
||||
let fdate = date.split('-');
|
||||
return {
|
||||
year: Number(fdate[0] || 1920),
|
||||
month: Number(fdate[1] || 1),
|
||||
day: Number(fdate[2] || 1)
|
||||
}
|
||||
},
|
||||
openDisAbled: function(year, month, day) {
|
||||
let bool = true;
|
||||
let date = `${year}/${month}/${day}`;
|
||||
// let today = this.today.replace(/\-/g, '/');
|
||||
let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
|
||||
let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
|
||||
let timestamp = new Date(date).getTime();
|
||||
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
|
||||
bool = false;
|
||||
}
|
||||
return bool;
|
||||
},
|
||||
generateArray: function(start, end) {
|
||||
return Array.from(new Array(end + 1).keys()).slice(start);
|
||||
},
|
||||
formatNum: function(num) {
|
||||
return num < 10 ? '0' + num : num + '';
|
||||
},
|
||||
//一个月有多少天
|
||||
getMonthDay(year, month) {
|
||||
let days = new Date(year, month, 0).getDate();
|
||||
return days;
|
||||
},
|
||||
getWeekday(year, month) {
|
||||
let date = new Date(`${year}/${month}/01 00:00:00`);
|
||||
return date.getDay();
|
||||
},
|
||||
checkRange(year) {
|
||||
let overstep = false;
|
||||
if (year < this.minYear || year > this.maxYear) {
|
||||
uni.showToast({
|
||||
title: "日期超出范围啦~",
|
||||
icon: 'none'
|
||||
})
|
||||
overstep = true;
|
||||
}
|
||||
return overstep;
|
||||
},
|
||||
changeMonthHandler(isAdd) {
|
||||
if (isAdd) {
|
||||
let month = this.month + 1;
|
||||
let year = month > 12 ? this.year + 1 : this.year;
|
||||
if (!this.checkRange(year)) {
|
||||
this.month = month > 12 ? 1 : month;
|
||||
this.year = year;
|
||||
this.changeData();
|
||||
}
|
||||
|
||||
} else {
|
||||
let month = this.month - 1;
|
||||
let year = month < 1 ? this.year - 1 : this.year;
|
||||
if (!this.checkRange(year)) {
|
||||
this.month = month < 1 ? 12 : month;
|
||||
this.year = year;
|
||||
this.changeData();
|
||||
}
|
||||
}
|
||||
},
|
||||
changeYearHandler(isAdd) {
|
||||
let year = isAdd ? this.year + 1 : this.year - 1;
|
||||
if (!this.checkRange(year)) {
|
||||
this.year = year;
|
||||
this.changeData();
|
||||
}
|
||||
},
|
||||
changeData() {
|
||||
this.days = this.getMonthDay(this.year, this.month);
|
||||
this.daysArr=this.generateArray(1,this.days)
|
||||
this.weekday = this.getWeekday(this.year, this.month);
|
||||
this.weekdayArr=this.generateArray(1,this.weekday)
|
||||
this.showTitle = `${this.year}年${this.month}月`;
|
||||
if (this.isChange && this.mode == 'date') {
|
||||
this.btnFix(true);
|
||||
}
|
||||
},
|
||||
dateClick: function(day) {
|
||||
day += 1;
|
||||
if (!this.openDisAbled(this.year, this.month, day)) {
|
||||
this.day = day;
|
||||
let date = `${this.year}-${this.month}-${day}`;
|
||||
if (this.mode == 'date') {
|
||||
this.activeDate = date;
|
||||
} else {
|
||||
let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
|
||||
if (this.isStart || compare) {
|
||||
this.startDate = date;
|
||||
this.startYear = this.year;
|
||||
this.startMonth = this.month;
|
||||
this.startDay = this.day;
|
||||
this.endYear = 0;
|
||||
this.endMonth = 0;
|
||||
this.endDay = 0;
|
||||
this.endDate = "";
|
||||
this.activeDate = "";
|
||||
this.isStart = false;
|
||||
} else {
|
||||
this.endDate = date;
|
||||
this.endYear = this.year;
|
||||
this.endMonth = this.month;
|
||||
this.endDay = this.day;
|
||||
this.isStart = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
close() {
|
||||
// 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
|
||||
this.$emit('input', false);
|
||||
},
|
||||
getWeekText(date) {
|
||||
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
|
||||
let week = date.getDay();
|
||||
return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
|
||||
},
|
||||
btnFix(show) {
|
||||
if (!show) {
|
||||
this.close();
|
||||
}
|
||||
if (this.mode == 'date') {
|
||||
let arr = this.activeDate.split('-')
|
||||
let year = this.isChange ? this.year : Number(arr[0]);
|
||||
let month = this.isChange ? this.month : Number(arr[1]);
|
||||
let day = this.isChange ? this.day : Number(arr[2]);
|
||||
//当前月有多少天
|
||||
let days = this.getMonthDay(year, month);
|
||||
let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
|
||||
let weekText = this.getWeekText(result);
|
||||
let isToday = false;
|
||||
if (`${year}-${month}-${day}` == this.today) {
|
||||
//今天
|
||||
isToday = true;
|
||||
}
|
||||
this.$emit('change', {
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
days: days,
|
||||
result: result,
|
||||
week: weekText,
|
||||
isToday: isToday,
|
||||
// switch: show //是否是切换年月操作
|
||||
});
|
||||
} else {
|
||||
if (!this.startDate || !this.endDate) return;
|
||||
let startMonth = this.formatNum(this.startMonth);
|
||||
let startDay = this.formatNum(this.startDay);
|
||||
let startDate = `${this.startYear}-${startMonth}-${startDay}`;
|
||||
let startWeek = this.getWeekText(startDate)
|
||||
|
||||
let endMonth = this.formatNum(this.endMonth);
|
||||
let endDay = this.formatNum(this.endDay);
|
||||
let endDate = `${this.endYear}-${endMonth}-${endDay}`;
|
||||
let endWeek = this.getWeekText(endDate);
|
||||
this.$emit('change', {
|
||||
startYear: this.startYear,
|
||||
startMonth: this.startMonth,
|
||||
startDay: this.startDay,
|
||||
startDate: startDate,
|
||||
startWeek: startWeek,
|
||||
endYear: this.endYear,
|
||||
endMonth: this.endMonth,
|
||||
endDay: this.endDay,
|
||||
endDate: endDate,
|
||||
endWeek: endWeek
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-calendar {
|
||||
color: $u-content-color;
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 30rpx;
|
||||
background-color: #fff;
|
||||
color: $u-main-color;
|
||||
|
||||
&__text {
|
||||
margin-top: 30rpx;
|
||||
padding: 0 60rpx;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
padding: 40rpx 0 40rpx 0;
|
||||
|
||||
&__icon {
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
&__text {
|
||||
padding: 0 16rpx;
|
||||
color: $u-main-color;
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__week-day {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 0;
|
||||
overflow: hidden;
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
@include vue-flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 0;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
|
||||
&--end-date {
|
||||
border-top-right-radius: 8rpx;
|
||||
border-bottom-right-radius: 8rpx;
|
||||
}
|
||||
|
||||
&--start-date {
|
||||
border-top-left-radius: 8rpx;
|
||||
border-bottom-left-radius: 8rpx;
|
||||
}
|
||||
|
||||
&__item {
|
||||
width: 14.2857%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&__inner {
|
||||
height: 84rpx;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 32rpx;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
|
||||
&__desc {
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
transform: scale(0.75);
|
||||
transform-origin: center center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
bottom: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__tips {
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transform: scale(0.8);
|
||||
transform-origin: center center;
|
||||
text-align: center;
|
||||
bottom: 8rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__bg-month {
|
||||
position: absolute;
|
||||
font-size: 130px;
|
||||
line-height: 130px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #e4e7ed;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
width: 100%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
padding: 0 40rpx 30rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
|
||||
&__choose {
|
||||
height: 50rpx;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,257 @@
|
|||
<template>
|
||||
<view class="u-keyboard" @touchmove.stop.prevent="() => {}">
|
||||
<view class="u-keyboard-grids">
|
||||
<block>
|
||||
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
|
||||
<view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
|
||||
v-for="(item, j) in group" :key="j">
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
|
||||
hover-class="u-hover-class">
|
||||
<u-icon :size="38" name="backspace" :bold="true"></u-icon>
|
||||
</view>
|
||||
<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
|
||||
<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
|
||||
/
|
||||
<text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "u-keyboard",
|
||||
props: {
|
||||
// 是否打乱键盘按键的顺序
|
||||
random: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
|
||||
abc: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
areaList() {
|
||||
let data = [
|
||||
'京',
|
||||
'沪',
|
||||
'粤',
|
||||
'津',
|
||||
'冀',
|
||||
'豫',
|
||||
'云',
|
||||
'辽',
|
||||
'黑',
|
||||
'湘',
|
||||
'皖',
|
||||
'鲁',
|
||||
'苏',
|
||||
'浙',
|
||||
'赣',
|
||||
'鄂',
|
||||
'桂',
|
||||
'甘',
|
||||
'晋',
|
||||
'陕',
|
||||
'蒙',
|
||||
'吉',
|
||||
'闽',
|
||||
'贵',
|
||||
'渝',
|
||||
'川',
|
||||
'青',
|
||||
'琼',
|
||||
'宁',
|
||||
'挂',
|
||||
'藏',
|
||||
'港',
|
||||
'澳',
|
||||
'新',
|
||||
'使',
|
||||
'学'
|
||||
];
|
||||
let tmp = [];
|
||||
// 打乱顺序
|
||||
if (this.random) data = this.$u.randomArray(data);
|
||||
// 切割成二维数组
|
||||
tmp[0] = data.slice(0, 10);
|
||||
tmp[1] = data.slice(10, 20);
|
||||
tmp[2] = data.slice(20, 30);
|
||||
tmp[3] = data.slice(30, 36);
|
||||
return tmp;
|
||||
},
|
||||
EngKeyBoardList() {
|
||||
let data = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
0,
|
||||
'Q',
|
||||
'W',
|
||||
'E',
|
||||
'R',
|
||||
'T',
|
||||
'Y',
|
||||
'U',
|
||||
'I',
|
||||
'O',
|
||||
'P',
|
||||
'A',
|
||||
'S',
|
||||
'D',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'Z',
|
||||
'X',
|
||||
'C',
|
||||
'V',
|
||||
'B',
|
||||
'N',
|
||||
'M'
|
||||
];
|
||||
let tmp = [];
|
||||
if (this.random) data = this.$u.randomArray(data);
|
||||
tmp[0] = data.slice(0, 10);
|
||||
tmp[1] = data.slice(10, 20);
|
||||
tmp[2] = data.slice(20, 30);
|
||||
tmp[3] = data.slice(30, 36);
|
||||
return tmp;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击键盘按钮
|
||||
carInputClick(i, j) {
|
||||
let value = '';
|
||||
// 不同模式,获取不同数组的值
|
||||
if (this.abc) value = this.EngKeyBoardList[i][j];
|
||||
else value = this.areaList[i][j];
|
||||
this.$emit('change', value);
|
||||
},
|
||||
// 修改汽车牌键盘的输入模式,中文|英文
|
||||
changeCarInputMode() {
|
||||
this.abc = !this.abc;
|
||||
},
|
||||
// 点击退格键
|
||||
backspaceClick() {
|
||||
this.$emit('backspace');
|
||||
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
|
||||
this.timer = null;
|
||||
this.timer = setInterval(() => {
|
||||
this.$emit('backspace');
|
||||
}, 250);
|
||||
},
|
||||
clearTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-keyboard-grids {
|
||||
background: rgb(215, 215, 217);
|
||||
padding: 24rpx 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u-keyboard-grids-item {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-keyboard-grids-btn {
|
||||
text-decoration: none;
|
||||
width: 62rpx;
|
||||
flex: 0 0 64rpx;
|
||||
height: 80rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
font-size: 36rpx;
|
||||
text-align: center;
|
||||
line-height: 80rpx;
|
||||
background-color: #fff;
|
||||
margin: 8rpx 5rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 0rpx #888992;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-carinput-hover {
|
||||
background-color: rgb(185, 188, 195) !important;
|
||||
}
|
||||
|
||||
.u-keyboard-back {
|
||||
position: absolute;
|
||||
width: 96rpx;
|
||||
right: 22rpx;
|
||||
bottom: 32rpx;
|
||||
height: 80rpx;
|
||||
background-color: rgb(185, 188, 195);
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
border-radius: 8rpx;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 0rpx #888992;
|
||||
}
|
||||
|
||||
.u-keyboard-change {
|
||||
font-size: 24rpx;
|
||||
box-shadow: 0 2rpx 0rpx #888992;
|
||||
position: absolute;
|
||||
width: 96rpx;
|
||||
left: 22rpx;
|
||||
line-height: 1;
|
||||
bottom: 32rpx;
|
||||
height: 80rpx;
|
||||
background-color: #ffffff;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
border-radius: 8rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-keyboard-change .inactive.zh {
|
||||
transform: scale(0.85) translateY(-10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .inactive.en {
|
||||
transform: scale(0.85) translateY(10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .active {
|
||||
color: rgb(237, 112, 64);
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.u-keyboard-change .zh {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .en {
|
||||
transform: translateY(10rpx);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,299 @@
|
|||
<template>
|
||||
<view
|
||||
class="u-card"
|
||||
@tap.stop="click"
|
||||
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
|
||||
:style="{
|
||||
borderRadius: borderRadius + 'rpx',
|
||||
margin: margin,
|
||||
boxShadow: boxShadow
|
||||
}"
|
||||
>
|
||||
<view
|
||||
v-if="showHead"
|
||||
class="u-card__head"
|
||||
:style="[{padding: padding + 'rpx'}, headStyle]"
|
||||
:class="{
|
||||
'u-border-bottom': headBorderBottom
|
||||
}"
|
||||
@tap="headClick"
|
||||
>
|
||||
<view v-if="!$slots.head" class="u-flex u-row-between">
|
||||
<view class="u-card__head--left u-flex u-line-1" v-if="title">
|
||||
<image
|
||||
:src="thumb"
|
||||
class="u-card__head--left__thumb"
|
||||
mode="aspectfull"
|
||||
v-if="thumb"
|
||||
:style="{
|
||||
height: thumbWidth + 'rpx',
|
||||
width: thumbWidth + 'rpx',
|
||||
borderRadius: thumbCircle ? '100rpx' : '6rpx'
|
||||
}"
|
||||
></image>
|
||||
<text
|
||||
class="u-card__head--left__title u-line-1"
|
||||
:style="{
|
||||
fontSize: titleSize + 'rpx',
|
||||
color: titleColor
|
||||
}"
|
||||
>
|
||||
{{ title }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="u-card__head--right u-line-1" v-if="subTitle">
|
||||
<text
|
||||
class="u-card__head__title__text"
|
||||
:style="{
|
||||
fontSize: subTitleSize + 'rpx',
|
||||
color: subTitleColor
|
||||
}"
|
||||
>
|
||||
{{ subTitle }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<slot name="head" v-else />
|
||||
</view>
|
||||
<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
|
||||
<view
|
||||
v-if="showFoot"
|
||||
class="u-card__foot"
|
||||
@tap="footClick"
|
||||
:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
|
||||
:class="{
|
||||
'u-border-top': footBorderTop
|
||||
}"
|
||||
>
|
||||
<slot name="foot" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* card 卡片
|
||||
* @description 卡片组件一般用于多个列表条目,且风格统一的场景
|
||||
* @tutorial https://www.uviewui.com/components/card.html
|
||||
* @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
|
||||
* @property {String} title 头部左边的标题
|
||||
* @property {String} title-color 标题颜色(默认#303133)
|
||||
* @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
|
||||
* @property {String} sub-title 头部右边的副标题
|
||||
* @property {String} sub-title-color 副标题颜色(默认#909399)
|
||||
* @property {String | Number} sub-title-size 副标题字体大小(默认26)
|
||||
* @property {Boolean} border 是否显示边框(默认true)
|
||||
* @property {String | Number} index 用于标识点击了第几个卡片
|
||||
* @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
|
||||
* @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
|
||||
* @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
|
||||
* @property {Object} head-style 头部自定义样式,对象形式
|
||||
* @property {Object} body-style 中部自定义样式,对象形式
|
||||
* @property {Object} foot-style 底部自定义样式,对象形式
|
||||
* @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
|
||||
* @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
|
||||
* @property {Boolean} show-head 是否显示头部(默认true)
|
||||
* @property {Boolean} show-head 是否显示尾部(默认true)
|
||||
* @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
|
||||
* @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
|
||||
* @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
|
||||
* @event {Function} click 整个卡片任意位置被点击时触发
|
||||
* @event {Function} head-click 卡片头部被点击时触发
|
||||
* @event {Function} body-click 卡片主体部分被点击时触发
|
||||
* @event {Function} foot-click 卡片底部部分被点击时触发
|
||||
* @example <u-card padding="30" title="card"></u-card>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-card',
|
||||
props: {
|
||||
// 与屏幕两侧是否留空隙
|
||||
full: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 标题颜色
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#303133'
|
||||
},
|
||||
// 标题字体大小,单位rpx
|
||||
titleSize: {
|
||||
type: [Number, String],
|
||||
default: '30'
|
||||
},
|
||||
// 副标题
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 副标题颜色
|
||||
subTitleColor: {
|
||||
type: String,
|
||||
default: '#909399'
|
||||
},
|
||||
// 副标题字体大小,单位rpx
|
||||
subTitleSize: {
|
||||
type: [Number, String],
|
||||
default: '26'
|
||||
},
|
||||
// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 用于标识点击了第几个
|
||||
index: {
|
||||
type: [Number, String, Object],
|
||||
default: ''
|
||||
},
|
||||
// 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
|
||||
margin: {
|
||||
type: String,
|
||||
default: '30rpx'
|
||||
},
|
||||
// card卡片的圆角
|
||||
borderRadius: {
|
||||
type: [Number, String],
|
||||
default: '16'
|
||||
},
|
||||
// 头部自定义样式,对象形式
|
||||
headStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 主体自定义样式,对象形式
|
||||
bodyStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 底部自定义样式,对象形式
|
||||
footStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 头部是否下边框
|
||||
headBorderBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 底部是否有上边框
|
||||
footBorderTop: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 标题左边的缩略图
|
||||
thumb: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 缩略图宽高,单位rpx
|
||||
thumbWidth: {
|
||||
type: [String, Number],
|
||||
default: '60'
|
||||
},
|
||||
// 缩略图是否为圆形
|
||||
thumbCircle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 给head,body,foot的内边距
|
||||
padding: {
|
||||
type: [String, Number],
|
||||
default: '30'
|
||||
},
|
||||
// 是否显示头部
|
||||
showHead: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示尾部
|
||||
showFoot: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 卡片外围阴影,字符串形式
|
||||
boxShadow: {
|
||||
type: String,
|
||||
default: 'none'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click', this.index);
|
||||
},
|
||||
headClick() {
|
||||
this.$emit('head-click', this.index);
|
||||
},
|
||||
bodyClick() {
|
||||
this.$emit('body-click', this.index);
|
||||
},
|
||||
footClick() {
|
||||
this.$emit('foot-click', this.index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
|
||||
&-full {
|
||||
// 如果是与屏幕之间不留空隙,应该设置左右边距为0
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--border:after {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__head {
|
||||
&--left {
|
||||
color: $u-main-color;
|
||||
|
||||
&__thumb {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
max-width: 400rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
color: $u-tips-color;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
color: $u-content-color;
|
||||
}
|
||||
|
||||
&__foot {
|
||||
color: $u-tips-color;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<view class="u-cell-box">
|
||||
<view class="u-cell-title" v-if="title" :style="[titleStyle]">
|
||||
{{title}}
|
||||
</view>
|
||||
<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* cellGroup 单元格父组件Group
|
||||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
|
||||
* @tutorial https://www.uviewui.com/components/cell.html
|
||||
* @property {String} title 分组标题
|
||||
* @property {Boolean} border 是否显示外边框(默认true)
|
||||
* @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
|
||||
* @example <u-cell-group title="设置喜好">
|
||||
*/
|
||||
export default {
|
||||
name: "u-cell-group",
|
||||
props: {
|
||||
// 分组标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示分组list上下边框
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 分组标题的样式,对象形式,注意驼峰属性写法
|
||||
// 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
index: 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-cell-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.u-cell-title {
|
||||
padding: 30rpx 32rpx 10rpx 32rpx;
|
||||
font-size: 30rpx;
|
||||
text-align: left;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.u-cell-item-box {
|
||||
background-color: #FFFFFF;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,316 @@
|
|||
<template>
|
||||
<view
|
||||
@tap="click"
|
||||
class="u-cell"
|
||||
:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
|
||||
hover-stay-time="150"
|
||||
:hover-class="hoverClass"
|
||||
:style="{
|
||||
backgroundColor: bgColor
|
||||
}"
|
||||
>
|
||||
<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
|
||||
<view class="u-flex" v-else>
|
||||
<slot name="icon"></slot>
|
||||
</view>
|
||||
<view
|
||||
class="u-cell_title"
|
||||
:style="[
|
||||
{
|
||||
width: titleWidth ? titleWidth + 'rpx' : 'auto'
|
||||
},
|
||||
titleStyle
|
||||
]"
|
||||
>
|
||||
<block v-if="title !== ''">{{ title }}</block>
|
||||
<slot name="title" v-else></slot>
|
||||
|
||||
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
|
||||
<block v-if="label !== ''">{{ label }}</block>
|
||||
<slot name="label" v-else></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="u-cell__value" :style="[valueStyle]">
|
||||
<block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
|
||||
<slot v-else></slot>
|
||||
</view>
|
||||
<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
|
||||
<slot name="right-icon"></slot>
|
||||
</view>
|
||||
<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* cellItem 单元格Item
|
||||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
|
||||
* @tutorial https://www.uviewui.com/components/cell.html
|
||||
* @property {String} title 左侧标题
|
||||
* @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
|
||||
* @property {Object} icon-style 左边图标的样式,对象形式
|
||||
* @property {String} value 右侧内容
|
||||
* @property {String} label 标题下方的描述信息
|
||||
* @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
|
||||
* @property {Boolean} border-top 是否显示cell的上边框(默认false)
|
||||
* @property {Boolean} center 是否使内容垂直居中(默认false)
|
||||
* @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
|
||||
* // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
|
||||
* @property {Boolean} arrow 是否显示右侧箭头(默认true)
|
||||
* @property {Boolean} required 箭头方向,可选值(默认right)
|
||||
* @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
|
||||
* @property {Object} title-style 标题样式,对象形式
|
||||
* @property {Object} value-style 右侧内容样式,对象形式
|
||||
* @property {Object} label-style 标题下方描述信息的样式,对象形式
|
||||
* @property {String} bg-color 背景颜色(默认transparent)
|
||||
* @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
|
||||
* @property {String Number} title-width 标题的宽度,单位rpx
|
||||
* @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-cell-item',
|
||||
props: {
|
||||
// 左侧图标名称(只能uView内置图标),或者图标src
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 左侧标题
|
||||
title: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 右侧内容
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 标题下方的描述信息
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 是否显示下边框
|
||||
borderBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示上边框
|
||||
borderTop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
|
||||
// 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
|
||||
// borderGap: {
|
||||
// type: Boolean,
|
||||
// default: true
|
||||
// },
|
||||
// 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: 'u-cell-hover'
|
||||
},
|
||||
// 是否显示右侧箭头
|
||||
arrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 内容是否垂直居中
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示左边表示必填的星号
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题的宽度,单位rpx
|
||||
titleWidth: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 右侧箭头方向,可选值:right|up|down,默认为right
|
||||
arrowDirection: {
|
||||
type: String,
|
||||
default: 'right'
|
||||
},
|
||||
// 控制标题的样式
|
||||
titleStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 右侧显示内容的样式
|
||||
valueStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 描述信息的样式
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent'
|
||||
},
|
||||
// 用于识别被点击的是第几个cell
|
||||
index: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 是否使用lable插槽
|
||||
useLabelSlot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 左边图标的大小,单位rpx,只对传入icon字段时有效
|
||||
iconSize: {
|
||||
type: [Number, String],
|
||||
default: 34
|
||||
},
|
||||
// 左边图标的样式,对象形式
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
arrowStyle() {
|
||||
let style = {};
|
||||
if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
|
||||
else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
|
||||
else style.transform = 'rotate(0deg)';
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click', this.index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
.u-cell {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
width: 100%;
|
||||
padding: 26rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 54rpx;
|
||||
color: $u-content-color;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.u-cell_title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.u-cell__left-icon-wrap {
|
||||
margin-right: 10rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.u-cell__right-icon-wrap {
|
||||
margin-left: 10rpx;
|
||||
color: #969799;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.u-cell__left-icon-wrap,
|
||||
.u-cell__right-icon-wrap {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.u-cell-border:after {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
content: ' ';
|
||||
pointer-events: none;
|
||||
border-bottom: 1px solid $u-border-color;
|
||||
/* #endif */
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.u-cell-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u-cell__label {
|
||||
margin-top: 6rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
color: $u-tips-color;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-wrap: break-word;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-cell__value {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
/* #ifndef APP-NVUE */
|
||||
vertical-align: middle;
|
||||
/* #endif */
|
||||
color: $u-tips-color;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.u-cell__title,
|
||||
.u-cell__value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.u-cell--required {
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: visible;
|
||||
/* #endif */
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-cell--required:before {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
content: '*';
|
||||
/* #endif */
|
||||
left: 8px;
|
||||
margin-top: 4rpx;
|
||||
font-size: 14px;
|
||||
color: $u-type-error;
|
||||
}
|
||||
|
||||
.u-cell_right {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<view class="u-checkbox-group u-clearfix">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Emitter from '../../libs/util/emitter.js';
|
||||
/**
|
||||
* checkboxGroup 开关选择器父组件Group
|
||||
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
|
||||
* @tutorial https://www.uviewui.com/components/checkbox.html
|
||||
* @property {String Number} max 最多能选中多少个checkbox(默认999)
|
||||
* @property {String Number} size 组件整体的大小,单位rpx(默认40)
|
||||
* @property {Boolean} disabled 是否禁用所有checkbox(默认false)
|
||||
* @property {String Number} icon-size 图标大小,单位rpx(默认20)
|
||||
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
|
||||
* @property {String} width 宽度,需带单位
|
||||
* @property {String} width 宽度,需带单位
|
||||
* @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
|
||||
* @property {Boolean} wrap 是否每个checkbox都换行(默认false)
|
||||
* @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
|
||||
* @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
|
||||
* @example <u-checkbox-group></u-checkbox-group>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-checkbox-group',
|
||||
mixins: [Emitter],
|
||||
props: {
|
||||
// 最多能选中多少个checkbox
|
||||
max: {
|
||||
type: [Number, String],
|
||||
default: 999
|
||||
},
|
||||
// 所有选中项的 name
|
||||
// value: {
|
||||
// default: Array,
|
||||
// default() {
|
||||
// return []
|
||||
// }
|
||||
// },
|
||||
// 是否禁用所有复选框
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 在表单内提交时的标识符
|
||||
name: {
|
||||
type: [Boolean, String],
|
||||
default: ''
|
||||
},
|
||||
// 是否禁止点击提示语选中复选框
|
||||
labelDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 形状,square为方形,circle为原型
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'square'
|
||||
},
|
||||
// 选中状态下的颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#2979ff'
|
||||
},
|
||||
// 组件的整体大小
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 34
|
||||
},
|
||||
// 每个checkbox占u-checkbox-group的宽度
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
// 是否每个checkbox都换行
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 图标的大小,单位rpx
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: 20
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错
|
||||
this.children = [];
|
||||
},
|
||||
methods: {
|
||||
emitEvent() {
|
||||
let values = [];
|
||||
this.children.map(val => {
|
||||
if(val.value) values.push(val.name);
|
||||
})
|
||||
this.$emit('change', values);
|
||||
// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
|
||||
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
|
||||
setTimeout(() => {
|
||||
// 将当前的值发送到 u-form-item 进行校验
|
||||
this.dispatch('u-form-item', 'on-form-change', values);
|
||||
}, 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-checkbox-group {
|
||||
/* #ifndef MP || APP-NVUE */
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,284 @@
|
|||
<template>
|
||||
<view class="u-checkbox" :style="[checkboxStyle]">
|
||||
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
|
||||
<u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
|
||||
</view>
|
||||
<view class="u-checkbox__label" @tap="onClickLabel" :style="{
|
||||
fontSize: $u.addUnit(labelSize)
|
||||
}">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* checkbox 复选框
|
||||
* @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
|
||||
* @tutorial https://www.uviewui.com/components/checkbox.html
|
||||
* @property {String Number} icon-size 图标大小,单位rpx(默认20)
|
||||
* @property {String Number} label-size label字体大小,单位rpx(默认28)
|
||||
* @property {String Number} name checkbox组件的标示符
|
||||
* @property {String} shape 形状,见官网说明(默认circle)
|
||||
* @property {Boolean} disabled 是否禁用
|
||||
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
|
||||
* @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
|
||||
* @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
|
||||
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
|
||||
*/
|
||||
export default {
|
||||
name: "u-checkbox",
|
||||
props: {
|
||||
// checkbox的名称
|
||||
name: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 形状,square为方形,circle为原型
|
||||
shape: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否为选中状态
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: [String, Boolean],
|
||||
default: ''
|
||||
},
|
||||
// 是否禁止点击提示语选中复选框
|
||||
labelDisabled: {
|
||||
type: [String, Boolean],
|
||||
default: ''
|
||||
},
|
||||
// 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图标的大小,单位rpx
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// label的字体大小,rpx单位
|
||||
labelSize: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 组件的整体大小
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
parentDisabled: false,
|
||||
newParams: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
|
||||
this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
|
||||
// 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
|
||||
this.parent && this.parent.children.push(this);
|
||||
},
|
||||
computed: {
|
||||
// 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
|
||||
isDisabled() {
|
||||
return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
|
||||
},
|
||||
// 是否禁用label点击
|
||||
isLabelDisabled() {
|
||||
return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
|
||||
},
|
||||
// 组件尺寸,对应size的值,默认值为34rpx
|
||||
checkboxSize() {
|
||||
return this.size ? this.size : (this.parent ? this.parent.size : 34);
|
||||
},
|
||||
// 组件的勾选图标的尺寸,默认20
|
||||
checkboxIconSize() {
|
||||
return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
|
||||
},
|
||||
// 组件选中激活时的颜色
|
||||
elActiveColor() {
|
||||
return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
|
||||
},
|
||||
// 组件的形状
|
||||
elShape() {
|
||||
return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
|
||||
},
|
||||
iconStyle() {
|
||||
let style = {};
|
||||
// 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
|
||||
if (this.elActiveColor && this.value && !this.isDisabled) {
|
||||
style.borderColor = this.elActiveColor;
|
||||
style.backgroundColor = this.elActiveColor;
|
||||
}
|
||||
style.width = this.$u.addUnit(this.checkboxSize);
|
||||
style.height = this.$u.addUnit(this.checkboxSize);
|
||||
return style;
|
||||
},
|
||||
// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
|
||||
iconColor() {
|
||||
return this.value ? '#ffffff' : 'transparent';
|
||||
},
|
||||
iconClass() {
|
||||
let classes = [];
|
||||
classes.push('u-checkbox__icon-wrap--' + this.elShape);
|
||||
if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
|
||||
if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
|
||||
if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
|
||||
// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
|
||||
return classes.join(' ');
|
||||
},
|
||||
checkboxStyle() {
|
||||
let style = {};
|
||||
if(this.parent && this.parent.width) {
|
||||
style.width = this.parent.width;
|
||||
// #ifdef MP
|
||||
// 各家小程序因为它们特殊的编译结构,使用float布局
|
||||
style.float = 'left';
|
||||
// #endif
|
||||
// #ifndef MP
|
||||
// H5和APP使用flex布局
|
||||
style.flex = `0 0 ${this.parent.width}`;
|
||||
// #endif
|
||||
}
|
||||
if(this.parent && this.parent.wrap) {
|
||||
style.width = '100%';
|
||||
// #ifndef MP
|
||||
// H5和APP使用flex布局,将宽度设置100%,即可自动换行
|
||||
style.flex = '0 0 100%';
|
||||
// #endif
|
||||
}
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClickLabel() {
|
||||
if (!this.isLabelDisabled && !this.isDisabled) {
|
||||
this.setValue();
|
||||
}
|
||||
},
|
||||
toggle() {
|
||||
if (!this.isDisabled) {
|
||||
this.setValue();
|
||||
}
|
||||
},
|
||||
emitEvent() {
|
||||
this.$emit('change', {
|
||||
value: !this.value,
|
||||
name: this.name
|
||||
})
|
||||
// 执行父组件u-checkbox-group的事件方法
|
||||
// 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
|
||||
setTimeout(() => {
|
||||
if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
|
||||
}, 80);
|
||||
},
|
||||
// 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
|
||||
setValue() {
|
||||
// 判断是否超过了可选的最大数量
|
||||
let checkedNum = 0;
|
||||
if(this.parent && this.parent.children) {
|
||||
// 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
|
||||
this.parent.children.map(val => {
|
||||
if (val.value) checkedNum++;
|
||||
})
|
||||
}
|
||||
// 如果原来为选中状态,那么可以取消
|
||||
if (this.value == true) {
|
||||
this.emitEvent();
|
||||
this.$emit('input', !this.value);
|
||||
} else {
|
||||
// 如果超出最多可选项,提示
|
||||
if(this.parent && checkedNum >= this.parent.max) {
|
||||
return this.$u.toast(`最多可选${this.parent.max}项`);
|
||||
}
|
||||
// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
|
||||
this.emitEvent();
|
||||
this.$emit('input', !this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-checkbox {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
line-height: 1.8;
|
||||
|
||||
&__icon-wrap {
|
||||
color: $u-content-color;
|
||||
flex: none;
|
||||
display: -webkit-flex;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
color: transparent;
|
||||
text-align: center;
|
||||
transition-property: color, border-color, background-color;
|
||||
font-size: 20px;
|
||||
border: 1px solid #c8c9cc;
|
||||
transition-duration: 0.2s;
|
||||
|
||||
/* #ifdef MP-TOUTIAO */
|
||||
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
|
||||
&__icon {
|
||||
line-height: 0;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
&--circle {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&--square {
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
color: #fff;
|
||||
background-color: $u-type-primary;
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: #ebedf0;
|
||||
border-color: #c8c9cc;
|
||||
}
|
||||
|
||||
&--disabled--checked {
|
||||
color: #c8c9cc !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
word-wrap: break-word;
|
||||
margin-left: 10rpx;
|
||||
margin-right: 24rpx;
|
||||
color: $u-content-color;
|
||||
font-size: 30rpx;
|
||||
|
||||
&--disabled {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<view
|
||||
class="u-circle-progress"
|
||||
:style="{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px',
|
||||
backgroundColor: bgColor
|
||||
}"
|
||||
>
|
||||
<!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
|
||||
<canvas
|
||||
class="u-canvas-bg"
|
||||
:canvas-id="elBgId"
|
||||
:id="elBgId"
|
||||
:style="{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px'
|
||||
}"
|
||||
></canvas>
|
||||
<canvas
|
||||
class="u-canvas"
|
||||
:canvas-id="elId"
|
||||
:id="elId"
|
||||
:style="{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px'
|
||||
}"
|
||||
></canvas>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* circleProgress 环形进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
|
||||
* @tutorial https://www.uviewui.com/components/circleProgress.html
|
||||
* @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
|
||||
* @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
|
||||
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
|
||||
* @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
|
||||
* @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
|
||||
* @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
|
||||
* @property {String} type 如设置,active-color值将会失效
|
||||
* @property {String} bg-color 整个组件背景颜色,默认为白色
|
||||
* @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-circle-progress',
|
||||
props: {
|
||||
// 圆环进度百分比值
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
// 限制值在0到100之间
|
||||
validator: val => {
|
||||
return val >= 0 && val <= 100;
|
||||
}
|
||||
},
|
||||
// 底部圆环的颜色(灰色的圆环)
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#ececec'
|
||||
},
|
||||
// 圆环激活部分的颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#19be6b'
|
||||
},
|
||||
// 圆环线条的宽度,单位rpx
|
||||
borderWidth: {
|
||||
type: [Number, String],
|
||||
default: 14
|
||||
},
|
||||
// 整个圆形的宽度,单位rpx
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 200
|
||||
},
|
||||
// 整个圆环执行一圈的时间,单位ms
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 1500
|
||||
},
|
||||
// 主题类型
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 整个圆环进度区域的背景色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// #ifdef MP-WEIXIN
|
||||
elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
|
||||
elId: 'uCircleProgressElId',
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
|
||||
elId: this.$u.guid(),
|
||||
// #endif
|
||||
widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
|
||||
borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
|
||||
startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
|
||||
progressContext: null, // 活动圆的canvas上下文
|
||||
newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
|
||||
oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
percent(nVal, oVal = 0) {
|
||||
if (nVal > 100) nVal = 100;
|
||||
if (nVal < 0) oVal = 0;
|
||||
// 此值其实等于this.percent,命名一个新
|
||||
this.newPercent = nVal;
|
||||
this.oldPercent = oVal;
|
||||
setTimeout(() => {
|
||||
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
|
||||
// 将此值减少或者新增到新的百分比值
|
||||
this.drawCircleByProgress(oVal);
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 赋值,用于加载后第一个画圆使用
|
||||
this.newPercent = this.percent;
|
||||
this.oldPercent = 0;
|
||||
},
|
||||
computed: {
|
||||
// 有type主题时,优先起作用
|
||||
circleColor() {
|
||||
if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
|
||||
else return this.activeColor;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
|
||||
setTimeout(() => {
|
||||
this.drawProgressBg();
|
||||
this.drawCircleByProgress(this.oldPercent);
|
||||
}, 50);
|
||||
},
|
||||
methods: {
|
||||
drawProgressBg() {
|
||||
let ctx = uni.createCanvasContext(this.elBgId, this);
|
||||
ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
|
||||
ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
|
||||
ctx.beginPath(); // 开始描绘路径
|
||||
// 设置一个原点(110,110),半径为100的圆的路径到当前路径
|
||||
let radius = this.widthPx / 2;
|
||||
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
|
||||
ctx.stroke(); // 对路径进行描绘
|
||||
ctx.draw();
|
||||
},
|
||||
drawCircleByProgress(progress) {
|
||||
// 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
|
||||
let ctx = this.progressContext;
|
||||
if (!ctx) {
|
||||
ctx = uni.createCanvasContext(this.elId, this);
|
||||
this.progressContext = ctx;
|
||||
}
|
||||
// 表示进度的两端为圆形
|
||||
ctx.setLineCap('round');
|
||||
// 设置线条的宽度和颜色
|
||||
ctx.setLineWidth(this.borderWidthPx);
|
||||
ctx.setStrokeStyle(this.circleColor);
|
||||
// 将总过渡时间除以100,得出每修改百分之一进度所需的时间
|
||||
let time = Math.floor(this.duration / 100);
|
||||
// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
|
||||
// 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
|
||||
let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
|
||||
ctx.beginPath();
|
||||
// 半径为整个canvas宽度的一半
|
||||
let radius = this.widthPx / 2;
|
||||
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
|
||||
ctx.stroke();
|
||||
ctx.draw();
|
||||
// 如果变更后新值大于旧值,意味着增大了百分比
|
||||
if (this.newPercent > this.oldPercent) {
|
||||
// 每次递增百分之一
|
||||
progress++;
|
||||
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
|
||||
if (progress > this.newPercent) return;
|
||||
} else {
|
||||
// 同理于上面
|
||||
progress--;
|
||||
if (progress < this.newPercent) return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
// 定时器,每次操作间隔为time值,为了让进度条有动画效果
|
||||
this.drawCircleByProgress(progress);
|
||||
}, time);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
.u-circle-progress {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-canvas-bg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.u-canvas {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<view class="u-progress" :style="{
|
||||
borderRadius: round ? '100rpx' : 0,
|
||||
height: height + 'rpx',
|
||||
backgroundColor: inactiveColor
|
||||
}">
|
||||
<view :class="[
|
||||
type ? `u-type-${type}-bg` : '',
|
||||
striped ? 'u-striped' : '',
|
||||
striped && stripedActive ? 'u-striped-active' : ''
|
||||
]" class="u-active" :style="[progressStyle]">
|
||||
<slot v-if="$slots.default || $slots.$default" />
|
||||
<block v-else-if="showPercent">
|
||||
{{percent + '%'}}
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* lineProgress 线型进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
|
||||
* @tutorial https://www.uviewui.com/components/lineProgress.html
|
||||
* @property {String Number} percent 进度条百分比值,为数值类型,0-100
|
||||
* @property {Boolean} round 进度条两端是否为半圆(默认true)
|
||||
* @property {String} type 如设置,active-color值将会失效
|
||||
* @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
|
||||
* @property {String} inactive-color 进度条的底色(默认#ececec)
|
||||
* @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
|
||||
* @property {String Number} height 进度条的高度,单位rpx(默认28)
|
||||
* @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
|
||||
* @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
|
||||
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
|
||||
*/
|
||||
export default {
|
||||
name: "u-line-progress",
|
||||
props: {
|
||||
// 两端是否显示半圆形
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 主题颜色
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 激活部分的颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#19be6b'
|
||||
},
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#ececec'
|
||||
},
|
||||
// 进度百分比,数值
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 是否在进度条内部显示百分比的值
|
||||
showPercent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 进度条的高度,单位rpx
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 28
|
||||
},
|
||||
// 是否显示条纹
|
||||
striped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 条纹是否显示活动状态
|
||||
stripedActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
progressStyle() {
|
||||
let style = {};
|
||||
style.width = this.percent + '%';
|
||||
if(this.activeColor) style.backgroundColor = this.activeColor;
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-progress {
|
||||
overflow: hidden;
|
||||
height: 15px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
|
||||
.u-active {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
@include vue-flex;
|
||||
justify-items: flex-end;
|
||||
justify-content: space-around;
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.u-striped {
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
|
||||
background-size: 39px 39px;
|
||||
}
|
||||
|
||||
.u-striped-active {
|
||||
animation: progress-stripes 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-stripes {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 39px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<view class="u-col" :class="[
|
||||
'u-col-' + span
|
||||
]" :style="{
|
||||
padding: `0 ${Number(gutter)/2 + 'rpx'}`,
|
||||
marginLeft: 100 / 12 * offset + '%',
|
||||
flex: `0 0 ${100 / 12 * span}%`,
|
||||
alignItems: uAlignItem,
|
||||
justifyContent: uJustify,
|
||||
textAlign: textAlign
|
||||
}"
|
||||
@tap="click">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* col 布局单元格
|
||||
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
|
||||
* @tutorial https://www.uviewui.com/components/layout.html
|
||||
* @property {String Number} span 栅格占据的列数,总12等分(默认0)
|
||||
* @property {String} text-align 文字水平对齐方式(默认left)
|
||||
* @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
|
||||
* @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
|
||||
*/
|
||||
export default {
|
||||
name: "u-col",
|
||||
props: {
|
||||
// 占父容器宽度的多少等分,总分为12份
|
||||
span: {
|
||||
type: [Number, String],
|
||||
default: 12
|
||||
},
|
||||
// 指定栅格左侧的间隔数(总12栏)
|
||||
offset: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
|
||||
justify: {
|
||||
type: String,
|
||||
default: 'start'
|
||||
},
|
||||
// 垂直对齐方式,可选值为top、center、bottom
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center'
|
||||
},
|
||||
// 文字对齐方式
|
||||
textAlign: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// 是否阻止事件传播
|
||||
stop: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.parent = false;
|
||||
},
|
||||
mounted() {
|
||||
// 获取父组件实例,并赋值给对应的参数
|
||||
this.parent = this.$u.$parent.call(this, 'u-row');
|
||||
if (this.parent) {
|
||||
this.gutter = this.parent.gutter;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uJustify() {
|
||||
if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
|
||||
else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
|
||||
else return this.justify;
|
||||
},
|
||||
uAlignItem() {
|
||||
if (this.align == 'top') return 'flex-start';
|
||||
if (this.align == 'bottom') return 'flex-end';
|
||||
else return this.align;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
this.$emit('click');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-col {
|
||||
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
|
||||
float: left;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-col-0 {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.u-col-1 {
|
||||
width: calc(100%/12);
|
||||
}
|
||||
|
||||
.u-col-2 {
|
||||
width: calc(100%/12 * 2);
|
||||
}
|
||||
|
||||
.u-col-3 {
|
||||
width: calc(100%/12 * 3);
|
||||
}
|
||||
|
||||
.u-col-4 {
|
||||
width: calc(100%/12 * 4);
|
||||
}
|
||||
|
||||
.u-col-5 {
|
||||
width: calc(100%/12 * 5);
|
||||
}
|
||||
|
||||
.u-col-6 {
|
||||
width: calc(100%/12 * 6);
|
||||
}
|
||||
|
||||
.u-col-7 {
|
||||
width: calc(100%/12 * 7);
|
||||
}
|
||||
|
||||
.u-col-8 {
|
||||
width: calc(100%/12 * 8);
|
||||
}
|
||||
|
||||
.u-col-9 {
|
||||
width: calc(100%/12 * 9);
|
||||
}
|
||||
|
||||
.u-col-10 {
|
||||
width: calc(100%/12 * 10);
|
||||
}
|
||||
|
||||
.u-col-11 {
|
||||
width: calc(100%/12 * 11);
|
||||
}
|
||||
|
||||
.u-col-12 {
|
||||
width: calc(100%/12 * 12);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,204 @@
|
|||
<template>
|
||||
<view class="u-collapse-item" :style="[itemStyle]">
|
||||
<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
|
||||
<block v-if="!$slots['title-all']">
|
||||
<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
|
||||
isShow && activeStyle && !arrow ? activeStyle : '']">
|
||||
{{ title }}
|
||||
</view>
|
||||
<slot v-else name="title" />
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
|
||||
class="u-arrow-down-icon" name="arrow-down"></u-icon>
|
||||
</view>
|
||||
</block>
|
||||
<slot v-else name="title-all" />
|
||||
</view>
|
||||
<view class="u-collapse-body" :style="[{
|
||||
height: isShow ? height + 'px' : '0'
|
||||
}]">
|
||||
<view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* collapseItem 手风琴Item
|
||||
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
|
||||
* @tutorial https://www.uviewui.com/components/collapse.html
|
||||
* @property {String} title 面板标题
|
||||
* @property {String Number} index 主要用于事件的回调,标识那个Item被点击
|
||||
* @property {Boolean} disabled 面板是否可以打开或收起(默认false)
|
||||
* @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
|
||||
* @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
|
||||
* @property {String} align 标题的对齐方式(默认left)
|
||||
* @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
|
||||
* @event {Function} change 某个item被打开或者收起时触发
|
||||
* @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
|
||||
*/
|
||||
export default {
|
||||
name: "u-collapse-item",
|
||||
props: {
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 标题的对齐方式
|
||||
align: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// 是否可以点击收起
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// collapse显示与否
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 唯一标识符
|
||||
name: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
//活动样式
|
||||
activeStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 标识当前为第几个
|
||||
index: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
elId: this.$u.guid(),
|
||||
height: 0, // body内容的高度
|
||||
headStyle: {}, // 头部样式,对象形式
|
||||
bodyStyle: {}, // 主体部分样式
|
||||
itemStyle: {}, // 每个item的整体样式
|
||||
arrowColor: '', // 箭头的颜色
|
||||
hoverClass: '', // 头部按下时的效果样式类
|
||||
arrow: true, // 是否显示右侧箭头
|
||||
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
open(val) {
|
||||
this.isShow = val;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.parent = false;
|
||||
// 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
|
||||
this.isShow = this.open;
|
||||
},
|
||||
methods: {
|
||||
// 异步获取内容,或者动态修改了内容时,需要重新初始化
|
||||
init() {
|
||||
this.parent = this.$u.$parent.call(this, 'u-collapse');
|
||||
if(this.parent) {
|
||||
this.nameSync = this.name ? this.name : this.parent.childrens.length;
|
||||
this.parent.childrens.push(this);
|
||||
this.headStyle = this.parent.headStyle;
|
||||
this.bodyStyle = this.parent.bodyStyle;
|
||||
this.arrowColor = this.parent.arrowColor;
|
||||
this.hoverClass = this.parent.hoverClass;
|
||||
this.arrow = this.parent.arrow;
|
||||
this.itemStyle = this.parent.itemStyle;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.queryRect();
|
||||
});
|
||||
},
|
||||
// 点击collapsehead头部
|
||||
headClick() {
|
||||
if (this.disabled) return;
|
||||
if (this.parent && this.parent.accordion == true) {
|
||||
this.parent.childrens.map(val => {
|
||||
// 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
|
||||
if (this != val) {
|
||||
val.isShow = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.isShow = !this.isShow;
|
||||
// 触发本组件的事件
|
||||
this.$emit('change', {
|
||||
index: this.index,
|
||||
show: this.isShow
|
||||
})
|
||||
// 只有在打开时才发出事件
|
||||
if (this.isShow) this.parent && this.parent.onChange();
|
||||
this.$forceUpdate();
|
||||
},
|
||||
// 查询内容高度
|
||||
queryRect() {
|
||||
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
|
||||
// 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
|
||||
this.$uGetRect('#' + this.elId).then(res => {
|
||||
this.height = res.height;
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-collapse-head {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: $u-main-color;
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
padding: 24rpx 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.u-collapse-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-arrow-down-icon {
|
||||
transition: all 0.3s;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 14rpx;
|
||||
}
|
||||
|
||||
.u-arrow-down-icon-active {
|
||||
transform: rotate(180deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.u-collapse-body {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.u-collapse-content {
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: $u-tips-color;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<view class="u-collapse">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* collapse 手风琴
|
||||
* @description 通过折叠面板收纳内容区域
|
||||
* @tutorial https://www.uviewui.com/components/collapse.html
|
||||
* @property {Boolean} accordion 是否手风琴模式(默认true)
|
||||
* @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
|
||||
* @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
|
||||
* @property {Object} head-style 标题自定义样式,对象形式
|
||||
* @property {Object} body-style 主体自定义样式,对象形式
|
||||
* @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
|
||||
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
|
||||
* @example <u-collapse></u-collapse>
|
||||
*/
|
||||
export default {
|
||||
name:"u-collapse",
|
||||
props: {
|
||||
// 是否手风琴模式
|
||||
accordion: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 头部的样式
|
||||
headStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 主体的样式
|
||||
bodyStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 每一个item的样式
|
||||
itemStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 是否显示右侧的箭头
|
||||
arrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 箭头的颜色
|
||||
arrowColor: {
|
||||
type: String,
|
||||
default: '#909399'
|
||||
},
|
||||
// 标题部分按压时的样式类,"none"为无效果
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: 'u-hover-class'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.childrens = []
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
|
||||
init() {
|
||||
this.childrens.forEach((vm, index) => {
|
||||
vm.init();
|
||||
})
|
||||
},
|
||||
// collapse item被点击,由collapse item调用父组件方法
|
||||
onChange() {
|
||||
let activeItem = [];
|
||||
this.childrens.forEach((vm, index) => {
|
||||
if (vm.isShow) {
|
||||
activeItem.push(vm.nameSync);
|
||||
}
|
||||
})
|
||||
// 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
|
||||
if (this.accordion) activeItem = activeItem.join('');
|
||||
this.$emit('change', activeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
</style>
|
|
@ -0,0 +1,237 @@
|
|||
<template>
|
||||
<view
|
||||
class="u-notice-bar"
|
||||
:style="{
|
||||
background: computeBgColor,
|
||||
padding: padding
|
||||
}"
|
||||
:class="[
|
||||
type ? `u-type-${type}-light-bg` : ''
|
||||
]"
|
||||
>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
|
||||
</view>
|
||||
<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
|
||||
<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
|
||||
<view
|
||||
class="u-news-item u-line-1"
|
||||
:style="[textStyle]"
|
||||
@tap="click(index)"
|
||||
:class="['u-type-' + type]"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
|
||||
<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
// 显示的内容,数组
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 显示的主题,success|error|primary|info|warning
|
||||
type: {
|
||||
type: String,
|
||||
default: 'warning'
|
||||
},
|
||||
// 是否显示左侧的音量图标
|
||||
volumeIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示右侧的右箭头图标
|
||||
moreIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示右侧的关闭图标
|
||||
closeIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否自动播放
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 文字颜色,各图标也会使用文字颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 滚动方向,row-水平滚动,column-垂直滚动
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'row'
|
||||
},
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 字体大小,单位rpx
|
||||
fontSize: {
|
||||
type: [Number, String],
|
||||
default: 26
|
||||
},
|
||||
// 滚动一个周期的时间长,单位ms
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 2000
|
||||
},
|
||||
// 音量喇叭的大小
|
||||
volumeSize: {
|
||||
type: [Number, String],
|
||||
default: 34
|
||||
},
|
||||
// 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
|
||||
speed: {
|
||||
type: Number,
|
||||
default: 160
|
||||
},
|
||||
// 水平滚动时,是否采用衔接形式滚动
|
||||
isCircular: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 滚动方向,horizontal-水平滚动,vertical-垂直滚动
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'horizontal'
|
||||
},
|
||||
// 播放状态,play-播放,paused-暂停
|
||||
playState: {
|
||||
type: String,
|
||||
default: 'play'
|
||||
},
|
||||
// 是否禁止用手滑动切换
|
||||
// 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
|
||||
disableTouch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 通知的边距
|
||||
padding: {
|
||||
type: [Number, String],
|
||||
default: '18rpx 24rpx'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 计算字体颜色,如果没有自定义的,就用uview主题颜色
|
||||
computeColor() {
|
||||
if (this.color) return this.color;
|
||||
// 如果是无主题,就默认使用content-color
|
||||
else if(this.type == 'none') return '#606266';
|
||||
else return this.type;
|
||||
},
|
||||
// 文字内容的样式
|
||||
textStyle() {
|
||||
let style = {};
|
||||
if (this.color) style.color = this.color;
|
||||
else if(this.type == 'none') style.color = '#606266';
|
||||
style.fontSize = this.fontSize + 'rpx';
|
||||
return style;
|
||||
},
|
||||
// 垂直或者水平滚动
|
||||
vertical() {
|
||||
if(this.mode == 'horizontal') return false;
|
||||
else return true;
|
||||
},
|
||||
// 计算背景颜色
|
||||
computeBgColor() {
|
||||
if (this.bgColor) return this.bgColor;
|
||||
else if(this.type == 'none') return 'transparent';
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// animation: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 点击通告栏
|
||||
click(index) {
|
||||
this.$emit('click', index);
|
||||
},
|
||||
// 点击关闭按钮
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
// 点击更多箭头按钮
|
||||
getMore() {
|
||||
this.$emit('getMore');
|
||||
},
|
||||
change(e) {
|
||||
let index = e.detail.current;
|
||||
if(index == this.list.length - 1) {
|
||||
this.$emit('end');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-notice-bar {
|
||||
width: 100%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 18rpx 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-swiper {
|
||||
font-size: 26rpx;
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.u-swiper-item {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-news-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-right-icon {
|
||||
margin-left: 12rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-left-icon {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,318 @@
|
|||
<template>
|
||||
<view class="u-countdown">
|
||||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))">
|
||||
<view class="u-countdown-time" :style="[letterStyle]">
|
||||
{{ d }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
|
||||
v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
|
||||
>
|
||||
{{ separator == 'colon' ? ':' : '天' }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
|
||||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
|
||||
{{ h }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
|
||||
v-if="showHours"
|
||||
>
|
||||
{{ separator == 'colon' ? ':' : '时' }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
|
||||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
|
||||
{{ i }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
|
||||
v-if="showMinutes"
|
||||
>
|
||||
{{ separator == 'colon' ? ':' : '分' }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
|
||||
<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
|
||||
{{ s }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
|
||||
v-if="showSeconds && separator == 'zh'"
|
||||
>
|
||||
秒
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* countDown 倒计时
|
||||
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
|
||||
* @tutorial https://www.uviewui.com/components/countDown.html
|
||||
* @property {String Number} timestamp 倒计时,单位为秒
|
||||
* @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
|
||||
* @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
|
||||
* @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30)
|
||||
* @property {String} separator-color 分隔符的颜色(默认#303133)
|
||||
* @property {String Number} font-size 倒计时字体大小,单位rpx(默认30)
|
||||
* @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
|
||||
* @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
|
||||
* @property {String} border-color 数字边框的颜色(默认#303133)
|
||||
* @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff)
|
||||
* @property {String} color 倒计时数字的颜色(默认#303133)
|
||||
* @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
|
||||
* @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
|
||||
* @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
|
||||
* @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
|
||||
* @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
|
||||
* @event {Function} end 倒计时结束
|
||||
* @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
|
||||
* @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-count-down',
|
||||
props: {
|
||||
// 倒计时的时间,秒为单位
|
||||
timestamp: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 是否自动开始倒计时
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒"
|
||||
separator: {
|
||||
type: String,
|
||||
default: 'colon'
|
||||
},
|
||||
// 分隔符的大小,单位rpx
|
||||
separatorSize: {
|
||||
type: [Number, String],
|
||||
default: 30
|
||||
},
|
||||
// 分隔符颜色
|
||||
separatorColor: {
|
||||
type: String,
|
||||
default: "#303133"
|
||||
},
|
||||
// 字体颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#303133'
|
||||
},
|
||||
// 字体大小,单位rpx
|
||||
fontSize: {
|
||||
type: [Number, String],
|
||||
default: 30
|
||||
},
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
// 数字框高度,单位rpx
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 'auto'
|
||||
},
|
||||
// 是否显示数字框
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 边框颜色
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: '#303133'
|
||||
},
|
||||
// 是否显示秒
|
||||
showSeconds: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示分钟
|
||||
showMinutes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示小时
|
||||
showHours: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示“天”
|
||||
showDays: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 当"天"的部分为0时,不显示
|
||||
hideZeroDay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听时间戳的变化
|
||||
timestamp(newVal, oldVal) {
|
||||
// 如果倒计时间发生变化,清除定时器,重新开始倒计时
|
||||
this.clearTimer();
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
d: '00', // 天的默认值
|
||||
h: '00', // 小时的默认值
|
||||
i: '00', // 分钟的默认值
|
||||
s: '00', // 秒的默认值
|
||||
timer: null ,// 定时器
|
||||
seconds: 0, // 记录不停倒计过程中变化的秒数
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 倒计时item的样式,item为分别的时分秒部分的数字
|
||||
itemStyle() {
|
||||
let style = {};
|
||||
if(this.height) {
|
||||
style.height = this.height + 'rpx';
|
||||
style.width = this.height + 'rpx';
|
||||
}
|
||||
if(this.showBorder) {
|
||||
style.borderStyle = 'solid';
|
||||
style.borderColor = this.borderColor;
|
||||
style.borderWidth = '1px';
|
||||
}
|
||||
if(this.bgColor) {
|
||||
style.backgroundColor = this.bgColor;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
// 倒计时数字的样式
|
||||
letterStyle() {
|
||||
let style = {};
|
||||
if(this.fontSize) style.fontSize = this.fontSize + 'rpx';
|
||||
if(this.color) style.color = this.color;
|
||||
return style;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 如果自动倒计时
|
||||
this.autoplay && this.timestamp && this.start();
|
||||
},
|
||||
methods: {
|
||||
// 倒计时
|
||||
start() {
|
||||
// 避免可能出现的倒计时重叠情况
|
||||
this.clearTimer();
|
||||
if (this.timestamp <= 0) return;
|
||||
this.seconds = Number(this.timestamp);
|
||||
this.formatTime(this.seconds);
|
||||
this.timer = setInterval(() => {
|
||||
this.seconds--;
|
||||
// 发出change事件
|
||||
this.$emit('change', this.seconds);
|
||||
if (this.seconds < 0) {
|
||||
return this.end();
|
||||
}
|
||||
this.formatTime(this.seconds);
|
||||
}, 1000);
|
||||
},
|
||||
// 格式化时间
|
||||
formatTime(seconds) {
|
||||
// 小于等于0的话,结束倒计时
|
||||
seconds <= 0 && this.end();
|
||||
let [day, hour, minute, second] = [0, 0, 0, 0];
|
||||
day = Math.floor(seconds / (60 * 60 * 24));
|
||||
// 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
|
||||
// hour为给后面计算秒和分等用的(基于显示天的前提下计算)
|
||||
hour = Math.floor(seconds / (60 * 60)) - day * 24;
|
||||
// showHour为需要显示的小时
|
||||
let showHour = null;
|
||||
if(this.showDays) {
|
||||
showHour = hour;
|
||||
} else {
|
||||
// 如果不显示天数,将“天”部分的时间折算到小时中去
|
||||
showHour = Math.floor(seconds / (60 * 60));
|
||||
}
|
||||
minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
|
||||
second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
|
||||
// 如果小于10,在前面补上一个"0"
|
||||
showHour = showHour < 10 ? '0' + showHour : showHour;
|
||||
minute = minute < 10 ? '0' + minute : minute;
|
||||
second = second < 10 ? '0' + second : second;
|
||||
day = day < 10 ? '0' + day : day;
|
||||
this.d = day;
|
||||
this.h = showHour;
|
||||
this.i = minute;
|
||||
this.s = second;
|
||||
},
|
||||
// 停止倒计时
|
||||
end() {
|
||||
this.clearTimer();
|
||||
this.$emit('end', {});
|
||||
},
|
||||
// 清除定时器
|
||||
clearTimer() {
|
||||
if(this.timer) {
|
||||
// 清除定时器
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-countdown {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-countdown-item {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rpx;
|
||||
border-radius: 6rpx;
|
||||
white-space: nowrap;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.u-countdown-time {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.u-countdown-colon {
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
padding: 0 5rpx;
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.u-countdown-scale {
|
||||
transform: scale(0.9);
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<view
|
||||
class="u-count-num"
|
||||
:style="{
|
||||
fontSize: fontSize + 'rpx',
|
||||
fontWeight: bold ? 'bold' : 'normal',
|
||||
color: color
|
||||
}"
|
||||
>
|
||||
{{ displayValue }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* countTo 数字滚动
|
||||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
|
||||
* @tutorial https://www.uviewui.com/components/countTo.html
|
||||
* @property {String Number} start-val 开始值
|
||||
* @property {String Number} end-val 结束值
|
||||
* @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
|
||||
* @property {Boolean} autoplay 是否自动开始滚动(默认true)
|
||||
* @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
|
||||
* @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
|
||||
* @property {String} separator 千位分隔符,见官网说明
|
||||
* @property {String} color 字体颜色(默认#303133)
|
||||
* @property {String Number} font-size 字体大小,单位rpx(默认50)
|
||||
* @property {Boolean} bold 字体是否加粗(默认false)
|
||||
* @event {Function} end 数值滚动到目标值时触发
|
||||
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-count-to',
|
||||
props: {
|
||||
// 开始的数值,默认从0增长到某一个数
|
||||
startVal: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 要滚动的目标数值,必须
|
||||
endVal: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
// 滚动到目标数值的动画持续时间,单位为毫秒(ms)
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 2000
|
||||
},
|
||||
// 设置数值后是否自动开始滚动
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 要显示的小数位数
|
||||
decimals: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 是否在即将到达目标数值的时候,使用缓慢滚动的效果
|
||||
useEasing: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 十进制分割
|
||||
decimal: {
|
||||
type: [Number, String],
|
||||
default: '.'
|
||||
},
|
||||
// 字体颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#303133'
|
||||
},
|
||||
// 字体大小
|
||||
fontSize: {
|
||||
type: [Number, String],
|
||||
default: 50
|
||||
},
|
||||
// 是否加粗字体
|
||||
bold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 千位分隔符,类似金额的分割(¥23,321.05中的",")
|
||||
separator: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localStartVal: this.startVal,
|
||||
displayValue: this.formatNumber(this.startVal),
|
||||
printVal: null,
|
||||
paused: false, // 是否暂停
|
||||
localDuration: Number(this.duration),
|
||||
startTime: null, // 开始的时间
|
||||
timestamp: null, // 时间戳
|
||||
remaining: null, // 停留的时间
|
||||
rAF: null,
|
||||
lastTime: 0 // 上一次的时间
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
countDown() {
|
||||
return this.startVal > this.endVal;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
startVal() {
|
||||
this.autoplay && this.start();
|
||||
},
|
||||
endVal() {
|
||||
this.autoplay && this.start();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.autoplay && this.start();
|
||||
},
|
||||
methods: {
|
||||
easingFn(t, b, c, d) {
|
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
|
||||
},
|
||||
requestAnimationFrame(callback) {
|
||||
const currTime = new Date().getTime();
|
||||
// 为了使setTimteout的尽可能的接近每秒60帧的效果
|
||||
const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
|
||||
const id = setTimeout(() => {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
this.lastTime = currTime + timeToCall;
|
||||
return id;
|
||||
},
|
||||
|
||||
cancelAnimationFrame(id) {
|
||||
clearTimeout(id);
|
||||
},
|
||||
// 开始滚动数字
|
||||
start() {
|
||||
this.localStartVal = this.startVal;
|
||||
this.startTime = null;
|
||||
this.localDuration = this.duration;
|
||||
this.paused = false;
|
||||
this.rAF = this.requestAnimationFrame(this.count);
|
||||
},
|
||||
// 暂定状态,重新再开始滚动;或者滚动状态下,暂停
|
||||
reStart() {
|
||||
if (this.paused) {
|
||||
this.resume();
|
||||
this.paused = false;
|
||||
} else {
|
||||
this.stop();
|
||||
this.paused = true;
|
||||
}
|
||||
},
|
||||
// 暂停
|
||||
stop() {
|
||||
this.cancelAnimationFrame(this.rAF);
|
||||
},
|
||||
// 重新开始(暂停的情况下)
|
||||
resume() {
|
||||
this.startTime = null;
|
||||
this.localDuration = this.remaining;
|
||||
this.localStartVal = this.printVal;
|
||||
this.requestAnimationFrame(this.count);
|
||||
},
|
||||
// 重置
|
||||
reset() {
|
||||
this.startTime = null;
|
||||
this.cancelAnimationFrame(this.rAF);
|
||||
this.displayValue = this.formatNumber(this.startVal);
|
||||
},
|
||||
count(timestamp) {
|
||||
if (!this.startTime) this.startTime = timestamp;
|
||||
this.timestamp = timestamp;
|
||||
const progress = timestamp - this.startTime;
|
||||
this.remaining = this.localDuration - progress;
|
||||
if (this.useEasing) {
|
||||
if (this.countDown) {
|
||||
this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
|
||||
} else {
|
||||
this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
|
||||
}
|
||||
} else {
|
||||
if (this.countDown) {
|
||||
this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
|
||||
} else {
|
||||
this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
|
||||
}
|
||||
}
|
||||
if (this.countDown) {
|
||||
this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
|
||||
} else {
|
||||
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
|
||||
}
|
||||
this.displayValue = this.formatNumber(this.printVal);
|
||||
if (progress < this.localDuration) {
|
||||
this.rAF = this.requestAnimationFrame(this.count);
|
||||
} else {
|
||||
this.$emit('end');
|
||||
}
|
||||
},
|
||||
// 判断是否数字
|
||||
isNumber(val) {
|
||||
return !isNaN(parseFloat(val));
|
||||
},
|
||||
formatNumber(num) {
|
||||
// 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
|
||||
num = Number(num);
|
||||
num = num.toFixed(Number(this.decimals));
|
||||
num += '';
|
||||
const x = num.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? this.decimal + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (this.separator && !this.isNumber(this.separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + this.separator + '$2');
|
||||
}
|
||||
}
|
||||
return x1 + x2;
|
||||
},
|
||||
destroyed() {
|
||||
this.cancelAnimationFrame(this.rAF);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-count-num {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<view class="u-divider" :style="{
|
||||
height: height == 'auto' ? 'auto' : height + 'rpx',
|
||||
backgroundColor: bgColor,
|
||||
marginBottom: marginBottom + 'rpx',
|
||||
marginTop: marginTop + 'rpx'
|
||||
}" @tap="click">
|
||||
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
|
||||
<view v-if="useSlot" class="u-divider-text" :style="{
|
||||
color: color,
|
||||
fontSize: fontSize + 'rpx'
|
||||
}"><slot /></view>
|
||||
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* divider 分割线
|
||||
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
|
||||
* @tutorial https://www.uviewui.com/components/divider.html
|
||||
* @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
|
||||
* @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
|
||||
* @property {String} color 文字颜色(默认#909399)
|
||||
* @property {String Number} fontSize 字体大小,单位rpx(默认26)
|
||||
* @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
|
||||
* @property {String Number} height 整个divider的高度,单位rpx(默认40)
|
||||
* @property {String} type 将线条设置主题色(默认primary)
|
||||
* @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
|
||||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
|
||||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
|
||||
* @event {Function} click divider组件被点击时触发
|
||||
* @example <u-divider color="#fa3534">长河落日圆</u-divider>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-divider',
|
||||
props: {
|
||||
// 单一边divider横线的宽度(数值),单位rpx。或者百分比
|
||||
halfWidth: {
|
||||
type: [Number, String],
|
||||
default: 150
|
||||
},
|
||||
// divider横线的颜色,如设置,
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: '#dcdfe6'
|
||||
},
|
||||
// 主题色,可以是primary|info|success|warning|error之一值
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
// 文字颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#909399'
|
||||
},
|
||||
// 文字大小,单位rpx
|
||||
fontSize: {
|
||||
type: [Number, String],
|
||||
default: 26
|
||||
},
|
||||
// 整个divider的背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
// 整个divider的高度单位rpx
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 'auto'
|
||||
},
|
||||
// 上边距
|
||||
marginTop: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 下边距
|
||||
marginBottom: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
|
||||
useSlot: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lineStyle() {
|
||||
let style = {};
|
||||
if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
|
||||
else style.width = this.halfWidth + 'rpx';
|
||||
// borderColor优先级高于type值
|
||||
if(this.borderColor) style.borderColor = this.borderColor;
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
.u-divider {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.u-divider-line {
|
||||
border-bottom: 1px solid $u-border-color;
|
||||
transform: scale(1, 0.5);
|
||||
transform-origin: center;
|
||||
|
||||
&--bordercolor--primary {
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--bordercolor--success {
|
||||
border-color: $u-type-success;
|
||||
}
|
||||
|
||||
&--bordercolor--error {
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--bordercolor--info {
|
||||
border-color: $u-type-info;
|
||||
}
|
||||
|
||||
&--bordercolor--warning {
|
||||
border-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
|
||||
.u-divider-text {
|
||||
white-space: nowrap;
|
||||
padding: 0 16rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
|
||||
<block v-if="!$slots.default && !$slots.$default">
|
||||
<scroll-view scroll-y="true" :style="{
|
||||
height: $u.addUnit(height)
|
||||
}">
|
||||
<view class="u-dropdown-item__options">
|
||||
<u-cell-group>
|
||||
<u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
|
||||
:key="index" :title-style="{
|
||||
color: value == item.value ? activeColor : inactiveColor
|
||||
}">
|
||||
<u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
|
||||
</u-cell-item>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</block>
|
||||
<slot v-else />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* dropdown-item 下拉菜单
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
|
||||
* @tutorial http://uviewui.com/components/dropdown.html
|
||||
* @property {String | Number} v-model 双向绑定选项卡选择值
|
||||
* @property {String} title 菜单项标题
|
||||
* @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
|
||||
* @property {Boolean} disabled 是否禁用此选项卡(默认false)
|
||||
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
|
||||
* @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
|
||||
* @example <u-dropdown-item title="标题"></u-dropdown-item>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-dropdown-item',
|
||||
props: {
|
||||
// 当前选中项的value值
|
||||
value: {
|
||||
type: [Number, String, Array],
|
||||
default: ''
|
||||
},
|
||||
// 菜单项标题
|
||||
title: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 选项数据,如果传入了默认slot,此参数无效
|
||||
options: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// 是否禁用此菜单项
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 下拉弹窗的高度
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 'auto'
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false, // 当前项是否处于展开状态
|
||||
activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色
|
||||
inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
|
||||
propsChange() {
|
||||
return `${this.title}-${this.disabled}`;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
propsChange(n) {
|
||||
// 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
|
||||
// 将所有子组件数据重新整理一遍
|
||||
if (this.parent) this.parent.init();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 父组件的实例
|
||||
this.parent = false;
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
// 获取父组件u-dropdown
|
||||
let parent = this.$u.$parent.call(this, 'u-dropdown');
|
||||
if (parent) {
|
||||
this.parent = parent;
|
||||
// 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
|
||||
this.activeColor = parent.activeColor;
|
||||
this.inactiveColor = parent.inactiveColor;
|
||||
// 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
|
||||
// push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
|
||||
let exist = parent.children.find(val => {
|
||||
return this === val;
|
||||
})
|
||||
if (!exist) parent.children.push(this);
|
||||
if (parent.children.length == 1) this.active = true;
|
||||
// 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
|
||||
parent.menuList.push({
|
||||
title: this.title,
|
||||
disabled: this.disabled
|
||||
});
|
||||
}
|
||||
},
|
||||
// cell被点击
|
||||
cellClick(value) {
|
||||
// 修改通过v-model绑定的值
|
||||
this.$emit('input', value);
|
||||
// 通知父组件(u-dropdown)收起菜单
|
||||
this.parent.close();
|
||||
// 发出事件,抛出当前勾选项的value
|
||||
this.$emit('change', value);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
</style>
|
|
@ -0,0 +1,298 @@
|
|||
<template>
|
||||
<view class="u-dropdown">
|
||||
<view class="u-dropdown__menu" :style="{
|
||||
height: $u.addUnit(height)
|
||||
}" :class="{
|
||||
'u-border-bottom': borderBottom
|
||||
}">
|
||||
<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
|
||||
<view class="u-flex">
|
||||
<text class="u-dropdown__menu__item__text" :style="{
|
||||
color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
|
||||
fontSize: $u.addUnit(titleSize)
|
||||
}">{{item.title}}</text>
|
||||
<view class="u-dropdown__menu__item__arrow" :class="{
|
||||
'u-dropdown__menu__item__arrow--rotate': index === current
|
||||
}">
|
||||
<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-dropdown__content" :style="[contentStyle, {
|
||||
transition: `opacity ${duration / 1000}s linear`,
|
||||
top: $u.addUnit(height),
|
||||
height: contentHeight + 'px'
|
||||
}]"
|
||||
@tap="maskClick" @touchmove.stop.prevent>
|
||||
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="u-dropdown__content__mask"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* dropdown 下拉菜单
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
|
||||
* @tutorial http://uviewui.com/components/dropdown.html
|
||||
* @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
|
||||
* @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
|
||||
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
|
||||
* @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
|
||||
* @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
|
||||
* @property {String | Number} height 标题菜单的高度,单位任意(默认80)
|
||||
* @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
|
||||
* @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
|
||||
* @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
|
||||
* @event {Function} open 下拉菜单被打开时触发
|
||||
* @event {Function} close 下拉菜单被关闭时触发
|
||||
* @example <u-dropdown></u-dropdown>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-dropdown',
|
||||
props: {
|
||||
// 菜单标题和选项的激活态颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#2979ff'
|
||||
},
|
||||
// 菜单标题和选项的未激活态颜色
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#606266'
|
||||
},
|
||||
// 点击遮罩是否关闭菜单
|
||||
closeOnClickMask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 点击当前激活项标题是否关闭菜单
|
||||
closeOnClickSelf: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 过渡时间
|
||||
duration: {
|
||||
type: [Number, String],
|
||||
default: 300
|
||||
},
|
||||
// 标题菜单的高度,单位任意,数值默认为rpx单位
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 80
|
||||
},
|
||||
// 是否显示下边框
|
||||
borderBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题的字体大小
|
||||
titleSize: {
|
||||
type: [Number, String],
|
||||
default: 28
|
||||
},
|
||||
// 下拉出来的内容部分的圆角值
|
||||
borderRadius: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// 菜单右侧的icon图标
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: 'arrow-down'
|
||||
},
|
||||
// 菜单右侧图标的大小
|
||||
menuIconSize: {
|
||||
type: [Number, String],
|
||||
default: 26
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDropdown: true, // 是否打开下来菜单,
|
||||
menuList: [], // 显示的菜单
|
||||
active: false, // 下拉菜单的状态
|
||||
// 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
|
||||
// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
|
||||
current: 99999,
|
||||
// 外层内容的样式,初始时处于底层,且透明
|
||||
contentStyle: {
|
||||
zIndex: -1,
|
||||
opacity: 0
|
||||
},
|
||||
// 让某个菜单保持高亮的状态
|
||||
highlightIndex: 99999,
|
||||
contentHeight: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 下拉出来部分的样式
|
||||
popupStyle() {
|
||||
let style = {};
|
||||
// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
|
||||
style.transform = `translateY(${this.active ? 0 : '-100%'})`
|
||||
style['transition-duration'] = this.duration / 1000 + 's';
|
||||
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
|
||||
return style;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
|
||||
this.children = [];
|
||||
},
|
||||
mounted() {
|
||||
this.getContentHeight();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
// 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
|
||||
// 以保证数据的正确性
|
||||
this.menuList = [];
|
||||
this.children.map(child => {
|
||||
child.init();
|
||||
})
|
||||
},
|
||||
// 点击菜单
|
||||
menuClick(index) {
|
||||
// 判断是否被禁用
|
||||
if (this.menuList[index].disabled) return;
|
||||
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
|
||||
if (index === this.current && this.closeOnClickSelf) {
|
||||
this.close();
|
||||
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
|
||||
setTimeout(() => {
|
||||
this.children[index].active = false;
|
||||
}, this.duration)
|
||||
return;
|
||||
}
|
||||
this.open(index);
|
||||
},
|
||||
// 打开下拉菜单
|
||||
open(index) {
|
||||
// 重置高亮索引,否则会造成多个菜单同时高亮
|
||||
// this.highlightIndex = 9999;
|
||||
// 展开时,设置下拉内容的样式
|
||||
this.contentStyle = {
|
||||
zIndex: 11,
|
||||
}
|
||||
// 标记展开状态以及当前展开项的索引
|
||||
this.active = true;
|
||||
this.current = index;
|
||||
// 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
|
||||
// 之所以不是因display: none,是因为nvue没有display这个属性
|
||||
this.children.map((val, idx) => {
|
||||
val.active = index == idx ? true : false;
|
||||
})
|
||||
this.$emit('open', this.current);
|
||||
},
|
||||
// 设置下拉菜单处于收起状态
|
||||
close() {
|
||||
this.$emit('close', this.current);
|
||||
// 设置为收起状态,同时current归位,设置为空字符串
|
||||
this.active = false;
|
||||
this.current = 99999;
|
||||
// 下拉内容的样式进行调整,不透明度设置为0
|
||||
this.contentStyle = {
|
||||
zIndex: -1,
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
// 点击遮罩
|
||||
maskClick() {
|
||||
// 如果不允许点击遮罩,直接返回
|
||||
if (!this.closeOnClickMask) return;
|
||||
this.close();
|
||||
},
|
||||
// 外部手动设置某个菜单高亮
|
||||
highlight(index = undefined) {
|
||||
this.highlightIndex = index !== undefined ? index : 99999;
|
||||
},
|
||||
// 获取下拉菜单内容的高度
|
||||
getContentHeight() {
|
||||
// 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
|
||||
// 才能让遮罩占满菜单一下,直到屏幕底部的高度
|
||||
// this.$u.sys()为uView封装的获取设备信息的方法
|
||||
let windowHeight = this.$u.sys().windowHeight;
|
||||
this.$uGetRect('.u-dropdown__menu').then(res => {
|
||||
// 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
|
||||
// H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
|
||||
// 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
|
||||
// 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
|
||||
this.contentHeight = windowHeight - res.bottom;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-dropdown {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&__menu {
|
||||
@include vue-flex;
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
height: 80rpx;
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: $u-content-color;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
margin-left: 6rpx;
|
||||
transition: transform .3s;
|
||||
align-items: center;
|
||||
@include vue-flex;
|
||||
|
||||
&--rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
background: rgba(0, 0, 0, .3);
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__popup {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: all 0.3s;
|
||||
transform: translate3D(0, -100%, 0);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<view class="u-empty" v-if="show" :style="{
|
||||
marginTop: marginTop + 'rpx'
|
||||
}">
|
||||
<u-icon
|
||||
:name="src ? src : 'empty-' + mode"
|
||||
:custom-style="iconStyle"
|
||||
:label="text ? text : icons[mode]"
|
||||
label-pos="bottom"
|
||||
:label-color="color"
|
||||
:label-size="fontSize"
|
||||
:size="iconSize"
|
||||
:color="iconColor"
|
||||
margin-top="14"
|
||||
></u-icon>
|
||||
<view class="u-slot-wrap">
|
||||
<slot name="bottom"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* empty 内容为空
|
||||
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
|
||||
* @tutorial https://www.uviewui.com/components/empty.html
|
||||
* @property {String} color 文字颜色(默认#c0c4cc)
|
||||
* @property {String} text 文字提示(默认“无内容”)
|
||||
* @property {String} src 自定义图标路径,如定义,mode参数会失效
|
||||
* @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
|
||||
* @property {String} mode 内置的图标,见官网说明(默认data)
|
||||
* @property {String Number} img-width 图标的宽度,单位rpx(默认240)
|
||||
* @property {String} img-height 图标的高度,单位rpx(默认auto)
|
||||
* @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
|
||||
* @property {Boolean} show 是否显示组件(默认true)
|
||||
* @event {Function} click 点击组件时触发
|
||||
* @event {Function} close 点击关闭按钮时触发
|
||||
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
|
||||
*/
|
||||
export default {
|
||||
name: "u-empty",
|
||||
props: {
|
||||
// 图标路径
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 提示文字
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 文字颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#c0c4cc'
|
||||
},
|
||||
// 图标的颜色
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#c0c4cc'
|
||||
},
|
||||
// 图标的大小
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: 120
|
||||
},
|
||||
// 文字大小,单位rpx
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: 26
|
||||
},
|
||||
// 选择预置的图标类型
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'data'
|
||||
},
|
||||
// 图标宽度,单位rpx
|
||||
imgWidth: {
|
||||
type: [String, Number],
|
||||
default: 120
|
||||
},
|
||||
// 图标高度,单位rpx
|
||||
imgHeight: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
// 是否显示组件
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 组件距离上一个元素之间的距离
|
||||
marginTop: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
icons: {
|
||||
car: '购物车为空',
|
||||
page: '页面不存在',
|
||||
search: '没有搜索结果',
|
||||
address: '没有收货地址',
|
||||
wifi: '没有WiFi',
|
||||
order: '订单为空',
|
||||
coupon: '没有优惠券',
|
||||
favor: '暂无收藏',
|
||||
permission: '无权限',
|
||||
history: '无历史记录',
|
||||
news: '无新闻列表',
|
||||
message: '消息列表为空',
|
||||
list: '列表为空',
|
||||
data: '数据为空'
|
||||
},
|
||||
// icons: [{
|
||||
// icon: 'car',
|
||||
// text: '购物车为空'
|
||||
// },{
|
||||
// icon: 'page',
|
||||
// text: '页面不存在'
|
||||
// },{
|
||||
// icon: 'search',
|
||||
// text: '没有搜索结果'
|
||||
// },{
|
||||
// icon: 'address',
|
||||
// text: '没有收货地址'
|
||||
// },{
|
||||
// icon: 'wifi',
|
||||
// text: '没有WiFi'
|
||||
// },{
|
||||
// icon: 'order',
|
||||
// text: '订单为空'
|
||||
// },{
|
||||
// icon: 'coupon',
|
||||
// text: '没有优惠券'
|
||||
// },{
|
||||
// icon: 'favor',
|
||||
// text: '暂无收藏'
|
||||
// },{
|
||||
// icon: 'permission',
|
||||
// text: '无权限'
|
||||
// },{
|
||||
// icon: 'history',
|
||||
// text: '无历史记录'
|
||||
// },{
|
||||
// icon: 'news',
|
||||
// text: '无新闻列表'
|
||||
// },{
|
||||
// icon: 'message',
|
||||
// text: '消息列表为空'
|
||||
// },{
|
||||
// icon: 'list',
|
||||
// text: '列表为空'
|
||||
// },{
|
||||
// icon: 'data',
|
||||
// text: '数据为空'
|
||||
// }],
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-empty {
|
||||
@include vue-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.u-image {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.u-slot-wrap {
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,384 @@
|
|||
<template>
|
||||
<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
|
||||
<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
|
||||
<view class="u-label" :class="[required ? 'u-required' : '']" :style="{
|
||||
justifyContent: justifyContent,
|
||||
flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
|
||||
}">
|
||||
<view class="u-icon-wrap" v-if="icon">
|
||||
<u-icon size="40" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
|
||||
</view>
|
||||
<slot name="icon"></slot>
|
||||
<text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
|
||||
</view>
|
||||
<view class="fild-body">
|
||||
<view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
|
||||
<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
|
||||
:placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
|
||||
:focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
|
||||
@tap="fieldClick" />
|
||||
<input
|
||||
v-else
|
||||
:style="[fieldStyle]"
|
||||
:type="type"
|
||||
class="u-flex-1 u-field__input-wrap"
|
||||
:value="value"
|
||||
:password="password || this.type === 'password'"
|
||||
:placeholder="placeholder"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
:disabled="disabled"
|
||||
:maxlength="inputMaxlength"
|
||||
:focus="focus"
|
||||
:confirmType="confirmType"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
@confirm="onConfirm"
|
||||
@tap="fieldClick"
|
||||
/>
|
||||
</view>
|
||||
<u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
|
||||
<view class="u-button-wrap"><slot name="right" /></view>
|
||||
<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="38" class="u-arror-right" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
|
||||
paddingLeft: labelWidth + 'rpx'
|
||||
}">{{ errorMessage }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* field 输入框
|
||||
* @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
|
||||
* @tutorial https://www.uviewui.com/components/field.html
|
||||
* @property {String} type 输入框的类型(默认text)
|
||||
* @property {String} icon label左边的图标,限uView的图标名称
|
||||
* @property {Object} icon-style 左边图标的样式,对象形式
|
||||
* @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
|
||||
* @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
|
||||
* @property {String} label 输入框左边的文字提示
|
||||
* @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
|
||||
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
|
||||
* @property {Number String} label-width label的宽度,单位rpx(默认130)
|
||||
* @property {String} label-align label的文字对齐方式(默认left)
|
||||
* @property {Object} field-style 自定义输入框的样式,对象形式
|
||||
* @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
|
||||
* @property {String} input-align 输入框内容对齐方式(默认left)
|
||||
* @property {Boolean} border-bottom 是否显示field的下边框(默认true)
|
||||
* @property {Boolean} border-top 是否显示field的上边框(默认false)
|
||||
* @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
|
||||
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
|
||||
* @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
|
||||
* @property {String} placeholder 输入框的提示文字
|
||||
* @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
|
||||
* @property {Boolean} focus 是否自动获得焦点(默认false)
|
||||
* @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
|
||||
* @property {Boolean} disabled 是否不可输入(默认false)
|
||||
* @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
|
||||
* @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
|
||||
* @event {Function} input 输入框内容发生变化时触发
|
||||
* @event {Function} focus 输入框获得焦点时触发
|
||||
* @event {Function} blur 输入框失去焦点时触发
|
||||
* @event {Function} confirm 点击完成按钮时触发
|
||||
* @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
|
||||
* @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
|
||||
* @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
|
||||
*/
|
||||
export default {
|
||||
name:"u-field",
|
||||
props: {
|
||||
icon: String,
|
||||
rightIcon: String,
|
||||
// arrowDirection: {
|
||||
// type: String,
|
||||
// default: 'right'
|
||||
// },
|
||||
required: Boolean,
|
||||
label: String,
|
||||
password: Boolean,
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 左边标题的宽度单位rpx
|
||||
labelWidth: {
|
||||
type: [Number, String],
|
||||
default: 130
|
||||
},
|
||||
// 对齐方式,left|center|right
|
||||
labelAlign: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
inputAlign: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#999'
|
||||
},
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
errorMessage: {
|
||||
type: [String, Boolean],
|
||||
default: ''
|
||||
},
|
||||
placeholder: String,
|
||||
placeholderStyle: String,
|
||||
focus: Boolean,
|
||||
fixed: Boolean,
|
||||
value: [Number, String],
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxlength: {
|
||||
type: [Number, String],
|
||||
default: 140
|
||||
},
|
||||
confirmType: {
|
||||
type: String,
|
||||
default: 'done'
|
||||
},
|
||||
// lable的位置,可选为 left-左边,top-上边
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// 输入框的自定义样式
|
||||
fieldStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 清除按钮的大小
|
||||
clearSize: {
|
||||
type: [Number, String],
|
||||
default: 30
|
||||
},
|
||||
// lable左边的图标样式,对象形式
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 是否显示上边框
|
||||
borderTop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示下边框
|
||||
borderBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否自动去除两端的空格
|
||||
trim: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
focused: false,
|
||||
itemIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputWrapStyle() {
|
||||
let style = {};
|
||||
style.textAlign = this.inputAlign;
|
||||
// 判断lable的位置,如果是left的话,让input左边两边有间隙
|
||||
if(this.labelPosition == 'left') {
|
||||
style.margin = `0 8rpx`;
|
||||
} else {
|
||||
// 如果lable是top的,input的左边就没必要有间隙了
|
||||
style.marginRight = `8rpx`;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
rightIconStyle() {
|
||||
let style = {};
|
||||
if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
|
||||
if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
|
||||
else style.transform = 'roate(0deg)';
|
||||
return style;
|
||||
},
|
||||
labelStyle() {
|
||||
let style = {};
|
||||
if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
|
||||
if(this.labelAlign == 'center') style.justifyContent = 'center';
|
||||
if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
|
||||
return style;
|
||||
},
|
||||
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
|
||||
justifyContent() {
|
||||
if(this.labelAlign == 'left') return 'flex-start';
|
||||
if(this.labelAlign == 'center') return 'center';
|
||||
if(this.labelAlign == 'right') return 'flex-end';
|
||||
},
|
||||
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
|
||||
inputMaxlength() {
|
||||
return Number(this.maxlength)
|
||||
},
|
||||
// label的位置
|
||||
fieldInnerStyle() {
|
||||
let style = {};
|
||||
if(this.labelPosition == 'left') {
|
||||
style.flexDirection = 'row';
|
||||
} else {
|
||||
style.flexDirection = 'column';
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput(event) {
|
||||
let value = event.detail.value;
|
||||
// 判断是否去除空格
|
||||
if(this.trim) value = this.$u.trim(value);
|
||||
this.$emit('input', value);
|
||||
},
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onBlur(event) {
|
||||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
|
||||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
|
||||
setTimeout(() => {
|
||||
this.focused = false;
|
||||
}, 100)
|
||||
this.$emit('blur', event);
|
||||
},
|
||||
onConfirm(e) {
|
||||
this.$emit('confirm', e.detail.value);
|
||||
},
|
||||
onClear(event) {
|
||||
this.$emit('input', '');
|
||||
},
|
||||
rightIconClick() {
|
||||
this.$emit('right-icon-click');
|
||||
this.$emit('click');
|
||||
},
|
||||
fieldClick() {
|
||||
this.$emit('click');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-field {
|
||||
font-size: 28rpx;
|
||||
padding: 20rpx;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
color: $u-main-color;
|
||||
}
|
||||
|
||||
.u-field-inner {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-textarea-inner {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.u-textarea-class {
|
||||
min-height: 96rpx;
|
||||
width: auto;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.fild-body {
|
||||
@include vue-flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-arror-right {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.u-label-text {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-label-left-gap {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.u-label-postion-top {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.u-label {
|
||||
width: 130rpx;
|
||||
flex: 1 1 130rpx;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-required::before {
|
||||
content: '*';
|
||||
position: absolute;
|
||||
left: -16rpx;
|
||||
font-size: 14px;
|
||||
color: $u-type-error;
|
||||
height: 9px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.u-field__input-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
height: 48rpx;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.u-clear-icon {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-error-message {
|
||||
color: $u-type-error;
|
||||
font-size: 26rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.placeholder-style {
|
||||
color: rgb(150, 151, 153);
|
||||
}
|
||||
|
||||
.u-input-class {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.u-button-wrap {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,431 @@
|
|||
<template>
|
||||
<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
|
||||
<view class="u-form-item__body" :style="{
|
||||
flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
|
||||
}">
|
||||
<!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
|
||||
<view class="u-form-item--left" :style="{
|
||||
width: uLabelWidth,
|
||||
flex: `0 0 ${uLabelWidth}`,
|
||||
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
|
||||
}">
|
||||
<!-- 为了块对齐 -->
|
||||
<view class="u-form-item--left__content" v-if="required || leftIcon || label">
|
||||
<!-- nvue不支持伪元素before -->
|
||||
<text v-if="required" class="u-form-item--left__content--required">*</text>
|
||||
<view class="u-form-item--left__content__icon" v-if="leftIcon">
|
||||
<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
|
||||
</view>
|
||||
<view class="u-form-item--left__content__label" :style="[elLabelStyle, {
|
||||
'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
|
||||
}]">
|
||||
{{label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-form-item--right u-flex">
|
||||
<view class="u-form-item--right__content">
|
||||
<view class="u-form-item--right__content__slot ">
|
||||
<slot />
|
||||
</view>
|
||||
<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
|
||||
<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
|
||||
paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
|
||||
}">{{validateMessage}}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Emitter from '../../libs/util/emitter.js';
|
||||
import schema from '../../libs/util/async-validator';
|
||||
// 去除警告信息
|
||||
schema.warning = function() {};
|
||||
|
||||
/**
|
||||
* form-item 表单item
|
||||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
|
||||
* @tutorial http://uviewui.com/components/form.html
|
||||
* @property {String} label 左侧提示文字
|
||||
* @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
|
||||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
|
||||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
|
||||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
|
||||
* @property {Object} label-style lable的样式,对象形式
|
||||
* @property {String} label-align lable的对齐方式
|
||||
* @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
|
||||
* @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
|
||||
* @property {Object} left-icon-style 左侧图标的样式,对象形式
|
||||
* @property {Object} right-icon-style 右侧图标的样式,对象形式
|
||||
* @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
|
||||
* @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'u-form-item',
|
||||
mixins: [Emitter],
|
||||
inject: {
|
||||
uForm: {
|
||||
default () {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// input的label提示语
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 绑定的值
|
||||
prop: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示表单域的下划线边框
|
||||
borderBottom: {
|
||||
type: [String, Boolean],
|
||||
default: ''
|
||||
},
|
||||
// label的位置,left-左边,top-上边
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// label的宽度,单位rpx
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// lable的样式,对象形式
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// lable字体的对齐方式
|
||||
labelAlign: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 右侧图标
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 左侧图标
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 左侧图标的样式
|
||||
leftIconStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 左侧图标的样式
|
||||
rightIconStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
initialValue: '', // 存储的默认值
|
||||
// isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
|
||||
validateState: '', // 是否校验成功
|
||||
validateMessage: '', // 校验失败的提示语
|
||||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
|
||||
errorType: ['message'],
|
||||
fieldValue: '', // 获取当前子组件input的输入的值
|
||||
// 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
|
||||
parentData: {
|
||||
borderBottom: true,
|
||||
labelWidth: 90,
|
||||
labelPosition: 'left',
|
||||
labelStyle: {},
|
||||
labelAlign: 'left',
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
validateState(val) {
|
||||
this.broadcastInputError();
|
||||
},
|
||||
// 监听u-form组件的errorType的变化
|
||||
"uForm.errorType"(val) {
|
||||
this.errorType = val;
|
||||
this.broadcastInputError();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// 计算后的label宽度,由于需要多个判断,故放到computed中
|
||||
uLabelWidth() {
|
||||
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
|
||||
return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
|
||||
.elLabelWidth)) : '100%';
|
||||
},
|
||||
showError() {
|
||||
return type => {
|
||||
// 如果errorType数组中含有none,或者toast提示类型
|
||||
if (this.errorType.indexOf('none') >= 0) return false;
|
||||
else if (this.errorType.indexOf(type) >= 0) return true;
|
||||
else return false;
|
||||
}
|
||||
},
|
||||
// label的宽度
|
||||
elLabelWidth() {
|
||||
// label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
|
||||
return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
|
||||
.labelWidth :
|
||||
90);
|
||||
},
|
||||
// label的样式
|
||||
elLabelStyle() {
|
||||
return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
|
||||
{});
|
||||
},
|
||||
// label的位置,左侧或者上方
|
||||
elLabelPosition() {
|
||||
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
|
||||
'left');
|
||||
},
|
||||
// label的对齐方式
|
||||
elLabelAlign() {
|
||||
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
|
||||
},
|
||||
// label的下划线
|
||||
elBorderBottom() {
|
||||
// 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
|
||||
return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
|
||||
true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
broadcastInputError() {
|
||||
// 子组件发出事件,第三个参数为true或者false,true代表有错误
|
||||
this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
|
||||
},
|
||||
// 判断是否需要required校验
|
||||
setRules() {
|
||||
let that = this;
|
||||
// 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
|
||||
// 从父组件u-form拿到当前u-form-item需要验证 的规则
|
||||
// let rules = this.getRules();
|
||||
// if (rules.length) {
|
||||
// this.isRequired = rules.some(rule => {
|
||||
// // 如果有必填项,就返回,没有的话,就是undefined
|
||||
// return rule.required;
|
||||
// });
|
||||
// }
|
||||
|
||||
// blur事件
|
||||
this.$on('on-form-blur', that.onFieldBlur);
|
||||
// change事件
|
||||
this.$on('on-form-change', that.onFieldChange);
|
||||
},
|
||||
|
||||
// 从u-form的rules属性中,取出当前u-form-item的校验规则
|
||||
getRules() {
|
||||
// 父组件的所有规则
|
||||
let rules = this.parent.rules;
|
||||
rules = rules ? rules[this.prop] : [];
|
||||
// 保证返回的是一个数组形式
|
||||
return [].concat(rules || []);
|
||||
},
|
||||
|
||||
// blur事件时进行表单校验
|
||||
onFieldBlur() {
|
||||
this.validation('blur');
|
||||
},
|
||||
|
||||
// change事件进行表单校验
|
||||
onFieldChange() {
|
||||
this.validation('change');
|
||||
},
|
||||
|
||||
// 过滤出符合要求的rule规则
|
||||
getFilteredRule(triggerType = '') {
|
||||
let rules = this.getRules();
|
||||
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
|
||||
if (!triggerType) return rules;
|
||||
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
|
||||
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
|
||||
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
|
||||
return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
|
||||
},
|
||||
|
||||
// 校验数据
|
||||
validation(trigger, callback = () => {}) {
|
||||
// 检验之间,先获取需要校验的值
|
||||
this.fieldValue = this.parent.model[this.prop];
|
||||
// blur和change是否有当前方式的校验规则
|
||||
let rules = this.getFilteredRule(trigger);
|
||||
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
|
||||
// 对count变量的统计错误而无法进入上一层的回调
|
||||
if (!rules || rules.length === 0) {
|
||||
return callback('');
|
||||
}
|
||||
// 设置当前的装填,标识为校验中
|
||||
this.validateState = 'validating';
|
||||
// 调用async-validator的方法
|
||||
let validator = new schema({
|
||||
[this.prop]: rules
|
||||
});
|
||||
validator.validate({
|
||||
[this.prop]: this.fieldValue
|
||||
}, {
|
||||
firstFields: true
|
||||
}, (errors, fields) => {
|
||||
// 记录状态和报错信息
|
||||
this.validateState = !errors ? 'success' : 'error';
|
||||
this.validateMessage = errors ? errors[0].message : '';
|
||||
// 调用回调方法
|
||||
callback(this.validateMessage);
|
||||
});
|
||||
},
|
||||
|
||||
// 清空当前的u-form-item
|
||||
resetField() {
|
||||
this.parent.model[this.prop] = this.initialValue;
|
||||
// 设置为`success`状态,只是为了清空错误标记
|
||||
this.validateState = 'success';
|
||||
}
|
||||
},
|
||||
|
||||
// 组件创建完成时,将当前实例保存到u-form中
|
||||
mounted() {
|
||||
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
|
||||
this.parent = this.$u.$parent.call(this, 'u-form');
|
||||
if (this.parent) {
|
||||
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData
|
||||
Object.keys(this.parentData).map(key => {
|
||||
this.parentData[key] = this.parent[key];
|
||||
});
|
||||
// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
|
||||
if (this.prop) {
|
||||
// 将本实例添加到父组件中
|
||||
this.parent.fields.push(this);
|
||||
this.errorType = this.parent.errorType;
|
||||
// 设置初始值
|
||||
this.initialValue = this.fieldValue;
|
||||
// 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
|
||||
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
|
||||
this.$nextTick(() => {
|
||||
this.setRules();
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 组件销毁前,将实例从u-form的缓存中移除
|
||||
beforeDestroy() {
|
||||
// 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
|
||||
if (this.parent && this.prop) {
|
||||
this.parent.fields.map((item, index) => {
|
||||
if (item === this) this.parent.fields.splice(index, 1);
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-form-item {
|
||||
@include vue-flex;
|
||||
// align-items: flex-start;
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
box-sizing: border-box;
|
||||
line-height: $u-form-item-height;
|
||||
flex-direction: column;
|
||||
|
||||
&__border-bottom--error:after {
|
||||
border-color: $u-type-error;
|
||||
}
|
||||
|
||||
&__body {
|
||||
@include vue-flex;
|
||||
}
|
||||
|
||||
&--left {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
padding-right: 10rpx;
|
||||
flex: 1;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
&--required {
|
||||
position: absolute;
|
||||
left: -16rpx;
|
||||
vertical-align: middle;
|
||||
color: $u-type-error;
|
||||
padding-top: 6rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
flex: 1;
|
||||
|
||||
&__content {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
&__slot {
|
||||
flex: 1;
|
||||
/* #ifndef MP */
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: 10rpx;
|
||||
color: $u-light-color;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
color: $u-type-error;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<view class="u-form"><slot /></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* form 表单
|
||||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
|
||||
* @tutorial http://uviewui.com/components/form.html
|
||||
* @property {Object} model 表单数据对象
|
||||
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
|
||||
* @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
|
||||
* @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
|
||||
* @property {Object} label-style lable的样式,对象形式
|
||||
* @property {String} label-align lable的对齐方式
|
||||
* @property {Object} rules 通过ref设置,见官网说明
|
||||
* @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
|
||||
* @example <u-form :model="form" ref="uForm"></u-form>
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'u-form',
|
||||
props: {
|
||||
// 当前form的需要验证字段的集合
|
||||
model: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 验证规则
|
||||
// rules: {
|
||||
// type: [Object, Function, Array],
|
||||
// default() {
|
||||
// return {};
|
||||
// }
|
||||
// },
|
||||
// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
|
||||
// border-bottom-下边框呈现红色,none-无提示
|
||||
errorType: {
|
||||
type: Array,
|
||||
default() {
|
||||
return ['message', 'toast']
|
||||
}
|
||||
},
|
||||
// 是否显示表单域的下划线边框
|
||||
borderBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// label的位置,left-左边,top-上边
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// label的宽度,单位rpx
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: 90
|
||||
},
|
||||
// lable字体的对齐方式
|
||||
labelAlign: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// lable的样式,对象形式
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
uForm: this
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rules: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 存储当前form下的所有u-form-item的实例
|
||||
// 不能定义在data中,否则微信小程序会造成循环引用而报错
|
||||
this.fields = [];
|
||||
},
|
||||
methods: {
|
||||
setRules(rules) {
|
||||
this.rules = rules;
|
||||
},
|
||||
// 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
|
||||
resetFields() {
|
||||
this.fields.map(field => {
|
||||
field.resetField();
|
||||
});
|
||||
},
|
||||
// 校验全部数据
|
||||
validate(callback) {
|
||||
return new Promise(resolve => {
|
||||
// 对所有的u-form-item进行校验
|
||||
let valid = true; // 默认通过
|
||||
let count = 0; // 用于标记是否检查完毕
|
||||
let errorArr = []; // 存放错误信息
|
||||
this.fields.map(field => {
|
||||
// 调用每一个u-form-item实例的validation的校验方法
|
||||
field.validation('', error => {
|
||||
// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
|
||||
if (error) {
|
||||
valid = false;
|
||||
errorArr.push(error);
|
||||
}
|
||||
// 当历遍了所有的u-form-item时,调用promise的then方法
|
||||
if (++count === this.fields.length) {
|
||||
resolve(valid); // 进入promise的then方法
|
||||
// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
|
||||
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
|
||||
this.$u.toast(errorArr[0]);
|
||||
}
|
||||
// 调用回调方法
|
||||
if (typeof callback == 'function') callback(valid);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
</style>
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
|
||||
<view class="u-update-content">
|
||||
<rich-text :nodes="content"></rich-text>
|
||||
</view>
|
||||
</u-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
content: `
|
||||
1. 修复badge组件的size参数无效问题<br>
|
||||
2. 新增Modal模态框组件<br>
|
||||
3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
|
||||
4. 修复键盘组件在微信小程序上遮罩无效的问题
|
||||
`,
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
this.show = true;
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.closeModal();
|
||||
},
|
||||
confirm() {
|
||||
this.closeModal();
|
||||
},
|
||||
closeModal() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-full-content {
|
||||
background-color: #00C777;
|
||||
}
|
||||
|
||||
.u-update-content {
|
||||
font-size: 26rpx;
|
||||
color: $u-content-color;
|
||||
line-height: 1.7;
|
||||
padding: 30rpx;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<view class="u-gap" :style="[gapStyle]"></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* gap 间隔槽
|
||||
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
|
||||
* @tutorial https://www.uviewui.com/components/gap.html
|
||||
* @property {String} bg-color 背景颜色(默认#f3f4f6)
|
||||
* @property {String Number} height 分割槽高度,单位rpx(默认30)
|
||||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
|
||||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
|
||||
* @example <u-gap height="80" bg-color="#bbb"></u-gap>
|
||||
*/
|
||||
export default {
|
||||
name: "u-gap",
|
||||
props: {
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'transparent ' // 背景透明
|
||||
},
|
||||
// 高度
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 30
|
||||
},
|
||||
// 与上一个组件的距离
|
||||
marginTop: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 与下一个组件的距离
|
||||
marginBottom: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
gapStyle() {
|
||||
return {
|
||||
backgroundColor: this.bgColor,
|
||||
height: this.height + 'rpx',
|
||||
marginTop: this.marginTop + 'rpx',
|
||||
marginBottom: this.marginBottom + 'rpx'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
</style>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<view class="u-grid-item" :hover-class="parentData.hoverClass"
|
||||
:hover-stay-time="200" @tap="click" :style="{
|
||||
background: bgColor,
|
||||
width: width,
|
||||
}">
|
||||
<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* gridItem 提示
|
||||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
|
||||
* @tutorial https://www.uviewui.com/components/grid.html
|
||||
* @property {String} bg-color 宫格的背景颜色(默认#ffffff)
|
||||
* @property {String Number} index 点击宫格时,返回的值
|
||||
* @property {Object} custom-style 自定义样式,对象形式
|
||||
* @event {Function} click 点击宫格触发
|
||||
* @example <u-grid-item></u-grid-item>
|
||||
*/
|
||||
export default {
|
||||
name: "u-grid-item",
|
||||
props: {
|
||||
// 背景颜色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
// 点击时返回的index
|
||||
index: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 自定义样式,对象形式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
padding: '30rpx 0'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
parentData: {
|
||||
hoverClass: '', // 按下去的时候,是否显示背景灰色
|
||||
col: 3, // 父组件划分的宫格数
|
||||
border: true, // 是否显示边框,根据父组件决定
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 父组件的实例
|
||||
this.updateParentData();
|
||||
// this.parent在updateParentData()中定义
|
||||
this.parent.children.push(this);
|
||||
},
|
||||
computed: {
|
||||
// 每个grid-item的宽度
|
||||
width() {
|
||||
return 100 / Number(this.parentData.col) + '%';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取父组件的参数
|
||||
updateParentData() {
|
||||
// 此方法写在mixin中
|
||||
this.getParentData('u-grid');
|
||||
},
|
||||
click() {
|
||||
this.$emit('click', this.index);
|
||||
this.parent && this.parent.click(this.index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-grid-item {
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
|
||||
/* #ifdef MP */
|
||||
position: relative;
|
||||
float: left;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-grid-item-hover {
|
||||
background: #f7f7f7 !important;
|
||||
}
|
||||
|
||||
.u-grid-marker-box {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.u-grid-marker-wrap {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.u-grid-item-box {
|
||||
padding: 30rpx 0;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* grid 宫格布局
|
||||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
|
||||
* @tutorial https://www.uviewui.com/components/grid.html
|
||||
* @property {String Number} col 宫格的列数(默认3)
|
||||
* @property {Boolean} border 是否显示宫格的边框(默认true)
|
||||
* @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
|
||||
* @event {Function} click 点击宫格触发
|
||||
* @example <u-grid :col="3" @click="click"></u-grid>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-grid',
|
||||
props: {
|
||||
// 分成几列
|
||||
col: {
|
||||
type: [Number, String],
|
||||
default: 3
|
||||
},
|
||||
// 是否显示边框
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
|
||||
align: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
// 宫格按压时的样式类,"none"为无效果
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: 'u-hover-class'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
index: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
|
||||
parentData() {
|
||||
if(this.children.length) {
|
||||
this.children.map(child => {
|
||||
// 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
|
||||
typeof(child.updateParentData) == 'function' && child.updateParentData();
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 如果将children定义在data中,在微信小程序会造成循环引用而报错
|
||||
this.children = [];
|
||||
},
|
||||
computed: {
|
||||
// 计算父组件的值是否发生变化
|
||||
parentData() {
|
||||
return [this.hoverClass, this.col, this.size, this.border];
|
||||
},
|
||||
// 宫格对齐方式
|
||||
gridStyle() {
|
||||
let style = {};
|
||||
switch(this.align) {
|
||||
case 'left':
|
||||
style.justifyContent = 'flex-start';
|
||||
break;
|
||||
case 'center':
|
||||
style.justifyContent = 'center';
|
||||
break;
|
||||
case 'right':
|
||||
style.justifyContent = 'flex-end';
|
||||
break;
|
||||
default: style.justifyContent = 'flex-start';
|
||||
};
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click(index) {
|
||||
this.$emit('click', index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-grid {
|
||||
width: 100%;
|
||||
/* #ifdef MP */
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef MP */
|
||||
@include vue-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,336 @@
|
|||
<template>
|
||||
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
|
||||
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
|
||||
<text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
|
||||
@touchstart="touchstart">
|
||||
<text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
|
||||
class="u-icon__decimal">
|
||||
</text>
|
||||
</text>
|
||||
<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
|
||||
<text v-if="label !== ''" class="u-icon__label" :style="{
|
||||
color: labelColor,
|
||||
fontSize: $u.addUnit(labelSize),
|
||||
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
|
||||
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
|
||||
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
|
||||
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
|
||||
}">{{ label }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* icon 图标
|
||||
* @description 基于字体的图标集,包含了大多数常见场景的图标。
|
||||
* @tutorial https://www.uviewui.com/components/icon.html
|
||||
* @property {String} name 图标名称,见示例图标集
|
||||
* @property {String} color 图标颜色(默认inherit)
|
||||
* @property {String | Number} size 图标字体大小,单位rpx(默认32)
|
||||
* @property {String | Number} label-size label字体大小,单位rpx(默认28)
|
||||
* @property {String} label 图标右侧的label文字(默认28)
|
||||
* @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
|
||||
* @property {String} label-color label字体颜色(默认#606266)
|
||||
* @property {Object} custom-style icon的样式,对象形式
|
||||
* @property {String} custom-prefix 自定义字体图标库时,需要写上此值
|
||||
* @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
|
||||
* @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
|
||||
* @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
|
||||
* @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
|
||||
* @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
|
||||
* @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
|
||||
* @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
|
||||
* @property {String} width 显示图片小图标时的宽度
|
||||
* @property {String} height 显示图片小图标时的高度
|
||||
* @property {String} top 图标在垂直方向上的定位
|
||||
* @property {String} top 图标在垂直方向上的定位
|
||||
* @property {String} top 图标在垂直方向上的定位
|
||||
* @property {Boolean} show-decimal-icon 是否为DecimalIcon
|
||||
* @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
|
||||
* @property {String | Number} percent 显示的百分比,仅Decimal时有效
|
||||
* @event {Function} click 点击图标时触发
|
||||
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-icon',
|
||||
props: {
|
||||
// 图标类名
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图标颜色,可接受主题色
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 字体大小,单位rpx
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 'inherit'
|
||||
},
|
||||
// 是否显示粗体
|
||||
bold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
|
||||
index: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
// 触摸图标时的类名
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 自定义扩展前缀,方便用户扩展自己的图标库
|
||||
customPrefix: {
|
||||
type: String,
|
||||
default: 'uicon'
|
||||
},
|
||||
// 图标右边或者下面的文字
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// label的位置,只能右边或者下边
|
||||
labelPos: {
|
||||
type: String,
|
||||
default: 'right'
|
||||
},
|
||||
// label的大小
|
||||
labelSize: {
|
||||
type: [String, Number],
|
||||
default: '28'
|
||||
},
|
||||
// label的颜色
|
||||
labelColor: {
|
||||
type: String,
|
||||
default: '#606266'
|
||||
},
|
||||
// label与图标的距离(横向排列)
|
||||
marginLeft: {
|
||||
type: [String, Number],
|
||||
default: '6'
|
||||
},
|
||||
// label与图标的距离(竖向排列)
|
||||
marginTop: {
|
||||
type: [String, Number],
|
||||
default: '6'
|
||||
},
|
||||
// label与图标的距离(竖向排列)
|
||||
marginRight: {
|
||||
type: [String, Number],
|
||||
default: '6'
|
||||
},
|
||||
// label与图标的距离(竖向排列)
|
||||
marginBottom: {
|
||||
type: [String, Number],
|
||||
default: '6'
|
||||
},
|
||||
// 图片的mode
|
||||
imgMode: {
|
||||
type: String,
|
||||
default: 'widthFix'
|
||||
},
|
||||
// 自定义样式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 用于显示图片小图标时,图片的宽度
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 用于显示图片小图标时,图片的高度
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
// 用于解决某些情况下,让图标垂直居中的用途
|
||||
top: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 是否为DecimalIcon
|
||||
showDecimalIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 背景颜色,可接受主题色,仅Decimal时有效
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#ececec'
|
||||
},
|
||||
// 显示的百分比,仅Decimal时有效
|
||||
percent: {
|
||||
type: [Number, String],
|
||||
default: '50'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
customClass() {
|
||||
let classes = []
|
||||
classes.push(this.customPrefix + '-' + this.name)
|
||||
// uView的自定义图标类名为u-iconfont
|
||||
if (this.customPrefix == 'uicon') {
|
||||
classes.push('u-iconfont')
|
||||
} else {
|
||||
classes.push(this.customPrefix)
|
||||
}
|
||||
// 主题色,通过类配置
|
||||
if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
|
||||
classes.push('u-icon__icon--' + this.inactiveColor)
|
||||
} else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-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.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
|
||||
fontWeight: this.bold ? 'bold' : 'normal',
|
||||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
|
||||
top: this.$u.addUnit(this.top)
|
||||
}
|
||||
// 非主题色值时,才当作颜色值
|
||||
if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
|
||||
style.color = this.inactiveColor
|
||||
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
|
||||
|
||||
return style
|
||||
},
|
||||
// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
|
||||
isImg() {
|
||||
return this.name.indexOf('/') !== -1
|
||||
},
|
||||
imgStyle() {
|
||||
let style = {}
|
||||
// 如果设置width和height属性,则优先使用,否则使用size属性
|
||||
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
|
||||
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
|
||||
return style
|
||||
},
|
||||
decimalIconStyle() {
|
||||
let style = {}
|
||||
style = {
|
||||
fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
|
||||
fontWeight: this.bold ? 'bold' : 'normal',
|
||||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
|
||||
top: this.$u.addUnit(this.top),
|
||||
width: this.percent + '%'
|
||||
}
|
||||
// 非主题色值时,才当作颜色值
|
||||
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
|
||||
return style
|
||||
},
|
||||
decimalIconClass() {
|
||||
let classes = []
|
||||
classes.push(this.customPrefix + '-' + this.name)
|
||||
// uView的自定义图标类名为u-iconfont
|
||||
if (this.customPrefix == 'uicon') {
|
||||
classes.push('u-iconfont')
|
||||
} else {
|
||||
classes.push(this.customPrefix)
|
||||
}
|
||||
// 主题色,通过类配置
|
||||
if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
|
||||
else classes.push('u-icon__icon--primary')
|
||||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
|
||||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
|
||||
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
|
||||
classes = classes.join(' ')
|
||||
//#endif
|
||||
return classes
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click', this.index)
|
||||
},
|
||||
touchstart() {
|
||||
this.$emit('touchstart', this.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../libs/css/style.components.scss";
|
||||
@import '../../iconfont.css';
|
||||
|
||||
.u-icon {
|
||||
display: inline-flex;
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
&--primary {
|
||||
color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: $u-type-success;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: $u-type-error;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: $u-type-warning;
|
||||
}
|
||||
|
||||
&--info {
|
||||
color: $u-type-info;
|
||||
}
|
||||
}
|
||||
|
||||
&__decimal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__img {
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
&__label {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,267 @@
|
|||
<template>
|
||||
<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
|
||||
<image
|
||||
v-if="!isError"
|
||||
:src="src"
|
||||
:mode="mode"
|
||||
@error="onErrorHandler"
|
||||
@load="onLoadHandler"
|
||||
:lazy-load="lazyLoad"
|
||||
class="u-image__image"
|
||||
:show-menu-by-longpress="showMenuByLongpress"
|
||||
:style="{
|
||||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
|
||||
}"
|
||||
></image>
|
||||
<view
|
||||
v-if="showLoading && loading"
|
||||
class="u-image__loading"
|
||||
:style="{
|
||||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
|
||||
backgroundColor: this.bgColor
|
||||
}"
|
||||
>
|
||||
<slot v-if="$slots.loading" name="loading" />
|
||||
<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
|
||||
</view>
|
||||
<view
|
||||
v-if="showError && isError && !loading"
|
||||
class="u-image__error"
|
||||
:style="{
|
||||
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
|
||||
}"
|
||||
>
|
||||
<slot v-if="$slots.error" name="error" />
|
||||
<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Image 图片
|
||||
* @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
|
||||
* @tutorial https://uviewui.com/components/image.html
|
||||
* @property {String} src 图片地址
|
||||
* @property {String} mode 裁剪模式,见官网说明
|
||||
* @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
|
||||
* @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
|
||||
* @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
|
||||
* @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
|
||||
* @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
|
||||
* @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
|
||||
* @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
|
||||
* @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
|
||||
* @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
|
||||
* @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
|
||||
* @property {Boolean} fade 是否需要淡入效果(默认 true)
|
||||
* @property {String Number} width 传入图片路径时图片的宽度
|
||||
* @property {String Number} height 传入图片路径时图片的高度
|
||||
* @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
|
||||
* @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
|
||||
* @event {Function} click 点击图片时触发
|
||||
* @event {Function} error 图片加载失败时触发
|
||||
* @event {Function} load 图片加载成功时触发
|
||||
* @example <u-image width="100%" height="300rpx" :src="src"></u-image>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-image',
|
||||
props: {
|
||||
// 图片地址
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 裁剪模式
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'aspectFill'
|
||||
},
|
||||
// 宽度,单位任意
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
},
|
||||
// 高度,单位任意
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
// 图片形状,circle-圆形,square-方形
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'square'
|
||||
},
|
||||
// 圆角,单位任意
|
||||
borderRadius: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
// 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
|
||||
lazyLoad: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 开启长按图片显示识别微信小程序码菜单
|
||||
showMenuByLongpress: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 加载中的图标,或者小图片
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: 'photo'
|
||||
},
|
||||
// 加载失败的图标,或者小图片
|
||||
errorIcon: {
|
||||
type: String,
|
||||
default: 'error-circle'
|
||||
},
|
||||
// 是否显示加载中的图标或者自定义的slot
|
||||
showLoading: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示加载错误的图标或者自定义的slot
|
||||
showError: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否需要淡入效果
|
||||
fade: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 只支持网络资源,只对微信小程序有效
|
||||
webp: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 过渡时间,单位ms
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
default: 500
|
||||
},
|
||||
// 背景颜色,用于深色页面加载图片时,为了和背景色融合
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#f3f4f6'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 图片是否加载错误,如果是,则显示错误占位图
|
||||
isError: false,
|
||||
// 初始化组件时,默认为加载中状态
|
||||
loading: true,
|
||||
// 不透明度,为了实现淡入淡出的效果
|
||||
opacity: 1,
|
||||
// 过渡时间,因为props的值无法修改,故需要一个中间值
|
||||
durationTime: this.duration,
|
||||
// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
|
||||
backgroundStyle: {}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
src: {
|
||||
immediate: true,
|
||||
handler (n) {
|
||||
if(!n) {
|
||||
// 如果传入null或者'',或者false,或者undefined,标记为错误状态
|
||||
this.isError = true;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.isError = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wrapStyle() {
|
||||
let style = {};
|
||||
// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
|
||||
style.width = this.$u.addUnit(this.width);
|
||||
style.height = this.$u.addUnit(this.height);
|
||||
// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
|
||||
style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
|
||||
// 如果设置圆角,必须要有hidden,否则可能圆角无效
|
||||
style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
|
||||
if (this.fade) {
|
||||
style.opacity = this.opacity;
|
||||
style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击图片
|
||||
onClick() {
|
||||
this.$emit('click');
|
||||
},
|
||||
// 图片加载失败
|
||||
onErrorHandler(err) {
|
||||
this.loading = false;
|
||||
this.isError = true;
|
||||
this.$emit('error', err);
|
||||
},
|
||||
// 图片加载完成,标记loading结束
|
||||
onLoadHandler() {
|
||||
this.loading = false;
|
||||
this.isError = false;
|
||||
this.$emit('load');
|
||||
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
|
||||
// 否则无需fade效果时,png图片依然能看到下方的背景色
|
||||
if (!this.fade) return this.removeBgColor();
|
||||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
|
||||
this.opacity = 0;
|
||||
// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
|
||||
// 到图片展示的过程中的淡入效果
|
||||
this.durationTime = 0;
|
||||
// 延时50ms,否则在浏览器H5,过渡效果无效
|
||||
setTimeout(() => {
|
||||
this.durationTime = this.duration;
|
||||
this.opacity = 1;
|
||||
setTimeout(() => {
|
||||
this.removeBgColor();
|
||||
}, this.durationTime);
|
||||
}, 50);
|
||||
},
|
||||
// 移除图片的背景色
|
||||
removeBgColor() {
|
||||
// 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
|
||||
this.backgroundStyle = {
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-image {
|
||||
position: relative;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__loading,
|
||||
&__error {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $u-bg-color;
|
||||
color: $u-tips-color;
|
||||
font-size: 46rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
|
||||
<view>
|
||||
<view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
|
||||
<view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
|
||||
<slot v-if="useSlot" />
|
||||
<block v-else>
|
||||
<text>{{ index }}</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* indexAnchor 索引列表锚点
|
||||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
|
||||
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
|
||||
* @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
|
||||
* @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
|
||||
* @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
|
||||
* @event {Function} default 锚点位置显示内容,默认为索引字符
|
||||
* @example <u-index-anchor :index="item" />
|
||||
*/
|
||||
export default {
|
||||
name: "u-index-anchor",
|
||||
props: {
|
||||
useSlot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
index: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
wrapperStyle: {},
|
||||
anchorStyle: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.parent = false;
|
||||
},
|
||||
mounted() {
|
||||
this.parent = this.$u.$parent.call(this, 'u-index-list');
|
||||
if(this.parent) {
|
||||
this.parent.children.push(this);
|
||||
this.parent.updateData();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
customAnchorStyle() {
|
||||
return Object.assign(this.anchorStyle, this.customStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/style.components.scss";
|
||||
|
||||
.u-index-anchor {
|
||||
box-sizing: border-box;
|
||||
padding: 14rpx 24rpx;
|
||||
color: #606266;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.2;
|
||||
background-color: rgb(245, 245, 245);
|
||||
}
|
||||
|
||||
.u-index-anchor--active {
|
||||
right: 0;
|
||||
left: 0;
|
||||
color: #2979ff;
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue