uni-app引入使用uView

This commit is contained in:
sfmind
2022-04-06 16:08:26 +08:00
parent 9ae44336db
commit 9709f200c2
386 changed files with 38777 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<template>
<uvForm
ref="uForm"
:model="model"
:rules="rules"
:errorType="errorType"
:borderBottom="borderBottom"
:labelPosition="labelPosition"
:labelWidth="labelWidth"
:labelAlign="labelAlign"
:labelStyle="labelStyle"
:customStyle="customStyle"
>
<slot />
</uvForm>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-form被uni-app官方占用了u-form在nvue中相当于form组件
* 所以在nvue下取名为u--form内部其实还是u-form.vue只不过做一层中转
*/
import uvForm from '../u-form/u-form.vue';
import props from '../u-form/props.js'
export default {
// #ifdef MP-WEIXIN
name: 'u-form',
// #endif
// #ifndef MP-WEIXIN
name: 'u--form',
// #endif
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
components: {
uvForm
},
created() {
this.children = []
},
methods: {
// 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
setRules(rules) {
this.$refs.uForm.setRules(rules)
},
validate() {
/**
* 在微信小程序中通过this.$parent拿到的父组件是u--form而不是其内嵌的u-form
* 导致在u-form组件中拿不到对应的children数组从而校验无效所以这里每次调用u-form组件中的
* 对应方法的时候在小程序中都先将u--form的children赋值给u-form中的children
*/
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.validate()
},
validateField(value, callback) {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.validateField(value, callback)
},
resetFields() {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.resetFields()
},
clearValidate(props) {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.clearValidate(props)
},
setMpData() {
this.$refs.uForm.children = this.children
}
},
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<uvImage
:src="src"
:mode="mode"
:width="width"
:height="height"
:shape="shape"
:radius="radius"
:lazyLoad="lazyLoad"
:showMenuByLongpress="showMenuByLongpress"
:loadingIcon="loadingIcon"
:errorIcon="errorIcon"
:showLoading="showLoading"
:showError="showError"
:fade="fade"
:webp="webp"
:duration="duration"
:bgColor="bgColor"
:customStyle="customStyle"
@click="$emit('click')"
@error="$emit('error')"
@load="$emit('load')"
>
<template v-slot:loading>
<slot name="loading"></slot>
</template>
<template v-slot:error>
<slot name="error"></slot>
</template>
</uvImage>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-image被uni-app官方占用了u-image在nvue中相当于image组件
* 所以在nvue下取名为u--image内部其实还是u-iamge.vue只不过做一层中转
*/
import uvImage from '../u-image/u-image.vue';
import props from '../u-image/props.js';
export default {
name: 'u--image',
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
components: {
uvImage
},
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<uvInput
:value="value"
:type="type"
:fixed="fixed"
:disabled="disabled"
:disabledColor="disabledColor"
:clearable="clearable"
:password="password"
:maxlength="maxlength"
:placeholder="placeholder"
:placeholderClass="placeholderClass"
:placeholderStyle="placeholderStyle"
:showWordLimit="showWordLimit"
:confirmType="confirmType"
:confirmHold="confirmHold"
:holdKeyboard="holdKeyboard"
:focus="focus"
:autoBlur="autoBlur"
:disableDefaultPadding="disableDefaultPadding"
:cursor="cursor"
:cursorSpacing="cursorSpacing"
:selectionStart="selectionStart"
:selectionEnd="selectionEnd"
:adjustPosition="adjustPosition"
:inputAlign="inputAlign"
:fontSize="fontSize"
:color="color"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:suffixIconStyle="suffixIconStyle"
:prefixIconStyle="prefixIconStyle"
:border="border"
:readonly="readonly"
:shape="shape"
:customStyle="customStyle"
:formatter="formatter"
@focus="$emit('focus')"
@blur="$emit('blur')"
@keyboardheightchange="$emit('keyboardheightchange')"
@change="e => $emit('change', e)"
@input="e => $emit('input', e)"
@confirm="e => $emit('confirm', e)"
@clear="$emit('clear')"
@click="$emit('click')"
>
<!-- #ifdef MP -->
<slot name="prefix"></slot>
<slot name="suffix"></slot>
<!-- #endif -->
<!-- #ifndef MP -->
<slot name="prefix" slot="prefix"></slot>
<slot name="suffix" slot="suffix"></slot>
<!-- #endif -->
</uvInput>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-input被uni-app官方占用了u-input在nvue中相当于input组件
* 所以在nvue下取名为u--input内部其实还是u-input.vue只不过做一层中转
*/
import uvInput from '../u-input/u-input.vue';
import props from '../u-input/props.js'
export default {
name: 'u--input',
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
components: {
uvInput
},
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<uvText
:type="type"
:show="show"
:text="text"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:mode="mode"
:href="href"
:format="format"
:call="call"
:openType="openType"
:bold="bold"
:block="block"
:lines="lines"
:color="color"
:decoration="decoration"
:size="size"
:iconStyle="iconStyle"
:margin="margin"
:lineHeight="lineHeight"
:align="align"
:wordWrap="wordWrap"
:customStyle="customStyle"
@click="$emit('click')"
></uvText>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-text被uni-app官方占用了u-text在nvue中相当于input组件
* 所以在nvue下取名为u--input内部其实还是u-text.vue只不过做一层中转
* 不使用v-bind="$attrs",而是分开独立写传参,是因为微信小程序不支持此写法
*/
import uvText from "../u-text/u-text.vue";
import props from "../u-text/props.js";
export default {
name: "u--text",
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
components: {
uvText,
},
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<uvTextarea
:value="value"
:placeholder="placeholder"
:height="height"
:confirmType="confirmType"
:disabled="disabled"
:count="count"
:focus="focus"
:autoHeight="autoHeight"
:fixed="fixed"
:cursorSpacing="cursorSpacing"
:cursor="cursor"
:showConfirmBar="showConfirmBar"
:selectionStart="selectionStart"
:selectionEnd="selectionEnd"
:adjustPosition="adjustPosition"
:disableDefaultPadding="disableDefaultPadding"
:holdKeyboard="holdKeyboard"
:maxlength="maxlength"
:border="border"
:customStyle="customStyle"
:formatter="formatter"
@focus="e => $emit('focus')"
@blur="e => $emit('blur')"
@linechange="e => $emit('linechange', e)"
@confirm="e => $emit('confirm')"
@input="e => $emit('input', e)"
@keyboardheightchange="e => $emit('keyboardheightchange')"
></uvTextarea>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u--textarea被uni-app官方占用了u-textarea在nvue中相当于textarea组件
* 所以在nvue下取名为u--textarea内部其实还是u-textarea.vue只不过做一层中转
*/
import uvTextarea from '../u-textarea/u-textarea.vue';
import props from '../u-textarea/props.js'
export default {
name: 'u--textarea',
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
components: {
uvTextarea
},
}
</script>

View File

@@ -0,0 +1,54 @@
export default {
props: {
// 操作菜单是否展示 默认false
show: {
type: Boolean,
default: uni.$u.props.actionSheet.show
},
// 标题
title: {
type: String,
default: uni.$u.props.actionSheet.title
},
// 选项上方的描述信息
description: {
type: String,
default: uni.$u.props.actionSheet.description
},
// 数据
actions: {
type: Array,
default: uni.$u.props.actionSheet.actions
},
// 取消按钮的文字,不为空时显示按钮
cancelText: {
type: String,
default: uni.$u.props.actionSheet.cancelText
},
// 点击某个菜单项时是否关闭弹窗
closeOnClickAction: {
type: Boolean,
default: uni.$u.props.actionSheet.closeOnClickAction
},
// 处理底部安全区默认true
safeAreaInsetBottom: {
type: Boolean,
default: uni.$u.props.actionSheet.safeAreaInsetBottom
},
// 小程序的打开方式
openType: {
type: String,
default: uni.$u.props.actionSheet.openType
},
// 点击遮罩是否允许关闭 (默认true)
closeOnClickOverlay: {
type: Boolean,
default: uni.$u.props.actionSheet.closeOnClickOverlay
},
// 圆角值
round: {
type: [Boolean, String, Number],
default: uni.$u.props.actionSheet.round
}
}
}

View File

@@ -0,0 +1,278 @@
<template>
<u-popup
:show="show"
mode="bottom"
@close="closeHandler"
:safeAreaInsetBottom="safeAreaInsetBottom"
:round="round"
>
<view class="u-action-sheet">
<view
class="u-action-sheet__header"
v-if="title"
>
<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
<view
class="u-action-sheet__header__icon-wrap"
@tap.stop="close"
>
<u-icon
name="close"
size="17"
color="#c8c9cc"
bold
></u-icon>
</view>
</view>
<text
class="u-action-sheet__description"
:style="[{
marginTop: `${title && description ? 0 : '18px'}`
}]"
v-if="description"
>{{description}}</text>
<slot>
<u-line v-if="description"></u-line>
<view class="u-action-sheet__item-wrap">
<template v-for="(item, index) in actions">
<!-- #ifdef MP -->
<button
:key="index"
class="u-reset-button"
:openType="item.openType"
@getuserinfo="onGetUserInfo"
@contact="onContact"
@getphonenumber="onGetPhoneNumber"
@error="onError"
@launchapp="onLaunchApp"
@opensetting="onOpenSetting"
:lang="lang"
:session-from="sessionFrom"
:send-message-title="sendMessageTitle"
:send-message-path="sendMessagePath"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
:app-parameter="appParameter"
@tap="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
>
<!-- #endif -->
<view
class="u-action-sheet__item-wrap__item"
@tap.stop="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
:hover-stay-time="150"
>
<template v-if="!item.loading">
<text
class="u-action-sheet__item-wrap__item__name"
:style="[itemStyle(index)]"
>{{ item.name }}</text>
<text
v-if="item.subname"
class="u-action-sheet__item-wrap__item__subname"
>{{ item.subname }}</text>
</template>
<u-loading-icon
v-else
custom-class="van-action-sheet__loading"
size="18"
mode="circle"
/>
</view>
<!-- #ifdef MP -->
</button>
<!-- #endif -->
<u-line v-if="index !== actions.length - 1"></u-line>
</template>
</view>
</slot>
<u-gap
bgColor="#eaeaec"
height="6"
v-if="cancelText"
></u-gap>
<view hover-class="u-action-sheet--hover">
<text
@touchmove.stop.prevent
:hover-stay-time="150"
v-if="cancelText"
class="u-action-sheet__cancel-text"
@tap="cancel"
>{{cancelText}}</text>
</view>
</view>
</u-popup>
</template>
<script>
import openType from '../../libs/mixin/openType'
import button from '../../libs/mixin/button'
import props from './props.js';
/**
* ActionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI配置更加灵活所有平台都表现一致。
* @tutorial https://www.uviewui.com/components/actionSheet.html
*
* @property {Boolean} show 操作菜单是否展示 (默认 false
* @property {String} title 操作菜单标题
* @property {String} description 选项上方的描述信息
* @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
* @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
* @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true
* @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true
* @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting getPhoneNumber error )
* @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
* @property {String} lang 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文
* @property {String} sessionFrom 会话来源openType="contact"时有效
* @property {String} sendMessageTitle 会话内消息卡片标题openType="contact"时有效
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径openType="contact"时有效
* @property {String} sendMessageImg 会话内消息卡片图片openType="contact"时有效
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息openType="contact"时有效 (默认 false
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数openType=launchApp 时有效
*
* @event {Function} select 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致openType="getUserInfo"时有效
* @event {Function} contact 客服消息回调openType="contact"时有效
* @event {Function} getphonenumber 获取用户手机号回调openType="getPhoneNumber"时有效
* @event {Function} error 当使用开放能力时发生错误的回调openType="error"时有效
* @event {Function} launchapp 打开 APP 成功的回调openType="launchApp"时有效
* @event {Function} opensetting 在打开授权设置页后回调openType="openSetting"时有效
* @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
// 一些props参数和methods方法通过mixin混入因为其他文件也会用到
mixins: [openType, button, uni.$u.mixin, props],
data() {
return {
}
},
computed: {
// 操作项目的样式
itemStyle() {
return (index) => {
let style = {};
if (this.actions[index].color) style.color = this.actions[index].color
if (this.actions[index].fontSize) style.fontSize = uni.$u.addUnit(this.actions[index].fontSize)
// 选项被禁用的样式
if (this.actions[index].disabled) style.color = '#c0c4cc'
return style;
}
},
},
methods: {
closeHandler() {
// 允许点击遮罩关闭时才发出close事件
if(this.closeOnClickOverlay) {
this.$emit('close')
}
},
// 点击取消按钮
cancel() {
this.$emit('close')
},
selectHandler(index) {
const item = this.actions[index]
if (item && !item.disabled && !item.loading) {
this.$emit('select', item)
if (this.closeOnClickAction) {
this.$emit('close')
}
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-action-sheet-reset-button-width:100% !default;
$u-action-sheet-title-font-size: 16px !default;
$u-action-sheet-title-padding: 12px 30px !default;
$u-action-sheet-title-color: $u-main-color !default;
$u-action-sheet-header-icon-wrap-right:15px !default;
$u-action-sheet-header-icon-wrap-top:15px !default;
$u-action-sheet-description-font-size:13px !default;
$u-action-sheet-description-color:14px !default;
$u-action-sheet-description-margin: 18px 15px !default;
$u-action-sheet-item-wrap-item-padding:15px !default;
$u-action-sheet-item-wrap-name-font-size:16px !default;
$u-action-sheet-item-wrap-subname-font-size:13px !default;
$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
$u-action-sheet-item-wrap-subname-margin-top:10px !default;
$u-action-sheet-cancel-text-font-size:16px !default;
$u-action-sheet-cancel-text-color:$u-content-color !default;
$u-action-sheet-cancel-text-font-size:15px !default;
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
.u-reset-button {
width: $u-action-sheet-reset-button-width;
}
.u-action-sheet {
text-align: center;
&__header {
position: relative;
padding: $u-action-sheet-title-padding;
&__title {
font-size: $u-action-sheet-title-font-size;
color: $u-action-sheet-title-color;
font-weight: bold;
text-align: center;
}
&__icon-wrap {
position: absolute;
right: $u-action-sheet-header-icon-wrap-right;
top: $u-action-sheet-header-icon-wrap-top;
}
}
&__description {
font-size: $u-action-sheet-description-font-size;
color: $u-tips-color;
margin: $u-action-sheet-description-margin;
text-align: center;
}
&__item-wrap {
&__item {
padding: $u-action-sheet-item-wrap-item-padding;
@include flex;
align-items: center;
justify-content: center;
flex-direction: column;
&__name {
font-size: $u-action-sheet-item-wrap-name-font-size;
color: $u-main-color;
text-align: center;
}
&__subname {
font-size: $u-action-sheet-item-wrap-subname-font-size;
color: $u-action-sheet-item-wrap-subname-color;
margin-top: $u-action-sheet-item-wrap-subname-margin-top;
text-align: center;
}
}
}
&__cancel-text {
font-size: $u-action-sheet-cancel-text-font-size;
color: $u-action-sheet-cancel-text-color;
text-align: center;
padding: $u-action-sheet-cancel-text-font-size;
}
&--hover {
background-color: $u-action-sheet-cancel-text-hover-background-color;
}
}
</style>

View File

@@ -0,0 +1,59 @@
export default {
props: {
// 图片地址Array<String>|Array<Object>形式
urls: {
type: Array,
default: uni.$u.props.album.urls
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: uni.$u.props.album.keyName
},
// 单图时,图片长边的长度
singleSize: {
type: [String, Number],
default: uni.$u.props.album.singleSize
},
// 多图时,图片边长
multipleSize: {
type: [String, Number],
default: uni.$u.props.album.multipleSize
},
// 多图时,图片水平和垂直之间的间隔
space: {
type: [String, Number],
default: uni.$u.props.album.space
},
// 单图时,图片缩放裁剪的模式
singleMode: {
type: String,
default: uni.$u.props.album.singleMode
},
// 多图时,图片缩放裁剪的模式
multipleMode: {
type: String,
default: uni.$u.props.album.multipleMode
},
// 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
maxCount: {
type: [String, Number],
default: uni.$u.props.album.maxCount
},
// 是否可以预览图片
previewFullImage: {
type: Boolean,
default: uni.$u.props.album.previewFullImage
},
// 每行展示图片数量如设置singleSize和multipleSize将会无效
rowCount: {
type: [String, Number],
default: uni.$u.props.album.rowCount
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: uni.$u.props.album.showMore
}
}
}

View File

@@ -0,0 +1,259 @@
<template>
<view class="u-album">
<view
class="u-album__row"
ref="u-album__row"
v-for="(arr, index) in showUrls"
:forComputedUse="albumWidth"
:key="index"
>
<view
class="u-album__row__wrapper"
v-for="(item, index1) in arr"
:key="index1"
:style="[imageStyle(index + 1, index1 + 1)]"
@tap="previewFullImage ? onPreviewTap(getSrc(item)) : ''"
>
<image
:src="getSrc(item)"
:mode="
urls.length === 1
? imageHeight > 0
? singleMode
: 'widthFix'
: multipleMode
"
:style="[
{
width: imageWidth,
height: imageHeight
}
]"
></image>
<view
v-if="
showMore &&
urls.length > rowCount * showUrls.length &&
index === showUrls.length - 1 &&
index1 === showUrls[showUrls.length - 1].length - 1
"
class="u-album__row__wrapper__text"
>
<u--text
:text="`+${urls.length - maxCount}`"
color="#fff"
:size="multipleSize * 0.3"
align="center"
customStyle="justify-content: center"
></u--text>
</view>
</view>
</view>
</view>
</template>
<script>
import props from './props.js'
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物所以不支持百分比单位这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* Album 相册
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
* @tutorial https://www.uviewui.com/components/album.html
*
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180
* @property {String | Number} multipleSize 多图时,图片边长 (默认 70
* @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6
* @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill'
* @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill'
* @property {String | Number} maxCount 取消按钮的提示文字 (默认 9
* @property {Boolean} previewFullImage 是否可以预览图片 (默认 true
* @property {String | Number} rowCount 每行展示图片数量如设置singleSize和multipleSize将会无效 (默认 3
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true
*
* @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width
* @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
*/
export default {
name: 'u-album',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 单图的宽度
singleWidth: 0,
// 单图的高度
singleHeight: 0,
// 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
singlePercent: 0.6
}
},
watch: {
urls: {
immediate: true,
handler(newVal) {
if (newVal.length === 1) {
this.getImageRect()
}
}
}
},
computed: {
imageStyle() {
return (index1, index2) => {
const { space, rowCount, multipleSize, urls } = this,
{ addUnit, addStyle } = uni.$u,
rowLen = this.showUrls.length,
allLen = this.urls.length
const style = {
marginRight: addUnit(space),
marginBottom: addUnit(space)
}
// 如果为最后一行,则每个图片都无需下边框
if (index1 === rowLen) style.marginBottom = 0
// 每行的最右边一张和总长度的最后一张无需右边框
if (
index2 === rowCount ||
(index1 === rowLen &&
index2 === this.showUrls[index1 - 1].length)
)
style.marginRight = 0
return style
}
},
// 将数组划分为二维数组
showUrls() {
const arr = []
this.urls.map((item, index) => {
// 限制最大展示数量
if (index + 1 <= this.maxCount) {
// 计算该元素为第几个素组内
const itemIndex = Math.floor(index / this.rowCount)
// 判断对应的索引是否存在
if (!arr[itemIndex]) {
arr[itemIndex] = []
}
arr[itemIndex].push(item)
}
})
return arr
},
imageWidth() {
return uni.$u.addUnit(
this.urls.length === 1 ? this.singleWidth : this.multipleSize
)
},
imageHeight() {
return uni.$u.addUnit(
this.urls.length === 1 ? this.singleHeight : this.multipleSize
)
},
// 此变量无实际用途仅仅是为了利用computed特性让其在urls长度等变化时重新计算图片的宽度
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
albumWidth() {
let width = 0
if (this.urls.length === 1) {
width = this.singleWidth
} else {
width =
this.showUrls[0].length * this.multipleSize +
this.space * (this.showUrls[0].length - 1)
}
this.$emit('albumWidth', width)
return width
}
},
methods: {
// 预览图片
onPreviewTap(url) {
const urls = this.urls.map((item) => {
return this.getSrc(item)
})
uni.previewImage({
current: url,
urls
})
},
// 获取图片的路径
getSrc(item) {
return uni.$u.test.object(item)
? (this.keyName && item[this.keyName]) || item.src
: item
},
// 单图时,获取图片的尺寸
// 在小程序中需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
getImageRect() {
const src = this.getSrc(this.urls[0])
uni.getImageInfo({
src,
success: (res) => {
// 判断图片横向还是竖向展示方式
const isHorizotal = res.width >= res.height
this.singleWidth = isHorizotal
? this.singleSize
: (res.width / res.height) * this.singleSize
this.singleHeight = !isHorizotal
? this.singleSize
: (res.height / res.width) * this.singleWidth
},
fail: () => {
this.getComponentWidth()
}
})
},
// 获取组件的宽度
async getComponentWidth() {
// 延时一定时间以获取dom尺寸
await uni.$u.sleep(30)
// #ifndef APP-NVUE
this.$uGetRect('.u-album__row').then((size) => {
this.singleWidth = size.width * this.singlePercent
})
// #endif
// #ifdef APP-NVUE
// 这里ref="u-album__row"所在的标签为通过for循环出来导致this.$refs['u-album__row']是一个数组
const ref = this.$refs['u-album__row'][0]
ref &&
dom.getComponentRect(ref, (res) => {
this.singleWidth = res.size.width * this.singlePercent
})
// #endif
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-album {
@include flex(column);
&__row {
@include flex(row);
flex-wrap: wrap;
&__wrapper {
position: relative;
&__text {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex(row);
justify-content: center;
align-items: center;
}
}
}
}
</style>

View File

@@ -0,0 +1,44 @@
export default {
props: {
// 显示文字
title: {
type: String,
default: uni.$u.props.alert.title
},
// 主题success/warning/info/error
type: {
type: String,
default: uni.$u.props.alert.type
},
// 辅助性文字
description: {
type: String,
default: uni.$u.props.alert.description
},
// 是否可关闭
closable: {
type: Boolean,
default: uni.$u.props.alert.closable
},
// 是否显示图标
showIcon: {
type: Boolean,
default: uni.$u.props.alert.showIcon
},
// 浅或深色调light-浅色dark-深色
effect: {
type: String,
default: uni.$u.props.alert.effect
},
// 文字是否居中
center: {
type: Boolean,
default: uni.$u.props.alert.center
},
// 字体大小
fontSize: {
type: [String, Number],
default: uni.$u.props.alert.fontSize
}
}
}

View File

@@ -0,0 +1,243 @@
<template>
<u-transition
mode="fade"
:show="show"
>
<view
class="u-alert"
:class="[`u-alert--${type}--${effect}`]"
@tap.stop="clickHandler"
:style="[$u.addStyle(customStyle)]"
>
<view
class="u-alert__icon"
v-if="showIcon"
>
<u-icon
:name="iconName"
size="18"
:color="iconColor"
></u-icon>
</view>
<view
class="u-alert__content"
:style="[{
paddingRight: closable ? '20px' : 0
}]"
>
<text
class="u-alert__content__title"
v-if="title"
:style="[{
fontSize: $u.addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ title }}</text>
<text
class="u-alert__content__desc"
v-if="description"
:style="[{
fontSize: $u.addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ description }}</text>
</view>
<view
class="u-alert__close"
v-if="closable"
@tap.stop="closeHandler"
>
<u-icon
name="close"
:color="iconColor"
size="15"
></u-icon>
</view>
</view>
</u-transition>
</template>
<script>
import props from './props.js';
/**
* Alert 警告提示
* @description 警告提示,展现需要关注的信息。
* @tutorial https://www.uviewui.com/components/alertTips.html
*
* @property {String} title 显示的文字
* @property {String} type 使用预设的颜色 (默认 'warning'
* @property {String} description 辅助性文字颜色比title浅一点字号也小一点可选
* @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false
* @property {Boolean} showIcon 是否显示左边的辅助图标 默认 false
* @property {String} effect 多图时,图片缩放裁剪的模式 (默认 'light'
* @property {Boolean} center 文字是否居中 (默认 false
* @property {String | Number} fontSize 字体大小 (默认 14
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click 点击组件时触发
* @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
*/
export default {
name: 'u-alert',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
show: true
}
},
computed: {
iconColor() {
return this.effect === 'light' ? this.type : '#fff'
},
// 不同主题对应不同的图标
iconName() {
switch (this.type) {
case 'success':
return 'checkmark-circle-fill';
break;
case 'error':
return 'close-circle-fill';
break;
case 'warning':
return 'error-circle-fill';
break;
case 'info':
return 'info-circle-fill';
break;
case 'primary':
return 'more-circle-fill';
break;
default:
return 'error-circle-fill';
}
}
},
methods: {
// 点击内容
clickHandler() {
this.$emit('click')
},
// 点击关闭按钮
closeHandler() {
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-alert {
position: relative;
background-color: $u-primary;
padding: 8px 10px;
@include flex(row);
align-items: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
&--primary--dark {
background-color: $u-primary;
}
&--primary--light {
background-color: #ecf5ff;
}
&--error--dark {
background-color: $u-error;
}
&--error--light {
background-color: #FEF0F0;
}
&--success--dark {
background-color: $u-success;
}
&--success--light {
background-color: #f5fff0;
}
&--warning--dark {
background-color: $u-warning;
}
&--warning--light {
background-color: #FDF6EC;
}
&--info--dark {
background-color: $u-info;
}
&--info--light {
background-color: #f4f4f5;
}
&__icon {
margin-right: 5px;
}
&__content {
@include flex(column);
flex: 1;
&__title {
color: $u-main-color;
font-size: 14px;
font-weight: bold;
color: #fff;
margin-bottom: 2px;
}
&__desc {
color: $u-main-color;
font-size: 14px;
flex-wrap: wrap;
color: #fff;
}
}
&__title--dark,
&__desc--dark {
color: #FFFFFF;
}
&__text--primary--light,
&__text--primary--light {
color: $u-primary;
}
&__text--success--light,
&__text--success--light {
color: $u-success;
}
&__text--warning--light,
&__text--warning--light {
color: $u-warning;
}
&__text--error--light,
&__text--error--light {
color: $u-error;
}
&__text--info--light,
&__text--info--light {
color: $u-info;
}
&__close {
position: absolute;
top: 11px;
right: 10px;
}
}
</style>

View File

@@ -0,0 +1,52 @@
export default {
props: {
// 头像图片组
urls: {
type: Array,
default: uni.$u.props.avatarGroup.urls
},
// 最多展示的头像数量
maxCount: {
type: [String, Number],
default: uni.$u.props.avatarGroup.maxCount
},
// 头像形状
shape: {
type: String,
default: uni.$u.props.avatarGroup.shape
},
// 图片裁剪模式
mode: {
type: String,
default: uni.$u.props.avatarGroup.mode
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: uni.$u.props.avatarGroup.showMore
},
// 头像大小
size: {
type: [String, Number],
default: uni.$u.props.avatarGroup.size
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: uni.$u.props.avatarGroup.keyName
},
// 头像之间的遮挡比例
gap: {
type: [String, Number],
validator(value) {
return value >= 0 && value <= 1
},
default: uni.$u.props.avatarGroup.gap
},
// 需额外显示的值
extraValue: {
type: [Number, String],
default: uni.$u.props.avatarGroup.extraValue
}
}
}

View File

@@ -0,0 +1,103 @@
<template>
<view class="u-avatar-group">
<view
class="u-avatar-group__item"
v-for="(item, index) in showUrl"
:key="index"
:style="{
marginLeft: index === 0 ? 0 : $u.addUnit(-size * gap)
}"
>
<u-avatar
:size="size"
:shape="shape"
:mode="mode"
:src="$u.test.object(item) ? keyName && item[keyName] || item.url : item"
></u-avatar>
<view
class="u-avatar-group__item__show-more"
v-if="showMore && index === showUrl.length - 1 && (urls.length > maxCount || extraValue > 0)"
@tap="clickHandler"
>
<u--text
color="#ffffff"
:size="size * 0.4"
:text="`+${extraValue || urls.length - showUrl.length}`"
align="center"
customStyle="justify-content: center"
></u--text>
</view>
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* AvatarGroup 头像组
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uviewui.com/components/avatar.html
*
* @property {Array} urls 头像图片组 (默认 []
* @property {String | Number} maxCount 最多展示的头像数量 默认 5
* @property {String} shape 头像形状( 'circle' (默认) | 'square'
* @property {String} mode 图片裁剪模式(默认 'scaleToFill'
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true
* @property {String | Number} size 头像大小 (默认 40
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} gap 头像之间的遮挡比例0.4代表遮挡40% (默认 0.5
* @property {String | Number} extraValue 需额外显示的值
* @event {Function} showMore 头像组更多点击
* @example <u-avatar-group:urls="urls" size="35" gap="0.4" ></u-avatar-group:urls=>
*/
export default {
name: 'u-avatar-group',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
}
},
computed: {
showUrl() {
return this.urls.slice(0, this.maxCount)
}
},
methods: {
clickHandler() {
this.$emit('showMore')
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar-group {
@include flex;
&__item {
margin-left: -10px;
position: relative;
&--no-indent {
// 如果你想质疑作者不会使用:first-child说明你太年轻因为nvue不支持
margin-left: 0;
}
&__show-more {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex;
align-items: center;
justify-content: center;
border-radius: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
export default {
props: {
// 头像图片路径(不能为相对路径)
src: {
type: String,
default: uni.$u.props.avatar.src
},
// 头像形状circle-圆形square-方形
shape: {
type: String,
default: uni.$u.props.avatar.shape
},
// 头像尺寸
size: {
type: [String, Number],
default: uni.$u.props.avatar.size
},
// 裁剪模式
mode: {
type: String,
default: uni.$u.props.avatar.mode
},
// 显示的文字
text: {
type: String,
default: uni.$u.props.avatar.text
},
// 背景色
bgColor: {
type: String,
default: uni.$u.props.avatar.bgColor
},
// 文字颜色
color: {
type: String,
default: uni.$u.props.avatar.color
},
// 文字大小
fontSize: {
type: [String, Number],
default: uni.$u.props.avatar.fontSize
},
// 显示的图标
icon: {
type: String,
default: uni.$u.props.avatar.icon
},
// 显示小程序头像只对百度微信QQ小程序有效
mpAvatar: {
type: Boolean,
default: uni.$u.props.avatar.mpAvatar
},
// 是否使用随机背景色
randomBgColor: {
type: Boolean,
default: uni.$u.props.avatar.randomBgColor
},
// 加载失败的默认头像(组件有内置默认图片)
defaultUrl: {
type: String,
default: uni.$u.props.avatar.defaultUrl
},
// 如果配置了randomBgColor为true且配置了此值则从默认的背景色数组中取出对应索引的颜色值取值0-19之间
colorIndex: {
type: [String, Number],
// 校验参数规则索引在0-19之间
validator(n) {
return uni.$u.test.range(n, [0, 19]) || n === ''
},
default: uni.$u.props.avatar.colorIndex
},
// 组件标识符
name: {
type: String,
default: uni.$u.props.avatar.name
}
}
}

View File

@@ -0,0 +1,172 @@
<template>
<view
class="u-avatar"
:class="[`u-avatar--${shape}`]"
:style="[{
backgroundColor: (text || icon) ? (randomBgColor ? colors[colorIndex !== '' ? colorIndex : $u.random(0, 19)] : bgColor) : 'transparent',
width: $u.addUnit(size),
height: $u.addUnit(size),
}, $u.addStyle(customStyle)]"
@tap="clickHandler"
>
<slot>
<!-- #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU -->
<open-data
v-if="mpAvatar && allowMp"
type="userAvatarUrl"
:style="[{
width: $u.addUnit(size),
height: $u.addUnit(size)
}]"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN && MP-QQ && MP-BAIDU -->
<template v-if="mpAvatar && allowMp"></template>
<!-- #endif -->
<u-icon
v-else-if="icon"
:name="icon"
:size="fontSize"
:color="color"
></u-icon>
<u--text
v-else-if="text"
:text="text"
:size="fontSize"
:color="color"
align="center"
customStyle="justify-content: center"
></u--text>
<image
class="u-avatar__image"
v-else
:class="[`u-avatar__image--${shape}`]"
:src="avatarUrl || defaultUrl"
:mode="mode"
@error="errorHandler"
:style="[{
width: $u.addUnit(size),
height: $u.addUnit(size)
}]"
></image>
</slot>
</view>
</template>
<script>
import props from './props.js';
const 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} src 头像路径,如加载失败,将会显示默认头像(不能为相对路径)
* @property {String} shape 头像形状 circle (默认) | square
* @property {String | Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值 (默认 40
* @property {String} mode 头像图片的裁剪类型与uni的image组件的mode参数一致如效果达不到需求可尝试传widthFix值 (默认 'scaleToFill'
* @property {String} text 用文字替代图片级别优先于src
* @property {String} bgColor 背景颜色,一般显示文字时用 (默认 '#c0c4cc'
* @property {String} color 文字颜色 (默认 '#ffffff'
* @property {String | Number} fontSize 文字大小 (默认 18
* @property {String} icon 显示的图标
* @property {Boolean} mpAvatar 显示小程序头像只对百度微信QQ小程序有效 (默认 false
* @property {Boolean} randomBgColor 是否使用随机背景色 (默认 false
* @property {String} defaultUrl 加载失败的默认头像(组件有内置默认图片)
* @property {String | Number} colorIndex 如果配置了randomBgColor为true且配置了此值则从默认的背景色数组中取出对应索引的颜色值取值0-19之间
* @property {String} name 组件标识符 (默认 'level'
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击组件时触发 index: 用户传递的标识符
* @example <u-avatar :src="src" mode="square"></u-avatar>
*/
export default {
name: 'u-avatar',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 如果配置randomBgColor参数为true在图标或者文字的模式下会随机从中取出一个颜色值当做背景色
colors: ['#ffb34b', '#f2bba9', '#f7a196', '#f18080', '#88a867', '#bfbf39', '#89c152', '#94d554', '#f19ec2',
'#afaae4', '#e1b0df', '#c38cc1', '#72dcdc', '#9acdcb', '#77b1cc', '#448aca', '#86cefa', '#98d1ee',
'#73d1f1',
'#80a7dc'
],
avatarUrl: this.src,
allowMp: false
}
},
watch: {
// 监听头像src的变化赋值给内部的avatarUrl变量因为图片加载失败时需要修改图片的src为默认值
// 而组件内部不能直接修改props的值所以需要一个中间变量
src: {
immediate: true,
handler(newVal) {
this.avatarUrl = newVal
// 如果没有传src则主动触发error事件用于显示默认的头像否则src为''空字符等的时候,会无内容展示
if(!newVal) {
this.errorHandler()
}
}
}
},
computed: {
imageStyle() {
const style = {}
return style
}
},
created() {
this.init()
},
methods: {
init() {
// 目前只有这几个小程序平台具有open-data标签
// 其他平台可以通过uni.getUserInfo类似接口获取信息但是需要弹窗授权(首次),不合符组件逻辑
// 故目前自动获取小程序头像只支持这几个平台
// #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU
this.allowMp = true
// #endif
},
// 判断传入的name属性是否图片路径只要带有"/"均认为是图片形式
isImg() {
return this.src.indexOf('/') !== -1
},
// 图片加载时失败时触发
errorHandler() {
this.avatarUrl = this.defaultUrl || base64Avatar
},
clickHandler() {
this.$emit('click', this.name)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar {
@include flex;
align-items: center;
justify-content: center;
&--circle {
border-radius: 100px;
}
&--square {
border-radius: 4px;
}
&__image {
&--circle {
border-radius: 100px;
}
&--square {
border-radius: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,54 @@
export default {
props: {
// 返回顶部的形状circle-圆形square-方形
mode: {
type: String,
default: uni.$u.props.backtop.mode
},
// 自定义图标
icon: {
type: String,
default: uni.$u.props.backtop.icon
},
// 提示文字
text: {
type: String,
default: uni.$u.props.backtop.text
},
// 返回顶部滚动时间
duration: {
type: [String, Number],
default: uni.$u.props.backtop.duration
},
// 滚动距离
scrollTop: {
type: [String, Number],
default: uni.$u.props.backtop.scrollTop
},
// 距离顶部多少距离显示单位px
top: {
type: [String, Number],
default: uni.$u.props.backtop.top
},
// 返回顶部按钮到底部的距离单位px
bottom: {
type: [String, Number],
default: uni.$u.props.backtop.bottom
},
// 返回顶部按钮到右边的距离单位px
right: {
type: [String, Number],
default: uni.$u.props.backtop.right
},
// 层级
zIndex: {
type: [String, Number],
default: uni.$u.props.backtop.zIndex
},
// 图标的样式,对象形式
iconStyle: {
type: Object,
default: uni.$u.props.backtop.iconStyle
}
}
}

View File

@@ -0,0 +1,129 @@
<template>
<u-transition
mode="fade"
:customStyle="backTopStyle"
:show="show"
>
<view
class="u-back-top"
:style="[contentStyle]"
v-if="!$slots.default && !$slots.$default"
@click="backToTop"
>
<u-icon
:name="icon"
:custom-style="iconStyle"
></u-icon>
<text
v-if="text"
class="u-back-top__text"
>{{text}}</text>
</view>
<slot v-else />
</u-transition>
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
/**
* backTop 返回顶部
* @description 本组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
* @tutorial https://uviewui.com/components/backTop.html
*
* @property {String} mode 返回顶部的形状circle-圆形square-方形 (默认 'circle'
* @property {String} icon 自定义图标 (默认 'arrow-upward' 见官方文档示例
* @property {String} text 提示文字
* @property {String | Number} duration 返回顶部滚动时间 (默认 100
* @property {String | Number} scrollTop 滚动距离 (默认 0
* @property {String | Number} top 距离顶部多少距离显示单位px (默认 400
* @property {String | Number} bottom 返回顶部按钮到底部的距离单位px (默认 100
* @property {String | Number} right 返回顶部按钮到右边的距离单位px (默认 20
* @property {String | Number} zIndex 层级 (默认 9
* @property {Object<Object>} iconStyle 图标的样式,对象形式 (默认 {color: '#909399',fontSize: '19px'}
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <u-back-top :scrollTop="scrollTop"></u-back-top>
*/
export default {
name: 'u-back-top',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
backTopStyle() {
// 动画组件样式
const style = {
bottom: uni.$u.addUnit(this.bottom),
right: uni.$u.addUnit(this.right),
width: '40px',
height: '40px',
position: 'fixed',
zIndex: 10,
}
return style
},
show() {
return uni.$u.getPx(this.scrollTop) > uni.$u.getPx(this.top)
},
contentStyle() {
const style = {}
let radius = 0
// 是否圆形
if(this.mode === 'circle') {
radius = '100px'
} else {
radius = '4px'
}
// 为了兼容安卓nvue只能这么分开写
style.borderTopLeftRadius = radius
style.borderTopRightRadius = radius
style.borderBottomLeftRadius = radius
style.borderBottomRightRadius = radius
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
},
methods: {
backToTop() {
// #ifdef APP-NVUE
if (!this.$parent.$refs['u-back-top']) {
uni.$u.error(`nvue页面需要给页面最外层元素设置"ref='u-back-top'`)
}
dom.scrollToElement(this.$parent.$refs['u-back-top'], {
offset: 0
})
// #endif
// #ifndef APP-NVUE
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
// #endif
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-back-top-flex:1 !default;
$u-back-top-height:100% !default;
$u-back-top-background-color:#E1E1E1 !default;
$u-back-top-tips-font-size:12px !default;
.u-back-top {
@include flex;
flex-direction: column;
align-items: center;
flex:$u-back-top-flex;
height: $u-back-top-height;
justify-content: center;
background-color: $u-back-top-background-color;
&__tips {
font-size:$u-back-top-tips-font-size;
transform: scale(0.8);
}
}
</style>

View File

@@ -0,0 +1,72 @@
export default {
props: {
// 是否显示圆点
isDot: {
type: Boolean,
default: uni.$u.props.badge.isDot
},
// 显示的内容
value: {
type: [Number, String],
default: uni.$u.props.badge.value
},
// 是否显示
show: {
type: Boolean,
default: uni.$u.props.badge.show
},
// 最大值,超过最大值会显示 '{max}+'
max: {
type: [Number, String],
default: uni.$u.props.badge.max
},
// 主题类型error|warning|success|primary
type: {
type: String,
default: uni.$u.props.badge.type
},
// 当数值为 0 时,是否展示 Badge
showZero: {
type: Boolean,
default: uni.$u.props.badge.showZero
},
// 背景颜色优先级比type高如设置type参数会失效
bgColor: {
type: [String, null],
default: uni.$u.props.badge.bgColor
},
// 字体颜色
color: {
type: [String, null],
default: uni.$u.props.badge.color
},
// 徽标形状circle-四角均为圆角horn-左下角为直角
shape: {
type: String,
default: uni.$u.props.badge.shape
},
// 设置数字的显示方式overflow|ellipsis|limit
// overflow会根据max字段判断超出显示`${max}+`
// ellipsis会根据max判断超出显示`${max}...`
// limit会依据1000作为判断条件超出1000显示`${value/1000}K`比如2.2k、3.34w最多保留2位小数
numberType: {
type: String,
default: uni.$u.props.badge.numberType
},
// 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值absolute为true时有效
offset: {
type: Array,
default: uni.$u.props.badge.offset
},
// 是否反转背景和字体颜色
inverted: {
type: Boolean,
default: uni.$u.props.badge.inverted
},
// 是否绝对定位
absolute: {
type: Boolean,
default: uni.$u.props.badge.absolute
}
}
}

View File

@@ -0,0 +1,171 @@
<template>
<text
v-if="show && ((Number(value) === 0 ? showZero : true) || isDot)"
:class="[isDot ? 'u-badge--dot' : 'u-badge--not-dot', inverted && 'u-badge--inverted', shape === 'horn' && 'u-badge--horn', `u-badge--${type}${inverted ? '--inverted' : ''}`]"
:style="[$u.addStyle(customStyle), badgeStyle]"
class="u-badge"
>{{ isDot ? '' :showValue }}</text>
</template>
<script>
import props from './props.js';
/**
* badge 徽标数
* @description 该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
* @tutorial https://uviewui.com/components/badge.html
*
* @property {Boolean} isDot 是否显示圆点 (默认 false
* @property {String | Number} value 显示的内容
* @property {Boolean} show 是否显示 (默认 true
* @property {String | Number} max 最大值,超过最大值会显示 '{max}+' 默认999
* @property {String} type 主题类型error|warning|success|primary (默认 'error'
* @property {Boolean} showZero 当数值为 0 时,是否展示 Badge (默认 false
* @property {String} bgColor 背景颜色优先级比type高如设置type参数会失效
* @property {String} color 字体颜色 (默认 '#ffffff'
* @property {String} shape 徽标形状circle-四角均为圆角horn-左下角为直角 (默认 'circle'
* @property {String} numberType 设置数字的显示方式overflow|ellipsis|limit (默认 'overflow'
* @property {Array}} offset 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值absolute为true时有效
* @property {Boolean} inverted 是否反转背景和字体颜色(默认 false
* @property {Boolean} absolute 是否绝对定位(默认 false
* @property {Object} customStyle 定义需要用到的外部样式
* @example <u-badge :type="type" :count="count"></u-badge>
*/
export default {
name: 'u-badge',
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
computed: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
let style = {};
return style;
},
// 整个组件的样式
badgeStyle() {
const style = {}
if(this.color) {
style.color = this.color
}
if (this.bgColor && !this.inverted) {
style.backgroundColor = this.bgColor
}
if (this.absolute) {
style.position = 'absolute'
// 如果有设置offset参数
if(this.offset.length) {
// top和right分为为offset的第一个和第二个值如果没有第二个值则right等于top
const top = this.offset[0]
const right = this.offset[1] || top
style.top = uni.$u.addUnit(top)
style.right = uni.$u.addUnit(right)
}
}
return style
},
showValue() {
switch (this.numberType) {
case "overflow":
return Number(this.value) > Number(this.max) ? this.max + "+" : this.value
break;
case "ellipsis":
return Number(this.value) > Number(this.max) ? "..." : this.value
break;
case "limit":
return Number(this.value) > 999 ? Number(this.value) >= 9999 ?
Math.floor(this.value / 1e4 * 100) / 100 + "w" : Math.floor(this.value /
1e3 * 100) / 100 + "k" : this.value
break;
default:
return Number(this.value)
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-badge-primary: $u-primary !default;
$u-badge-error: $u-error !default;
$u-badge-success: $u-success !default;
$u-badge-info: $u-info !default;
$u-badge-warning: $u-warning !default;
$u-badge-dot-radius: 100px !default;
$u-badge-dot-size: 8px !default;
$u-badge-dot-right: 4px !default;
$u-badge-dot-top: 0 !default;
$u-badge-text-font-size: 11px !default;
$u-badge-text-right: 10px !default;
$u-badge-text-padding: 2px 5px !default;
$u-badge-text-align: center !default;
$u-badge-text-color: #FFFFFF !default;
.u-badge {
border-top-right-radius: $u-badge-dot-radius;
border-top-left-radius: $u-badge-dot-radius;
border-bottom-left-radius: $u-badge-dot-radius;
border-bottom-right-radius: $u-badge-dot-radius;
@include flex;
line-height: $u-badge-text-font-size;
text-align: $u-badge-text-align;
font-size: $u-badge-text-font-size;
color: $u-badge-text-color;
&--dot {
height: $u-badge-dot-size;
width: $u-badge-dot-size;
}
&--inverted {
font-size: 13px;
}
&--not-dot {
padding: $u-badge-text-padding;
}
&--horn {
border-bottom-left-radius: 0;
}
&--primary {
background-color: $u-badge-primary;
}
&--primary--inverted {
color: $u-badge-primary;
}
&--error {
background-color: $u-badge-error;
}
&--error--inverted {
color: $u-badge-error;
}
&--success {
background-color: $u-badge-success;
}
&--success--inverted {
color: $u-badge-success;
}
&--info {
background-color: $u-badge-info;
}
&--info--inverted {
color: $u-badge-info;
}
&--warning {
background-color: $u-badge-warning;
}
&--warning--inverted {
color: $u-badge-warning;
}
}
</style>

View File

@@ -0,0 +1,46 @@
$u-button-active-opacity:0.75 !default;
$u-button-loading-text-margin-left:4px !default;
$u-button-text-color: #FFFFFF !default;
$u-button-text-plain-error-color:$u-error !default;
$u-button-text-plain-warning-color:$u-warning !default;
$u-button-text-plain-success-color:$u-success !default;
$u-button-text-plain-info-color:$u-info !default;
$u-button-text-plain-primary-color:$u-primary !default;
.u-button {
&--active {
opacity: $u-button-active-opacity;
}
&--active--plain {
background-color: rgb(217, 217, 217);
}
&__loading-text {
margin-left:$u-button-loading-text-margin-left;
}
&__text,
&__loading-text {
color:$u-button-text-color;
}
&__text--plain--error {
color:$u-button-text-plain-error-color;
}
&__text--plain--warning {
color:$u-button-text-plain-warning-color;
}
&__text--plain--success{
color:$u-button-text-plain-success-color;
}
&__text--plain--info {
color:$u-button-text-plain-info-color;
}
&__text--plain--primary {
color:$u-button-text-plain-primary-color;
}
}

View File

@@ -0,0 +1,161 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-16 10:04:04
* @LastAuthor : LQ
* @lastTime : 2021-08-16 10:04:24
* @FilePath : /u-view2.0/uview-ui/components/u-button/props.js
*/
export default {
props: {
// 是否细边框
hairline: {
type: Boolean,
default: uni.$u.props.button.hairline
},
// 按钮的预置样式infoprimaryerrorwarningsuccess
type: {
type: String,
default: uni.$u.props.button.type
},
// 按钮尺寸largenormalsmallmini
size: {
type: String,
default: uni.$u.props.button.size
},
// 按钮形状circle两边为半圆square带圆角
shape: {
type: String,
default: uni.$u.props.button.shape
},
// 按钮是否镂空
plain: {
type: Boolean,
default: uni.$u.props.button.plain
},
// 是否禁止状态
disabled: {
type: Boolean,
default: uni.$u.props.button.disabled
},
// 是否加载中
loading: {
type: Boolean,
default: uni.$u.props.button.loading
},
// 加载中提示文字
loadingText: {
type: [String, Number],
default: uni.$u.props.button.loadingText
},
// 加载状态图标类型
loadingMode: {
type: String,
default: uni.$u.props.button.loadingMode
},
// 加载图标大小
loadingSize: {
type: [String, Number],
default: uni.$u.props.button.loadingSize
},
// 开放能力具体请看uniapp稳定关于button组件部分说明
// https://uniapp.dcloud.io/component/button
openType: {
type: String,
default: uni.$u.props.button.openType
},
// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
// 取值为submit提交表单reset重置表单
formType: {
type: String,
default: uni.$u.props.button.formType
},
// 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效
// 只微信小程序、QQ小程序有效
appParameter: {
type: String,
default: uni.$u.props.button.appParameter
},
// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
hoverStopPropagation: {
type: Boolean,
default: uni.$u.props.button.hoverStopPropagation
},
// 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文。只微信小程序有效
lang: {
type: String,
default: uni.$u.props.button.lang
},
// 会话来源open-type="contact"时有效。只微信小程序有效
sessionFrom: {
type: String,
default: uni.$u.props.button.sessionFrom
},
// 会话内消息卡片标题open-type="contact"时有效
// 默认当前标题,只微信小程序有效
sendMessageTitle: {
type: String,
default: uni.$u.props.button.sendMessageTitle
},
// 会话内消息卡片点击跳转小程序路径open-type="contact"时有效
// 默认当前分享路径,只微信小程序有效
sendMessagePath: {
type: String,
default: uni.$u.props.button.sendMessagePath
},
// 会话内消息卡片图片open-type="contact"时有效
// 默认当前页面截图,只微信小程序有效
sendMessageImg: {
type: String,
default: uni.$u.props.button.sendMessageImg
},
// 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
// 用户点击后可以快速发送小程序消息open-type="contact"时有效
showMessageCard: {
type: Boolean,
default: uni.$u.props.button.showMessageCard
},
// 额外传参参数用于小程序的data-xxx属性通过target.dataset.name获取
dataName: {
type: String,
default: uni.$u.props.button.dataName
},
// 节流,一定时间内只能触发一次
throttleTime: {
type: [String, Number],
default: uni.$u.props.button.throttleTime
},
// 按住后多久出现点击态,单位毫秒
hoverStartTime: {
type: [String, Number],
default: uni.$u.props.button.hoverStartTime
},
// 手指松开后点击态保留时间,单位毫秒
hoverStayTime: {
type: [String, Number],
default: uni.$u.props.button.hoverStayTime
},
// 按钮文字之所以通过props传入是因为slot传入的话
// nvue中无法控制文字的样式
text: {
type: [String, Number],
default: uni.$u.props.button.text
},
// 按钮图标
icon: {
type: String,
default: uni.$u.props.button.icon
},
// 按钮图标
iconColor: {
type: String,
default: uni.$u.props.button.icon
},
// 按钮颜色支持传入linear-gradient渐变色
color: {
type: String,
default: uni.$u.props.button.color
}
}
}

View File

@@ -0,0 +1,490 @@
<template>
<!-- #ifndef APP-NVUE -->
<button
:hover-start-time="Number(hoverStartTime)"
:hover-stay-time="Number(hoverStayTime)"
: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"
:hover-class="!disabled && !loading ? 'u-button--active' : ''"
class="u-button u-reset-button"
:style="[baseColor, $u.addStyle(customStyle)]"
@tap="clickHandler"
:class="bemClass"
>
<template v-if="loading">
<u-loading-icon
:mode="loadingMode"
:size="textSize * 1.15"
:color="loadingColor"
></u-loading-icon>
<text
class="u-button__loading-text"
:style="[{ fontSize: textSize + 'px' }]"
>{{ loadingText || text }}</text
>
</template>
<template v-else>
<u-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
:customStyle="{ marginRight: '2px' }"
></u-icon>
<slot>
<text
class="u-button__text"
:style="[{ fontSize: textSize + 'px' }]"
>{{ text }}</text
>
</slot>
</template>
</button>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view
:hover-start-time="Number(hoverStartTime)"
:hover-stay-time="Number(hoverStayTime)"
class="u-button"
:hover-class="
!disabled && !loading && !color && (plain || type === 'info')
? 'u-button--active--plain'
: !disabled && !loading && !plain
? 'u-button--active'
: ''
"
@tap="clickHandler"
:class="bemClass"
:style="[baseColor, $u.addStyle(customStyle)]"
>
<template v-if="loading">
<u-loading-icon
:mode="loadingMode"
:size="textSize * 1.15"
:color="loadingColor"
></u-loading-icon>
<text
class="u-button__loading-text"
:style="[nvueTextStyle]"
:class="[plain && `u-button__text--plain--${type}`]"
>{{ loadingText || text }}</text
>
</template>
<template v-else>
<u-icon
v-if="icon"
:name="icon"
:color="iconColorCom"
:size="textSize * 1.35"
></u-icon>
<text
class="u-button__text"
:style="[
{
marginLeft: icon ? '2px' : 0,
},
nvueTextStyle,
]"
:class="[plain && `u-button__text--plain--${type}`]"
>{{ text }}</text
>
</template>
</view>
<!-- #endif -->
</template>
<script>
import button from "../../libs/mixin/button.js";
import openType from "../../libs/mixin/openType.js";
import props from "./props.js";
/**
* button 按钮
* @description Button 按钮
* @tutorial https://www.uviewui.com/components/button.html
*
* @property {Boolean} hairline 是否显示按钮的细边框 (默认 true )
* @property {String} type 按钮的预置样式infoprimaryerrorwarningsuccess (默认 'info' )
* @property {String} size 按钮尺寸largenormalmini (默认 normal
* @property {String} shape 按钮形状circle两边为半圆square带圆角 (默认 'square'
* @property {Boolean} plain 按钮是否镂空,背景色透明 (默认 false
* @property {Boolean} disabled 是否禁用 (默认 false
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花Android上为圆圈) (默认 false
* @property {String | Number} loadingText 加载中提示文字
* @property {String} loadingMode 加载状态图标类型 (默认 'spinner'
* @property {String | Number} loadingSize 加载图标大小 (默认 15
* @property {String} openType 开放能力具体请看uniapp稳定关于button组件部分说明
* @property {String} formType 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效 只微信小程序、QQ小程序有效
* @property {Boolean} hoverStopPropagation 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效(默认 true
* @property {String} lang 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文(默认 en
* @property {String} sessionFrom 会话来源openType="contact"时有效
* @property {String} sendMessageTitle 会话内消息卡片标题openType="contact"时有效
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径openType="contact"时有效
* @property {String} sendMessageImg 会话内消息卡片图片openType="contact"时有效
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息openType="contact"时有效默认false
* @property {String} dataName 额外传参参数用于小程序的data-xxx属性通过target.dataset.name获取
* @property {String | Number} throttleTime 节流,一定时间内只能触发一次 (默认 0 )
* @property {String | Number} hoverStartTime 按住后多久出现点击态,单位毫秒 (默认 0 )
* @property {String | Number} hoverStayTime 手指松开后点击态保留时间,单位毫秒 (默认 200 )
* @property {String | Number} text 按钮文字之所以通过props传入是因为slot传入的话nvue中无法控制文字的样式
* @property {String} icon 按钮图标
* @property {String} iconColor 按钮图标颜色
* @property {String} color 按钮颜色支持传入linear-gradient渐变色
* @property {Object} customStyle 定义需要用到的外部样式
*
* @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",
// #ifdef MP
mixins: [uni.$u.mpMixin, uni.$u.mixin, button, openType, props],
// #endif
// #ifndef MP
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
// #endif
data() {
return {};
},
computed: {
// 生成bem风格的类名
bemClass() {
// this.bem为一个computed变量在mixin中
if (!this.color) {
return this.bem(
"button",
["type", "shape", "size"],
["disabled", "plain", "hairline"]
);
} else {
// 由于nvue的原因在有color参数时不需要传入type否则会生成type相关的类型影响最终的样式
return this.bem(
"button",
["shape", "size"],
["disabled", "plain", "hairline"]
);
}
},
loadingColor() {
if (this.plain) {
// 如果有设置color值则用color值否则使用type主题颜色
return this.color
? this.color
: uni.$u.config.color[`u-${this.type}`];
}
if (this.type === "info") {
return "#c9c9c9";
}
return "rgb(200, 200, 200)";
},
iconColorCom() {
// 如果是镂空状态设置了color就用color值否则使用主题颜色
// u-icon的color能接受一个主题颜色的值
if (this.iconColor) return this.iconColor;
if (this.plain) {
return this.color ? this.color : this.type;
} else {
return this.type === "info" ? "#000000" : "#ffffff";
}
},
baseColor() {
let style = {};
if (this.color) {
// 针对自定义了color颜色的情况镂空状态下就是用自定义的颜色
style.color = this.plain ? this.color : "white";
if (!this.plain) {
// 非镂空,背景色使用自定义的颜色
style["background-color"] = this.color;
}
if (this.color.indexOf("gradient") !== -1) {
// 如果自定义的颜色为渐变色不显示边框以及通过backgroundImage设置渐变色
// weex文档说明可以写borderWidth的形式为什么这里需要分开写
// 因为weex是阿里巴巴为了部门业绩考核而做的你懂的东西所以需要这么写才有效
style.borderTopWidth = 0;
style.borderRightWidth = 0;
style.borderBottomWidth = 0;
style.borderLeftWidth = 0;
if (!this.plain) {
style.backgroundImage = this.color;
}
} else {
// 非渐变色,则设置边框相关的属性
style.borderColor = this.color;
style.borderWidth = "1px";
style.borderStyle = "solid";
}
}
return style;
},
// nvue版本按钮的字体不会继承父组件的颜色需要对每一个text组件进行单独的设置
nvueTextStyle() {
let style = {};
// 针对自定义了color颜色的情况镂空状态下就是用自定义的颜色
if (this.type === "info") {
style.color = "#323233";
}
if (this.color) {
style.color = this.plain ? this.color : "white";
}
style.fontSize = this.textSize + "px";
return style;
},
// 字体大小
textSize() {
let fontSize = 14,
{ size } = this;
if (size === "large") fontSize = 16;
if (size === "normal") fontSize = 14;
if (size === "small") fontSize = 12;
if (size === "mini") fontSize = 10;
return fontSize;
},
},
methods: {
clickHandler() {
// 非禁止并且非加载中,才能点击
if (!this.disabled && !this.loading) {
// 进行节流控制每this.throttle毫秒内只在开始处执行
uni.$u.throttle(() => {
this.$emit("click");
}, this.throttleTime);
}
},
// 下面为对接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 lang="scss" scoped>
@import "../../libs/css/components.scss";
/* #ifndef APP-NVUE */
@import "./vue.scss";
/* #endif */
/* #ifdef APP-NVUE */
@import "./nvue.scss";
/* #endif */
$u-button-u-button-height: 40px !default;
$u-button-text-font-size: 15px !default;
$u-button-loading-text-font-size: 15px !default;
$u-button-loading-text-margin-left: 4px !default;
$u-button-large-width: 100% !default;
$u-button-large-height: 50px !default;
$u-button-normal-padding: 0 12px !default;
$u-button-large-padding: 0 15px !default;
$u-button-normal-font-size: 14px !default;
$u-button-small-min-width: 60px !default;
$u-button-small-height: 30px !default;
$u-button-small-padding: 0px 8px !default;
$u-button-mini-padding: 0px 8px !default;
$u-button-small-font-size: 12px !default;
$u-button-mini-height: 22px !default;
$u-button-mini-font-size: 10px !default;
$u-button-mini-min-width: 50px !default;
$u-button-disabled-opacity: 0.5 !default;
$u-button-info-color: #323233 !default;
$u-button-info-background-color: #fff !default;
$u-button-info-border-color: #ebedf0 !default;
$u-button-info-border-width: 1px !default;
$u-button-info-border-style: solid !default;
$u-button-success-color: #fff !default;
$u-button-success-background-color: $u-success !default;
$u-button-success-border-color: $u-button-success-background-color !default;
$u-button-success-border-width: 1px !default;
$u-button-success-border-style: solid !default;
$u-button-primary-color: #fff !default;
$u-button-primary-background-color: $u-primary !default;
$u-button-primary-border-color: $u-button-primary-background-color !default;
$u-button-primary-border-width: 1px !default;
$u-button-primary-border-style: solid !default;
$u-button-error-color: #fff !default;
$u-button-error-background-color: $u-error !default;
$u-button-error-border-color: $u-button-error-background-color !default;
$u-button-error-border-width: 1px !default;
$u-button-error-border-style: solid !default;
$u-button-warning-color: #fff !default;
$u-button-warning-background-color: $u-warning !default;
$u-button-warning-border-color: $u-button-warning-background-color !default;
$u-button-warning-border-width: 1px !default;
$u-button-warning-border-style: solid !default;
$u-button-block-width: 100% !default;
$u-button-circle-border-top-right-radius: 100px !default;
$u-button-circle-border-top-left-radius: 100px !default;
$u-button-circle-border-bottom-left-radius: 100px !default;
$u-button-circle-border-bottom-right-radius: 100px !default;
$u-button-square-border-top-right-radius: 3px !default;
$u-button-square-border-top-left-radius: 3px !default;
$u-button-square-border-bottom-left-radius: 3px !default;
$u-button-square-border-bottom-right-radius: 3px !default;
$u-button-icon-min-width: 1em !default;
$u-button-plain-background-color: #fff !default;
$u-button-hairline-border-width: 0.5px !default;
.u-button {
height: $u-button-u-button-height;
position: relative;
align-items: center;
justify-content: center;
@include flex;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
flex-direction: row;
&__text {
font-size: $u-button-text-font-size;
}
&__loading-text {
font-size: $u-button-loading-text-font-size;
margin-left: $u-button-loading-text-margin-left;
}
&--large {
/* #ifndef APP-NVUE */
width: $u-button-large-width;
/* #endif */
height: $u-button-large-height;
padding: $u-button-large-padding;
}
&--normal {
padding: $u-button-normal-padding;
font-size: $u-button-normal-font-size;
}
&--small {
/* #ifndef APP-NVUE */
min-width: $u-button-small-min-width;
/* #endif */
height: $u-button-small-height;
padding: $u-button-small-padding;
font-size: $u-button-small-font-size;
}
&--mini {
height: $u-button-mini-height;
font-size: $u-button-mini-font-size;
/* #ifndef APP-NVUE */
min-width: $u-button-mini-min-width;
/* #endif */
padding: $u-button-mini-padding;
}
&--disabled {
opacity: $u-button-disabled-opacity;
}
&--info {
color: $u-button-info-color;
background-color: $u-button-info-background-color;
border-color: $u-button-info-border-color;
border-width: $u-button-info-border-width;
border-style: $u-button-info-border-style;
}
&--success {
color: $u-button-success-color;
background-color: $u-button-success-background-color;
border-color: $u-button-success-border-color;
border-width: $u-button-success-border-width;
border-style: $u-button-success-border-style;
}
&--primary {
color: $u-button-primary-color;
background-color: $u-button-primary-background-color;
border-color: $u-button-primary-border-color;
border-width: $u-button-primary-border-width;
border-style: $u-button-primary-border-style;
}
&--error {
color: $u-button-error-color;
background-color: $u-button-error-background-color;
border-color: $u-button-error-border-color;
border-width: $u-button-error-border-width;
border-style: $u-button-error-border-style;
}
&--warning {
color: $u-button-warning-color;
background-color: $u-button-warning-background-color;
border-color: $u-button-warning-border-color;
border-width: $u-button-warning-border-width;
border-style: $u-button-warning-border-style;
}
&--block {
@include flex;
width: $u-button-block-width;
}
&--circle {
border-top-right-radius: $u-button-circle-border-top-right-radius;
border-top-left-radius: $u-button-circle-border-top-left-radius;
border-bottom-left-radius: $u-button-circle-border-bottom-left-radius;
border-bottom-right-radius: $u-button-circle-border-bottom-right-radius;
}
&--square {
border-bottom-left-radius: $u-button-square-border-top-right-radius;
border-bottom-right-radius: $u-button-square-border-top-left-radius;
border-top-left-radius: $u-button-square-border-bottom-left-radius;
border-top-right-radius: $u-button-square-border-bottom-right-radius;
}
&__icon {
/* #ifndef APP-NVUE */
min-width: $u-button-icon-min-width;
line-height: inherit !important;
vertical-align: top;
/* #endif */
}
&--plain {
background-color: $u-button-plain-background-color;
}
&--hairline {
border-width: $u-button-hairline-border-width !important;
}
}
</style>

View File

@@ -0,0 +1,80 @@
// nvue下hover-class无效
$u-button-before-top:50% !default;
$u-button-before-left:50% !default;
$u-button-before-width:100% !default;
$u-button-before-height:100% !default;
$u-button-before-transform:translate(-50%, -50%) !default;
$u-button-before-opacity:0 !default;
$u-button-before-background-color:#000 !default;
$u-button-before-border-color:#000 !default;
$u-button-active-before-opacity:.15 !default;
$u-button-icon-margin-left:4px !default;
$u-button-plain-u-button-info-color:$u-info;
$u-button-plain-u-button-success-color:$u-success;
$u-button-plain-u-button-error-color:$u-error;
$u-button-plain-u-button-warning-color:$u-error;
.u-button {
width: 100%;
&__text {
white-space: nowrap;
line-height: 1;
}
&:before {
position: absolute;
top:$u-button-before-top;
left:$u-button-before-left;
width:$u-button-before-width;
height:$u-button-before-height;
border: inherit;
border-radius: inherit;
transform:$u-button-before-transform;
opacity:$u-button-before-opacity;
content: " ";
background-color:$u-button-before-background-color;
border-color:$u-button-before-border-color;
}
&--active {
&:before {
opacity: .15
}
}
&__icon+&__text:not(:empty),
&__loading-text {
margin-left:$u-button-icon-margin-left;
}
&--plain {
&.u-button--primary {
color: $u-primary;
}
}
&--plain {
&.u-button--info {
color:$u-button-plain-u-button-info-color;
}
}
&--plain {
&.u-button--success {
color:$u-button-plain-u-button-success-color;
}
}
&--plain {
&.u-button--error {
color:$u-button-plain-u-button-error-color;
}
}
&--plain {
&.u-button--warning {
color:$u-button-plain-u-button-warning-color;
}
}
}

View File

@@ -0,0 +1,99 @@
<template>
<view class="u-calendar-header u-border-bottom">
<text
class="u-calendar-header__title"
v-if="showTitle"
>{{ title }}</text>
<text
class="u-calendar-header__subtitle"
v-if="showSubtitle"
>{{ subtitle }}</text>
<view class="u-calendar-header__weekdays">
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
<text class="u-calendar-header__weekdays__weekday"></text>
</view>
</view>
</template>
<script>
export default {
name: 'u-calendar-header',
mixins: [uni.$u.mpMixin, uni.$u.mixin],
props: {
// 标题
title: {
type: String,
default: ''
},
// 副标题
subtitle: {
type: String,
default: ''
},
// 是否显示标题
showTitle: {
type: Boolean,
default: true
},
// 是否显示副标题
showSubtitle: {
type: Boolean,
default: true
},
},
data() {
return {
}
},
methods: {
name() {
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-calendar-header {
padding-bottom: 4px;
&__title {
font-size: 16px;
color: $u-main-color;
text-align: center;
height: 42px;
line-height: 42px;
font-weight: bold;
}
&__subtitle {
font-size: 14px;
color: $u-main-color;
height: 40px;
text-align: center;
line-height: 40px;
font-weight: bold;
}
&__weekdays {
@include flex;
justify-content: space-between;
&__weekday {
font-size: 13px;
color: $u-main-color;
line-height: 30px;
flex: 1;
text-align: center;
}
}
}
</style>

View File

@@ -0,0 +1,579 @@
<template>
<view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper">
<view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]"
:ref="`u-calendar-month-${index}`" :id="`month-${index}`">
<text v-if="index !== 0" class="u-calendar-month__title">{{ item.year }}{{ item.month }}</text>
<view class="u-calendar-month__days">
<view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper">
<text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text>
</view>
<view class="u-calendar-month__days__day" v-for="(item1, index1) in item.date" :key="index1"
:style="[dayStyle(index, index1, item1)]" @tap="clickHandler(index, index1, item1)"
:class="[item1.selected && 'u-calendar-month__days__day__select--selected']">
<view class="u-calendar-month__days__day__select" :style="[daySelectStyle(index, index1, item1)]">
<text class="u-calendar-month__days__day__select__info"
:class="[item1.disabled && 'u-calendar-month__days__day__select__info--disabled']"
:style="[textStyle(item1)]">{{ item1.day }}</text>
<text v-if="getBottomInfo(index, index1, item1)"
class="u-calendar-month__days__day__select__buttom-info"
:class="[item1.disabled && 'u-calendar-month__days__day__select__buttom-info--disabled']"
:style="[textStyle(item1)]">{{ getBottomInfo(index, index1, item1) }}</text>
<text v-if="item1.dot" class="u-calendar-month__days__day__select__dot"></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
// 由于nvue不支持百分比单位需要查询宽度来计算每个日期的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
import dayjs from '../../libs/util/dayjs.js';
export default {
name: 'u-calendar-month',
mixins: [uni.$u.mpMixin, uni.$u.mixin],
props: {
// 是否显示月份背景色
showMark: {
type: Boolean,
default: true
},
// 主题色,对底部按钮和选中日期有效
color: {
type: String,
default: '#3c9cff'
},
// 月份数据
months: {
type: Array,
default: () => []
},
// 日期选择类型
mode: {
type: String,
default: 'single'
},
// 日期行高
rowHeight: {
type: [String, Number],
default: 58
},
// mode=multiple时最多可选多少个日期
maxCount: {
type: [String, Number],
default: Infinity
},
// mode=range时第一个日期底部的提示文字
startText: {
type: String,
default: '开始'
},
// mode=range时最后一个日期底部的提示文字
endText: {
type: String,
default: '结束'
},
// 默认选中的日期mode为multiple或range是必须为数组格式
defaultDate: {
type: [Array, String, Date],
default: null
},
// 最小的可选日期
minDate: {
type: [String, Number],
default: 0
},
// 最大可选日期
maxDate: {
type: [String, Number],
default: 0
},
// 如果没有设置maxDate则往后推多少个月
maxMonth: {
type: [String, Number],
default: 2
},
// 是否为只读状态,只读状态下禁止选择日期
readonly: {
type: Boolean,
default: uni.$u.props.calendar.readonly
},
// 日期区间最多可选天数默认无限制mode = range时有效
maxRange: {
type: [Number, String],
default: Infinity
},
// 范围选择超过最多可选天数时的提示文案mode = range时有效
rangePrompt: {
type: String,
default: ''
},
// 范围选择超过最多可选天数时是否展示提示文案mode = range时有效
showRangePrompt: {
type: Boolean,
default: true
},
// 是否允许日期范围的起止时间为同一天mode = range时有效
allowSameDay: {
type: Boolean,
default: false
}
},
data() {
return {
// 每个日期的宽度
width: 0,
// 当前选中的日期item
item: {},
selected: []
}
},
watch: {
selectedChange: {
immediate: true,
handler(n) {
this.setDefaultDate()
}
}
},
computed: {
// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
selectedChange() {
return [this.minDate, this.maxDate, this.defaultDate]
},
dayStyle(index1, index2, item) {
return (index1, index2, item) => {
const style = {}
let week = item.week
// 不进行四舍五入的形式保留2位小数
const dayWidth = Number(parseFloat(this.width / 7).toFixed(3).slice(0, -1))
// 得出每个日期的宽度
// #ifdef APP-NVUE
style.width = uni.$u.addUnit(dayWidth)
// #endif
style.height = uni.$u.addUnit(this.rowHeight)
if (index2 === 0) {
// 获取当前为星期几如果为0则为星期天减一为每月第一天时需要向左偏移的item个数
week = (week === 0 ? 7 : week) - 1
style.marginLeft = uni.$u.addUnit(week * dayWidth)
}
if (this.mode === 'range') {
// 之所以需要这么写是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
style.paddingLeft = 0
style.paddingRight = 0
style.paddingBottom = 0
style.paddingTop = 0
}
return style
}
},
daySelectStyle() {
return (index1, index2, item) => {
let date = dayjs(item.date).format("YYYY-MM-DD"),
style = {}
// 判断date是否在selected数组中因为月份可能会需要补0所以使用dateSame判断而不用数组的includes判断
if (this.selected.some(item => this.dateSame(item, date))) {
style.backgroundColor = this.color
}
if (this.mode === 'single') {
if (date === this.selected[0]) {
// 因为需要对nvue的兼容只能这么写无法缩写也无法通过类名控制等等
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
style.borderTopRightRadius = '3px'
style.borderBottomRightRadius = '3px'
}
} else if (this.mode === 'range') {
if (this.selected.length >= 2) {
const len = this.selected.length - 1
// 第一个日期设置左上角和左下角的圆角
if (this.dateSame(date, this.selected[0])) {
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
}
// 最后一个日期设置右上角和右下角的圆角
if (this.dateSame(date, this.selected[len])) {
style.borderTopRightRadius = '3px'
style.borderBottomRightRadius = '3px'
}
// 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
.selected[len]))) {
style.backgroundColor = uni.$u.colorGradient(this.color, '#ffffff', 100)[90]
// 增加一个透明度让范围区间的背景色也能看到底部的mark水印字符
style.opacity = 0.7
}
} else if (this.selected.length === 1) {
// 之所以需要这么写是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
// 进行还原操作否则在nvue的iOSuni-app有bug会导致诡异的表现
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
}
} else {
if (this.selected.some(item => this.dateSame(item, date))) {
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
style.borderTopRightRadius = '3px'
style.borderBottomRightRadius = '3px'
}
}
return style
}
},
// 某个日期是否被选中
textStyle() {
return (item) => {
const date = dayjs(item.date).format("YYYY-MM-DD"),
style = {}
// 选中的日期,提示文字设置白色
if (this.selected.some(item => this.dateSame(item, date))) {
style.color = '#ffffff'
}
if (this.mode === 'range') {
const len = this.selected.length - 1
// 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
.selected[len]))) {
style.color = this.color
}
}
return style
}
},
// 获取底部的提示文字
getBottomInfo() {
return (index1, index2, item) => {
const date = dayjs(item.date).format("YYYY-MM-DD")
const bottomInfo = item.bottomInfo
// 当为日期范围模式时且选择的日期个数大于0时
if (this.mode === 'range' && this.selected.length > 0) {
if (this.selected.length === 1) {
// 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
if (this.dateSame(date, this.selected[0])) return this.startText
else return bottomInfo
} else {
const len = this.selected.length - 1
// 如果数组中的日期大于2个时第一个和最后一个显示为开始和结束日期
if (this.dateSame(date, this.selected[0]) && this.dateSame(date, this.selected[1]) &&
len === 1) {
// 如果长度为2且第一个等于第二个日期则提示语放在同一个item中
return `${this.startText}/${this.endText}`
} else if (this.dateSame(date, this.selected[0])) {
return this.startText
} else if (this.dateSame(date, this.selected[len])) {
return this.endText
} else {
return bottomInfo
}
}
} else {
return bottomInfo
}
}
}
},
mounted() {
this.init()
},
methods: {
init() {
// 初始化默认选中
this.$emit('monthSelected', this.selected)
this.$nextTick(() => {
// 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
// 因为nvue下$nextTick并不是100%可靠的
uni.$u.sleep(10).then(() => {
this.getWrapperWidth()
this.getMonthRect()
})
})
},
// 判断两个日期是否相等
dateSame(date1, date2) {
return dayjs(date1).isSame(dayjs(date2))
},
// 获取月份数据区域的宽度因为nvue不支持百分比所以无法通过css设置每个日期item的宽度
getWrapperWidth() {
// #ifdef APP-NVUE
dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => {
this.width = res.size.width
})
// #endif
// #ifndef APP-NVUE
this.$uGetRect('.u-calendar-month-wrapper').then(size => {
this.width = size.width
})
// #endif
},
getMonthRect() {
// 获取每个月份数据的尺寸用于父组件在scroll-view滚动事件中监听当前滚动到了第几个月份
const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise(
`u-calendar-month-${index}`))
// 一次性返回
Promise.all(promiseAllArr).then(
sizes => {
let height = 1
const topArr = []
for (let i = 0; i < this.months.length; i++) {
// 添加到months数组中供scroll-view滚动事件中判断当前滚动到哪个月份
topArr[i] = height
height += sizes[i].height
}
// 由于微信下无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值所以使用事件形式对外发出
this.$emit('updateMonthTop', topArr)
})
},
// 获取每个月份区域的尺寸
getMonthRectByPromise(el) {
// #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法详见文档介绍https://www.uviewui.com/js/getRect.html
// 组件内部一般用this.$uGetRect对外的为uni.$u.getRect二者功能一致名称不同
return new Promise(resolve => {
this.$uGetRect(`.${el}`).then(size => {
resolve(size)
})
})
// #endif
// #ifdef APP-NVUE
// nvue下使用dom模块查询元素高度
// 返回一个promise让调用此方法的主体能使用then回调
return new Promise(resolve => {
dom.getComponentRect(this.$refs[el][0], res => {
resolve(res.size)
})
})
// #endif
},
// 点击某一个日期
clickHandler(index1, index2, item) {
if (this.readonly) {
return;
}
this.item = item
const date = dayjs(item.date).format("YYYY-MM-DD")
if (item.disabled) return
// 对上一次选择的日期数组进行深度克隆
let selected = uni.$u.deepClone(this.selected)
if (this.mode === 'single') {
// 单选情况下,让数组中的元素为当前点击的日期
selected = [date]
} else if (this.mode === 'multiple') {
if (selected.some(item => this.dateSame(item, date))) {
// 如果点击的日期已在数组中,则进行移除操作,也就是达到反选的效果
const itemIndex = selected.findIndex(item => item === date)
selected.splice(itemIndex, 1)
} else {
// 如果点击的日期不在数组中,且已有的长度小于总可选长度时,则添加到数组中去
if (selected.length < this.maxCount) selected.push(date)
}
} else {
// 选择区间形式
if (selected.length === 0 || selected.length >= 2) {
// 如果原来就为0或者大于2的长度则当前点击的日期就是开始日期
selected = [date]
} else if (selected.length === 1) {
// 如果已经选择了开始日期
const existsDate = selected[0]
// 如果当前选择的日期小于上一次选择的日期,则当前的日期定为开始日期
if (dayjs(date).isBefore(existsDate)) {
selected = [date]
} else if (dayjs(date).isAfter(existsDate)) {
// 当前日期减去最大可选的日期天数,如果大于起始时间,则进行提示
if(dayjs(dayjs(date).subtract(this.maxRange, 'day')).isAfter(dayjs(selected[0])) && this.showRangePrompt) {
if(this.rangePrompt) {
uni.$u.toast(this.rangePrompt)
} else {
uni.$u.toast(`选择天数不能超过 ${this.maxRange}`)
}
return
}
// 如果当前日期大于已有日期,将当前的添加到数组尾部
selected.push(date)
const startDate = selected[0]
const endDate = selected[1]
const arr = []
let i = 0
do {
// 将开始和结束日期之间的日期添加到数组中
arr.push(dayjs(startDate).add(i, 'day').format("YYYY-MM-DD"))
i++
// 累加的日期小于结束日期时,继续下一次的循环
} while (dayjs(startDate).add(i, 'day').isBefore(dayjs(endDate)))
// 为了一次性修改数组避免computed中多次触发这里才用arr变量一次性赋值的方式同时将最后一个日期添加近来
arr.push(endDate)
selected = arr
} else {
// 选择区间时,只有一个日期的情况下,且不允许选择起止为同一天的话,不允许选择自己
if (selected[0] === date && !this.allowSameDay) return
selected.push(date)
}
}
}
this.setSelected(selected)
},
// 设置默认日期
setDefaultDate() {
if (!this.defaultDate) {
// 如果没有设置默认日期,则将当天日期设置为默认选中的日期
const selected = [dayjs().format("YYYY-MM-DD")]
return this.setSelected(selected, false)
}
let defaultDate = []
const minDate = this.minDate || dayjs().format("YYYY-MM-DD")
const maxDate = this.maxDate || dayjs(minDate).add(this.maxMonth - 1, 'month').format("YYYY-MM-DD")
if (this.mode === 'single') {
// 单选模式可以是字符串或数组Date对象等
if (!uni.$u.test.array(this.defaultDate)) {
defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")]
} else {
defaultDate = [this.defaultDate[0]]
}
} else {
// 如果为非数组,则不执行
if (!uni.$u.test.array(this.defaultDate)) return
defaultDate = this.defaultDate
}
// 过滤用户传递的默认数组,取出只在可允许最大值与最小值之间的元素
defaultDate = defaultDate.filter(item => {
return dayjs(item).isAfter(dayjs(minDate).subtract(1, 'day')) && dayjs(item).isBefore(dayjs(
maxDate).add(1, 'day'))
})
this.setSelected(defaultDate, false)
},
setSelected(selected, event = true) {
this.selected = selected
event && this.$emit('monthSelected', this.selected)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-calendar-month-wrapper {
margin-top: 4px;
}
.u-calendar-month {
&__title {
font-size: 14px;
line-height: 42px;
height: 42px;
color: $u-main-color;
text-align: center;
font-weight: bold;
}
&__days {
position: relative;
@include flex;
flex-wrap: wrap;
&__month-mark-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
@include flex;
justify-content: center;
align-items: center;
&__text {
font-size: 155px;
color: rgba(231, 232, 234, 0.83);
}
}
&__day {
@include flex;
padding: 2px;
/* #ifndef APP-NVUE */
// vue下使用css进行宽度计算因为某些安卓机会无法进行js获取父元素宽度进行计算得出会有偏移
width: calc(100% / 7);
box-sizing: border-box;
/* #endif */
&__select {
flex: 1;
@include flex;
align-items: center;
justify-content: center;
position: relative;
&__dot {
width: 7px;
height: 7px;
border-radius: 100px;
background-color: $u-error;
position: absolute;
top: 12px;
right: 7px;
}
&__buttom-info {
color: $u-content-color;
text-align: center;
position: absolute;
bottom: 5px;
font-size: 10px;
text-align: center;
left: 0;
right: 0;
&--selected {
color: #ffffff;
}
&--disabled {
color: #cacbcd;
}
}
&__info {
text-align: center;
font-size: 16px;
&--selected {
color: #ffffff;
}
&--disabled {
color: #cacbcd;
}
}
&--selected {
background-color: $u-primary;
@include flex;
justify-content: center;
align-items: center;
flex: 1;
border-radius: 3px;
}
&--range-selected {
opacity: 0.3;
border-radius: 0;
}
&--range-start-selected {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&--range-end-selected {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,144 @@
export default {
props: {
// 日历顶部标题
title: {
type: String,
default: uni.$u.props.calendar.title
},
// 是否显示标题
showTitle: {
type: Boolean,
default: uni.$u.props.calendar.showTitle
},
// 是否显示副标题
showSubtitle: {
type: Boolean,
default: uni.$u.props.calendar.showSubtitle
},
// 日期类型选择single-选择单个日期multiple-可以选择多个日期range-选择日期范围
mode: {
type: String,
default: uni.$u.props.calendar.mode
},
// mode=range时第一个日期底部的提示文字
startText: {
type: String,
default: uni.$u.props.calendar.startText
},
// mode=range时最后一个日期底部的提示文字
endText: {
type: String,
default: uni.$u.props.calendar.endText
},
// 自定义列表
customList: {
type: Array,
default: uni.$u.props.calendar.customList
},
// 主题色,对底部按钮和选中日期有效
color: {
type: String,
default: uni.$u.props.calendar.color
},
// 最小的可选日期
minDate: {
type: [String, Number],
default: uni.$u.props.calendar.minDate
},
// 最大可选日期
maxDate: {
type: [String, Number],
default: uni.$u.props.calendar.maxDate
},
// 默认选中的日期mode为multiple或range是必须为数组格式
defaultDate: {
type: [Array, String, Date, null],
default: uni.$u.props.calendar.defaultDate
},
// mode=multiple时最多可选多少个日期
maxCount: {
type: [String, Number],
default: uni.$u.props.calendar.maxCount
},
// 日期行高
rowHeight: {
type: [String, Number],
default: uni.$u.props.calendar.rowHeight
},
// 日期格式化函数
formatter: {
type: [Function, null],
default: uni.$u.props.calendar.formatter
},
// 是否显示农历
showLunar: {
type: Boolean,
default: uni.$u.props.calendar.showLunar
},
// 是否显示月份背景色
showMark: {
type: Boolean,
default: uni.$u.props.calendar.showMark
},
// 确定按钮的文字
confirmText: {
type: String,
default: uni.$u.props.calendar.confirmText
},
// 确认按钮处于禁用状态时的文字
confirmDisabledText: {
type: String,
default: uni.$u.props.calendar.confirmDisabledText
},
// 是否显示日历弹窗
show: {
type: Boolean,
default: uni.$u.props.calendar.show
},
// 是否允许点击遮罩关闭日历
closeOnClickOverlay: {
type: Boolean,
default: uni.$u.props.calendar.closeOnClickOverlay
},
// 是否为只读状态,只读状态下禁止选择日期
readonly: {
type: Boolean,
default: uni.$u.props.calendar.readonly
},
// 是否展示确认按钮
showConfirm: {
type: Boolean,
default: uni.$u.props.calendar.showConfirm
},
// 日期区间最多可选天数默认无限制mode = range时有效
maxRange: {
type: [Number, String],
default: uni.$u.props.calendar.maxRange
},
// 范围选择超过最多可选天数时的提示文案mode = range时有效
rangePrompt: {
type: String,
default: uni.$u.props.calendar.rangePrompt
},
// 范围选择超过最多可选天数时是否展示提示文案mode = range时有效
showRangePrompt: {
type: Boolean,
default: uni.$u.props.calendar.showRangePrompt
},
// 是否允许日期范围的起止时间为同一天mode = range时有效
allowSameDay: {
type: Boolean,
default: uni.$u.props.calendar.allowSameDay
},
// 圆角值
round: {
type: [Boolean, String, Number],
default: uni.$u.props.calendar.round
},
// 最多展示月份数量
monthNum: {
type: [Number, String],
default: 3
}
}
}

View File

@@ -0,0 +1,383 @@
<template>
<u-popup
:show="show"
mode="bottom"
closeable
@close="close"
:round="round"
:closeOnClickOverlay="closeOnClickOverlay"
>
<view class="u-calendar">
<uHeader
:title="title"
:subtitle="subtitle"
:showSubtitle="showSubtitle"
:showTitle="showTitle"
></uHeader>
<scroll-view
:style="{
height: $u.addUnit(listHeight)
}"
scroll-y
@scroll="onScroll"
:scroll-top="scrollTop"
:scrollIntoView="scrollIntoView"
>
<uMonth
:color="color"
:rowHeight="rowHeight"
:showMark="showMark"
:months="months"
:mode="mode"
:maxCount="maxCount"
:startText="startText"
:endText="endText"
:defaultDate="defaultDate"
:minDate="innerMinDate"
:maxDate="innerMaxDate"
:maxMonth="monthNum"
:readonly="readonly"
:maxRange="maxRange"
:rangePrompt="rangePrompt"
:showRangePrompt="showRangePrompt"
:allowSameDay="allowSameDay"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="updateMonthTop"
></uMonth>
</scroll-view>
<slot name="footer" v-if="showConfirm">
<view class="u-calendar__confirm">
<u-button
shape="circle"
:text="
buttonDisabled ? confirmDisabledText : confirmText
"
:color="color"
@click="confirm"
:disabled="buttonDisabled"
></u-button>
</view>
</slot>
</view>
</u-popup>
</template>
<script>
import uHeader from './header.vue'
import uMonth from './month.vue'
import props from './props.js'
import util from './util.js'
import dayjs from '../../libs/util/dayjs.js'
import Calendar from '../../libs/util/calendar.js'
/**
* Calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
* @tutorial https://www.uviewui.com/components/calendar.html
*
* @property {String} title 标题内容 (默认 日期选择 )
* @property {Boolean} showTitle 是否显示标题 (默认 true )
* @property {Boolean} showSubtitle 是否显示副标题 (默认 true )
* @property {String} mode 日期类型选择 single-选择单个日期multiple-可以选择多个日期range-选择日期范围 默认 'single' )
* @property {String} startText mode=range时第一个日期底部的提示文字 (默认 '开始' )
* @property {String} endText mode=range时最后一个日期底部的提示文字 (默认 '结束' )
* @property {Array} customList 自定义列表
* @property {String} color 主题色,对底部按钮和选中日期有效 (默认 #3c9cff' )
* @property {String | Number} minDate 最小的可选日期 (默认 0 )
* @property {String | Number} maxDate 最大可选日期 (默认 0 )
* @property {Array | String| Date} defaultDate 默认选中的日期mode为multiple或range是必须为数组格式
* @property {String | Number} maxCount mode=multiple时最多可选多少个日期 (默认 Number.MAX_SAFE_INTEGER )
* @property {String | Number} rowHeight 日期行高 (默认 56 )
* @property {Function} formatter 日期格式化函数
* @property {Boolean} showLunar 是否显示农历 (默认 false )
* @property {Boolean} showMark 是否显示月份背景色 (默认 true )
* @property {String} confirmText 确定按钮的文字 (默认 '确定' )
* @property {String} confirmDisabledText 确认按钮处于禁用状态时的文字 (默认 '确定' )
* @property {Boolean} show 是否显示日历弹窗 (默认 false )
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false )
* @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false )
* @property {String | Number} maxRange 日期区间最多可选天数默认无限制mode = range时有效
* @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案mode = range时有效
* @property {Boolean} showRangePrompt 范围选择超过最多可选天数时是否展示提示文案mode = range时有效 (默认 true )
* @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天mode = range时有效 (默认 false )
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
* @property {Number|String} monthNum 最多展示的月份数量 (默认 3 )
*
* @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数
* @event {Function()} close 日历关闭时触发 可定义页面关闭时的回调事件
* @example <u-calendar :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm">
</u-calendar>
* */
export default {
name: 'u-calendar',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
components: {
uHeader,
uMonth
},
data() {
return {
// 需要显示的月份的数组
months: [],
// 在月份滚动区域中当前视图中月份的index索引
monthIndex: 0,
// 月份滚动区域的高度
listHeight: 0,
// month组件中选择的日期数组
selected: [],
scrollIntoView: '',
scrollTop:0,
// 过滤处理方法
innerFormatter: (value) => value
}
},
watch: {
selectedChange: {
immediate: true,
handler(n) {
this.setMonth()
}
},
// 打开弹窗时,设置月份数据
show: {
immediate: true,
handler(n) {
this.setMonth()
}
}
},
computed: {
// 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳)但是dayjs如果接受字符串形式的时间戳会有问题这里进行处理
innerMaxDate() {
return uni.$u.test.number(this.maxDate)
? Number(this.maxDate)
: this.maxDate
},
innerMinDate() {
return uni.$u.test.number(this.minDate)
? Number(this.minDate)
: this.minDate
},
// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
selectedChange() {
return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
},
subtitle() {
// 初始化时this.months为空数组所以需要特别判断处理
if (this.months.length) {
return `${this.months[this.monthIndex].year}${
this.months[this.monthIndex].month
}`
} else {
return ''
}
},
buttonDisabled() {
// 如果为range类型且选择的日期个数不足1个时让底部的按钮出于disabled状态
if (this.mode === 'range') {
if (this.selected.length <= 1) {
return true
} else {
return false
}
} else {
return false
}
}
},
mounted() {
this.start = Date.now()
this.init()
},
methods: {
// 在微信小程序中不支持将函数当做props参数故只能通过ref形式调用
setFormatter(e) {
this.innerFormatter = e
},
// month组件内部选择日期后通过事件通知给父组件
monthSelected(e) {
this.selected = e
if (!this.showConfirm) {
// 在不需要确认按钮的情况下如果为单选或者范围多选且已选长度大于2则直接进行返还
if (
this.mode === 'multiple' ||
this.mode === 'single' ||
(this.mode === 'range' && this.selected.length >= 2)
) {
this.$emit('confirm', this.selected)
}
}
},
init() {
// 校验maxDate不能小于当前时间
if (
this.innerMaxDate &&
new Date(this.innerMaxDate).getTime() <= Date.now()
) {
return uni.$u.error('maxDate不能小于当前时间')
}
// 滚动区域的高度
this.listHeight = this.rowHeight * 5 + 30
this.setMonth()
},
close() {
this.$emit('close')
},
// 点击确定按钮
confirm() {
if (!this.buttonDisabled) {
this.$emit('confirm', this.selected)
}
},
// 获得两个日期之间的月份数
getMonths(minDate, maxDate) {
const minYear = dayjs(minDate).year()
const minMonth = dayjs(minDate).month() + 1
const maxYear = dayjs(maxDate).year()
const maxMonth = dayjs(maxDate).month() + 1
return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1
},
// 设置月份数据
setMonth() {
// 最小日期的毫秒数
const minDate = this.innerMinDate || dayjs().valueOf()
// 如果没有指定最大日期则往后推3个月
const maxDate =
this.innerMaxDate ||
dayjs(minDate)
.add(this.monthNum - 1, 'month')
.valueOf()
// 最大最小月份之间的共有多少个月份,
const months = uni.$u.range(
1,
this.monthNum,
this.getMonths(minDate, maxDate)
)
// 先清空数组
this.months = []
for (let i = 0; i < months; i++) {
this.months.push({
date: new Array(
dayjs(minDate).add(i, 'month').daysInMonth()
)
.fill(1)
.map((item, index) => {
// 日期取值1-31
let day = index + 1
// 星期0-60为周日
const week = dayjs(minDate)
.add(i, 'month')
.date(day)
.day()
const date = dayjs(minDate)
.add(i, 'month')
.date(day)
.format('YYYY-MM-DD')
let bottomInfo = ''
if (this.showLunar) {
// 将日期转为农历格式
const lunar = Calendar.solar2lunar(
dayjs(date).year(),
dayjs(date).month() + 1,
dayjs(date).date()
)
bottomInfo = lunar.IDayCn
}
let config = {
day,
week,
// 小于最小允许的日期或者大于最大的日期则设置为disabled状态
disabled:
dayjs(date).isBefore(
dayjs(minDate).format('YYYY-MM-DD')
) ||
dayjs(date).isAfter(
dayjs(maxDate).format('YYYY-MM-DD')
),
// 返回一个日期对象供外部的formatter获取当前日期的年月日等信息进行加工处理
date: new Date(date),
bottomInfo,
dot: false,
month:
dayjs(minDate).add(i, 'month').month() + 1
}
const formatter =
this.formatter || this.innerFormatter
return formatter(config)
}),
// 当前所属的月份
month: dayjs(minDate).add(i, 'month').month() + 1,
// 当前年份
year: dayjs(minDate).add(i, 'month').year()
})
}
},
// 滚动到默认设置的月份
scrollIntoDefaultMonth(selected) {
// 查询默认日期在可选列表的下标
const _index = this.months.findIndex(({
year,
month
}) => {
month = uni.$u.padZero(month)
return `${year}-${month}` === selected
})
if (_index !== -1) {
// #ifndef MP-WEIXIN
this.$nextTick(() => {
this.scrollIntoView = `month-${_index}`
})
// #endif
// #ifdef MP-WEIXIN
this.scrollTop = this.months[_index].top || 0;
// #endif
}
},
// scroll-view滚动监听
onScroll(event) {
// 不允许小于0的滚动值如果scroll-view到顶了继续下拉会出现负数值
const scrollTop = Math.max(0, event.detail.scrollTop)
// 将当前滚动条数值,除以滚动区域的高度,可以得出当前滚动到了哪一个月份的索引
for (let i = 0; i < this.months.length; i++) {
if (scrollTop >= (this.months[i].top || this.listHeight)) {
this.monthIndex = i
}
}
},
// 更新月份的top值
updateMonthTop(topArr = []) {
// 设置对应月份的top值用于onScroll方法更新月份
topArr.map((item, index) => {
this.months[index].top = item
})
// 获取默认日期的下标
if (!this.defaultDate) {
// 如果没有设置默认日期,则将当天日期设置为默认选中的日期
const selected = dayjs().format("YYYY-MM")
this.scrollIntoDefaultMonth(selected)
return
}
let selected = dayjs().format("YYYY-MM");
// 单选模式可以是字符串或数组Date对象等
if (!uni.$u.test.array(this.defaultDate)) {
selected = dayjs(this.defaultDate).format("YYYY-MM")
} else {
selected = dayjs(this.defaultDate[0]).format("YYYY-MM");
}
this.scrollIntoDefaultMonth(selected)
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-calendar {
&__confirm {
padding: 7px 18px;
}
}
</style>

View File

@@ -0,0 +1,85 @@
export default {
methods: {
// 设置月份数据
setMonth() {
// 月初是周几
const day = dayjs(this.date).date(1).day()
const start = day == 0 ? 6 : day - 1
// 本月天数
const days = dayjs(this.date).endOf('month').format('D')
// 上个月天数
const prevDays = dayjs(this.date).endOf('month').subtract(1, 'month').format('D')
// 日期数据
const arr = []
// 清空表格
this.month = []
// 添加上月数据
arr.push(
...new Array(start).fill(1).map((e, i) => {
const day = prevDays - start + i + 1
return {
value: day,
disabled: true,
date: dayjs(this.date).subtract(1, 'month').date(day).format('YYYY-MM-DD')
}
})
)
// 添加本月数据
arr.push(
...new Array(days - 0).fill(1).map((e, i) => {
const day = i + 1
return {
value: day,
date: dayjs(this.date).date(day).format('YYYY-MM-DD')
}
})
)
// 添加下个月
arr.push(
...new Array(42 - days - start).fill(1).map((e, i) => {
const day = i + 1
return {
value: day,
disabled: true,
date: dayjs(this.date).add(1, 'month').date(day).format('YYYY-MM-DD')
}
})
)
// 分割数组
for (let n = 0; n < arr.length; n += 7) {
this.month.push(
arr.slice(n, n + 7).map((e, i) => {
e.index = i + n
// 自定义信息
const custom = this.customList.find((c) => c.date == e.date)
// 农历
if (this.lunar) {
const {
IDayCn,
IMonthCn
} = this.getLunar(e.date)
e.lunar = IDayCn == '初一' ? IMonthCn : IDayCn
}
return {
...e,
...custom
}
})
)
}
}
}
}

View File

@@ -0,0 +1,14 @@
export default {
props: {
// 是否打乱键盘按键的顺序
random: {
type: Boolean,
default: false
},
// 输入一个中文后,是否自动切换到英文
autoChange: {
type: Boolean,
default: false
}
}
}

View File

@@ -0,0 +1,311 @@
<template>
<view
class="u-keyboard"
@touchmove.stop.prevent="noop"
>
<view
v-for="(group, i) in abc ? engKeyBoardList : areaList"
:key="i"
class="u-keyboard__button"
:index="i"
:class="[i + 1 === 4 && 'u-keyboard__button--center']"
>
<view
v-if="i === 3"
class="u-keyboard__button__inner-wrapper"
>
<view
class="u-keyboard__button__inner-wrapper__left"
hover-class="u-hover-class"
:hover-stay-time="200"
@tap="changeCarInputMode"
>
<text
class="u-keyboard__button__inner-wrapper__left__lang"
:class="[!abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
></text>
<text class="u-keyboard__button__inner-wrapper__left__line">/</text>
<text
class="u-keyboard__button__inner-wrapper__left__lang"
:class="[abc && 'u-keyboard__button__inner-wrapper__left__lang--active']"
></text>
</view>
</view>
<view
class="u-keyboard__button__inner-wrapper"
v-for="(item, j) in group"
:key="j"
>
<view
class="u-keyboard__button__inner-wrapper__inner"
:hover-stay-time="200"
@tap="carInputClick(i, j)"
hover-class="u-hover-class"
>
<text class="u-keyboard__button__inner-wrapper__inner__text">{{ item }}</text>
</view>
</view>
<view
v-if="i === 3"
@touchstart="backspaceClick"
@touchend="clearTimer"
class="u-keyboard__button__inner-wrapper"
>
<view
class="u-keyboard__button__inner-wrapper__right"
hover-class="u-hover-class"
:hover-stay-time="200"
>
<u-icon
size="28"
name="backspace"
color="#303133"
></u-icon>
</view>
</view>
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* keyboard 键盘组件
* @description 此为uView自定义的键盘面板内含了数字键盘车牌号键身份证号键盘3种模式都有可以打乱按键顺序的选项。
* @tutorial https://uviewui.com/components/keyboard.html
* @property {Boolean} random 是否打乱键盘的顺序
* @event {Function} change 点击键盘触发
* @event {Function} backspace 点击退格键触发
* @example <u-keyboard ref="uKeyboard" mode="car" v-model="show"></u-keyboard>
*/
export default {
name: "u-keyboard",
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 车牌输入时abc=true为输入车牌号码bac=false为输入省份中文简称
abc: false
};
},
computed: {
areaList() {
let data = [
'京',
'沪',
'粤',
'津',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'陕',
'蒙',
'吉',
'闽',
'贵',
'渝',
'川',
'青',
'琼',
'宁',
'挂',
'藏',
'港',
'澳',
'新',
'使',
'学'
];
let tmp = [];
// 打乱顺序
if (this.random) data = uni.$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 = uni.$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];
// 如果允许自动切换,则将中文状态切换为英文
if (!this.abc && this.autoChange) uni.$u.sleep(200).then(() => this.abc = true)
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/components.scss";
$u-car-keyboard-background-color: rgb(224, 228, 230) !default;
$u-car-keyboard-padding:6px 0 6px !default;
$u-car-keyboard-button-inner-width:64rpx !default;
$u-car-keyboard-button-inner-background-color:#FFFFFF !default;
$u-car-keyboard-button-height:80rpx !default;
$u-car-keyboard-button-inner-box-shadow:0 1px 0px #999992 !default;
$u-car-keyboard-button-border-radius:4px !default;
$u-car-keyboard-button-inner-margin:8rpx 5rpx !default;
$u-car-keyboard-button-text-font-size:16px !default;
$u-car-keyboard-button-text-color:$u-main-color !default;
$u-car-keyboard-center-inner-margin: 0 4rpx !default;
$u-car-keyboard-special-button-width:134rpx !default;
$u-car-keyboard-lang-font-size:16px !default;
$u-car-keyboard-lang-color:$u-main-color !default;
$u-car-keyboard-active-color:$u-primary !default;
$u-car-keyboard-line-font-size:15px !default;
$u-car-keyboard-line-color:$u-main-color !default;
$u-car-keyboard-line-margin:0 1px !default;
$u-car-keyboard-u-hover-class-background-color:#BBBCC6 !default;
.u-keyboard {
@include flex(column);
justify-content: space-around;
background-color: $u-car-keyboard-background-color;
align-items: stretch;
padding: $u-car-keyboard-padding;
&__button {
@include flex;
justify-content: center;
flex: 1;
/* #ifndef APP-NVUE */
/* #endif */
&__inner-wrapper {
box-shadow: $u-car-keyboard-button-inner-box-shadow;
margin: $u-car-keyboard-button-inner-margin;
border-radius: $u-car-keyboard-button-border-radius;
&__inner {
@include flex;
justify-content: center;
align-items: center;
width: $u-car-keyboard-button-inner-width;
background-color: $u-car-keyboard-button-inner-background-color;
height: $u-car-keyboard-button-height;
border-radius: $u-car-keyboard-button-border-radius;
&__text {
font-size: $u-car-keyboard-button-text-font-size;
color: $u-car-keyboard-button-text-color;
}
}
&__left,
&__right {
border-radius: $u-car-keyboard-button-border-radius;
width: $u-car-keyboard-special-button-width;
height: $u-car-keyboard-button-height;
background-color: $u-car-keyboard-u-hover-class-background-color;
@include flex;
justify-content: center;
align-items: center;
box-shadow: $u-car-keyboard-button-inner-box-shadow;
}
&__left {
&__line {
font-size: $u-car-keyboard-line-font-size;
color: $u-car-keyboard-line-color;
margin: $u-car-keyboard-line-margin;
}
&__lang {
font-size: $u-car-keyboard-lang-font-size;
color: $u-car-keyboard-lang-color;
&--active {
color: $u-car-keyboard-active-color;
}
}
}
}
}
}
.u-hover-class {
background-color: $u-car-keyboard-u-hover-class-background-color;
}
</style>

View File

@@ -0,0 +1,14 @@
export default {
props: {
// 分组标题
title: {
type: String,
default: uni.$u.props.cellGroup.title
},
// 是否显示外边框
border: {
type: Boolean,
default: uni.$u.props.cellGroup.border
}
}
}

View File

@@ -0,0 +1,61 @@
<template>
<view :style="[$u.addStyle(customStyle)]" :class="[customClass]" class="u-cell-group">
<view v-if="title" class="u-cell-group__title">
<slot name="title">
<text class="u-cell-group__title__text">{{ title }}</text>
</slot>
</view>
<view class="u-cell-group__wrapper">
<u-line v-if="border"></u-line>
<slot />
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* cellGroup 单元格
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等。
* @tutorial https://uviewui.com/components/cell.html
*
* @property {String} title 分组标题
* @property {Boolean} border 是否显示外边框 (默认 true )
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击cell列表时触发
* @example <u-cell-group title="设置喜好">
*/
export default {
name: 'u-cell-group',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-cell-group-title-padding: 16px 16px 8px !default;
$u-cell-group-title-font-size: 15px !default;
$u-cell-group-title-line-height: 16px !default;
$u-cell-group-title-color: $u-main-color !default;
.u-cell-group {
flex: 1;
&__title {
padding: $u-cell-group-title-padding;
&__text {
font-size: $u-cell-group-title-font-size;
line-height: $u-cell-group-title-line-height;
color: $u-cell-group-title-color;
}
}
&__wrapper {
position: relative;
}
}
</style>

View File

@@ -0,0 +1,110 @@
export default {
props: {
// 标题
title: {
type: [String, Number],
default: uni.$u.props.cell.title
},
// 标题下方的描述信息
label: {
type: [String, Number],
default: uni.$u.props.cell.label
},
// 右侧的内容
value: {
type: [String, Number],
default: uni.$u.props.cell.value
},
// 左侧图标名称,或者图片链接(本地文件建议使用绝对地址)
icon: {
type: String,
default: uni.$u.props.cell.icon
},
// 是否禁用cell
disabled: {
type: Boolean,
default: uni.$u.props.cell.disabled
},
// 是否显示下边框
border: {
type: Boolean,
default: uni.$u.props.cell.border
},
// 内容是否垂直居中(主要是针对右侧的value部分)
center: {
type: Boolean,
default: uni.$u.props.cell.center
},
// 点击后跳转的URL地址
url: {
type: String,
default: uni.$u.props.cell.url
},
// 链接跳转的方式内部使用的是uView封装的route方法可能会进行拦截操作
linkType: {
type: String,
default: uni.$u.props.cell.linkType
},
// 是否开启点击反馈(表现为点击时加上灰色背景)
clickable: {
type: Boolean,
default: uni.$u.props.cell.clickable
},
// 是否展示右侧箭头并开启点击反馈
isLink: {
type: Boolean,
default: uni.$u.props.cell.isLink
},
// 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件)
required: {
type: Boolean,
default: uni.$u.props.cell.required
},
// 右侧的图标箭头
rightIcon: {
type: String,
default: uni.$u.props.cell.rightIcon
},
// 右侧箭头的方向可选值为leftupdown
arrowDirection: {
type: String,
default: uni.$u.props.cell.arrowDirection
},
// 左侧图标样式
iconStyle: {
type: [Object, String],
default: () => {
return uni.$u.props.cell.iconStyle
}
},
// 右侧箭头图标的样式
rightIconStyle: {
type: [Object, String],
default: () => {
return uni.$u.props.cell.rightIconStyle
}
},
// 标题的样式
titleStyle: {
type: [Object, String],
default: () => {
return uni.$u.props.cell.titleStyle
}
},
// 单位元的大小可选值为large
size: {
type: String,
default: uni.$u.props.cell.size
},
// 点击cell是否阻止事件传播
stop: {
type: Boolean,
default: uni.$u.props.cell.stop
},
// 标识符cell被点击时返回
name: {
type: [Number, String],
default: uni.$u.props.cell.name
}
}
}

View File

@@ -0,0 +1,229 @@
<template>
<view class="u-cell" :class="[customClass]" :style="[$u.addStyle(customStyle)]"
:hover-class="(!disabled && (clickable || isLink)) ? 'u-cell--clickable' : ''" :hover-stay-time="250"
@tap="clickHandler">
<view class="u-cell__body" :class="[ center && 'u-cell--center', size === 'large' && 'u-cell__body--large']">
<view class="u-cell__body__content">
<view class="u-cell__left-icon-wrap" v-if="$slots.icon || icon">
<slot name="icon" v-if="$slots.icon">
</slot>
<u-icon v-else :name="icon" :custom-style="iconStyle" :size="size === 'large' ? 22 : 18"></u-icon>
</view>
<view class="u-cell__title">
<slot name="title">
<text v-if="title" class="u-cell__title-text" :style="[titleTextStyle]"
:class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__title-text--large']">{{ title }}</text>
</slot>
<slot name="label">
<text class="u-cell__label" v-if="label"
:class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__label--large']">{{ label }}</text>
</slot>
</view>
</view>
<slot name="value">
<text class="u-cell__value"
:class="[disabled && 'u-cell--disabled', size === 'large' && 'u-cell__value--large']"
v-if="!$u.test.empty(value)">{{ value }}</text>
</slot>
<view class="u-cell__right-icon-wrap" v-if="$slots['right-icon'] || isLink"
:class="[`u-cell__right-icon-wrap--${arrowDirection}`]">
<slot name="right-icon" v-if="$slots['right-icon']">
</slot>
<u-icon v-else :name="rightIcon" :custom-style="rightIconStyle" :color="disabled ? '#c8c9cc' : 'info'"
:size="size === 'large' ? 18 : 16"></u-icon>
</view>
</view>
<u-line v-if="border"></u-line>
</view>
</template>
<script>
import props from './props.js';
/**
* cell 单元格
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等。
* @tutorial https://uviewui.com/components/cell.html
* @property {String | Number} title 标题
* @property {String | Number} label 标题下方的描述信息
* @property {String | Number} value 右侧的内容
* @property {String} icon 左侧图标名称,或者图片链接(本地文件建议使用绝对地址)
* @property {Boolean} disabled 是否禁用cell
* @property {Boolean} border 是否显示下边框 (默认 true )
* @property {Boolean} center 内容是否垂直居中(主要是针对右侧的value部分) (默认 false )
* @property {String} url 点击后跳转的URL地址
* @property {String} linkType 链接跳转的方式内部使用的是uView封装的route方法可能会进行拦截操作 (默认 'navigateTo' )
* @property {Boolean} clickable 是否开启点击反馈(表现为点击时加上灰色背景) (默认 false
* @property {Boolean} isLink 是否展示右侧箭头并开启点击反馈 (默认 false
* @property {Boolean} required 是否显示表单状态下的必填星号(此组件可能会内嵌入input组件) (默认 false
* @property {String} rightIcon 右侧的图标箭头 (默认 'arrow-right'
* @property {String} arrowDirection 右侧箭头的方向可选值为leftupdown
* @property {Object | String} rightIconStyle 右侧箭头图标的样式
* @property {Object | String} titleStyle 标题的样式
* @property {Object | String} iconStyle 左侧图标样式
* @property {String} size 单位元的大小,可选值为 largenormalmini
* @property {Boolean} stop 点击cell是否阻止事件传播 (默认 true )
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击cell列表时触发
* @example 该组件需要搭配cell-group组件使用见官方文档示例
*/
export default {
name: 'u-cell',
data() {
return {
}
},
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
computed: {
titleTextStyle() {
return uni.$u.addStyle(this.titleStyle)
}
},
methods: {
// 点击cell
clickHandler(e) {
if (this.disabled) return
this.$emit('click', {
name: this.name
})
// 如果配置了url(此props参数通过mixin引入)参数,跳转页面
this.openPage()
// 是否阻止事件传播
this.stop && this.preventEvent(e)
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-cell-padding: 10px 15px !default;
$u-cell-font-size: 15px !default;
$u-cell-line-height: 24px !default;
$u-cell-color: $u-main-color !default;
$u-cell-icon-size: 16px !default;
$u-cell-title-font-size: 15px !default;
$u-cell-title-line-height: 22px !default;
$u-cell-title-color: $u-main-color !default;
$u-cell-label-font-size: 12px !default;
$u-cell-label-color: $u-tips-color !default;
$u-cell-label-line-height: 18px !default;
$u-cell-value-font-size: 14px !default;
$u-cell-value-color: $u-content-color !default;
$u-cell-clickable-color: $u-bg-color !default;
$u-cell-disabled-color: #c8c9cc !default;
$u-cell-padding-top-large: 13px !default;
$u-cell-padding-bottom-large: 13px !default;
$u-cell-value-font-size-large: 15px !default;
$u-cell-label-font-size-large: 14px !default;
$u-cell-title-font-size-large: 16px !default;
$u-cell-left-icon-wrap-margin-right: 4px !default;
$u-cell-right-icon-wrap-margin-left: 4px !default;
$u-cell-title-flex:1 !default;
$u-cell-label-margin-top:5px !default;
.u-cell {
&__body {
@include flex();
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
padding: $u-cell-padding;
font-size: $u-cell-font-size;
color: $u-cell-color;
// line-height: $u-cell-line-height;
align-items: center;
&__content {
@include flex(row);
align-items: center;
flex: 1;
}
&--large {
padding-top: $u-cell-padding-top-large;
padding-bottom: $u-cell-padding-bottom-large;
}
}
&__left-icon-wrap,
&__right-icon-wrap {
@include flex();
align-items: center;
// height: $u-cell-line-height;
font-size: $u-cell-icon-size;
}
&__left-icon-wrap {
margin-right: $u-cell-left-icon-wrap-margin-right;
}
&__right-icon-wrap {
margin-left: $u-cell-right-icon-wrap-margin-left;
transition: transform 0.3s;
&--up {
transform: rotate(-90deg);
}
&--down {
transform: rotate(90deg);
}
}
&__title {
flex: $u-cell-title-flex;
&-text {
font-size: $u-cell-title-font-size;
line-height: $u-cell-title-line-height;
color: $u-cell-title-color;
&--large {
font-size: $u-cell-title-font-size-large;
}
}
}
&__label {
margin-top: $u-cell-label-margin-top;
font-size: $u-cell-label-font-size;
color: $u-cell-label-color;
line-height: $u-cell-label-line-height;
&--large {
font-size: $u-cell-label-font-size-large;
}
}
&__value {
text-align: right;
font-size: $u-cell-value-font-size;
line-height: $u-cell-line-height;
color: $u-cell-value-color;
&--large {
font-size: $u-cell-value-font-size-large;
}
}
&--clickable {
background-color: $u-cell-clickable-color;
}
&--disabled {
color: $u-cell-disabled-color;
/* #ifndef APP-NVUE */
cursor: not-allowed;
/* #endif */
}
&--center {
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,82 @@
export default {
props: {
// 标识符
name: {
type: String,
default: uni.$u.props.checkboxGroup.name
},
// 绑定的值
value: {
type: Array,
default: uni.$u.props.checkboxGroup.value
},
// 形状circle-圆形square-方形
shape: {
type: String,
default: uni.$u.props.checkboxGroup.shape
},
// 是否禁用全部checkbox
disabled: {
type: Boolean,
default: uni.$u.props.checkboxGroup.disabled
},
// 选中状态下的颜色如设置此值将会覆盖parent的activeColor值
activeColor: {
type: String,
default: uni.$u.props.checkboxGroup.activeColor
},
// 未选中的颜色
inactiveColor: {
type: String,
default: uni.$u.props.checkboxGroup.inactiveColor
},
// 整个组件的尺寸默认px
size: {
type: [String, Number],
default: uni.$u.props.checkboxGroup.size
},
// 布局方式row-横向column-纵向
placement: {
type: String,
default: uni.$u.props.checkboxGroup.placement
},
// label的字体大小px单位
labelSize: {
type: [String, Number],
default: uni.$u.props.checkboxGroup.labelSize
},
// label的字体颜色
labelColor: {
type: [String],
default: uni.$u.props.checkboxGroup.labelColor
},
// 是否禁止点击文本操作
labelDisabled: {
type: Boolean,
default: uni.$u.props.checkboxGroup.labelDisabled
},
// 图标颜色
iconColor: {
type: String,
default: uni.$u.props.checkboxGroup.iconColor
},
// 图标的大小单位px
iconSize: {
type: [String, Number],
default: uni.$u.props.checkboxGroup.iconSize
},
// 勾选图标的对齐方式left-左边right-右边
iconPlacement: {
type: String,
default: uni.$u.props.checkboxGroup.iconPlacement
},
// 竖向配列时,是否显示下划线
borderBottom: {
type: Boolean,
default: uni.$u.props.checkboxGroup.borderBottom
}
}
}

View File

@@ -0,0 +1,103 @@
<template>
<view
class="u-checkbox-group"
:class="bemClass"
>
<slot></slot>
</view>
</template>
<script>
import props from './props.js';
/**
* checkboxGroup 复选框组
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String} name 标识符
* @property {Array} value 绑定的值
* @property {String} shape 形状circle-圆形square-方形 (默认 'square'
* @property {Boolean} disabled 是否禁用全部checkbox (默认 false
* @property {String} activeColor 选中状态下的颜色如设置此值将会覆盖parent的activeColor值 (默认 '#2979ff'
* @property {String} inactiveColor 未选中的颜色 (默认 '#c8c9cc'
* @property {String | Number} size 整个组件的尺寸 单位px (默认 18
* @property {String} placement 布局方式row-横向column-纵向 (默认 'row'
* @property {String | Number} labelSize label的字体大小px单位 (默认 14
* @property {String} labelColor label的字体颜色 (默认 '#303133'
* @property {Boolean} labelDisabled 是否禁止点击文本操作 (默认 false )
* @property {String} iconColor 图标颜色 (默认 '#ffffff'
* @property {String | Number} iconSize 图标的大小单位px (默认 12
* @property {String} iconPlacement 勾选图标的对齐方式left-左边right-右边 (默认 'left'
* @property {Boolean} borderBottom placement为row时是否显示下边框 (默认 false
* @event {Function} change 任一个checkbox状态发生变化时触发回调为一个对象
* @event {Function} input 修改通过v-model绑定的值时触发回调为一个对象
* @example <u-checkbox-group></u-checkbox-group>
*/
export default {
name: 'u-checkbox-group',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
// 这里computed的变量都是子组件u-checkbox需要用到的由于头条小程序的兼容性差异子组件无法实时监听父组件参数的变化
// 所以需要手动通知子组件这里返回一个parentData变量供watch监听在其中去通知每一个子组件重新从父组件(u-checkbox-group)
// 拉取父组件新的变化后的参数
parentData() {
return [this.value, this.disabled, this.inactiveColor, this.activeColor, this.size, this.labelDisabled, this.shape,
this.iconSize, this.borderBottom, this.placement
]
},
bemClass() {
// this.bem为一个computed变量在mixin中
return this.bem('checkbox-group', ['placement'])
},
},
watch: {
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(u-checkbox)如果有init方法的话就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(child.init) === 'function' && child.init()
})
}
},
},
data() {
return {
}
},
created() {
this.children = []
},
methods: {
// 将其他的checkbox设置为未选中的状态
unCheckedOther(childInstance) {
const values = []
this.children.map(child => {
// 将被选中的checkbox放到数组中返回
if (child.isChecked) {
values.push(child.name)
}
})
// 发出事件
this.$emit('change', values)
// 修改通过v-model绑定的值
this.$emit('input', values)
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-checkbox-group {
&--row {
@include flex;
}
&--column {
@include flex(column);
}
}
</style>

View File

@@ -0,0 +1,69 @@
export default {
props: {
// checkbox的名称
name: {
type: [String, Number, Boolean],
default: uni.$u.props.checkbox.name
},
// 形状square为方形circle为圆型
shape: {
type: String,
default: uni.$u.props.checkbox.shape
},
// 整体的大小
size: {
type: [String, Number],
default: uni.$u.props.checkbox.size
},
// 是否默认选中
checked: {
type: Boolean,
default: uni.$u.props.checkbox.checked
},
// 是否禁用
disabled: {
type: [String, Boolean],
default: uni.$u.props.checkbox.disabled
},
// 选中状态下的颜色如设置此值将会覆盖parent的activeColor值
activeColor: {
type: String,
default: uni.$u.props.checkbox.activeColor
},
// 未选中的颜色
inactiveColor: {
type: String,
default: uni.$u.props.checkbox.inactiveColor
},
// 图标的大小单位px
iconSize: {
type: [String, Number],
default: uni.$u.props.checkbox.iconSize
},
// 图标颜色
iconColor: {
type: String,
default: uni.$u.props.checkbox.iconColor
},
// label提示文字因为nvue下直接slot进来的文字由于特殊的结构无法修改样式
label: {
type: [String, Number],
default: uni.$u.props.checkbox.label
},
// label的字体大小px单位
labelSize: {
type: [String, Number],
default: uni.$u.props.checkbox.labelSize
},
// label的颜色
labelColor: {
type: String,
default: uni.$u.props.checkbox.labelColor
},
// 是否禁止点击提示语选中复选框
labelDisabled: {
type: [String, Boolean],
default: uni.$u.props.checkbox.labelDisabled
}
}
}

View File

@@ -0,0 +1,344 @@
<template>
<view
class="u-checkbox"
:style="[checkboxStyle]"
@tap.stop="wrapperClickHandler"
:class="[`u-checkbox-label--${parentData.iconPlacement}`, parentData.borderBottom && parentData.placement === 'column' && 'u-border-bottom']"
>
<view
class="u-checkbox__icon-wrap"
@tap.stop="iconClickHandler"
:class="iconClasses"
:style="[iconWrapStyle]"
>
<slot name="icon">
<u-icon
class="u-checkbox__icon-wrap__icon"
name="checkbox-mark"
:size="elIconSize"
:color="elIconColor"
/>
</slot>
</view>
<text
@tap.stop="labelClickHandler"
:style="{
color: elDisabled ? elInactiveColor : elLabelColor,
fontSize: elLabelSize,
lineHeight: elLabelSize
}"
>{{label}}</text>
</view>
</template>
<script>
import props from './props.js';
/**
* checkbox 复选框
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://uviewui.com/components/checkbox.html
* @property {String | Number | Boolean} name checkbox组件的标示符
* @property {String} shape 形状square为方形circle为圆型
* @property {String | Number} size 整体的大小
* @property {Boolean} checked 是否默认选中
* @property {String | Boolean} disabled 是否禁用
* @property {String} activeColor 选中状态下的颜色如设置此值将会覆盖parent的activeColor值
* @property {String} inactiveColor 未选中的颜色
* @property {String | Number} iconSize 图标的大小单位px
* @property {String} iconColor 图标颜色
* @property {String | Number} label label提示文字因为nvue下直接slot进来的文字由于特殊的结构无法修改样式
* @property {String} labelColor label的颜色
* @property {String | Number} labelSize label的字体大小px单位
* @property {String | Boolean} labelDisabled 是否禁止点击提示语选中复选框
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} change 任一个checkbox状态发生变化时触发回调为一个对象
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
*/
export default {
name: "u-checkbox",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
isChecked: false,
// 父组件的默认值因为头条小程序不支持在computed中使用this.parent.shape的形式
// 故只能使用如此方法
parentData: {
iconSize: 12,
labelDisabled: null,
disabled: null,
shape: 'square',
activeColor: null,
inactiveColor: null,
size: 18,
value: null,
iconColor: null,
placement: 'row',
borderBottom: false,
iconPlacement: 'left'
}
}
},
computed: {
// 是否禁用如果父组件u-raios-group禁用的话将会忽略子组件的配置
elDisabled() {
return this.disabled !== '' ? this.disabled : this.parentData.disabled !== null ? this.parentData.disabled : false;
},
// 是否禁用label点击
elLabelDisabled() {
return this.labelDisabled !== '' ? this.labelDisabled : this.parentData.labelDisabled !== null ? this.parentData.labelDisabled :
false;
},
// 组件尺寸对应size的值默认值为21px
elSize() {
return this.size ? this.size : (this.parentData.size ? this.parentData.size : 21);
},
// 组件的勾选图标的尺寸默认12px
elIconSize() {
return this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 12);
},
// 组件选中激活时的颜色
elActiveColor() {
return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#2979ff');
},
// 组件选未中激活时的颜色
elInactiveColor() {
return this.inactiveColor ? this.inactiveColor : (this.parentData.inactiveColor ? this.parentData.inactiveColor :
'#c8c9cc');
},
// label的颜色
elLabelColor() {
return this.labelColor ? this.labelColor : (this.parentData.labelColor ? this.parentData.labelColor : '#606266')
},
// 组件的形状
elShape() {
return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle');
},
// label大小
elLabelSize() {
return uni.$u.addUnit(this.labelSize ? this.labelSize : (this.parentData.labelSize ? this.parentData.labelSize :
'15'))
},
elIconColor() {
const iconColor = this.iconColor ? this.iconColor : (this.parentData.iconColor ? this.parentData.iconColor :
'#ffffff');
// 图标的颜色
if (this.elDisabled) {
// disabled状态下已勾选的checkbox图标改为elInactiveColor
return this.isChecked ? this.elInactiveColor : 'transparent'
} else {
return this.isChecked ? iconColor : 'transparent'
}
},
iconClasses() {
let classes = []
// 组件的形状
classes.push('u-checkbox__icon-wrap--' + this.elShape)
if (this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled')
}
if (this.isChecked && this.elDisabled) {
classes.push('u-checkbox__icon-wrap--disabled--checked')
}
// 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
// #ifdef MP-ALIPAY || MP-TOUTIAO
classes = classes.join(' ')
// #endif
return classes
},
iconWrapStyle() {
// checkbox的整体样式
const style = {}
style.backgroundColor = this.isChecked && !this.elDisabled ? this.elActiveColor : '#ffffff'
style.borderColor = this.isChecked && !this.elDisabled ? this.elActiveColor : this.elInactiveColor
style.width = uni.$u.addUnit(this.elSize)
style.height = uni.$u.addUnit(this.elSize)
// 如果是图标在右边的话,移除它的右边距
if (this.parentData.iconPlacement === 'right') {
style.marginRight = 0
}
return style
},
checkboxStyle() {
const style = {}
if (this.parentData.borderBottom && this.parentData.placement === 'row') {
uni.$u.error('检测到您将borderBottom设置为true需要同时将u-checkbox-group的placement设置为column才有效')
}
// 当父组件设置了显示下边框并且排列形式为纵向时,给内容和边框之间加上一定间隔
if (this.parentData.borderBottom && this.parentData.placement === 'column') {
style.paddingBottom = '8px'
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
},
mounted() {
this.init()
},
methods: {
init() {
// 支付宝小程序不支持provide/inject所以使用这个方法获取整个父组件在created定义避免循环引用
this.updateParentData()
if (!this.parent) {
uni.$u.error('u-checkbox必须搭配u-checkbox-group组件使用')
}
// 设置初始化时是否默认选中的状态父组件u-checkbox-group的value可能是array所以额外判断
if (this.checked) {
this.isChecked = true
} else if (uni.$u.test.array(this.parentData.value)) {
// 查找数组是是否存在this.name元素值
this.isChecked = this.parentData.value.some(item => {
return item === this.name
})
}
},
updateParentData() {
this.getParentData('u-checkbox-group')
},
// 横向两端排列时,点击组件即可触发选中事件
wrapperClickHandler(e) {
this.parentData.iconPlacement === 'right' && this.iconClickHandler(e)
},
// 点击图标
iconClickHandler(e) {
this.preventEvent(e)
// 如果整体被禁用,不允许被点击
if (!this.elDisabled) {
this.setRadioCheckedStatus()
}
},
// 点击label
labelClickHandler(e) {
this.preventEvent(e)
// 如果按钮整体被禁用或者label被禁用则不允许点击文字修改状态
if (!this.elLabelDisabled && !this.elDisabled) {
this.setRadioCheckedStatus()
}
},
emitEvent() {
this.$emit('change', this.isChecked)
// 尝试调用u-form的验证方法进行一定延迟否则微信小程序更新可能会不及时
this.$nextTick(() => {
uni.$u.formValidate(this, 'change')
})
},
// 改变组件选中状态
// 这里的改变的依据是更改本组件的checked值为true同时通过父组件遍历所有u-checkbox实例
// 将本组件外的其他u-checkbox的checked都设置为false(都被取消选中状态),因而只剩下一个为选中状态
setRadioCheckedStatus() {
// 将本组件标记为与原来相反的状态
this.isChecked = !this.isChecked
this.emitEvent()
typeof this.parent.unCheckedOther === 'function' && this.parent.unCheckedOther(this)
}
},
watch:{
checked(){
this.isChecked = this.checked
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-checkbox-icon-wrap-margin-right:6px !default;
$u-checkbox-icon-wrap-font-size:6px !default;
$u-checkbox-icon-wrap-border-width:1px !default;
$u-checkbox-icon-wrap-border-color:#c8c9cc !default;
$u-checkbox-icon-wrap-icon-line-height:0 !default;
$u-checkbox-icon-wrap-circle-border-radius:100% !default;
$u-checkbox-icon-wrap-square-border-radius:3px !default;
$u-checkbox-icon-wrap-checked-color:#fff !default;
$u-checkbox-icon-wrap-checked-background-color:red !default;
$u-checkbox-icon-wrap-checked-border-color:#2979ff !default;
$u-checkbox-icon-wrap-disabled-background-color:#ebedf0 !default;
$u-checkbox-icon-wrap-disabled-checked-color:#c8c9cc !default;
$u-checkbox-label-margin-left:5px !default;
$u-checkbox-label-margin-right:12px !default;
$u-checkbox-label-color:$u-content-color !default;
$u-checkbox-label-font-size:15px !default;
$u-checkbox-label-disabled-color:#c8c9cc !default;
.u-checkbox {
/* #ifndef APP-NVUE */
@include flex(row);
/* #endif */
overflow: hidden;
flex-direction: row;
align-items: center;
&-label--left {
flex-direction: row
}
&-label--right {
flex-direction: row-reverse;
justify-content: space-between
}
&__icon-wrap {
/* #ifndef APP-NVUE */
box-sizing: border-box;
// nvue下border-color过渡有问题
transition-property: border-color, background-color, color;
transition-duration: 0.2s;
/* #endif */
color: $u-content-color;
@include flex;
align-items: center;
justify-content: center;
color: transparent;
text-align: center;
margin-right: $u-checkbox-icon-wrap-margin-right;
font-size: $u-checkbox-icon-wrap-font-size;
border-width: $u-checkbox-icon-wrap-border-width;
border-color: $u-checkbox-icon-wrap-border-color;
border-style: solid;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题需要设置行高为0否则图标偏下
&__icon {
line-height: $u-checkbox-icon-wrap-icon-line-height;
}
/* #endif */
&--circle {
border-radius: $u-checkbox-icon-wrap-circle-border-radius;
}
&--square {
border-radius: $u-checkbox-icon-wrap-square-border-radius;
}
&--checked {
color: $u-checkbox-icon-wrap-checked-color;
background-color: $u-checkbox-icon-wrap-checked-background-color;
border-color: $u-checkbox-icon-wrap-checked-border-color;
}
&--disabled {
background-color: $u-checkbox-icon-wrap-disabled-background-color !important;
}
&--disabled--checked {
color: $u-checkbox-icon-wrap-disabled-checked-color !important;
}
}
&__label {
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
margin-left: $u-checkbox-label-margin-left;
margin-right: $u-checkbox-label-margin-right;
color: $u-checkbox-label-color;
font-size: $u-checkbox-label-font-size;
&--disabled {
color: $u-checkbox-label-disabled-color;
}
}
}
</style>

View File

@@ -0,0 +1,8 @@
export default {
props: {
percentage: {
type: [String, Number],
default: uni.$u.props.circleProgress.percentage
}
}
}

View File

@@ -0,0 +1,198 @@
<template>
<view class="u-circle-progress">
<view class="u-circle-progress__left">
<view
class="u-circle-progress__left__circle"
:style="[leftSyle]"
ref="left-circle"
>
</view>
</view>
<view
class="u-circle-progress__right"
>
<view
class="u-circle-progress__right__circle"
ref="right-circle"
:style="[rightSyle]"
>
</view>
</view>
<view class="u-circle-progress__circle">
</view>
</view>
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation')
// #endif
/**
* CircleProgress 圆形进度条 TODO: 待完善
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度环。
* @tutorial https://www.uviewui.com/components/circleProgress.html
* @property {String | Number} percentage 圆环进度百分比值为数值类型0-100 (默认 30 )
* @example
*/
export default {
name: 'u-circle-progress',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
leftBorderColor: 'rgb(200, 200, 200)',
rightBorderColor: 'rgb(200, 200, 200)',
}
},
computed: {
leftSyle() {
const style = {}
style.borderTopColor = this.leftBorderColor
style.borderRightColor = this.leftBorderColor
return style
},
rightSyle() {
const style = {}
style.borderLeftColor = this.rightBorderColor
style.borderBottomColor = this.rightBorderColor
return style
}
},
mounted() {
uni.$u.sleep().then(() => {
this.rightBorderColor = 'rgb(66, 185, 131)'
// this.init()
})
},
methods: {
init() {
animation.transition(this.$refs['right-circle'].ref, {
styles: {
transform: 'rotate(45deg)',
transformOrigin: 'center center'
},
}, () => {
this.rightBorderColor = 'rgb(66, 185, 131)'
// animation.transition(this.$refs['right-circle'].ref, {
// styles: {
// transform: 'rotate(225deg)',
// transformOrigin: 'center center'
// },
// duration: 3000,
// }, () => {
// animation.transition(this.$refs['left-circle'].ref, {
// styles: {
// transform: 'rotate(45deg)',
// transformOrigin: 'center center'
// },
// }, () => {
// this.leftBorderColor = 'rgb(66, 185, 131)'
// animation.transition(this.$refs['left-circle'].ref, {
// styles: {
// transform: 'rotate(225deg)',
// transformOrigin: 'center center'
// },
// duration: 1500,
// }, () => {
// })
// })
// })
})
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-circle-progress {
@include flex(row);
position: relative;
border-radius: 100px;
height: 100px;
width: 100px;
// transform: rotate(0deg);
// background-color: rgb(66, 185, 131);
background-color: rgb(200, 200, 200);
overflow: hidden;
justify-content: space-between;
&__circle {
border-radius: 100px;
height: 90px;
width: 90px;
transform: translate(-50%, -50%);
background-color: rgb(255, 255, 255);
left: 50px;
top: 50px;
position: absolute;
}
&__left {
position: absolute;
left: 0;
width: 50px;
height: 100px;
overflow: hidden;
box-sizing: border-box;
// background-color: rgb(66, 185, 131);
// background-color: rgb(200, 200, 200);
// transform-origin: left center;
&__circle {
box-sizing: border-box;
// background-color: red;
border-left-color: transparent;
border-bottom-color: transparent;
border-top-left-radius: 50px;
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
// border-left-color: rgb(66, 185, 131);
// border-bottom-color: rgb(66, 185, 131);
border-top-color: rgb(66, 185, 131);
border-right-color: rgb(66, 185, 131);
border-width: 5px;
width: 100px;
height: 100px;
transform: rotate(225deg);
// border-radius: 100px;
}
}
&__right {
position: absolute;
right: 0;
width: 50px;
height: 100px;
overflow: hidden;
&__circle {
position: absolute;
right: 0;
box-sizing: border-box;
// background-color: red;
border-top-color: transparent;
border-right-color: transparent;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
// border-left-color: rgb(66, 185, 131);
// border-bottom-color: rgb(66, 185, 131);
border-left-color: rgb(200, 200, 200);
border-bottom-color: rgb(200, 200, 200);
border-width: 5px;
width: 100px;
height: 100px;
transform: rotate(45deg);
transform-origin: center center;
// border-radius: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,74 @@
export default {
props: {
// 最大输入长度
maxlength: {
type: [String, Number],
default: uni.$u.props.codeInput.maxlength
},
// 是否用圆点填充
dot: {
type: Boolean,
default: uni.$u.props.codeInput.dot
},
// 显示模式box-盒子模式line-底部横线模式
mode: {
type: String,
default: uni.$u.props.codeInput.mode
},
// 是否细边框
hairline: {
type: Boolean,
default: uni.$u.props.codeInput.hairline
},
// 字符间的距离
space: {
type: [String, Number],
default: uni.$u.props.codeInput.space
},
// 预置值
value: {
type: [String, Number],
default: uni.$u.props.codeInput.value
},
// 是否自动获取焦点
focus: {
type: Boolean,
default: uni.$u.props.codeInput.focus
},
// 字体是否加粗
bold: {
type: Boolean,
default: uni.$u.props.codeInput.bold
},
// 字体颜色
color: {
type: String,
default: uni.$u.props.codeInput.color
},
// 字体大小
fontSize: {
type: [String, Number],
default: uni.$u.props.codeInput.fontSize
},
// 输入框的大小,宽等于高
size: {
type: [String, Number],
default: uni.$u.props.codeInput.size
},
// 是否隐藏原生键盘如果想用自定义键盘的话需设置此参数为true
disabledKeyboard: {
type: Boolean,
default: uni.$u.props.codeInput.disabledKeyboard
},
// 边框和线条颜色
borderColor: {
type: String,
default: uni.$u.props.codeInput.borderColor
},
// 是否禁止输入"."符号
disabledDot: {
type: Boolean,
default: uni.$u.props.codeInput.disabledDot
}
}
}

View File

@@ -0,0 +1,240 @@
<template>
<view class="u-code-input">
<view
class="u-code-input__item"
:style="[itemStyle(index)]"
v-for="(item, index) in codeLength"
:key="index"
>
<view
class="u-code-input__item__dot"
v-if="dot && codeArray.length > index"
></view>
<text
v-else
:style="{
fontSize: $u.addUnit(fontSize),
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>{{codeArray[index]}}</text>
<view
class="u-code-input__item__line"
v-if="mode === 'line'"
:style="[lineStyle]"
></view>
<view v-if="codeArray.length === index" :style="{backgroundColor: color}" class="u-code-input__item__cursor"></view>
</view>
<input
:disabled="disabledKeyboard"
type="number"
:focus="focus"
:value="inputValue"
:maxlength="maxlength"
class="u-code-input__input"
@input="inputHandler"
:style="{
height: $u.addUnit(size)
}"
/>
</view>
</template>
<script>
import props from './props.js';
/**
* CodeInput 验证码输入
* @description 该组件一般用于验证用户短信验证码的场景也可以结合uView的键盘组件使用
* @tutorial https://www.uviewui.com/components/codeInput.html
* @property {String | Number} maxlength 最大输入长度 (默认 6
* @property {Boolean} dot 是否用圆点填充 (默认 false
* @property {String} mode 显示模式box-盒子模式line-底部横线模式 (默认 'box'
* @property {Boolean} hairline 是否细边框 (默认 false
* @property {String | Number} space 字符间的距离 (默认 10
* @property {String | Number} value 预置值
* @property {Boolean} focus 是否自动获取焦点 (默认 false
* @property {Boolean} bold 字体和输入横线是否加粗 (默认 false
* @property {String} color 字体颜色 (默认 '#606266'
* @property {String | Number} fontSize 字体大小单位px (默认 18
* @property {String | Number} size 输入框的大小,宽等于高 (默认 35
* @property {Boolean} disabledKeyboard 是否隐藏原生键盘如果想用自定义键盘的话需设置此参数为true (默认 false
* @property {String} borderColor 边框和线条颜色 (默认 '#c9cacc'
* @property {Boolean} disabledDot 是否禁止输入"."符号 (默认 true
*
* @event {Function} change 输入内容发生改变时触发,具体见上方说明 value当前输入的值
* @event {Function} finish 输入字符个数达maxlength值时触发见上方说明 value当前输入的值
* @example <u-code-input v-model="value4" :focus="true"></u-code-input>
*/
export default {
name: 'u-code-input',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
inputValue: ''
}
},
watch: {
value: {
immediate: true,
handler(val) {
// 转为字符串,超出部分截掉
this.inputValue = String(val).substring(0, this.maxlength)
}
},
},
computed: {
// 根据长度循环输入框的个数因为头条小程序数值不能用于v-for
codeLength() {
return new Array(Number(this.maxlength))
},
// 循环item的样式
itemStyle() {
return index => {
const addUnit = uni.$u.addUnit
const style = {
width: addUnit(this.size),
height: addUnit(this.size)
}
// 盒子模式下,需要额外进行处理
if (this.mode === 'box') {
// 设置盒子的边框如果是细边框则设置为0.5px宽度
style.border = `${this.hairline ? 0.5 : 1}px solid ${this.borderColor}`
// 如果盒子间距为0的话
if (uni.$u.getPx(this.space) === 0) {
// 给第一和最后一个盒子设置圆角
if (index === 0) {
style.borderTopLeftRadius = '3px'
style.borderBottomLeftRadius = '3px'
}
if (index === this.codeLength.length - 1) {
style.borderTopRightRadius = '3px'
style.borderBottomRightRadius = '3px'
}
// 最后一个盒子的右边框需要保留
if (index !== this.codeLength.length - 1) {
style.borderRight = 'none'
}
}
}
if (index !== this.codeLength.length - 1) {
// 设置验证码字符之间的距离通过margin-right设置最后一个字符无需右边框
style.marginRight = addUnit(this.space)
} else {
// 最后一个盒子的有边框需要保留
style.marginRight = 0
}
return style
}
},
// 将输入的值转为数组给item历遍时根据当前的索引显示数组的元素
codeArray() {
return String(this.inputValue).split('')
},
// 下划线模式下,横线的样式
lineStyle() {
const style = {}
style.height = this.hairline ? '2px' : '4px'
style.width = uni.$u.addUnit(this.size)
// 线条模式下,背景色即为边框颜色
style.backgroundColor = this.borderColor
return style
}
},
methods: {
// 监听输入框的值发生变化
inputHandler(e) {
const value = e.detail.value
this.inputValue = value
// 是否允许输入“.”符号
if(this.disabledDot) {
this.$nextTick(() => {
this.inputValue = value.replace('.', '')
})
}
// 未达到maxlength之前发送change事件达到后发送finish事件
this.$emit('change', value)
// 修改通过v-model双向绑定的值
this.$emit('input', value)
// 达到用户指定输入长度时,发出完成事件
if (String(value).length >= Number(this.maxlength)) {
this.$emit('finish', value)
}
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-code-input-cursor-width: 1px;
$u-code-input-cursor-height: 40%;
$u-code-input-cursor-animation-duration: 1s;
$u-code-input-cursor-animation-name: u-cursor-flicker;
.u-code-input {
@include flex;
position: relative;
overflow: hidden;
&__item {
@include flex;
justify-content: center;
align-items: center;
position: relative;
&__text {
font-size: 15px;
color: $u-content-color;
}
&__dot {
width: 7px;
height: 7px;
border-radius: 100px;
background-color: $u-content-color;
}
&__line {
position: absolute;
bottom: 0;
height: 4px;
border-radius: 100px;
width: 40px;
background-color: $u-content-color;
}
&__cursor {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: $u-code-input-cursor-width;
height: $u-code-input-cursor-height;
animation: $u-code-input-cursor-animation-duration u-cursor-flicker infinite;
}
}
&__input {
// 之所以需要input输入框是因为有它才能唤起键盘
// 这里将它设置为两倍的屏幕宽度,再将左边的一半移出屏幕,为了不让用户看到输入的内容
position: absolute;
left: -750rpx;
width: 1500rpx;
top: 0;
background-color: transparent;
text-align: left;
}
}
@keyframes u-cursor-flicker {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,34 @@
export default {
props: {
// 倒计时总秒数
seconds: {
type: [String, Number],
default: uni.$u.props.code.seconds
},
// 尚未开始时提示
startText: {
type: String,
default: uni.$u.props.code.startText
},
// 正在倒计时中的提示
changeText: {
type: String,
default: uni.$u.props.code.changeText
},
// 倒计时结束时的提示
endText: {
type: String,
default: uni.$u.props.code.endText
},
// 是否在H5刷新或各端返回再进入时继续倒计时
keepRunning: {
type: Boolean,
default: uni.$u.props.code.keepRunning
},
// 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
uniqueKey: {
type: String,
default: uni.$u.props.code.uniqueKey
}
}
}

View File

@@ -0,0 +1,129 @@
<template>
<view class="u-code">
<!-- 此组件功能由js完成无需写html逻辑 -->
</view>
</template>
<script>
import props from './props.js';
/**
* Code 验证码输入框
* @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件 不提供界面显示,只提供提示语,由用户将提示语嵌入到具体的场景
* @tutorial https://www.uviewui.com/components/code.html
* @property {String | Number} seconds 倒计时所需的秒数(默认 60
* @property {String} startText 开始前的提示语,见官网说明(默认 '获取验证码'
* @property {String} changeText 倒计时期间的提示语,必须带有字母"x",见官网说明(默认 'X秒重新获取'
* @property {String} endText 倒计结束的提示语,见官网说明(默认 '重新获取'
* @property {Boolean} keepRunning 是否在H5刷新或各端返回再进入时继续倒计时 默认false
* @property {String} uniqueKey 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
*
* @event {Function} change 倒计时期间,每秒触发一次
* @event {Function} start 开始倒计时触发
* @event {Function} end 结束倒计时触发
* @example <u-code ref="uCode" @change="codeChange" seconds="20"></u-code>
*/
export default {
name: "u-code",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
secNum: this.seconds,
timer: null,
canGetCode: true, // 是否可以执行验证码操作
}
},
mounted() {
this.checkKeepRunning()
},
watch: {
seconds: {
immediate: true,
handler(n) {
this.secNum = n
}
}
},
methods: {
checkKeepRunning() {
// 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$uCountDownTimestamp'))
if(!lastTimestamp) return this.changeEvent(this.startText)
// 当前秒的时间戳
let nowTimestamp = Math.floor((+ new Date()) / 1000)
// 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
if(this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
// 剩余尚未执行完的倒计秒数
this.secNum = lastTimestamp - nowTimestamp
// 清除本地保存的变量
uni.removeStorageSync(this.uniqueKey + '_$uCountDownTimestamp')
// 开始倒计时
this.start()
} else {
// 如果不存在需要继续上一次的倒计时,执行正常的逻辑
this.changeEvent(this.startText)
}
},
// 开始倒计时
start() {
// 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
if(this.timer) {
clearInterval(this.timer)
this.timer = null
}
this.$emit('start')
this.canGetCode = false
// 这里放这句是为了一开始时就提示否则要等setInterval的1秒后才会有提示
this.changeEvent(this.changeText.replace(/x|X/, this.secNum))
this.setTimeToStorage()
this.timer = setInterval(() => {
if (--this.secNum) {
// 用当前倒计时的秒数替换提示字符串中的"x"字母
this.changeEvent(this.changeText.replace(/x|X/, this.secNum))
} else {
clearInterval(this.timer)
this.timer = null
this.changeEvent(this.endText)
this.secNum = this.seconds
this.$emit('end')
this.canGetCode = true
}
}, 1000)
},
// 重置,可以让用户再次获取验证码
reset() {
this.canGetCode = true
clearInterval(this.timer)
this.secNum = this.seconds
this.changeEvent(this.endText)
},
changeEvent(text) {
this.$emit('change', text)
},
// 保存时间戳为了防止倒计时尚未结束H5刷新或者各端的右上角返回上一页再进来
setTimeToStorage() {
if(!this.keepRunning || !this.timer) return
// 记录当前的时间戳,为了下次进入页面,如果还在倒计时内的话,继续倒计时
// 倒计时尚未结束结果大于0倒计时已经开始就会小于初始值如果等于初始值说明没有开始倒计时无需处理
if(this.secNum > 0 && this.secNum <= this.seconds) {
// 获取当前时间戳(+ new Date()为特殊写法)除以1000变成秒再去除小数部分
let nowTimestamp = Math.floor((+ new Date()) / 1000)
// 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
uni.setStorage({
key: this.uniqueKey + '_$uCountDownTimestamp',
data: nowTimestamp + Number(this.secNum)
})
}
}
},
// 组件销毁的时候,清除定时器,否则定时器会继续存在,系统不会自动清除
beforeDestroy() {
this.setTimeToStorage()
clearTimeout(this.timer)
this.timer = null
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>

View File

@@ -0,0 +1,29 @@
export default {
props: {
// 占父容器宽度的多少等分总分为12份
span: {
type: [String, Number],
default: uni.$u.props.col.span
},
// 指定栅格左侧的间隔数(总12栏)
offset: {
type: [String, Number],
default: uni.$u.props.col.offset
},
// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
justify: {
type: String,
default: uni.$u.props.col.justify
},
// 垂直对齐方式可选值为top、center、bottom、stretch
align: {
type: String,
default: uni.$u.props.col.align
},
// 文字对齐方式
textAlign: {
type: String,
default: uni.$u.props.col.textAlign
}
}
}

View File

@@ -0,0 +1,162 @@
<template>
<view
class="u-col"
ref="u-col"
:class="[
'u-col-' + span
]"
:style="[colStyle]"
@tap="clickHandler"
>
<slot></slot>
</view>
</template>
<script>
import props from './props.js';
/**
* CodeInput 栅格系统的列
* @description 该组件一般用于Layout 布局 通过基础的 12 分栏,迅速简便地创建布局
* @tutorial https://www.uviewui.com/components/Layout.html
* @property {String | Number} span 栅格占据的列数总12等份 (默认 12 )
* @property {String | Number} offset 分栏左边偏移计算方式与span相同 (默认 0 )
* @property {String} justify 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`) (默认 'start' )
* @property {String} align 垂直对齐方式可选值为top、center、bottom、stretch (默认 'stretch' )
* @property {String} textAlign 文字水平对齐方式 (默认 'left' )
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click col被点击会阻止事件冒泡到row
* @example <u-col span="3" offset="3" > <view class="demo-layout bg-purple"></view> </u-col>
*/
export default {
name: 'u-col',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
width: 0,
parentData: {
gutter: 0
},
gridNum: 12
}
},
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
},
colStyle() {
const style = {
// 这里写成"padding: 0 10px"的形式是因为nvue的需要
paddingLeft: uni.$u.addUnit(uni.$u.getPx(this.parentData.gutter)/2),
paddingRight: uni.$u.addUnit(uni.$u.getPx(this.parentData.gutter)/2),
alignItems: this.uAlignItem,
justifyContent: this.uJustify,
textAlign: this.textAlign,
// #ifndef APP-NVUE
// 在非nvue上使用百分比形式
flex: `0 0 ${100 / this.gridNum * this.span}%`,
marginLeft: 100 / 12 * this.offset + '%',
// #endif
// #ifdef APP-NVUE
// 在nvue上由于无法使用百分比单位这里需要获取父组件的宽度再计算得出该有对应的百分比尺寸
width: uni.$u.addUnit(Math.floor(this.width / this.gridNum * Number(this.span))),
marginLeft: uni.$u.addUnit(Math.floor(this.width / this.gridNum * Number(this.offset))),
// #endif
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
},
mounted() {
this.init()
},
methods: {
async init() {
// 支付宝小程序不支持provide/inject所以使用这个方法获取整个父组件在created定义避免循环引用
this.updateParentData()
this.width = await this.parent.getComponentWidth()
},
updateParentData() {
this.getParentData('u-row')
},
clickHandler(e) {
this.$emit('click');
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-col {
padding: 0;
/* #ifndef APP-NVUE */
box-sizing:border-box;
/* #endif */
/* #ifdef MP */
display: block;
/* #endif */
}
// nvue下百分比无效
/* #ifndef APP-NVUE */
.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);
}
/* #endif */
</style>

View File

@@ -0,0 +1,59 @@
export default {
props: {
// 标题
title: {
type: String,
default: uni.$u.props.collapseItem.title
},
// 标题右侧内容
value: {
type: String,
default: uni.$u.props.collapseItem.value
},
// 标题下方的描述信息
label: {
type: String,
default: uni.$u.props.collapseItem.label
},
// 是否禁用折叠面板
disabled: {
type: Boolean,
default: uni.$u.props.collapseItem.disabled
},
// 是否展示右侧箭头并开启点击反馈
isLink: {
type: Boolean,
default: uni.$u.props.collapseItem.isLink
},
// 是否开启点击反馈
clickable: {
type: Boolean,
default: uni.$u.props.collapseItem.clickable
},
// 是否显示内边框
border: {
type: Boolean,
default: uni.$u.props.collapseItem.border
},
// 标题的对齐方式
align: {
type: String,
default: uni.$u.props.collapseItem.align
},
// 唯一标识符
name: {
type: [String, Number],
default: uni.$u.props.collapseItem.name
},
// 标题左侧图片,可为绝对路径的图片或内置图标
icon: {
type: String,
default: uni.$u.props.collapseItem.icon
},
// 面板展开收起的过渡时间单位ms
duration: {
type: Number,
default: uni.$u.props.collapseItem.duration
}
}
}

View File

@@ -0,0 +1,225 @@
<template>
<view class="u-collapse-item">
<u-cell
:title="title"
:value="value"
:label="label"
:icon="icon"
:isLink="isLink"
:clickable="clickable"
:border="parentData.border && showBorder"
@click="clickHandler"
:arrowDirection="expanded ? 'up' : 'down'"
:disabled="disabled"
>
<!-- #ifndef MP-WEIXIN -->
<!-- 微信小程序不支持因为微信中不支持 <slot name="title" slot="title" />的写法 -->
<template slot="title">
<slot name="title"></slot>
</template>
<template slot="icon">
<slot name="icon"></slot>
</template>
<template slot="value">
<slot name="value"></slot>
</template>
<template slot="right-icon">
<slot name="right-icon"></slot>
</template>
<!-- #endif -->
</u-cell>
<view
class="u-collapse-item__content"
:animation="animationData"
ref="animation"
>
<view
class="u-collapse-item__content__text content-class"
:id="elId"
:ref="elId"
><slot /></view>
</view>
<u-line v-if="parentData.border"></u-line>
</view>
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation')
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* collapseItem 折叠面板Item
* @description 通过折叠面板收纳内容区域搭配u-collapse使用
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {String} title 标题
* @property {String} value 标题右侧内容
* @property {String} label 标题下方的描述信息
* @property {Boolean} disbled 是否禁用折叠面板 ( 默认 false )
* @property {Boolean} isLink 是否展示右侧箭头并开启点击反馈 ( 默认 true )
* @property {Boolean} clickable 是否开启点击反馈 ( 默认 true )
* @property {Boolean} border 是否显示内边框 ( 默认 true )
* @property {String} align 标题的对齐方式 ( 默认 'left' )
* @property {String | Number} name 唯一标识符
* @property {String} icon 标题左侧图片,可为绝对路径的图片或内置图标
* @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",
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
elId: uni.$u.guid(),
// uni.createAnimation的导出数据
animationData: {},
// 是否展开状态
expanded: false,
// 根据expanded确定是否显示border为了控制展开时cell的下划线更好的显示效果进行一定时间的延时
showBorder: false,
// 是否动画中,如果是则不允许继续触发点击
animating: false,
// 父组件u-collapse的参数
parentData: {
accordion: false,
border: false
}
};
},
watch: {
expanded(n) {
clearTimeout(this.timer)
this.timer = null
// 这里根据expanded的值来进行一定的延时是为了cell的下划线更好的显示效果
this.timer = setTimeout(() => {
this.showBorder = n
}, n ? 10 : 290)
}
},
mounted() {
this.init()
},
methods: {
// 异步获取内容,或者动态修改了内容时,需要重新初始化
init() {
// 初始化数据
this.updateParentData()
if (!this.parent) {
return uni.$u.error('u-collapse-item必须要搭配u-collapse组件使用')
}
const {
value,
accordion,
children = []
} = this.parent
if (accordion) {
if (uni.$u.test.array(value)) {
return uni.$u.error('手风琴模式下u-collapse组件的value参数不能为数组')
}
this.expanded = this.name == value
} else {
if (!uni.$u.test.array(value) && value !== null) {
return uni.$u.error('非手风琴模式下u-collapse组件的value参数必须为数组')
}
this.expanded = (value || []).some(item => item == this.name)
}
// 设置组件的展开或收起状态
this.$nextTick(function() {
this.setContentAnimate()
})
},
updateParentData() {
// 此方法在mixin中
this.getParentData('u-collapse')
},
async setContentAnimate() {
// 每次面板打开或者收起时,都查询元素尺寸
// 好处是,父组件从服务端获取内容后,变更折叠面板后可以获得最新的高度
const rect = await this.queryRect()
const height = this.expanded ? rect.height : 0
this.animating = true
// #ifdef APP-NVUE
const ref = this.$refs['animation'].ref
animation.transition(ref, {
styles: {
height: height + 'px'
},
duration: this.duration,
// 必须设置为true否则会到面板收起或展开时页面其他元素不会随之调整它们的布局
needLayout: true,
timingFunction: 'ease-in-out',
}, () => {
this.animating = false
})
// #endif
// #ifndef APP-NVUE
const animation = uni.createAnimation({
timingFunction: 'ease-in-out',
});
animation
.height(height)
.step({
duration: this.duration,
})
.step()
// 导出动画数据给面板的animationData值
this.animationData = animation.export()
// 标识动画结束
uni.$u.sleep(this.duration).then(() => {
this.animating = false
})
// #endif
},
// 点击collapsehead头部
clickHandler() {
if (this.disabled && this.animating) return
// 设置本组件为相反的状态
this.parent && this.parent.onChange(this)
},
// 查询内容高度
queryRect() {
// #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法详见文档介绍https://www.uviewui.com/js/getRect.html
// 组件内部一般用this.$uGetRect对外的为uni.$u.getRect二者功能一致名称不同
return new Promise(resolve => {
this.$uGetRect(`#${this.elId}`).then(size => {
resolve(size)
})
})
// #endif
// #ifdef APP-NVUE
// nvue下使用dom模块查询元素高度
// 返回一个promise让调用此方法的主体能使用then回调
return new Promise(resolve => {
dom.getComponentRect(this.$refs[this.elId], res => {
resolve(res.size)
})
})
// #endif
}
},
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-collapse-item {
&__content {
overflow: hidden;
height: 0;
&__text {
padding: 12px 15px;
color: $u-content-color;
font-size: 14px;
line-height: 18px;
}
}
}
</style>

View File

@@ -0,0 +1,19 @@
export default {
props: {
// 当前展开面板的name非手风琴模式[<string | number>]手风琴模式string | number
value: {
type: [String, Number, Array, null],
default: uni.$u.props.collapse.value
},
// 是否手风琴模式
accordion: {
type: Boolean,
default: uni.$u.props.collapse.accordion
},
// 是否显示外边框
border: {
type: Boolean,
default: uni.$u.props.collapse.border
}
}
}

View File

@@ -0,0 +1,90 @@
<template>
<view class="u-collapse">
<u-line v-if="border"></u-line>
<slot />
</view>
</template>
<script>
import props from './props.js';
/**
* collapse 折叠面板
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {String | Number | Array} value 当前展开面板的name非手风琴模式[<string | number>]手风琴模式string | number
* @property {Boolean} accordion 是否手风琴模式( 默认 false
* @property {Boolean} border 是否显示外边框 ( 默认 true
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式参数activeNames类型为String否则为Array)
* @example <u-collapse></u-collapse>
*/
export default {
name: "u-collapse",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
watch: {
needInit() {
this.init()
}
},
created() {
this.children = []
},
computed: {
needInit() {
// 通过computed同时监听accordion和value值的变化
// 再通过watch去执行init()方法,进行再一次的初始化
return [this.accordion, this.value]
}
},
watch: {
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(u-checkbox)如果有updateParentData方法的话就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
},
methods: {
// 重新初始化一次内部的所有子元素
init() {
this.children.map(child => {
child.init()
})
},
/**
* collapse-item被点击时触发由collapse统一处理各子组件的状态
* @param {Object} target 被操作的面板的实例
*/
onChange(target) {
let changeArr = []
this.children.map((child, index) => {
// 如果是手风琴模式,将其他的折叠面板收起来
if (this.accordion) {
child.expanded = child === target ? !target.expanded : false
child.setContentAnimate()
} else {
if(child === target) {
child.expanded = !child.expanded
child.setContentAnimate()
}
}
// 拼接change事件中数组元素的状态
changeArr.push({
// 如果没有定义name属性则默认返回组件的index索引
name: child.name || index,
status: child.expanded ? 'open' : 'close'
})
})
this.$emit('change', changeArr)
this.$emit(target.expanded ? 'open' : 'close', target.name)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>

View File

@@ -0,0 +1,55 @@
export default {
props: {
// 显示的内容,字符串
text: {
type: [Array],
default: uni.$u.props.columnNotice.text
},
// 是否显示左侧的音量图标
icon: {
type: String,
default: uni.$u.props.columnNotice.icon
},
// 通告模式link-显示右箭头closable-显示右侧关闭图标
mode: {
type: String,
default: uni.$u.props.columnNotice.mode
},
// 文字颜色,各图标也会使用文字颜色
color: {
type: String,
default: uni.$u.props.columnNotice.color
},
// 背景颜色
bgColor: {
type: String,
default: uni.$u.props.columnNotice.bgColor
},
// 字体大小单位px
fontSize: {
type: [String, Number],
default: uni.$u.props.columnNotice.fontSize
},
// 水平滚动时的滚动速度即每秒滚动多少px(px),这有利于控制文字无论多少时,都能有一个恒定的速度
speed: {
type: [String, Number],
default: uni.$u.props.columnNotice.speed
},
// direction = row时是否使用步进形式滚动
step: {
type: Boolean,
default: uni.$u.props.columnNotice.step
},
// 滚动一个周期的时间长单位ms
duration: {
type: [String, Number],
default: uni.$u.props.columnNotice.duration
},
// 是否禁止用手滑动切换
// 目前HX2.6.11只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
disableTouch: {
type: Boolean,
default: uni.$u.props.columnNotice.disableTouch
}
}
}

View File

@@ -0,0 +1,160 @@
<template>
<view
class="u-notice"
@tap="clickHandler"
>
<slot name="icon">
<view
class="u-notice__left-icon"
v-if="icon"
>
<u-icon
:name="icon"
:color="color"
size="19"
></u-icon>
</view>
</slot>
<swiper
:disable-touch="disableTouch"
:vertical="step ? false : true"
circular
:interval="duration"
:autoplay="true"
class="u-notice__swiper"
@change="noticeChange"
>
<swiper-item
v-for="(item, index) in text"
:key="index"
class="u-notice__swiper__item"
>
<text
class="u-notice__swiper__item__text u-line-1"
:style="[textStyle]"
>{{ item }}</text>
</swiper-item>
</swiper>
<view
class="u-notice__right-icon"
v-if="['link', 'closable'].includes(mode)"
>
<u-icon
v-if="mode === 'link'"
name="arrow-right"
:size="17"
:color="color"
></u-icon>
<u-icon
v-if="mode === 'closable'"
name="close"
:size="16"
:color="color"
@click="close"
></u-icon>
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* ColumnNotice 滚动通知中的垂直滚动 内部组件
* @description 该组件用于滚动通告场景,是其中的垂直滚动方式
* @tutorial https://www.uviewui.com/components/noticeBar.html
* @property {Array} text 显示的内容,字符串
* @property {String} icon 是否显示左侧的音量图标 默认 'volume'
* @property {String} mode 通告模式link-显示右箭头closable-显示右侧关闭图标
* @property {String} color 文字颜色,各图标也会使用文字颜色 默认 '#f9ae3d'
* @property {String} bgColor 背景颜色 默认 '#fdf6ec'
* @property {String | Number} fontSize 字体大小单位px 默认 14
* @property {String | Number} speed 水平滚动时的滚动速度即每秒滚动多少px(rpx),这有利于控制文字无论多少时,都能有一个恒定的速度 默认 80
* @property {Boolean} step direction = row时是否使用步进形式滚动 默认 false
* @property {String | Number} duration 滚动一个周期的时间长单位ms 默认 1500
* @property {Boolean} disableTouch 是否禁止用手滑动切换 目前HX2.6.11只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序 默认 true
* @example
*/
export default {
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
watch: {
text: {
immediate: true,
handler(newValue, oldValue) {
if(!uni.$u.test.array(newValue)) {
uni.$u.error('noticebar组件direction为column时要求text参数为数组形式')
}
}
}
},
computed: {
// 文字内容的样式
textStyle() {
let style = {}
style.color = this.color
style.fontSize = uni.$u.addUnit(this.fontSize)
return style
},
// 垂直或者水平滚动
vertical() {
if (this.mode == 'horizontal') return false
else return true
},
},
data() {
return {
index:0
}
},
methods: {
noticeChange(e){
this.index = e.detail.current
},
// 点击通告栏
clickHandler() {
this.$emit('click', this.index)
},
// 点击关闭按钮
close() {
this.$emit('close')
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-notice {
@include flex;
align-items: center;
justify-content: space-between;
&__left-icon {
align-items: center;
margin-right: 5px;
}
&__right-icon {
margin-left: 5px;
align-items: center;
}
&__swiper {
height: 16px;
@include flex;
align-items: center;
flex: 1;
&__item {
@include flex;
align-items: center;
overflow: hidden;
&__text {
font-size: 14px;
color: $u-warning;
}
}
}
}
</style>

View File

@@ -0,0 +1,24 @@
export default {
props: {
// 倒计时时长单位ms
time: {
type: [String, Number],
default: uni.$u.props.countDown.time
},
// 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒
format: {
type: String,
default: uni.$u.props.countDown.format
},
// 是否自动开始倒计时
autoStart: {
type: Boolean,
default: uni.$u.props.countDown.autoStart
},
// 是否展示毫秒倒计时
millisecond: {
type: Boolean,
default: uni.$u.props.countDown.millisecond
}
}
}

View File

@@ -0,0 +1,163 @@
<template>
<view class="u-count-down">
<slot>
<text class="u-count-down__text">{{ formattedTime }}</text>
</slot>
</view>
</template>
<script>
import props from './props.js';
import {
isSameSecond,
parseFormat,
parseTimeData
} from './utils';
/**
* u-count-down 倒计时
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
* @tutorial https://uviewui.com/components/countDown.html
* @property {String | Number} time 倒计时时长单位ms (默认 0
* @property {String} format 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒 (默认 'HH:mm:ss'
* @property {Boolean} autoStart 是否自动开始倒计时 (默认 true
* @property {Boolean} millisecond 是否展示毫秒倒计时 (默认 false
* @event {Function} finish 倒计时结束时触发
* @event {Function} change 倒计时变化时触发
* @event {Function} start 开始倒计时
* @event {Function} pause 暂停倒计时
* @event {Function} reset 重设倒计时,若 auto-start 为 true重设后会自动开始倒计时
* @example <u-count-down :time="time"></u-count-down>
*/
export default {
name: 'u-count-down',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
timer: null,
// 各单位(天,时,分等)剩余时间
timeData: parseTimeData(0),
// 格式化后的时间,如"03:23:21"
formattedTime: '0',
// 倒计时是否正在进行中
runing: false,
endTime: 0, // 结束的毫秒时间戳
remainTime: 0, // 剩余的毫秒时间
}
},
watch: {
time(n) {
this.reset()
}
},
mounted() {
this.init()
},
methods: {
init() {
this.reset()
},
// 开始倒计时
start() {
if (this.runing) return
// 标识为进行中
this.runing = true
// 结束时间戳 = 此刻时间戳 + 剩余的时间
this.endTime = Date.now() + this.remainTime
this.toTick()
},
// 根据是否展示毫秒,执行不同操作函数
toTick() {
if (this.millisecond) {
this.microTick()
} else {
this.macroTick()
}
},
macroTick() {
this.clearTimeout()
// 每隔一定时间,更新一遍定时器的值
// 同时此定时器的作用也能带来毫秒级的更新
this.timer = setTimeout(() => {
// 获取剩余时间
const remain = this.getRemainTime()
// 重设剩余时间
if (!isSameSecond(remain, this.remainTime) || remain === 0) {
this.setRemainTime(remain)
}
// 如果剩余时间不为0则继续检查更新倒计时
if (this.remainTime !== 0) {
this.macroTick()
}
}, 30)
},
microTick() {
this.clearTimeout()
this.timer = setTimeout(() => {
this.setRemainTime(this.getRemainTime())
if (this.remainTime !== 0) {
this.microTick()
}
}, 50)
},
// 获取剩余的时间
getRemainTime() {
// 取最大值防止出现小于0的剩余时间值
return Math.max(this.endTime - Date.now(), 0)
},
// 设置剩余的时间
setRemainTime(remain) {
this.remainTime = remain
// 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
const timeData = parseTimeData(remain)
this.$emit('change', timeData)
// 得出格式化后的时间
this.formattedTime = parseFormat(this.format, timeData)
// 如果时间已到,停止倒计时
if (remain <= 0) {
this.pause()
this.$emit('finish')
}
},
// 重置倒计时
reset() {
this.pause()
this.remainTime = this.time
this.setRemainTime(this.remainTime)
if (this.autoStart) {
this.start()
}
},
// 暂停倒计时
pause() {
this.runing = false;
this.clearTimeout()
},
// 清空定时器
clearTimeout() {
clearTimeout(this.timer)
this.timer = null
}
},
beforeDestroy() {
this.clearTimeout()
}
}
</script>
<style
lang="scss"
scoped
>
@import "../../libs/css/components.scss";
$u-count-down-text-color:$u-content-color !default;
$u-count-down-text-font-size:15px !default;
$u-count-down-text-line-height:22px !default;
.u-count-down {
&__text {
color: $u-count-down-text-color;
font-size: $u-count-down-text-font-size;
line-height: $u-count-down-text-line-height;
}
}
</style>

View File

@@ -0,0 +1,62 @@
// 补0如1 -> 01
function padZero(num, targetLength = 2) {
let str = `${num}`
while (str.length < targetLength) {
str = `0${str}`
}
return str
}
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
export function parseTimeData(time) {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
days,
hours,
minutes,
seconds,
milliseconds
}
}
export function parseFormat(format, timeData) {
let {
days,
hours,
minutes,
seconds,
milliseconds
} = timeData
// 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
if (format.indexOf('DD') === -1) {
hours += days * 24
} else {
// 对天补0
format = format.replace('DD', padZero(days))
}
// 其他同理于DD的格式化处理方式
if (format.indexOf('HH') === -1) {
minutes += hours * 60
} else {
format = format.replace('HH', padZero(hours))
}
if (format.indexOf('mm') === -1) {
seconds += minutes * 60
} else {
format = format.replace('mm', padZero(minutes))
}
if (format.indexOf('ss') === -1) {
milliseconds += seconds * 1000
} else {
format = format.replace('ss', padZero(seconds))
}
return format.replace('SSS', padZero(milliseconds, 3))
}
export function isSameSecond(time1, time2) {
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
}

View File

@@ -0,0 +1,59 @@
export default {
props: {
// 开始的数值默认从0增长到某一个数
startVal: {
type: [String, Number],
default: uni.$u.props.countTo.startVal
},
// 要滚动的目标数值,必须
endVal: {
type: [String, Number],
default: uni.$u.props.countTo.endVal
},
// 滚动到目标数值的动画持续时间单位为毫秒ms
duration: {
type: [String, Number],
default: uni.$u.props.countTo.duration
},
// 设置数值后是否自动开始滚动
autoplay: {
type: Boolean,
default: uni.$u.props.countTo.autoplay
},
// 要显示的小数位数
decimals: {
type: [String, Number],
default: uni.$u.props.countTo.decimals
},
// 是否在即将到达目标数值的时候,使用缓慢滚动的效果
useEasing: {
type: Boolean,
default: uni.$u.props.countTo.useEasing
},
// 十进制分割
decimal: {
type: [String, Number],
default: uni.$u.props.countTo.decimal
},
// 字体颜色
color: {
type: String,
default: uni.$u.props.countTo.color
},
// 字体大小
fontSize: {
type: [String, Number],
default: uni.$u.props.countTo.fontSize
},
// 是否加粗字体
bold: {
type: Boolean,
default: uni.$u.props.countTo.bold
},
// 千位分隔符,类似金额的分割(¥23,321.05中的",")
separator: {
type: String,
default: uni.$u.props.countTo.separator
}
}
}

View File

@@ -0,0 +1,184 @@
<template>
<text
class="u-count-num"
:style="{
fontSize: $u.addUnit(fontSize),
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>{{ displayValue }}</text>
</template>
<script>
import props from './props.js';
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
* @tutorial https://www.uviewui.com/components/countTo.html
* @property {String | Number} startVal 开始的数值默认从0增长到某一个数默认 0
* @property {String | Number} endVal 要滚动的目标数值,必须 (默认 0
* @property {String | Number} duration 滚动到目标数值的动画持续时间单位为毫秒ms (默认 2000
* @property {Boolean} autoplay 设置数值后是否自动开始滚动 (默认 true
* @property {String | Number} decimals 要显示的小数位数,见官网说明(默认 0
* @property {Boolean} useEasing 滚动结束时,是否缓动结尾,见官网说明(默认 true
* @property {String} decimal 十进制分割 默认 "."
* @property {String} color 字体颜色( 默认 '#606266' )
* @property {String | Number} fontSize 字体大小单位px 默认 22
* @property {Boolean} bold 字体是否加粗(默认 false
* @property {String} separator 千位分隔符,见官网说明
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
*/
export default {
name: 'u-count-to',
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 // 上一次的时间
};
},
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
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() {
if (!this.remaining) return
this.startTime = 0;
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) || 0;
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/components.scss";
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
}
</style>

View File

@@ -0,0 +1,116 @@
export default {
props: {
// 是否打开组件
show: {
type: Boolean,
default: uni.$u.props.datetimePicker.show
},
// 是否展示顶部的操作栏
showToolbar: {
type: Boolean,
default: uni.$u.props.datetimePicker.showToolbar
},
// 绑定值
value: {
type: [String, Number],
default: uni.$u.props.datetimePicker.value
},
// 顶部标题
title: {
type: String,
default: uni.$u.props.datetimePicker.title
},
// 展示格式mode=date为日期选择mode=time为时间选择mode=year-month为年月选择mode=datetime为日期时间选择
mode: {
type: String,
default: uni.$u.props.datetimePicker.mode
},
// 可选的最大时间
maxDate: {
type: Number,
// 最大默认值为后10年
default: uni.$u.props.datetimePicker.maxDate
},
// 可选的最小时间
minDate: {
type: Number,
// 最小默认值为前10年
default: uni.$u.props.datetimePicker.minDate
},
// 可选的最小小时仅mode=time有效
minHour: {
type: Number,
default: uni.$u.props.datetimePicker.minHour
},
// 可选的最大小时仅mode=time有效
maxHour: {
type: Number,
default: uni.$u.props.datetimePicker.maxHour
},
// 可选的最小分钟仅mode=time有效
minMinute: {
type: Number,
default: uni.$u.props.datetimePicker.minMinute
},
// 可选的最大分钟仅mode=time有效
maxMinute: {
type: Number,
default: uni.$u.props.datetimePicker.maxMinute
},
// 选项过滤函数
filter: {
type: [Function, null],
default: uni.$u.props.datetimePicker.filter
},
// 选项格式化函数
formatter: {
type: [Function, null],
default: uni.$u.props.datetimePicker.formatter
},
// 是否显示加载中状态
loading: {
type: Boolean,
default: uni.$u.props.datetimePicker.loading
},
// 各列中,单个选项的高度
itemHeight: {
type: [String, Number],
default: uni.$u.props.datetimePicker.itemHeight
},
// 取消按钮的文字
cancelText: {
type: String,
default: uni.$u.props.datetimePicker.cancelText
},
// 确认按钮的文字
confirmText: {
type: String,
default: uni.$u.props.datetimePicker.confirmText
},
// 取消按钮的颜色
cancelColor: {
type: String,
default: uni.$u.props.datetimePicker.cancelColor
},
// 确认按钮的颜色
confirmColor: {
type: String,
default: uni.$u.props.datetimePicker.confirmColor
},
// 每列中可见选项的数量
visibleItemCount: {
type: [String, Number],
default: uni.$u.props.datetimePicker.visibleItemCount
},
// 是否允许点击遮罩关闭选择器
closeOnClickOverlay: {
type: Boolean,
default: uni.$u.props.datetimePicker.closeOnClickOverlay
},
// 各列的默认索引
defaultIndex: {
type: Array,
default: uni.$u.props.datetimePicker.defaultIndex
}
}
}

View File

@@ -0,0 +1,360 @@
<template>
<u-picker
ref="picker"
:show="show"
:closeOnClickOverlay="closeOnClickOverlay"
:columns="columns"
:title="title"
:itemHeight="itemHeight"
:showToolbar="showToolbar"
:visibleItemCount="visibleItemCount"
:defaultIndex="innerDefaultIndex"
:cancelText="cancelText"
:confirmText="confirmText"
:cancelColor="cancelColor"
:confirmColor="confirmColor"
@close="close"
@cancel="cancel"
@confirm="confirm"
@change="change"
>
</u-picker>
</template>
<script>
function times(n, iteratee) {
let index = -1
const result = Array(n < 0 ? 0 : n)
while (++index < n) {
result[index] = iteratee(index)
}
return result
}
import props from './props.js';
import dayjs from '../../libs/util/dayjs.js';
/**
* DatetimePicker 时间日期选择器
* @description 此选择器用于时间日期
* @tutorial https://www.uviewui.com/components/datetimePicker.html
* @property {Boolean} show 用于控制选择器的弹出与收起 ( 默认 false )
* @property {Boolean} showToolbar 是否显示顶部的操作栏 ( 默认 true )
* @property {String | Number} value 绑定值
* @property {String} title 顶部标题
* @property {String} mode 展示格式 mode=date为日期选择mode=time为时间选择mode=year-month为年月选择mode=datetime为日期时间选择 ( 默认 datetime )
* @property {Number} maxDate 可选的最大时间 默认值为后10年
* @property {Number} minDate 可选的最小时间 默认值为前10年
* @property {Number} minHour 可选的最小小时仅mode=time有效 ( 默认 0 )
* @property {Number} maxHour 可选的最大小时仅mode=time有效 ( 默认 23 )
* @property {Number} minMinute 可选的最小分钟仅mode=time有效 ( 默认 0 )
* @property {Number} maxMinute 可选的最大分钟仅mode=time有效 ( 默认 59 )
* @property {Function} filter 选项过滤函数
* @property {Function} formatter 选项格式化函数
* @property {Boolean} loading 是否显示加载中状态 ( 默认 false )
* @property {String | Number} itemHeight 各列中,单个选项的高度 ( 默认 44 )
* @property {String} cancelText 取消按钮的文字 ( 默认 '取消' )
* @property {String} confirmText 确认按钮的文字 ( 默认 '确认' )
* @property {String} cancelColor 取消按钮的颜色 ( 默认 '#909193' )
* @property {String} confirmColor 确认按钮的颜色 ( 默认 '#3c9cff' )
* @property {String | Number} visibleItemCount 每列中可见选项的数量 ( 默认 5 )
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭选择器 ( 默认 false )
* @property {Array} defaultIndex 各列的默认索引
* @event {Function} close 关闭选择器时触发
* @event {Function} confirm 点击确定按钮,返回当前选择的值
* @event {Function} change 当选择值变化时触发
* @event {Function} cancel 点击取消按钮
* @example <u-datetime-picker :show="show" :value="value1" mode="datetime" ></u-datetime-picker>
*/
export default {
name: 'datetime-picker',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
columns: [],
innerDefaultIndex: [],
innerFormatter: (type, value) => value
}
},
watch: {
show(newValue, oldValue) {
if (newValue) {
this.updateColumnValue(this.innerValue)
}
},
propsChange() {
this.init()
}
},
computed: {
// 如果以下这些变量发生了变化,意味着需要重新初始化各列的值
propsChange() {
return [this.mode, this.maxDate, this.minDate, this.minHour, this.maxHour, this.minMinute, this.maxMinute, this.filter, ]
}
},
mounted() {
this.init()
},
methods: {
init() {
this.innerValue = this.correctValue(this.value)
this.updateColumnValue(this.innerValue)
},
// 在微信小程序中不支持将函数当做props参数故只能通过ref形式调用
setFormatter(e) {
this.innerFormatter = e
},
// 关闭选择器
close() {
if (this.closeOnClickOverlay) {
this.$emit('close')
}
},
// 点击工具栏的取消按钮
cancel() {
this.$emit('cancel')
},
// 点击工具栏的确定按钮
confirm() {
this.$emit('confirm', {
value: this.innerValue,
mode: this.mode
})
this.$emit('input', this.innerValue)
},
//用正则截取输出值,当出现多组数字时,抛出错误
intercept(e,type){
let judge = e.match(/\d+/g)
//判断是否掺杂数字
if(judge.length>1){
uni.$u.error("请勿在过滤或格式化函数时添加数字")
return 0
}else if(type&&judge[0].length==4){//判断是否是年份
return judge[0]
}else if(judge[0].length>2){
uni.$u.error("请勿在过滤或格式化函数时添加数字")
return 0
}else{
return judge[0]
}
},
// 列发生变化时触发
change(e) {
const { indexs, values } = e
let selectValue = ''
if(this.mode === 'time') {
// 根据value各列索引从各列数组中取出当前时间的选中值
selectValue = `${this.intercept(values[0][indexs[0]])}:${this.intercept(values[1][indexs[1]])}`
} else {
// 将选择的值转为数值,比如'03'转为数值的3'2019'转为数值的2019
const year = parseInt(this.intercept(values[0][indexs[0]],'year'))
const month = parseInt(this.intercept(values[1][indexs[1]]))
let date = parseInt(values[2] ? this.intercept(values[2][indexs[2]]) : 1)
let hour = 0, minute = 0
// 此月份的最大天数
const maxDate = dayjs(`${year}-${month}`).daysInMonth()
// year-month模式下date不会出现在列中设置为1为了符合后边需要减1的需求
if (this.mode === 'year-month') {
date = 1
}
// 不允许超过maxDate值
date = Math.min(maxDate, date)
if (this.mode === 'datetime') {
hour = parseInt(this.intercept(values[3][indexs[3]]))
minute = parseInt(this.intercept(values[4][indexs[4]]))
}
// 转为时间模式
selectValue = Number(new Date(year, month - 1, date, hour, minute))
}
// 取出准确的合法值,防止超越边界的情况
selectValue = this.correctValue(selectValue)
this.innerValue = selectValue
this.updateColumnValue(selectValue)
// 发出change时间value为当前选中的时间戳
this.$emit('change', {
value: selectValue,
// #ifndef MP-WEIXIN
// 微信小程序不能传递this实例会因为循环引用而报错
picker: this.$refs.picker,
// #endif
mode: this.mode
})
},
// 更新各列的值进行补0、格式化等操作
updateColumnValue(value) {
this.innerValue = value
this.updateColumns()
this.updateIndexs(value)
},
// 更新索引
updateIndexs(value) {
let values = []
const formatter = this.formatter || this.innerFormatter
const padZero = uni.$u.padZero
if (this.mode === 'time') {
// 将time模式的时间用:分隔成数组
const timeArr = value.split(':')
// 使用formatter格式化方法进行管道处理
values = [formatter('hour', timeArr[0]), formatter('minute', timeArr[1])]
} else {
const date = new Date(value)
values = [
formatter('year', `${dayjs(value).year()}`),
// 月份补0
formatter('month', padZero(dayjs(value).month() + 1))
]
if (this.mode === 'date') {
// date模式需要添加天列
values.push(formatter('day', padZero(dayjs(value).date())))
}
if (this.mode === 'datetime') {
// 数组的push方法可以写入多个参数
values.push(formatter('day', padZero(dayjs(value).date())), formatter('hour', padZero(dayjs(value).hour())), formatter('minute', padZero(dayjs(value).minute())))
}
}
// 根据当前各列的所有值,从各列默认值中找到默认值在各列中的索引
const indexs = this.columns.map((column, index) => {
// 通过取大值,可以保证不会出现找不到索引的-1情况
return Math.max(0, column.findIndex(item => item === values[index]))
})
this.innerDefaultIndex = indexs
},
// 更新各列的值
updateColumns() {
const formatter = this.formatter || this.innerFormatter
// 获取各列的值并且map后对各列的具体值进行补0操作
const results = this.getOriginColumns().map((column) => column.values.map((value) => formatter(column.type, value)))
this.columns = results
},
getOriginColumns() {
// 生成各列的值
const results = this.getRanges().map(({ type, range }) => {
let values = times(range[1] - range[0] + 1, (index) => {
let value = range[0] + index
value = type === 'year' ? `${value}` : uni.$u.padZero(value)
return value
})
// 进行过滤
if (this.filter) {
values = this.filter(type, values)
}
return { type, values }
})
return results
},
// 通过最大值和最小值生成数组
generateArray(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start)
},
// 得出合法的时间
correctValue(value) {
const isDateMode = this.mode !== 'time'
if (isDateMode && !uni.$u.test.date(value)) {
// 如果是日期类型,但是又没有设置合法的当前时间的话,使用最小时间为当前时间
value = this.minDate
} else if (!isDateMode && !value) {
// 如果是时间类型,而又没有默认值的话,就用最小时间
value = `${uni.$u.padZero(this.minHour)}:${uni.$u.padZero(this.minMinute)}`
}
// 时间类型
if (!isDateMode) {
if (String(value).indexOf(':') === -1) return uni.$u.error('时间错误请传递如12:24的格式')
let [hour, minute] = value.split(':')
// 对时间补零,同时控制在最小值和最大值之间
hour = uni.$u.padZero(uni.$u.range(this.minHour, this.maxHour, Number(hour)))
minute = uni.$u.padZero(uni.$u.range(this.minMinute, this.maxMinute, Number(minute)))
return `${ hour }:${ minute }`
} else {
// 如果是日期格式,控制在最小日期和最大日期之间
value = dayjs(value).isBefore(dayjs(this.minDate)) ? this.minDate : value
value = dayjs(value).isAfter(dayjs(this.maxDate)) ? this.maxDate : value
return value
}
},
// 获取每列的最大和最小值
getRanges() {
if (this.mode === 'time') {
return [
{
type: 'hour',
range: [this.minHour, this.maxHour],
},
{
type: 'minute',
range: [this.minMinute, this.maxMinute],
},
];
}
const { maxYear, maxDate, maxMonth, maxHour, maxMinute, } = this.getBoundary('max', this.innerValue);
const { minYear, minDate, minMonth, minHour, minMinute, } = this.getBoundary('min', this.innerValue);
const result = [
{
type: 'year',
range: [minYear, maxYear],
},
{
type: 'month',
range: [minMonth, maxMonth],
},
{
type: 'day',
range: [minDate, maxDate],
},
{
type: 'hour',
range: [minHour, maxHour],
},
{
type: 'minute',
range: [minMinute, maxMinute],
},
];
if (this.mode === 'date')
result.splice(3, 2);
if (this.mode === 'year-month')
result.splice(2, 3);
return result;
},
// 根据minDate、maxDate、minHour、maxHour等边界值判断各列的开始和结束边界值
getBoundary(type, innerValue) {
const value = new Date(innerValue)
const boundary = new Date(this[`${type}Date`])
const year = dayjs(boundary).year()
let month = 1
let date = 1
let hour = 0
let minute = 0
if (type === 'max') {
month = 12
// 月份的天数
date = dayjs(value).daysInMonth()
hour = 23
minute = 59
}
// 获取边界值,逻辑是:当年达到了边界值(最大或最小年),就检查月允许的最大和最小值,以此类推
if (dayjs(value).year() === year) {
month = dayjs(boundary).month() + 1
if (dayjs(value).month() + 1 === month) {
date = dayjs(boundary).date()
if (dayjs(value).date() === date) {
hour = dayjs(boundary).hour()
if (dayjs(value).hour() === hour) {
minute = dayjs(boundary).minute()
}
}
}
}
return {
[`${type}Year`]: year,
[`${type}Month`]: month,
[`${type}Date`]: date,
[`${type}Hour`]: hour,
[`${type}Minute`]: minute
}
},
},
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
</style>

View File

@@ -0,0 +1,44 @@
export default {
props: {
// 是否虚线
dashed: {
type: Boolean,
default: uni.$u.props.divider.dashed
},
// 是否细线
hairline: {
type: Boolean,
default: uni.$u.props.divider.hairline
},
// 是否以点替代文字优先于text字段起作用
dot: {
type: Boolean,
default: uni.$u.props.divider.dot
},
// 内容文本的位置left-左边center-中间right-右边
textPosition: {
type: String,
default: uni.$u.props.divider.textPosition
},
// 文本内容
text: {
type: [String, Number],
default: uni.$u.props.divider.text
},
// 文本大小
textSize: {
type: [String, Number],
default: uni.$u.props.divider.textSize
},
// 文本颜色
textColor: {
type: String,
default: uni.$u.props.divider.textColor
},
// 线条颜色
lineColor: {
type: String,
default: uni.$u.props.divider.lineColor
}
}
}

View File

@@ -0,0 +1,116 @@
<template>
<view
class="u-divider"
:style="[$u.addStyle(customStyle)]"
@tap="click"
>
<u-line
:color="lineColor"
:customStyle="leftLineStyle"
:hairline="hairline"
:dashed="dashed"
></u-line>
<text
v-if="dot"
class="u-divider__dot"
></text>
<text
v-else-if="text"
class="u-divider__text"
:style="[textStyle]"
>{{text}}</text>
<u-line
:color="lineColor"
:customStyle="rightLineStyle"
:hairline="hairline"
:dashed="dashed"
></u-line>
</view>
</template>
<script>
import props from './props.js';
/**
* divider 分割线
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
* @tutorial https://www.uviewui.com/components/divider.html
* @property {Boolean} dashed 是否虚线 (默认 false
* @property {Boolean} hairline 是否细线 (默认 true
* @property {Boolean} dot 是否以点替代文字优先于text字段起作用 (默认 false
* @property {String} textPosition 内容文本的位置left-左边center-中间right-右边 (默认 'center'
* @property {String | Number} text 文本内容
* @property {String | Number} textSize 文本大小 (默认 14
* @property {String} textColor 文本颜色 (默认 '#909399'
* @property {String} lineColor 线条颜色 (默认 '#dcdfe6'
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click divider组件被点击时触发
* @example <u-divider :color="color">锦瑟无端五十弦</u-divider>
*/
export default {
name:'u-divider',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
textStyle() {
const style = {}
style.fontSize = uni.$u.addUnit(this.textSize)
style.color = this.textColor
return style
},
// 左边线条的的样式
leftLineStyle() {
const style = {}
// 如果是在左边,设置左边的宽度为固定值
if (this.textPosition === 'left') {
style.width = '80rpx'
} else {
style.flex = 1
}
return style
},
// 右边线条的的样式
rightLineStyle() {
const style = {}
// 如果是在右边,设置右边的宽度为固定值
if (this.textPosition === 'right') {
style.width = '80rpx'
} else {
style.flex = 1
}
return style
}
},
methods: {
// divider组件被点击时触发
click() {
this.$emit('click');
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-divider-margin:15px 0 !default;
$u-divider-text-margin:0 15px !default;
$u-divider-dot-font-size:12px !default;
$u-divider-dot-margin:0 12px !default;
$u-divider-dot-color: #c0c4cc !default;
.u-divider {
@include flex;
flex-direction: row;
align-items: center;
margin: $u-divider-margin;
&__text {
margin: $u-divider-text-margin;
}
&__dot {
font-size: $u-divider-dot-font-size;
margin: $u-divider-dot-margin;
color: $u-divider-dot-color;
}
}
</style>

View File

@@ -0,0 +1,36 @@
export default {
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'
},
// 点击遮罩是否可以收起弹窗
closeOnClickOverlay: {
type: Boolean,
default: true
}
}
}

View File

@@ -0,0 +1,146 @@
<template>
<view class="u-drawdown-item">
<u-overlay
customStyle="top: 126px"
:show="show"
:closeOnClickOverlay="closeOnClickOverlay"
@click="overlayClick"
></u-overlay>
<view
class="u-drawdown-item__content"
:style="[style]"
:animation="animationData"
ref="animation"
>
<slot />
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation')
const dom = uni.requireNativePlugin('dom')
// #endif
import props from './props.js';
/**
* Drawdownitem
* @description
* @tutorial url
* @property {String}
* @event {Function}
* @example
*/
export default {
name: 'u-drawdown-item',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
show: false,
top: '126px',
// uni.createAnimation的导出数据
animationData: {},
}
},
mounted() {
this.init()
},
watch: {
// 发生变化时,需要去更新父组件对应的值
dataChange(newValue, oldValue) {
this.updateParentData()
}
},
computed: {
// 监听对应变量的变化
dataChange() {
return [this.title, this.disabled]
},
style() {
const style = {
zIndex: 10071,
position: 'fixed',
display: 'flex',
left: 0,
right: 0
}
style.top = uni.$u.addUnit(this.top)
return style
}
},
methods: {
init() {
this.updateParentData()
},
// 更新父组件所需的数据
updateParentData() {
// 获取父组件u-dropdown
this.getParentData('u-dropdown')
if (!this.parent) uni.$u.error('u-dropdown-item必须配合u-dropdown使用')
// 查找父组件menuList数组中对应的标题数据
const menuIndex = this.parent.menuList.findIndex(item => item.title === this.title)
const menuContent = {
title: this.title,
disabled: this.disabled
}
if (menuIndex >= 0) {
// 如果能找到,则直接修改
this.parent.menuList[menuIndex] = menuContent;
} else {
// 如果无法找到则为第一次添加直接push即可
this.parent.menuList.push(menuContent);
}
},
async setContentAnimate(height) {
this.animating = true
// #ifdef APP-NVUE
const ref = this.$refs['animation'].ref
animation.transition(ref, {
styles: {
height: uni.$u.addUnit(height)
},
duration: this.duration,
timingFunction: 'ease-in-out',
}, () => {
this.animating = false
})
// #endif
// #ifndef APP-NVUE
const animation = uni.createAnimation({
timingFunction: 'ease-in-out',
});
animation
.height(height)
.step({
duration: this.duration,
})
.step()
// 导出动画数据给面板的animationData值
this.animationData = animation.export()
// 标识动画结束
uni.$u.sleep(this.duration).then(() => {
this.animating = false
})
// #endif
},
overlayClick() {
this.show = false
this.setContentAnimate(0)
}
},
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-drawdown-item {
&__content {
background-color: #FFFFFF;
overflow: hidden;
height: 0;
}
}
</style>

View File

@@ -0,0 +1,65 @@
export default {
props: {
// 标题选中时的样式
activeStyle: {
type: [String, Object],
default: () => ({
color: '#2979ff',
fontSize: '14px'
})
},
// 标题未选中时的样式
inactiveStyle: {
type: [String, Object],
default: () => ({
color: '#606266',
fontSize: '14px'
})
},
// 点击遮罩是否关闭菜单
closeOnClickMask: {
type: Boolean,
default: true
},
// 点击当前激活项标题是否关闭菜单
closeOnClickSelf: {
type: Boolean,
default: true
},
// 过渡时间
duration: {
type: [Number, String],
default: 300
},
// 标题菜单的高度
height: {
type: [Number, String],
default: 40
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 标题的字体大小
titleSize: {
type: [Number, String],
default: 14
},
// 下拉出来的内容部分的圆角值
borderRadius: {
type: [Number, String],
default: 0
},
// 菜单右侧的icon图标
menuIcon: {
type: String,
default: 'arrow-down'
},
// 菜单右侧图标的大小
menuIconSize: {
type: [Number, String],
default: 14
}
}
}

View File

@@ -0,0 +1,127 @@
<template>
<view class="u-drawdown">
<view
class="u-dropdown__menu"
:style="{
height: $u.addUnit(height)
}"
ref="u-dropdown__menu"
>
<view
class="u-dropdown__menu__item"
v-for="(item, index) in menuList"
:key="index"
@tap.stop="clickHandler(item, index)"
>
<view class="u-dropdown__menu__item__content">
<text
class="u-dropdown__menu__item__content__text"
:style="[index === current ? activeStyle : inactiveStyle]"
>{{item.title}}</text>
<view
class="u-dropdown__menu__item__content__arrow"
:class="[index === current && 'u-dropdown__menu__item__content__arrow--rotate']"
>
<u-icon
:name="menuIcon"
:size="$u.addUnit(menuIconSize)"
></u-icon>
</view>
</view>
</view>
</view>
<view class="u-dropdown__content">
<slot />
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* Dropdown
* @description
* @tutorial url
* @property {String}
* @event {Function}
* @example
*/
export default {
name: 'u-dropdown',
mixins: [uni.$u.mixin, props],
data() {
return {
// <20>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD>
menuList: [],
current: 0
}
},
computed: {
},
created() {
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>(u-dropdown-item)<29><>this<69><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>data<74><61><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><CEA2>С<EFBFBD><D0A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD><EFBFBD><EFBFBD>ö<EFBFBD><C3B6><EFBFBD><EFBFBD><EFBFBD>
this.children = [];
},
methods: {
clickHandler(item, index) {
this.children.map(child => {
if(child.title === item.title) {
// this.queryRect('u-dropdown__menu').then(size => {
child.$emit('click')
child.setContentAnimate(child.show ? 0 : 300)
child.show = !child.show
// })
} else {
child.show = false
child.setContentAnimate(0)
}
})
},
// <20><>ȡ<EFBFBD><C8A1>ǩ<EFBFBD>ijߴ<C4B3>λ<EFBFBD><CEBB>
queryRect(el) {
// #ifndef APP-NVUE
// $uGetRectΪuView<65>Դ<EFBFBD><D4B4>Ľڵ<C4BD><DAB5><EFBFBD>ѯ<EFBFBD>򻯷<EFBFBD><F2BBAFB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ĵ<EFBFBD><C4B5><EFBFBD><EFBFBD>ܣ<EFBFBD>https://www.uviewui.com/js/getRect.html
// <20><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD>this.$uGetRect<63><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊthis.$u.getRect<63><74><EFBFBD><EFBFBD><EFBFBD>߹<EFBFBD><DFB9><EFBFBD>һ<EFBFBD>£<EFBFBD><C2A3><EFBFBD><EFBFBD>Ʋ<EFBFBD>ͬ
return new Promise(resolve => {
this.$uGetRect(`.${el}`).then(size => {
resolve(size)
})
})
// #endif
// #ifdef APP-NVUE
// nvue<75>£<EFBFBD>ʹ<EFBFBD><CAB9>domģ<6D><C4A3><EFBFBD><EFBFBD>ѯԪ<D1AF>ظ߶<D8B8>
// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>promise<73><65><EFBFBD>õ<EFBFBD><C3B5>ô˷<C3B4><CBB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9>then<65>ص<EFBFBD>
return new Promise(resolve => {
dom.getComponentRect(this.$refs[el], res => {
resolve(res.size)
})
})
// #endif
},
},
}
</script>
<style lang="scss">
@import '../../libs/css/components.scss';
.u-dropdown {
&__menu {
@include flex;
&__item {
flex: 1;
@include flex;
justify-content: center;
&__content {
@include flex;
align-items: center;
}
}
}
}
</style>

View File

@@ -0,0 +1,59 @@
export default {
props: {
// 内置图标名称,或图片路径,建议绝对路径
icon: {
type: String,
default: uni.$u.props.empty.icon
},
// 提示文字
text: {
type: String,
default: uni.$u.props.empty.text
},
// 文字颜色
textColor: {
type: String,
default: uni.$u.props.empty.textColor
},
// 文字大小
textSize: {
type: [String, Number],
default: uni.$u.props.empty.textSize
},
// 图标的颜色
iconColor: {
type: String,
default: uni.$u.props.empty.iconColor
},
// 图标的大小
iconSize: {
type: [String, Number],
default: uni.$u.props.empty.iconSize
},
// 选择预置的图标类型
mode: {
type: String,
default: uni.$u.props.empty.mode
},
// 图标宽度单位px
width: {
type: [String, Number],
default: uni.$u.props.empty.width
},
// 图标高度单位px
height: {
type: [String, Number],
default: uni.$u.props.empty.height
},
// 是否显示组件
show: {
type: Boolean,
default: uni.$u.props.empty.show
},
// 组件距离上一个元素之间的距离默认px单位
marginTop: {
type: [String, Number],
default: uni.$u.props.empty.marginTop
}
}
}

View File

@@ -0,0 +1,128 @@
<template>
<view
class="u-empty"
:style="[emptyStyle]"
v-if="show"
>
<u-icon
v-if="!isSrc"
:name="mode === 'message' ? 'chat' : `empty-${mode}`"
:size="iconSize"
:color="iconColor"
margin-top="14"
></u-icon>
<image
v-else
:style="{
width: $u.addUnit(width),
height: $u.addUnit(height),
}"
:src="icon"
mode="widthFix"
></image>
<text
class="u-empty__text"
:style="[textStyle]"
>{{text ? text : icons[mode]}}</text>
<view class="u-empty__wrap" v-if="$slots.default || $slots.$default">
<slot />
</view>
</view>
</template>
<script>
import props from './props.js';
/**
* empty 内容为空
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
* @tutorial https://www.uviewui.com/components/empty.html
* @property {String} icon 内置图标名称,或图片路径,建议绝对路径
* @property {String} text 提示文字
* @property {String} textColor 文字颜色 (默认 '#c0c4cc' )
* @property {String | Number} textSize 文字大小 (默认 14
* @property {String} iconColor 图标的颜色 (默认 '#c0c4cc'
* @property {String | Number} iconSize 图标的大小 (默认 90
* @property {String} mode 选择预置的图标类型 (默认 'data'
* @property {String | Number} width 图标宽度单位px (默认 160
* @property {String | Number} height 图标高度单位px (默认 160
* @property {Boolean} show 是否显示组件 (默认 true
* @property {String | Number} marginTop 组件距离上一个元素之间的距离默认px单位 (默认 0
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
*/
export default {
name: "u-empty",
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
icons: {
car: '购物车为空',
page: '页面不存在',
search: '没有搜索结果',
address: '没有收货地址',
wifi: '没有WiFi',
order: '订单为空',
coupon: '没有优惠券',
favor: '暂无收藏',
permission: '无权限',
history: '无历史记录',
news: '无新闻列表',
message: '消息列表为空',
list: '列表为空',
data: '数据为空',
comment: '暂无评论',
}
}
},
computed: {
// 组件样式
emptyStyle() {
const style = {}
style.marginTop = uni.$u.addUnit(this.marginTop)
// 合并customStyle样式此参数通过mixin中的props传递
return uni.$u.deepMerge(uni.$u.addStyle(this.customStyle), style)
},
// 文本样式
textStyle() {
const style = {}
style.color = this.textColor
style.fontSize = uni.$u.addUnit(this.textSize)
return style
},
// 判断icon是否图片路径
isSrc() {
return this.icon.indexOf('/') >= 0
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-empty-text-margin-top:20rpx !default;
$u-empty-slot-margin-top:20rpx !default;
.u-empty {
@include flex;
flex-direction: column;
justify-content: center;
align-items: center;
&__text {
@include flex;
justify-content: center;
align-items: center;
margin-top: $u-empty-text-margin-top;
}
}
.u-slot-wrap {
@include flex;
justify-content: center;
align-items: center;
margin-top:$u-empty-slot-margin-top;
}
</style>

View File

@@ -0,0 +1,43 @@
export default {
props: {
// input的label提示语
label: {
type: String,
default: uni.$u.props.formItem.label
},
// 绑定的值
prop: {
type: String,
default: uni.$u.props.formItem.prop
},
// 是否显示表单域的下划线边框
borderBottom: {
type: [String, Boolean],
default: uni.$u.props.formItem.borderBottom
},
// label的宽度单位px
labelWidth: {
type: [String, Number],
default: uni.$u.props.formItem.labelWidth
},
// 右侧图标
rightIcon: {
type: String,
default: uni.$u.props.formItem.rightIcon
},
// 左侧图标
leftIcon: {
type: String,
default: uni.$u.props.formItem.leftIcon
},
// 是否显示左边的必填星号只作显示用具体校验必填的逻辑请在rules中配置
required: {
type: Boolean,
default: uni.$u.props.formItem.required
},
leftIconStyle: {
type: [String, Object],
default: uni.$u.props.formItem.leftIconStyle,
}
}
}

View File

@@ -0,0 +1,235 @@
<template>
<view class="u-form-item">
<view
class="u-form-item__body"
@tap="clickHandler"
:style="[$u.addStyle(customStyle), {
flexDirection: parentData.labelPosition === 'left' ? 'row' : 'column'
}]"
>
<!-- 微信小程序中将一个参数设置空字符串结果会变成字符串"true" -->
<slot name="label">
<!-- {{required}} -->
<view
class="u-form-item__body__left"
v-if="required || leftIcon || label"
:style="{
width: $u.addUnit(labelWidth || parentData.labelWidth),
marginBottom: parentData.labelPosition === 'left' ? 0 : '5px',
}"
>
<!-- 为了块对齐 -->
<view class="u-form-item__body__left__content">
<!-- nvue不支持伪元素before -->
<text
v-if="required"
class="u-form-item__body__left__content__required"
>*</text>
<view
class="u-form-item__body__left__content__icon"
v-if="leftIcon"
>
<u-icon
:name="leftIcon"
:custom-style="leftIconStyle"
></u-icon>
</view>
<text
class="u-form-item__body__left__content__label"
:style="[parentData.labelStyle, {
justifyContent: parentData.labelAlign === 'left' ? 'flex-start' : parentData.labelAlign === 'center' ? 'center' : 'flex-end'
}]"
>{{ label }}</text>
</view>
</view>
</slot>
<view class="u-form-item__body__right">
<view class="u-form-item__body__right__content">
<view class="u-form-item__body__right__content__slot">
<slot />
</view>
<view
class="item__body__right__content__icon"
v-if="$slots.right"
>
<slot name="right" />
</view>
</view>
</view>
</view>
<slot name="error">
<text
v-if="!!message && parentData.errorType === 'message'"
class="u-form-item__body__right__message"
:style="{
marginLeft: $u.addUnit(parentData.labelPosition === 'top' ? 0 : (labelWidth || parentData.labelWidth))
}"
>{{ message }}</text>
</slot>
<u-line
v-if="borderBottom"
:color="message && parentData.errorType === 'border-bottom' ? $u.color.error : propsLine.color"
:customStyle="`margin-top: ${message && parentData.errorType === 'message' ? '5px' : 0}`"
></u-line>
</view>
</template>
<script>
import props from './props.js';
/**
* Form 表单
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等。
* @tutorial https://www.uviewui.com/components/form.html
* @property {String} label input的label提示语
* @property {String} prop 绑定的值
* @property {String | Boolean} borderBottom 是否显示表单域的下划线边框
* @property {String | Number} labelWidth label的宽度单位px
* @property {String} rightIcon 右侧图标
* @property {String} leftIcon 左侧图标
* @property {String | Object} leftIconStyle 左侧图标的样式
* @property {Boolean} required 是否显示左边的必填星号只作显示用具体校验必填的逻辑请在rules中配置 (默认 false )
*
* @example <u-form-item label="姓名" prop="userInfo.name" borderBottom ref="item1"></u-form-item>
*/
export default {
name: 'u-form-item',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 错误提示语
message: '',
parentData: {
// 提示文本的位置
labelPosition: 'left',
// 提示文本对齐方式
labelAlign: 'left',
// 提示文本的样式
labelStyle: {},
// 提示文本的宽度
labelWidth: 45,
// 错误提示方式
errorType: 'message'
}
}
},
// 组件创建完成时将当前实例保存到u-form中
computed: {
propsLine() {
return uni.$u.props.line
}
},
mounted() {
this.init()
},
methods: {
init() {
// 父组件的实例
this.updateParentData()
if (!this.parent) {
uni.$u.error('u-form-item需要结合u-form组件使用')
}
},
// 获取父组件的参数
updateParentData() {
// 此方法写在mixin中
this.getParentData('u-form');
},
// 移除u-form-item的校验结果
clearValidate() {
this.message = null
},
// 清空当前的组件的校验结果,并重置为初始值
resetField() {
// 找到原始值
const value = uni.$u.getProperty(this.parent.originalModel, this.prop)
// 将u-form的model的prop属性链还原原始值
uni.$u.setProperty(this.parent.model, this.prop, value)
// 移除校验结果
this.message = null
},
// 点击组件
clickHandler() {
this.$emit('click')
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-form-item {
@include flex(column);
font-size: 14px;
color: $u-main-color;
&__body {
@include flex;
padding: 10px 0;
&__left {
@include flex;
align-items: center;
&__content {
position: relative;
@include flex;
align-items: center;
padding-right: 10rpx;
flex: 1;
&__icon {
margin-right: 8rpx;
}
&__required {
position: absolute;
left: -9px;
color: $u-error;
line-height: 20px;
font-size: 20px;
top: 3px;
}
&__label {
@include flex;
align-items: center;
flex: 1;
color: $u-main-color;
font-size: 15px;
}
}
}
&__right {
flex: 1;
&__content {
@include flex;
align-items: center;
flex: 1;
&__slot {
flex: 1;
/* #ifndef MP */
@include flex;
align-items: center;
/* #endif */
}
&__icon {
margin-left: 10rpx;
color: $u-light-color;
font-size: 30rpx;
}
}
&__message {
font-size: 12px;
line-height: 12px;
color: $u-error;
}
}
}
}
</style>

View File

@@ -0,0 +1,45 @@
export default {
props: {
// 当前form的需要验证字段的集合
model: {
type: Object,
default: uni.$u.props.form.model
},
// 验证规则
rules: {
type: [Object, Function, Array],
default: uni.$u.props.form.rules
},
// 有错误时的提示方式message-提示信息toast-进行toast提示
// border-bottom-下边框呈现红色none-无提示
errorType: {
type: String,
default: uni.$u.props.form.errorType
},
// 是否显示表单域的下划线边框
borderBottom: {
type: Boolean,
default: uni.$u.props.form.borderBottom
},
// label的位置left-左边top-上边
labelPosition: {
type: String,
default: uni.$u.props.form.labelPosition
},
// label的宽度单位px
labelWidth: {
type: [String, Number],
default: uni.$u.props.form.labelWidth
},
// lable字体的对齐方式
labelAlign: {
type: String,
default: uni.$u.props.form.labelAlign
},
// lable的样式对象形式
labelStyle: {
type: Object,
default: uni.$u.props.form.labelStyle
}
}
}

View File

@@ -0,0 +1,214 @@
<template>
<view class="u-form">
<slot />
</view>
</template>
<script>
import props from "./props.js";
import Schema from "../../libs/util/async-validator";
// 去除警告信息
Schema.warning = function() {};
/**
* Form 表单
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等。
* @tutorial https://www.uviewui.com/components/form.html
* @property {Object} model 当前form的需要验证字段的集合
* @property {Object | Function | Array} rules 验证规则
* @property {String} errorType 错误的提示方式,见上方说明 ( 默认 message )
* @property {Boolean} borderBottom 是否显示表单域的下划线边框 ( 默认 true
* @property {String} labelPosition 表单域提示文字的位置left-左侧top-上方 ( 默认 'left'
* @property {String | Number} labelWidth 提示文字的宽度单位px ( 默认 45
* @property {String} labelAlign lable字体的对齐方式 ( 默认 left'
* @property {Object} labelStyle lable的样式对象形式
* @example <u--formlabelPosition="left" :model="model1" :rules="rules" ref="form1"></u--form>
*/
export default {
name: "u-form",
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
provide() {
return {
uForm: this,
};
},
data() {
return {
formRules: {},
// 规则校验器
validator: {},
// 原始的model快照用于resetFields方法重置表单时使用
originalModel: null,
};
},
watch: {
// 监听规则的变化
rules: {
immediate: true,
handler(n) {
this.setRules(n);
},
},
// 监听属性的变化通知子组件u-form-item重新获取信息
propsChange(n) {
if (this.children?.length) {
this.children.map((child) => {
// 判断子组件(u-form-item)如果有updateParentData方法的话就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof child.updateParentData == "function" &&
child.updateParentData();
});
}
},
// 监听model的初始值作为重置表单的快照
model: {
immediate: true,
handler(n) {
if (!this.originalModel) {
this.originalModel = uni.$u.deepClone(n);
}
},
},
},
computed: {
propsChange() {
return [
this.errorType,
this.borderBottom,
this.labelPosition,
this.labelWidth,
this.labelAlign,
this.labelStyle,
];
},
},
created() {
// 存储当前form下的所有u-form-item的实例
// 不能定义在data中否则微信小程序会造成循环引用而报错
this.children = [];
},
methods: {
// 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
setRules(rules) {
// 判断是否有规则
if (Object.keys(rules).length === 0) return;
if (process.env.NODE_ENV === 'development' && Object.keys(this.model).length === 0) {
uni.$u.error('设置rulesmodel必须设置如果已经设置请刷新页面。');
return;
};
this.formRules = rules;
// 重新将规则赋予Validator
this.validator = new Schema(rules);
},
// 清空所有u-form-item组件的内容本质上是调用了u-form-item组件中的resetField()方法
resetFields() {
this.resetModel();
},
// 重置model为初始值的快照
resetModel(obj) {
// 历遍所有u-form-item根据其prop属性还原model的原始快照
this.children.map((child) => {
const prop = child?.prop;
const value = uni.$u.getProperty(this.originalModel, prop);
uni.$u.setProperty(this.model, prop, value);
});
},
// 清空校验结果
clearValidate(props) {
props = [].concat(props);
this.children.map((child) => {
// 如果u-form-item的prop在props数组中则清除对应的校验结果信息
if (props[0] === undefined || props.includes(child.prop)) {
child.message = null;
}
});
},
// 对部分表单字段进行校验
async validateField(value, callback, event = null) {
// $nextTick是必须的否则model的变更可能会延后于此方法的执行
this.$nextTick(() => {
// 校验错误信息返回给回调方法用于存放所有form-item的错误信息
const errorsRes = [];
// 如果为字符串,转为数组
value = [].concat(value);
// 历遍children所有子form-item
this.children.map((child) => {
// 用于存放form-item的错误信息
const childErrors = [];
if (value.includes(child.prop)) {
// 获取对应的属性,通过类似'a.b.c'的形式
const propertyVal = uni.$u.getProperty(
this.model,
child.prop
);
// 属性链数组
const propertyChain = child.prop.split(".");
const propertyName =
propertyChain[propertyChain.length - 1];
const rule = this.formRules[child.prop];
// 如果不存在对应的规则,直接返回,否则校验器会报错
if (!rule) return;
// rule规则可为数组形式也可为对象形式此处拼接成为数组
const rules = [].concat(rule);
// 对rules数组进行校验
for (let i = 0; i < rules.length; i++) {
const ruleItem = rules[i];
// 将u-form-item的触发器转为数组形式
const trigger = [].concat(ruleItem?.trigger);
// 如果是有传入触发事件但是此form-item却没有配置此触发器的话不执行校验操作
if (event && !trigger.includes(event)) continue;
// 实例化校验对象,传入构造规则
const validator = new Schema({
[propertyName]: ruleItem,
});
validator.validate({
[propertyName]: propertyVal,
},
(errors, fields) => {
if (uni.$u.test.array(errors)) {
errorsRes.push(...errors);
childErrors.push(...errors);
}
child.message =
childErrors[0]?.message ?? null;
}
);
}
}
});
// 执行回调函数
typeof callback === "function" && callback(errorsRes);
});
},
// 校验全部数据
validate(callback) {
// 开发环境才提示,生产环境不会提示
if (process.env.NODE_ENV === 'development' && Object.keys(this.formRules).length === 0) {
uni.$u.error('未设置rules请看文档说明如果已经设置请刷新页面。');
return;
}
return new Promise((resolve, reject) => {
// $nextTick是必须的否则model的变更可能会延后于validate方法
this.$nextTick(() => {
// 获取所有form-item的prop交给validateField方法进行校验
const formItemProps = this.children.map(
(item) => item.prop
);
this.validateField(formItemProps, (errors) => {
if(errors.length) {
// 如果错误提示方式为toast则进行提示
this.errorType === 'toast' && uni.$u.toast(errors[0].message)
reject(errors)
} else {
resolve(true)
}
});
});
});
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,24 @@
export default {
props: {
// 背景颜色默认transparent
bgColor: {
type: String,
default: uni.$u.props.gap.bgColor
},
// 分割槽高度单位px默认30
height: {
type: [String, Number],
default: uni.$u.props.gap.height
},
// 与上一个组件的距离
marginTop: {
type: [String, Number],
default: uni.$u.props.gap.marginTop
},
// 与下一个组件的距离
marginBottom: {
type: [String, Number],
default: uni.$u.props.gap.marginBottom
}
}
}

View File

@@ -0,0 +1,38 @@
<template>
<view class="u-gap" :style="[gapStyle]"></view>
</template>
<script>
import props from './props.js';
/**
* gap 间隔槽
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
* @tutorial https://www.uviewui.com/components/gap.html
* @property {String} bgColor 背景颜色 (默认 'transparent'
* @property {String | Number} height 分割槽高度单位px (默认 20
* @property {String | Number} marginTop 与前一个组件的距离单位px 默认 0
* @property {String | Number} marginBottom 与后一个组件的距离单位px (默认 0
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <u-gap height="80" bg-color="#bbb"></u-gap>
*/
export default {
name: "u-gap",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
gapStyle() {
const style = {
backgroundColor: this.bgColor,
height: uni.$u.addUnit(this.height),
marginTop: uni.$u.addUnit(this.marginTop),
marginBottom: uni.$u.addUnit(this.marginBottom),
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>

View File

@@ -0,0 +1,14 @@
export default {
props: {
// 宫格的name
name: {
type: [String, Number, null],
default: uni.$u.props.gridItem.name
},
// 背景颜色
bgColor: {
type: String,
default: uni.$u.props.gridItem.bgColor
}
}
}

View File

@@ -0,0 +1,209 @@
<template>
<!-- #ifndef APP-NVUE -->
<view
class="u-grid-item"
hover-class="u-grid-item--hover-class"
:hover-stay-time="200"
@tap="clickHandler"
:class="classes"
:style="[itemStyle]"
>
<slot />
</view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view
class="u-grid-item"
:hover-stay-time="200"
@tap="clickHandler"
:class="classes"
:style="[itemStyle]"
>
<slot />
</view>
<!-- #endif -->
</template>
<script>
import props from './props.js';
/**
* gridItem 提示
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge)或者图标等也可以扩展为左右滑动的轮播形式。搭配u-grid使用
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String | Number} name 宫格的name ( 默认 null )
* @property {String} bgColor 宫格的背景颜色 (默认 'transparent'
* @property {Object} customStyle 自定义样式,对象形式
* @event {Function} click 点击宫格触发
* @example <u-grid-item></u-grid-item>
*/
export default {
name: "u-grid-item",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
parentData: {
col: 3, // 父组件划分的宫格数
border: true, // 是否显示边框,根据父组件决定
},
// #ifdef APP-NVUE
width: 0, // nvue下才这么计算vue下放到computed中否则会因为延时造成闪烁
// #endif
classes: [], // 类名集合,用于判断是否显示右边和下边框
};
},
mounted() {
this.init()
},
computed: {
// #ifndef APP-NVUE
// vue下放到computed中否则会因为延时造成闪烁
width() {
return 100 / Number(this.parentData.col) + '%'
},
// #endif
itemStyle() {
const style = {
background: this.bgColor,
width: this.width
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
},
methods: {
init() {
// 用于在父组件u-grid的children中被添加入子组件时
// 重新计算item的边框
uni.$on('$uGridItem', () => {
this.gridItemClasses()
})
// 父组件的实例
this.updateParentData()
// #ifdef APP-NVUE
// 获取元素该有的长度nvue下要延时才准确
this.$nextTick(function(){
this.getItemWidth()
})
// #endif
// 发出事件通知所有的grid-item都重新计算自己的边框
uni.$emit('$uGridItem')
this.gridItemClasses()
},
// 获取父组件的参数
updateParentData() {
// 此方法写在mixin中
this.getParentData('u-grid');
},
clickHandler() {
let name = this.name
// 如果没有设置name属性历遍父组件的children数组判断当前的元素是否和本实例this相等找出当前组件的索引
const children = this.parent?.children
if(children && this.name === null) {
name = children.findIndex(child => child === this)
}
// 调用父组件方法,发出事件
this.parent && this.parent.childClick(name)
this.$emit('click', name)
},
async getItemWidth() {
// 如果是nvue不能使用百分比只能使用固定宽度
let width = 0
if(this.parent) {
// 获取父组件宽度后除以栅格数得出每个item的宽度
const parentWidth = await this.getParentWidth()
width = parentWidth / Number(this.parentData.col) + 'px'
}
this.width = width
},
// 获取父元素的尺寸
getParentWidth() {
// #ifdef APP-NVUE
// 返回一个promise让调用者可以用await同步获取
const dom = uni.requireNativePlugin('dom')
return new Promise(resolve => {
// 调用父组件的ref
dom.getComponentRect(this.parent.$refs['u-grid'], res => {
resolve(res.size.width)
})
})
// #endif
},
gridItemClasses() {
if(this.parentData.border) {
const classes = []
this.parent.children.map((child, index) =>{
if(this === child) {
const len = this.parent.children.length
// 贴近右边屏幕边沿的child并且最后一个比如只有横向2个的时候无需右边框
if((index + 1) % this.parentData.col !== 0 && index + 1 !== len) {
classes.push('u-border-right')
}
// 总的宫格数量对列数取余的值
// 如果取余后值为0则意味着要将最后一排的宫格都不需要下边框
const lessNum = len % this.parentData.col === 0 ? this.parentData.col : len % this.parentData.col
// 最下面的一排child无需下边框
if(index < len - lessNum) {
classes.push('u-border-bottom')
}
}
})
// 支付宝,头条小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
// #ifdef MP-ALIPAY || MP-TOUTIAO
classes = classes.join(' ')
// #endif
this.classes = classes
}
}
},
beforeDestroy() {
// 移除事件监听,释放性能
uni.$off('$uGridItem')
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-grid-item-hover-class-opcatiy:.5 !default;
$u-grid-item-margin-top:1rpx !default;
$u-grid-item-border-right-width:0.5px !default;
$u-grid-item-border-bottom-width:0.5px !default;
$u-grid-item-border-right-color:$u-border-color !default;
$u-grid-item-border-bottom-color:$u-border-color !default;
.u-grid-item {
align-items: center;
justify-content: center;
position: relative;
flex-direction: column;
/* #ifndef APP-NVUE */
box-sizing: border-box;
display: flex;
/* #endif */
/* #ifdef MP */
position: relative;
float: left;
/* #endif */
/* #ifdef MP-WEIXIN */
margin-top:$u-grid-item-margin-top;
/* #endif */
&--hover-class {
opacity:$u-grid-item-hover-class-opcatiy;
}
}
/* #ifdef APP-NVUE */
// 由于nvue不支持组件内引入app.vue中再引入的样式所以需要写在这里
.u-border-right {
border-right-width:$u-grid-item-border-right-width;
border-color: $u-grid-item-border-right-color;
}
.u-border-bottom {
border-bottom-width:$u-grid-item-border-bottom-width;
border-color:$u-grid-item-border-bottom-color;
}
/* #endif */
</style>

View File

@@ -0,0 +1,19 @@
export default {
props: {
// 分成几列
col: {
type: [String, Number],
default: uni.$u.props.grid.col
},
// 是否显示边框
border: {
type: Boolean,
default: uni.$u.props.grid.border
},
// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
align: {
type: String,
default: uni.$u.props.grid.align
}
}
}

View File

@@ -0,0 +1,97 @@
<template>
<view
class="u-grid"
ref='u-grid'
:style="[gridStyle]"
>
<slot />
</view>
</template>
<script>
import props from './props.js';
/**
* grid 宫格布局
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String | Number} col 宫格的列数(默认 3
* @property {Boolean} border 是否显示宫格的边框(默认 false
* @property {String} align 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 (默认 'left'
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click 点击宫格触发
* @example <u-grid :col="3" @click="click"></u-grid>
*/
export default {
name: 'u-grid',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
index: 0,
width: 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 uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
}
},
methods: {
// 此方法由u-grid-item触发用于在u-grid发出事件
childClick(name) {
this.$emit('click', name)
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-grid-width:100% !default;
.u-grid {
/* #ifdef MP */
width: $u-grid-width;
position: relative;
box-sizing: border-box;
overflow: hidden;
display: block;
/* #endif */
justify-content: center;
@include flex;
flex-wrap: wrap;
align-items: center;
}
</style>

View File

@@ -0,0 +1,214 @@
export default {
'uicon-level': '\ue693',
'uicon-column-line': '\ue68e',
'uicon-checkbox-mark': '\ue807',
'uicon-folder': '\ue7f5',
'uicon-movie': '\ue7f6',
'uicon-star-fill': '\ue669',
'uicon-star': '\ue65f',
'uicon-phone-fill': '\ue64f',
'uicon-phone': '\ue622',
'uicon-apple-fill': '\ue881',
'uicon-chrome-circle-fill': '\ue885',
'uicon-backspace': '\ue67b',
'uicon-attach': '\ue632',
'uicon-cut': '\ue948',
'uicon-empty-car': '\ue602',
'uicon-empty-coupon': '\ue682',
'uicon-empty-address': '\ue646',
'uicon-empty-favor': '\ue67c',
'uicon-empty-permission': '\ue686',
'uicon-empty-news': '\ue687',
'uicon-empty-search': '\ue664',
'uicon-github-circle-fill': '\ue887',
'uicon-rmb': '\ue608',
'uicon-person-delete-fill': '\ue66a',
'uicon-reload': '\ue788',
'uicon-order': '\ue68f',
'uicon-server-man': '\ue6bc',
'uicon-search': '\ue62a',
'uicon-fingerprint': '\ue955',
'uicon-more-dot-fill': '\ue630',
'uicon-scan': '\ue662',
'uicon-share-square': '\ue60b',
'uicon-map': '\ue61d',
'uicon-map-fill': '\ue64e',
'uicon-tags': '\ue629',
'uicon-tags-fill': '\ue651',
'uicon-bookmark-fill': '\ue63b',
'uicon-bookmark': '\ue60a',
'uicon-eye': '\ue613',
'uicon-eye-fill': '\ue641',
'uicon-mic': '\ue64a',
'uicon-mic-off': '\ue649',
'uicon-calendar': '\ue66e',
'uicon-calendar-fill': '\ue634',
'uicon-trash': '\ue623',
'uicon-trash-fill': '\ue658',
'uicon-play-left': '\ue66d',
'uicon-play-right': '\ue610',
'uicon-minus': '\ue618',
'uicon-plus': '\ue62d',
'uicon-info': '\ue653',
'uicon-info-circle': '\ue7d2',
'uicon-info-circle-fill': '\ue64b',
'uicon-question': '\ue715',
'uicon-error': '\ue6d3',
'uicon-close': '\ue685',
'uicon-checkmark': '\ue6a8',
'uicon-android-circle-fill': '\ue67e',
'uicon-android-fill': '\ue67d',
'uicon-ie': '\ue87b',
'uicon-IE-circle-fill': '\ue889',
'uicon-google': '\ue87a',
'uicon-google-circle-fill': '\ue88a',
'uicon-setting-fill': '\ue872',
'uicon-setting': '\ue61f',
'uicon-minus-square-fill': '\ue855',
'uicon-plus-square-fill': '\ue856',
'uicon-heart': '\ue7df',
'uicon-heart-fill': '\ue851',
'uicon-camera': '\ue7d7',
'uicon-camera-fill': '\ue870',
'uicon-more-circle': '\ue63e',
'uicon-more-circle-fill': '\ue645',
'uicon-chat': '\ue620',
'uicon-chat-fill': '\ue61e',
'uicon-bag-fill': '\ue617',
'uicon-bag': '\ue619',
'uicon-error-circle-fill': '\ue62c',
'uicon-error-circle': '\ue624',
'uicon-close-circle': '\ue63f',
'uicon-close-circle-fill': '\ue637',
'uicon-checkmark-circle': '\ue63d',
'uicon-checkmark-circle-fill': '\ue635',
'uicon-question-circle-fill': '\ue666',
'uicon-question-circle': '\ue625',
'uicon-share': '\ue631',
'uicon-share-fill': '\ue65e',
'uicon-shopping-cart': '\ue621',
'uicon-shopping-cart-fill': '\ue65d',
'uicon-bell': '\ue609',
'uicon-bell-fill': '\ue640',
'uicon-list': '\ue650',
'uicon-list-dot': '\ue616',
'uicon-zhihu': '\ue6ba',
'uicon-zhihu-circle-fill': '\ue709',
'uicon-zhifubao': '\ue6b9',
'uicon-zhifubao-circle-fill': '\ue6b8',
'uicon-weixin-circle-fill': '\ue6b1',
'uicon-weixin-fill': '\ue6b2',
'uicon-twitter-circle-fill': '\ue6ab',
'uicon-twitter': '\ue6aa',
'uicon-taobao-circle-fill': '\ue6a7',
'uicon-taobao': '\ue6a6',
'uicon-weibo-circle-fill': '\ue6a5',
'uicon-weibo': '\ue6a4',
'uicon-qq-fill': '\ue6a1',
'uicon-qq-circle-fill': '\ue6a0',
'uicon-moments-circel-fill': '\ue69a',
'uicon-moments': '\ue69b',
'uicon-qzone': '\ue695',
'uicon-qzone-circle-fill': '\ue696',
'uicon-baidu-circle-fill': '\ue680',
'uicon-baidu': '\ue681',
'uicon-facebook-circle-fill': '\ue68a',
'uicon-facebook': '\ue689',
'uicon-car': '\ue60c',
'uicon-car-fill': '\ue636',
'uicon-warning-fill': '\ue64d',
'uicon-warning': '\ue694',
'uicon-clock-fill': '\ue638',
'uicon-clock': '\ue60f',
'uicon-edit-pen': '\ue612',
'uicon-edit-pen-fill': '\ue66b',
'uicon-email': '\ue611',
'uicon-email-fill': '\ue642',
'uicon-minus-circle': '\ue61b',
'uicon-minus-circle-fill': '\ue652',
'uicon-plus-circle': '\ue62e',
'uicon-plus-circle-fill': '\ue661',
'uicon-file-text': '\ue663',
'uicon-file-text-fill': '\ue665',
'uicon-pushpin': '\ue7e3',
'uicon-pushpin-fill': '\ue86e',
'uicon-grid': '\ue673',
'uicon-grid-fill': '\ue678',
'uicon-play-circle': '\ue647',
'uicon-play-circle-fill': '\ue655',
'uicon-pause-circle-fill': '\ue654',
'uicon-pause': '\ue8fa',
'uicon-pause-circle': '\ue643',
'uicon-eye-off': '\ue648',
'uicon-eye-off-outline': '\ue62b',
'uicon-gift-fill': '\ue65c',
'uicon-gift': '\ue65b',
'uicon-rmb-circle-fill': '\ue657',
'uicon-rmb-circle': '\ue677',
'uicon-kefu-ermai': '\ue656',
'uicon-server-fill': '\ue751',
'uicon-coupon-fill': '\ue8c4',
'uicon-coupon': '\ue8ae',
'uicon-integral': '\ue704',
'uicon-integral-fill': '\ue703',
'uicon-home-fill': '\ue964',
'uicon-home': '\ue965',
'uicon-hourglass-half-fill': '\ue966',
'uicon-hourglass': '\ue967',
'uicon-account': '\ue628',
'uicon-plus-people-fill': '\ue626',
'uicon-minus-people-fill': '\ue615',
'uicon-account-fill': '\ue614',
'uicon-thumb-down-fill': '\ue726',
'uicon-thumb-down': '\ue727',
'uicon-thumb-up': '\ue733',
'uicon-thumb-up-fill': '\ue72f',
'uicon-lock-fill': '\ue979',
'uicon-lock-open': '\ue973',
'uicon-lock-opened-fill': '\ue974',
'uicon-lock': '\ue97a',
'uicon-red-packet-fill': '\ue690',
'uicon-photo-fill': '\ue98b',
'uicon-photo': '\ue98d',
'uicon-volume-off-fill': '\ue659',
'uicon-volume-off': '\ue644',
'uicon-volume-fill': '\ue670',
'uicon-volume': '\ue633',
'uicon-red-packet': '\ue691',
'uicon-download': '\ue63c',
'uicon-arrow-up-fill': '\ue6b0',
'uicon-arrow-down-fill': '\ue600',
'uicon-play-left-fill': '\ue675',
'uicon-play-right-fill': '\ue676',
'uicon-rewind-left-fill': '\ue679',
'uicon-rewind-right-fill': '\ue67a',
'uicon-arrow-downward': '\ue604',
'uicon-arrow-leftward': '\ue601',
'uicon-arrow-rightward': '\ue603',
'uicon-arrow-upward': '\ue607',
'uicon-arrow-down': '\ue60d',
'uicon-arrow-right': '\ue605',
'uicon-arrow-left': '\ue60e',
'uicon-arrow-up': '\ue606',
'uicon-skip-back-left': '\ue674',
'uicon-skip-forward-right': '\ue672',
'uicon-rewind-right': '\ue66f',
'uicon-rewind-left': '\ue671',
'uicon-arrow-right-double': '\ue68d',
'uicon-arrow-left-double': '\ue68c',
'uicon-wifi-off': '\ue668',
'uicon-wifi': '\ue667',
'uicon-empty-data': '\ue62f',
'uicon-empty-history': '\ue684',
'uicon-empty-list': '\ue68b',
'uicon-empty-page': '\ue627',
'uicon-empty-order': '\ue639',
'uicon-man': '\ue697',
'uicon-woman': '\ue69c',
'uicon-man-add': '\ue61c',
'uicon-man-add-fill': '\ue64c',
'uicon-man-delete': '\ue61a',
'uicon-man-delete-fill': '\ue66a',
'uicon-zh': '\ue70a',
'uicon-en': '\ue692'
}

View File

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

View File

@@ -0,0 +1,234 @@
<template>
<view
class="u-icon"
@tap="clickHandler"
:class="['u-icon--' + labelPos]"
>
<image
class="u-icon__img"
v-if="isImg"
:src="name"
:mode="imgMode"
:style="[imgStyle, $u.addStyle(customStyle)]"
></image>
<text
v-else
class="u-icon__icon"
:class="uClasses"
:style="[iconStyle, $u.addStyle(customStyle)]"
:hover-class="hoverClass"
>{{icon}}</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(space) : 0,
marginTop: labelPos == 'bottom' ? $u.addUnit(space) : 0,
marginRight: labelPos == 'left' ? $u.addUnit(space) : 0,
marginBottom: labelPos == 'top' ? $u.addUnit(space) : 0,
}"
>{{ label }}</text>
</view>
</template>
<script>
// #ifdef APP-NVUE
// nvue通过weex的dom模块引入字体相关文档地址如下
// https://weex.apache.org/zh/docs/modules/dom.html#addrule
const fontUrl = 'https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf'
const domModule = weex.requireModule('dom')
domModule.addRule('fontFace', {
'fontFamily': "uicon-iconfont",
'src': `url('${fontUrl}')`
})
// #endif
// 引入图标名称已经对应的unicode
import icons from './icons'
import props from './props.js';;
/**
* icon 图标
* @description 基于字体的图标集,包含了大多数常见场景的图标。
* @tutorial https://www.uviewui.com/components/icon.html
* @property {String} name 图标名称,见示例图标集
* @property {String} color 图标颜色,可接受主题色 (默认 color['u-content-color']
* @property {String | Number} size 图标字体大小单位px (默认 '16px'
* @property {Boolean} bold 是否显示粗体 (默认 false
* @property {String | Number} index 点击图标的时候传递事件出去的index用于区分点击了哪一个
* @property {String} hoverClass 图标按下去的样式类用法同uni的view组件的hoverClass参数详情见官网
* @property {String} customPrefix 自定义扩展前缀,方便用户扩展自己的图标库 (默认 'uicon'
* @property {String | Number} label 图标右侧的label文字
* @property {String} labelPos label相对于图标的位置只能right或bottom (默认 'right'
* @property {String | Number} labelSize label字体大小单位px (默认 '15px'
* @property {String} labelColor 图标右侧的label文字颜色 默认 color['u-content-color']
* @property {String | Number} space label与图标的距离单位px (默认 '3px'
* @property {String} imgMode 图片的mode
* @property {String | Number} width 显示图片小图标时的宽度
* @property {String | Number} height 显示图片小图标时的高度
* @property {String | Number} top 图标在垂直方向上的定位 用于解决某些情况下,让图标垂直居中的用途 (默认 0
* @property {Boolean} stop 是否阻止事件传播 (默认 false
* @property {Object} customStyle icon的样式对象形式
* @event {Function} click 点击图标时触发
* @event {Function} touchstart 事件触摸时触发
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
*/
export default {
name: 'u-icon',
data() {
return {
}
},
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
uClasses() {
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 && uni.$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: uni.$u.addUnit(this.size),
lineHeight: uni.$u.addUnit(this.size),
fontWeight: this.bold ? 'bold' : 'normal',
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
top: uni.$u.addUnit(this.top)
}
// 非主题色值时,才当作颜色值
if (this.color && !uni.$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 ? uni.$u.addUnit(this.width) : uni.$u.addUnit(this.size)
style.height = this.height ? uni.$u.addUnit(this.height) : uni.$u.addUnit(this.size)
return style
},
// 通过图标名,查找对应的图标
icon() {
// 如果内置的图标中找不到对应的图标就直接返回name值因为用户可能传入的是unicode代码
return icons['uicon-' + this.name] || this.name
}
},
methods: {
clickHandler(e) {
this.$emit('click', this.index)
// 是否阻止事件冒泡
this.stop && this.preventEvent(e)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
// 变量定义
$u-icon-primary: $u-primary !default;
$u-icon-success: $u-success !default;
$u-icon-info: $u-info !default;
$u-icon-warning: $u-warning !default;
$u-icon-error: $u-error !default;
$u-icon-label-line-height:1 !default;
/* #ifndef APP-NVUE */
// 非nvue下加载字体
@font-face {
font-family: 'uicon-iconfont';
src: url('https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf') format('truetype');
}
/* #endif */
.u-icon {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
font-family: uicon-iconfont;
position: relative;
@include flex;
align-items: center;
&--primary {
color: $u-icon-primary;
}
&--success {
color: $u-icon-success;
}
&--error {
color: $u-icon-error;
}
&--warning {
color: $u-icon-warning;
}
&--info {
color: $u-icon-info;
}
}
&__img {
/* #ifndef APP-NVUE */
height: auto;
will-change: transform;
/* #endif */
}
&__label {
/* #ifndef APP-NVUE */
line-height: $u-icon-label-line-height;
/* #endif */
}
}
</style>

View File

@@ -0,0 +1,84 @@
export default {
props: {
// 图片地址
src: {
type: String,
default: uni.$u.props.image.src
},
// 裁剪模式
mode: {
type: String,
default: uni.$u.props.image.mode
},
// 宽度,单位任意
width: {
type: [String, Number],
default: uni.$u.props.image.width
},
// 高度,单位任意
height: {
type: [String, Number],
default: uni.$u.props.image.height
},
// 图片形状circle-圆形square-方形
shape: {
type: String,
default: uni.$u.props.image.shape
},
// 圆角,单位任意
radius: {
type: [String, Number],
default: uni.$u.props.image.radius
},
// 是否懒加载微信小程序、App、百度小程序、字节跳动小程序
lazyLoad: {
type: Boolean,
default: uni.$u.props.image.lazyLoad
},
// 开启长按图片显示识别微信小程序码菜单
showMenuByLongpress: {
type: Boolean,
default: uni.$u.props.image.showMenuByLongpress
},
// 加载中的图标,或者小图片
loadingIcon: {
type: String,
default: uni.$u.props.image.loadingIcon
},
// 加载失败的图标,或者小图片
errorIcon: {
type: String,
default: uni.$u.props.image.errorIcon
},
// 是否显示加载中的图标或者自定义的slot
showLoading: {
type: Boolean,
default: uni.$u.props.image.showLoading
},
// 是否显示加载错误的图标或者自定义的slot
showError: {
type: Boolean,
default: uni.$u.props.image.showError
},
// 是否需要淡入效果
fade: {
type: Boolean,
default: uni.$u.props.image.fade
},
// 只支持网络资源,只对微信小程序有效
webp: {
type: Boolean,
default: uni.$u.props.image.webp
},
// 过渡时间单位ms
duration: {
type: [String, Number],
default: uni.$u.props.image.duration
},
// 背景颜色,用于深色页面加载图片时,为了和背景色融合
bgColor: {
type: String,
default: uni.$u.props.image.bgColor
}
}
}

View File

@@ -0,0 +1,232 @@
<template>
<u-transition
mode="fade"
:show="show"
:duration="fade ? 1000 : 0"
>
<view
class="u-image"
@tap="onClick"
:style="[wrapStyle, backgroundStyle]"
>
<image
v-if="!isError"
:src="src"
:mode="mode"
@error="onErrorHandler"
@load="onLoadHandler"
:show-menu-by-longpress="showMenuByLongpress"
:lazy-load="lazyLoad"
class="u-image__image"
:style="{
borderRadius: shape == 'circle' ? '10000px' : $u.addUnit(radius),
width: $u.addUnit(width),
height: $u.addUnit(height)
}"
></image>
<view
v-if="showLoading && loading"
class="u-image__loading"
:style="{
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(radius),
backgroundColor: this.bgColor,
width: $u.addUnit(width),
height: $u.addUnit(height)
}"
>
<slot name="loading">
<u-icon
:name="loadingIcon"
:width="width"
:height="height"
></u-icon>
</slot>
</view>
<view
v-if="showError && isError && !loading"
class="u-image__error"
:style="{
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(radius),
width: $u.addUnit(width),
height: $u.addUnit(height)
}"
>
<slot name="error">
<u-icon
:name="errorIcon"
:width="width"
:height="height"
></u-icon>
</slot>
</view>
</view>
</u-transition>
</template>
<script>
import props from './props.js';
/**
* Image 图片
* @description 此组件为uni-app的image组件的加强版在继承了原有功能外还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
* @tutorial https://uviewui.com/components/image.html
* @property {String} src 图片地址
* @property {String} mode 裁剪模式,见官网说明 (默认 'aspectFill'
* @property {String | Number} width 宽度单位任意如果为数值则为px单位 (默认 '300'
* @property {String | Number} height 高度单位任意如果为数值则为px单位 (默认 '225'
* @property {String} shape 图片形状circle-圆形square-方形 (默认 'square'
* @property {String | Number} radius 圆角值单位任意如果为数值则为px单位 (默认 0
* @property {Boolean} lazyLoad 是否懒加载仅微信小程序、App、百度小程序、字节跳动小程序有效 (默认 true
* @property {Boolean} showMenuByLongpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效 (默认 true
* @property {String} loadingIcon 加载中的图标,或者小图片 (默认 'photo'
* @property {String} errorIcon 加载失败的图标,或者小图片 (默认 'error-circle'
* @property {Boolean} showLoading 是否显示加载中的图标或者自定义的slot (默认 true
* @property {Boolean} showError 是否显示加载错误的图标或者自定义的slot (默认 true
* @property {Boolean} fade 是否需要淡入效果 (默认 true
* @property {Boolean} webp 只支持网络资源,只对微信小程序有效 (默认 false
* @property {String | Number} duration 搭配fade参数的过渡时间单位ms (默认 500
* @property {String} bgColor 背景颜色,用于深色页面加载图片时,为了和背景色融合 (默认 '#f3f4f6' )
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click 点击图片时触发
* @event {Function} error 图片加载失败时触发
* @event {Function} load 图片加载成功时触发
* @example <u-image width="100%" height="300px" :src="src"></u-image>
*/
export default {
name: 'u-image',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 图片是否加载错误,如果是,则显示错误占位图
isError: false,
// 初始化组件时,默认为加载中状态
loading: true,
// 不透明度,为了实现淡入淡出的效果
opacity: 1,
// 过渡时间因为props的值无法修改故需要一个中间值
durationTime: this.duration,
// 图片加载完成时去掉背景颜色因为如果是png图片就会显示灰色的背景
backgroundStyle: {},
// 用于fade模式的控制组件显示与否
show: false
};
},
watch: {
src: {
immediate: true,
handler(n) {
if (!n) {
// 如果传入null或者''或者false或者undefined标记为错误状态
this.isError = true
} else {
this.isError = false;
this.loading = true;
}
}
}
},
computed: {
wrapStyle() {
let style = {};
// 通过调用addUnit()方法如果有单位如百分比px单位等直接返回如果是纯粹的数值则加上rpx单位
style.width = this.$u.addUnit(this.width);
style.height = this.$u.addUnit(this.height);
// 如果是显示圆形,设置一个很多的半径值即可
style.borderRadius = this.shape == 'circle' ? '10000px' : uni.$u.addUnit(this.radius)
// 如果设置圆角必须要有hidden否则可能圆角无效
style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible'
// if (this.fade) {
// style.opacity = this.opacity
// // nvue下这几个属性必须要分开写
// style.transitionDuration = `${this.durationTime}ms`
// style.transitionTimingFunction = 'ease-in-out'
// style.transitionProperty = 'opacity'
// }
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
}
},
mounted() {
this.show = true
},
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')
this.removeBgColor()
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
// 否则无需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 lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-image-error-top:0px !default;
$u-image-error-left:0px !default;
$u-image-error-width:100% !default;
$u-image-error-hight:100% !default;
$u-image-error-background-color:$u-bg-color !default;
$u-image-error-color:$u-tips-color !default;
$u-image-error-font-size: 46rpx !default;
.u-image {
position: relative;
transition: opacity 0.5s ease-in-out;
&__image {
width: 100%;
height: 100%;
}
&__loading,
&__error {
position: absolute;
top: $u-image-error-top;
left: $u-image-error-left;
width: $u-image-error-width;
height: $u-image-error-hight;
@include flex;
align-items: center;
justify-content: center;
background-color: $u-image-error-background-color;
color: $u-image-error-color;
font-size: $u-image-error-font-size;
}
}
</style>

View File

@@ -0,0 +1,29 @@
export default {
props: {
// 列表锚点文本内容
text: {
type: [String, Number],
default: uni.$u.props.indexAnchor.text
},
// 列表锚点文字颜色
color: {
type: String,
default: uni.$u.props.indexAnchor.color
},
// 列表锚点文字大小单位默认px
size: {
type: [String, Number],
default: uni.$u.props.indexAnchor.size
},
// 列表锚点背景颜色
bgColor: {
type: String,
default: uni.$u.props.indexAnchor.bgColor
},
// 列表锚点高度单位默认px
height: {
type: [String, Number],
default: uni.$u.props.indexAnchor.height
}
}
}

View File

@@ -0,0 +1,91 @@
<template>
<!-- #ifdef APP-NVUE -->
<header>
<!-- #endif -->
<view
class="u-index-anchor u-border-bottom"
:ref="`u-index-anchor-${text}`"
:style="{
height: $u.addUnit(height),
backgroundColor: bgColor
}"
>
<text
class="u-index-anchor__text"
:style="{
fontSize: $u.addUnit(size),
color: color
}"
>{{ text }}</text>
</view>
<!-- #ifdef APP-NVUE -->
</header>
<!-- #endif -->
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* IndexAnchor 列表锚点
* @description
* @tutorial https://uviewui.com/components/indexList.html
* @property {String | Number} text 列表锚点文本内容
* @property {String} color 列表锚点文字颜色 ( 默认 '#606266' )
* @property {String | Number} size 列表锚点文字大小单位默认px ( 默认 14 )
* @property {String} bgColor 列表锚点背景颜色 ( 默认 '#dedede' )
* @property {String | Number} height 列表锚点高度单位默认px ( 默认 32 )
* @example <u-index-anchor :text="indexList[index]"></u-index-anchor>
*/
export default {
name: 'u-index-anchor',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
}
},
mounted() {
this.init()
},
methods: {
init() {
// 此处会活动父组件实例并赋值给实例的parent属性
const indexList = uni.$u.$parent.call(this, 'u-index-list')
if (!indexList) {
return uni.$u.error('u-index-anchor必须要搭配u-index-list组件使用')
}
// 将当前实例放入到u-index-list中
indexList.anchors.push(this)
const indexListItem = uni.$u.$parent.call(this, 'u-index-item')
// #ifndef APP-NVUE
// 只有在非nvue下u-index-anchor才是嵌套在u-index-item中的
if (!indexListItem) {
return uni.$u.error('u-index-anchor必须要搭配u-index-item组件使用')
}
// 设置u-index-item的id为anchor的text标识符因为非nvue下滚动列表需要依赖scroll-view滚动到元素的特性
indexListItem.id = this.text.charCodeAt(0)
// #endif
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-index-anchor {
position: sticky;
top: 0;
@include flex;
align-items: center;
padding-left: 15px;
z-index: 1;
&__text {
@include flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,5 @@
export default {
props: {
}
}

View File

@@ -0,0 +1,87 @@
<template>
<!-- #ifdef APP-NVUE -->
<cell ref="u-index-item">
<!-- #endif -->
<view
class="u-index-item"
:id="`u-index-item-${id}`"
:class="[`u-index-item-${id}`]"
>
<slot />
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<!-- #endif -->
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物所以不支持百分比单位这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* IndexItem
* @description
* @tutorial https://uviewui.com/components/indexList.html
* @property {String}
* @event {Function}
* @example
*/
export default {
name: 'u-index-item',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
// 本组件到滚动条顶部的距离
top: 0,
height: 0,
id: ''
}
},
created() {
// 子组件u-index-anchor的实例
this.anchor = {}
},
mounted() {
this.init()
},
methods: {
init() {
// 此处会活动父组件实例并赋值给实例的parent属性
this.getParentData('u-index-list')
if (!this.parent) {
return uni.$u.error('u-index-item必须要搭配u-index-list组件使用')
}
uni.$u.sleep().then(() =>{
this.getIndexItemRect().then(size => {
// 由于对象的引用特性此处会同时生效到父组件的children数组的本实例的top属性中供父组件判断读取
this.top = Math.ceil(size.top)
this.height = Math.ceil(size.height)
})
})
},
getIndexItemRect() {
return new Promise(resolve => {
// #ifndef APP-NVUE
this.$uGetRect('.u-index-item').then(size => {
resolve(size)
})
// #endif
// #ifdef APP-NVUE
const ref = this.$refs['u-index-item']
dom.getComponentRect(ref, res => {
resolve(res.size)
})
// #endif
})
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
</style>

View File

@@ -0,0 +1,29 @@
export default {
props: {
// 右边锚点非激活的颜色
inactiveColor: {
type: String,
default: uni.$u.props.indexList.inactiveColor
},
// 右边锚点激活的颜色
activeColor: {
type: String,
default: uni.$u.props.indexList.activeColor
},
// 索引字符列表,数组形式
indexList: {
type: Array,
default: uni.$u.props.indexList.indexList
},
// 是否开启锚点自动吸顶
sticky: {
type: Boolean,
default: uni.$u.props.indexList.sticky
},
// 自定义导航栏的高度
customNavHeight: {
type: [String, Number],
default: uni.$u.props.indexList.customNavHeight
}
}
}

View File

@@ -0,0 +1,440 @@
<template>
<view class="u-index-list">
<!-- #ifdef APP-NVUE -->
<list
:scrollTop="scrollTop"
enable-back-to-top
:offset-accuracy="1"
:style="{
maxHeight: $u.addUnit(scrollViewHeight)
}"
@scroll="scrollHandler"
ref="uList"
>
<cell
v-if="$slots.header"
ref="header"
>
<slot name="header" />
</cell>
<slot />
<cell v-if="$slots.footer">
<slot name="footer" />
</cell>
</list>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<scroll-view
:scrollTop="scrollTop"
:scrollIntoView="scrollIntoView"
:offset-accuracy="1"
:style="{
maxHeight: $u.addUnit(scrollViewHeight)
}"
scroll-y
@scroll="scrollHandler"
ref="uList"
>
<view v-if="$slots.header">
<slot name="header" />
</view>
<slot />
<view v-if="$slots.footer">
<slot name="footer" />
</view>
</scroll-view>
<!-- #endif -->
<view
class="u-index-list__letter"
ref="u-index-list__letter"
:style="{ top: $u.addUnit(letterInfo.top || 100) }"
@touchstart="touchStart"
@touchmove.stop.prevent="touchMove"
@touchend.stop.prevent="touchEnd"
@touchcancel.stop.prevent="touchEnd"
>
<view
class="u-index-list__letter__item"
v-for="(item, index) in uIndexList"
:key="index"
:style="{
backgroundColor: activeIndex === index ? activeColor : 'transparent'
}"
>
<text
class="u-index-list__letter__item__index"
:style="{color: activeIndex === index ? '#fff' : inactiveColor}"
>{{ item }}</text>
</view>
</view>
<u-transition
mode="fade"
:show="touching"
:customStyle="{
position: 'fixed',
right: '50px',
top: $u.addUnit(indicatorTop),
zIndex: 2
}"
>
<view
class="u-index-list__indicator"
:class="['u-index-list__indicator--show']"
:style="{
height: $u.addUnit(indicatorHeight),
width: $u.addUnit(indicatorHeight)
}"
>
<text class="u-index-list__indicator__text">{{ uIndexList[activeIndex] }}</text>
</view>
</u-transition>
</view>
</template>
<script>
const indexList = () => {
const indexList = [];
const charCodeOfA = 'A'.charCodeAt(0);
for (let i = 0; i < 26; i++) {
indexList.push(String.fromCharCode(charCodeOfA + i));
}
return indexList;
}
import props from './props.js';
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物所以不支持百分比单位这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* IndexList 索引列表
* @description 通过折叠面板收纳内容区域
* @tutorial https://uviewui.com/components/indexList.html
* @property {String} inactiveColor 右边锚点非激活的颜色 ( 默认 '#606266' )
* @property {String} activeColor 右边锚点激活的颜色 ( 默认 '#5677fc' )
* @property {Array} indexList 索引字符列表,数组形式
* @property {Boolean} sticky 是否开启锚点自动吸顶 ( 默认 true )
* @property {String | Number} customNavHeight 自定义导航栏的高度 ( 默认 0 )
* */
export default {
name: 'u-index-list',
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
// #ifdef MP-WEIXIN
// 将自定义节点设置成虚拟的更加接近Vue组件的表现能更好的使用flex属性
options: {
virtualHost: true
},
// #endif
data() {
return {
// 当前正在被选中的字母索引
activeIndex: -1,
touchmoveIndex: 1,
// 索引字母的信息
letterInfo: {
height: 0,
itemHeight: 0,
top: 0
},
// 设置字母指示器的高度,后面为了让指示器跟随字母,并将尖角部分指向字母的中部,需要依赖此值
indicatorHeight: 50,
// 字母放大指示器的top值为了让其指向当前激活的字母
// indicatorTop: 0
// 当前是否正在被触摸状态
touching: false,
// 滚动条顶部top值
scrollTop: 0,
// scroll-view的高度
scrollViewHeight: 0,
// 系统信息
sys: uni.$u.sys(),
scrolling: false,
scrollIntoView: '',
}
},
computed: {
// 如果有传入外部的indexList锚点数组则使用否则使用内部生成A-Z字母
uIndexList() {
return this.indexList.length ? this.indexList : indexList()
},
// 字母放大指示器的top值为了让其指向当前激活的字母
indicatorTop() {
const {
top,
itemHeight
} = this.letterInfo
return Math.floor(top + itemHeight * this.activeIndex + itemHeight / 2 - this.indicatorHeight / 2)
}
},
watch: {
// 监听字母索引的变化,重新设置尺寸
uIndexList: {
immediate: true,
handler() {
uni.$u.sleep().then(() => {
this.setIndexListLetterInfo()
})
}
}
},
created() {
this.children = []
this.anchors = []
this.init()
},
mounted() {
this.setIndexListLetterInfo()
},
methods: {
init() {
// 设置列表的高度为整个屏幕的高度
//减去this.customNavHeight并将this.scrollViewHeight设置为maxHeight
//解决当u-index-list组件放在tabbar页面时,scroll-view内容较少时还能滚动
this.scrollViewHeight = this.sys.windowHeight - this.customNavHeight
},
// 索引列表被触摸
touchStart(e) {
// 获取触摸点信息
const touchStart = e.changedTouches[0]
if (!touchStart) return
this.touching = true
const {
pageY
} = touchStart
// 根据当前触摸点的坐标,获取当前触摸的为第几个字母
const currentIndex = this.getIndexListLetter(pageY)
this.setValueForTouch(currentIndex)
},
// 索引字母列表被触摸滑动中
touchMove(e) {
// 获取触摸点信息
let touchMove = e.changedTouches[0]
if (!touchMove) return;
// 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
if (!this.touching) {
this.touching = true
}
const {
pageY
} = touchMove
const currentIndex = this.getIndexListLetter(pageY)
this.setValueForTouch(currentIndex)
},
// 触摸结束
touchEnd(e) {
// 延时一定时间后再隐藏指示器为了让用户看的更直观同时也是为了消除快速切换u-transition的show带来的影响
uni.$u.sleep(300).then(() => {
this.touching = false
})
},
// 获取索引列表的尺寸以及单个字符的尺寸信息
getIndexListLetterRect() {
return new Promise(resolve => {
// 延时一定时间以获取dom尺寸
// #ifndef APP-NVUE
this.$uGetRect('.u-index-list__letter').then(size => {
resolve(size)
})
// #endif
// #ifdef APP-NVUE
const ref = this.$refs['u-index-list__letter']
dom.getComponentRect(ref, res => {
resolve(res.size)
})
// #endif
})
},
// 设置indexList索引的尺寸信息
setIndexListLetterInfo() {
this.getIndexListLetterRect().then(size => {
const {
height
} = size
const sys = uni.$u.sys()
const windowHeight = sys.windowHeight
let customNavHeight = 0
// 消除各端导航栏非原生和原生导致的差异,让索引列表字母对屏幕垂直居中
if (this.customNavHeight == 0) {
// #ifdef H5
customNavHeight = sys.windowTop
// #endif
// #ifndef H5
// 在非H5中为原生导航栏其高度不算在windowHeight内这里设置为负值后面相加时变成减去其高度的一半
customNavHeight = -(sys.statusBarHeight + 44)
// #endif
} else {
customNavHeight = uni.$u.getPx(this.customNavHeight)
}
this.letterInfo = {
height,
// 为了让字母列表对屏幕绝对居中,让其对导航栏进行修正,也即往上偏移导航栏的一半高度
top: (windowHeight - height) / 2 + customNavHeight / 2,
itemHeight: Math.floor(height / this.uIndexList.length)
}
})
},
// 获取当前被触摸的索引字母
getIndexListLetter(pageY) {
const {
top,
height,
itemHeight
} = this.letterInfo
// 对H5的pageY进行修正这是由于uni-app自作多情在H5中将触摸点的坐标跟H5的导航栏结合导致的问题
// #ifdef H5
pageY += uni.$u.sys().windowTop
// #endif
// 对第一和最后一个字母做边界处理,因为用户可能在字母列表上触摸到两端的尽头后依然继续滑动
if (pageY < top) {
return 0
} else if (pageY >= top + height) {
// 如果超出了,取最后一个字母
return this.uIndexList.length - 1
} else {
// 将触摸点的Y轴偏移值减去索引字母的top值除以每个字母的高度即可得到当前触摸点落在哪个字母上
return Math.floor((pageY - top) / itemHeight);
}
},
// 设置各项由触摸而导致变化的值
setValueForTouch(currentIndex) {
// 如果偏移量太小,前后得出的会是同一个索引字母,为了防抖,进行返回
if (currentIndex === this.activeIndex) return
this.activeIndex = currentIndex
// #ifndef APP-NVUE || MP-WEIXIN
// 在非nvue中由于anchor和item都在u-index-item中所以需要对index-item进行偏移
this.scrollIntoView = `u-index-item-${this.uIndexList[currentIndex].charCodeAt(0)}`
// #endif
// #ifdef MP-WEIXIN
// 微信小程序下scroll-view的scroll-into-view属性无法对slot中的内容的id生效只能通过设置scrollTop的形式去移动滚动条
this.scrollTop = this.children[currentIndex].top
// #endif
// #ifdef APP-NVUE
// 在nvue中由于cell和header为同级元素所以实际是需要对header(anchor)进行偏移
const anchor = `u-index-anchor-${this.uIndexList[currentIndex]}`
dom.scrollToElement(this.anchors[currentIndex].$refs[anchor], {
offset: 0,
animated: false
})
// #endif
},
getHeaderRect() {
// 获取header slot的高度因为list组件中获取元素的尺寸是没有top值的
return new Promise(resolve => {
dom.getComponentRect(this.$refs.header, res => {
resolve(res.size)
})
})
},
// scroll-view的滚动事件
async scrollHandler(e) {
if (this.touching || this.scrolling) return
// 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
this.scrolling = true
uni.$u.sleep(10).then(() => {
this.scrolling = false
})
let scrollTop = 0
const len = this.children.length
let children = this.children
const anchors = this.anchors
// #ifdef APP-NVUE
// nvue下获取的滚动条偏移为负数需要转为正数
scrollTop = Math.abs(e.contentOffset.y)
// 获取header slot的尺寸信息
const header = await this.getHeaderRect()
// item的top值在nvue下模拟出的anchor的top类似非nvue下的index-item的top
let top = header.height
// 由于list组件无法获取cell的top值这里通过header slot和各个item之间的height模拟出类似非nvue下的位置信息
children = this.children.map((item, index) => {
const child = {
height: item.height,
top
}
// 进行累加给下一个item提供计算依据
top += item.height + anchors[index].height
return child
})
// #endif
// #ifndef APP-NVUE
// 非nvue通过detail获取滚动条位移
scrollTop = e.detail.scrollTop
// #endif
for (let i = 0; i < len; i++) {
const item = children[i],
nextItem = children[i + 1]
// 如果滚动条高度小于第一个item的top值此时无需设置任意字母为高亮
if (scrollTop <= children[0].top || scrollTop >= children[len - 1].top + children[len -
1].height) {
this.activeIndex = -1
break
} else if (!nextItem) {
// 当不存在下一个item时意味着历遍到了最后一个
this.activeIndex = len - 1
break
} else if (scrollTop > item.top && scrollTop < nextItem.top) {
this.activeIndex = i
break
}
}
},
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-index-list {
&__letter {
position: fixed;
right: 0;
text-align: center;
z-index: 3;
padding: 0 6px;
&__item {
width: 16px;
height: 16px;
border-radius: 100px;
margin: 1px 0;
@include flex;
align-items: center;
justify-content: center;
&--active {
background-color: $u-primary;
}
&__index {
font-size: 12px;
text-align: center;
line-height: 12px;
}
}
}
&__indicator {
width: 50px;
height: 50px;
border-radius: 100px 100px 0 100px;
text-align: center;
color: #ffffff;
background-color: #c9c9c9;
transform: rotate(-45deg);
@include flex;
justify-content: center;
align-items: center;
&__text {
font-size: 28px;
line-height: 28px;
font-weight: bold;
color: #fff;
transform: rotate(45deg);
text-align: center;
}
}
}
</style>

View File

@@ -0,0 +1,182 @@
export default {
props: {
// 输入的值
value: {
type: [String, Number],
default: uni.$u.props.input.value
},
// 输入框类型
// number-数字输入键盘app-vue下可以输入浮点数app-nvue和小程序平台下只能输入整数
// idcard-身份证输入键盘微信、支付宝、百度、QQ小程序
// digit-带小数点的数字键盘App的nvue页面、微信、支付宝、百度、头条、QQ小程序
// text-文本输入键盘
type: {
type: String,
default: uni.$u.props.input.type
},
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
// 兼容性微信小程序、百度小程序、字节跳动小程序、QQ小程序
fixed: {
type: Boolean,
default: uni.$u.props.input.fixed
},
// 是否禁用输入框
disabled: {
type: Boolean,
default: uni.$u.props.input.disabled
},
// 禁用状态时的背景色
disabledColor: {
type: String,
default: uni.$u.props.input.disabledColor
},
// 是否显示清除控件
clearable: {
type: Boolean,
default: uni.$u.props.input.clearable
},
// 是否密码类型
password: {
type: Boolean,
default: uni.$u.props.input.password
},
// 最大输入长度,设置为 -1 的时候不限制最大长度
maxlength: {
type: [String, Number],
default: uni.$u.props.input.maxlength
},
// 输入框为空时的占位符
placeholder: {
type: String,
default: uni.$u.props.input.placeholder
},
// 指定placeholder的样式类注意页面或组件的style中写了scoped时需要在类名前写/deep/
placeholderClass: {
type: String,
default: uni.$u.props.input.placeholderClass
},
// 指定placeholder的样式
placeholderStyle: {
type: [String, Object],
default: uni.$u.props.input.placeholderStyle
},
// 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效
showWordLimit: {
type: Boolean,
default: uni.$u.props.input.showWordLimit
},
// 设置右下角按钮的文字有效值send|search|next|go|done兼容性详见uni-app文档
// https://uniapp.dcloud.io/component/input
// https://uniapp.dcloud.io/component/textarea
confirmType: {
type: String,
default: uni.$u.props.input.confirmType
},
// 点击键盘右下角按钮时是否保持键盘不收起H5无效
confirmHold: {
type: Boolean,
default: uni.$u.props.input.confirmHold
},
// focus时点击页面的时候不收起键盘微信小程序有效
holdKeyboard: {
type: Boolean,
default: uni.$u.props.input.holdKeyboard
},
// 自动获取焦点
// 在 H5 平台能否聚焦以及软键盘是否跟随弹出取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点
focus: {
type: Boolean,
default: uni.$u.props.input.focus
},
// 键盘收起时是否自动失去焦点目前仅App3.0.0+有效
autoBlur: {
type: Boolean,
default: uni.$u.props.input.autoBlur
},
// 是否去掉 iOS 下的默认内边距仅微信小程序且type=textarea时有效
disableDefaultPadding: {
type: Boolean,
default: uni.$u.props.input.disableDefaultPadding
},
// 指定focus时光标的位置
cursor: {
type: [String, Number],
default: uni.$u.props.input.cursor
},
// 输入框聚焦时底部与键盘的距离
cursorSpacing: {
type: [String, Number],
default: uni.$u.props.input.cursorSpacing
},
// 光标起始位置自动聚集时有效需与selection-end搭配使用
selectionStart: {
type: [String, Number],
default: uni.$u.props.input.selectionStart
},
// 光标结束位置自动聚集时有效需与selection-start搭配使用
selectionEnd: {
type: [String, Number],
default: uni.$u.props.input.selectionEnd
},
// 键盘弹起时,是否自动上推页面
adjustPosition: {
type: Boolean,
default: uni.$u.props.input.adjustPosition
},
// 输入框内容对齐方式可选值为left|center|right
inputAlign: {
type: String,
default: uni.$u.props.input.inputAlign
},
// 输入框字体的大小
fontSize: {
type: [String, Number],
default: uni.$u.props.input.fontSize
},
// 输入框字体颜色
color: {
type: String,
default: uni.$u.props.input.color
},
// 输入框前置图标
prefixIcon: {
type: String,
default: uni.$u.props.input.prefixIcon
},
// 前置图标样式,对象或字符串
prefixIconStyle: {
type: [String, Object],
default: uni.$u.props.input.prefixIconStyle
},
// 输入框后置图标
suffixIcon: {
type: String,
default: uni.$u.props.input.suffixIcon
},
// 后置图标样式,对象或字符串
suffixIconStyle: {
type: [String, Object],
default: uni.$u.props.input.suffixIconStyle
},
// 边框类型surround-四周边框bottom-底部边框none-无边框
border: {
type: String,
default: uni.$u.props.input.border
},
// 是否只读与disabled不同之处在于disabled会置灰组件而readonly则不会
readonly: {
type: Boolean,
default: uni.$u.props.input.readonly
},
// 输入框形状circle-圆形square-方形
shape: {
type: String,
default: uni.$u.props.input.shape
},
// 用于处理或者过滤输入框内容的方法
formatter: {
type: [Function, null],
default: uni.$u.props.input.formatter
}
}
}

View File

@@ -0,0 +1,353 @@
<template>
<view class="u-input" :class="inputClass" :style="[wrapperStyle]">
<view class="u-input__content">
<view
class="u-input__content__prefix-icon"
v-if="prefixIcon || $slots.prefix"
>
<slot name="prefix">
<u-icon
:name="prefixIcon"
size="18"
:customStyle="prefixIconStyle"
></u-icon>
</slot>
</view>
<view class="u-input__content__field-wrapper" @tap="clickHandler">
<!-- 根据uni-app的input组件文档H5和APP中只要声明了password参数(无论true还是false)type均失效此时
为了防止type=number时又存在password属性type无效此时需要设置password为undefined
-->
<input
class="u-input__content__field-wrapper__field"
:style="[inputStyle]"
:type="type"
:focus="focus"
:cursor="cursor"
:value="innerValue"
:auto-blur="autoBlur"
:disabled="disabled || readonly"
:maxlength="maxlength"
:placeholder="placeholder"
:placeholder-style="placeholderStyle"
:placeholder-class="placeholderClass"
:confirm-type="confirmType"
:confirm-hold="confirmHold"
:hold-keyboard="holdKeyboard"
:cursor-spacing="cursorSpacing"
:adjust-position="adjustPosition"
:selection-end="selectionEnd"
:selection-start="selectionStart"
:password="password || type === 'password' || undefined"
@input="onInput"
@blur="onBlur"
@focus="onFocus"
@confirm="onConfirm"
@keyboardheightchange="onkeyboardheightchange"
/>
</view>
<view
class="u-input__content__clear"
v-if="isShowClear"
@tap="onClear"
>
<u-icon
name="close"
size="11"
color="#ffffff"
customStyle="line-height: 12px"
></u-icon>
</view>
<view
class="u-input__content__subfix-icon"
v-if="suffixIcon || $slots.suffix"
>
<slot name="suffix">
<u-icon
:name="suffixIcon"
size="18"
:customStyle="suffixIconStyle"
></u-icon>
</slot>
</view>
</view>
</view>
</template>
<script>
import props from "./props.js";
/**
* Input 输入框
* @description 此组件为一个输入框默认没有边框和样式是专门为配合表单组件u-form而设计的利用它可以快速实现表单验证输入内容下拉选择等功能。
* @tutorial https://uviewui.com/components/input.html
* @property {String | Number} value 输入的值
* @property {String} type 输入框类型,见上方说明 默认 'text'
* @property {Boolean} fixed 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true兼容性微信小程序、百度小程序、字节跳动小程序、QQ小程序 默认 false
* @property {Boolean} disabled 是否禁用输入框 默认 false
* @property {String} disabledColor 禁用状态时的背景色( 默认 '#f5f7fa'
* @property {Boolean} clearable 是否显示清除控件 默认 false
* @property {Boolean} password 是否密码类型 默认 false
* @property {String | Number} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度 默认 -1
* @property {String} placeholder 输入框为空时的占位符
* @property {String} placeholderClass 指定placeholder的样式类注意页面或组件的style中写了scoped时需要在类名前写/deep/ 默认 'input-placeholder'
* @property {String | Object} placeholderStyle 指定placeholder的样式字符串/对象形式,如"color: red;"
* @property {Boolean} showWordLimit 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 默认 false
* @property {String} confirmType 设置右下角按钮的文字兼容性详见uni-app文档 默认 'done'
* @property {Boolean} confirmHold 点击键盘右下角按钮时是否保持键盘不收起H5无效 默认 false
* @property {Boolean} holdKeyboard focus时点击页面的时候不收起键盘微信小程序有效 默认 false
* @property {Boolean} focus 自动获取焦点,在 H5 平台能否聚焦以及软键盘是否跟随弹出取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 默认 false
* @property {Boolean} autoBlur 键盘收起时是否自动失去焦点目前仅App3.0.0+有效 默认 false
* @property {Boolean} disableDefaultPadding 是否去掉 iOS 下的默认内边距仅微信小程序且type=textarea时有效 默认 false
* @property {String Number} cursor 指定focus时光标的位置 默认 -1
* @property {String Number} cursorSpacing 输入框聚焦时底部与键盘的距离 默认 30
* @property {String Number} selectionStart 光标起始位置自动聚集时有效需与selection-end搭配使用 默认 -1
* @property {String Number} selectionEnd 光标结束位置自动聚集时有效需与selection-start搭配使用 默认 -1
* @property {Boolean} adjustPosition 键盘弹起时,是否自动上推页面 默认 true
* @property {String} inputAlign 输入框内容对齐方式( 默认 'left'
* @property {String | Number} fontSize 输入框字体的大小 默认 '15px'
* @property {String} color 输入框字体颜色 默认 '#303133'
* @property {Function} formatter 内容式化函数
* @property {String} prefixIcon 输入框前置图标
* @property {String | Object} prefixIconStyle 前置图标样式,对象或字符串
* @property {String} suffixIcon 输入框后置图标
* @property {String | Object} suffixIconStyle 后置图标样式,对象或字符串
* @property {String} border 边框类型surround-四周边框bottom-底部边框none-无边框 默认 'surround'
* @property {Boolean} readonly 是否只读与disabled不同之处在于disabled会置灰组件而readonly则不会 默认 false
* @property {String} shape 输入框形状circle-圆形square-方形 默认 'square'
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <u-input v-model="value" :password="true" suffix-icon="lock-fill" />
*/
export default {
name: "u-input",
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
data() {
return {
// 输入框的值
innerValue: "",
// 是否处于获得焦点状态
focused: false,
// value是否第一次变化在watch中由于加入immediate属性会在第一次触发此时不应该认为value发生了变化
firstChange: true,
// value绑定值的变化是由内部还是外部引起的
changeFromInner: false,
// 过滤处理方法
innerFormatter: value => value
};
},
watch: {
value: {
immediate: true,
handler(newVal, oldVal) {
this.innerValue = newVal;
/* #ifdef H5 */
// 在H5中外部value变化后修改input中的值不会触发@input事件此时手动调用值变化方法
if (
this.firstChange === false &&
this.changeFromInner === false
) {
this.valueChange();
}
/* #endif */
this.firstChange = false;
// 重置changeFromInner的值为false标识下一次引起默认为外部引起的
this.changeFromInner = false;
},
},
},
computed: {
// 是否显示清除控件
isShowClear() {
const { clearable, readonly, focused, innerValue } = this;
return !!clearable && !readonly && !!focused && innerValue !== "";
},
// 组件的类名
inputClass() {
let classes = [],
{ border, disabled, shape } = this;
border === "surround" &&
(classes = classes.concat(["u-border", "u-input--radius"]));
classes.push(`u-input--${shape}`);
border === "bottom" &&
(classes = classes.concat([
"u-border-bottom",
"u-input--no-radius",
]));
return classes.join(" ");
},
// 组件的样式
wrapperStyle() {
const style = {};
// 禁用状态下,被背景色加上对应的样式
if (this.disabled) {
style.backgroundColor = this.disabledColor;
}
// 无边框时,去除内边距
if (this.border === "none") {
style.padding = "0";
} else {
// 由于uni-app的iOS开发者能力有限导致需要分开写才有效
style.paddingTop = "6px";
style.paddingBottom = "6px";
style.paddingLeft = "9px";
style.paddingRight = "9px";
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle));
},
// 输入框的样式
inputStyle() {
const style = {
color: this.color,
fontSize: uni.$u.addUnit(this.fontSize),
textAlign: this.inputAlign
};
return style;
},
},
methods: {
// 在微信小程序中不支持将函数当做props参数故只能通过ref形式调用
setFormatter(e) {
this.innerFormatter = e
},
// 当键盘输入时触发input事件
onInput(e) {
let { value = "" } = e.detail || {};
// 格式化过滤方法
const formatter = this.formatter || this.innerFormatter
const formatValue = formatter(value)
// 为了避免props的单向数据流特性需要先将innerValue值设置为当前值再在$nextTick中重新赋予设置后的值才有效
this.innerValue = value
this.$nextTick(() => {
this.innerValue = formatValue;
this.valueChange();
})
},
// 输入框失去焦点时触发
onBlur(event) {
this.$emit("blur", event.detail.value);
// H5端的blur会先于点击清除控件的点击click事件触发导致focused
// 瞬间为false从而隐藏了清除控件而无法被点击到
uni.$u.sleep(50).then(() => {
this.focused = false;
});
// 尝试调用u-form的验证方法
uni.$u.formValidate(this, "blur");
},
// 输入框聚焦时触发
onFocus(event) {
this.focused = true;
this.$emit("focus");
},
// 点击完成按钮时触发
onConfirm(event) {
this.$emit("confirm", this.innerValue);
},
// 键盘高度发生变化的时候触发此事件
// 兼容性微信小程序2.7.0+、App 3.1.0+
onkeyboardheightchange() {
this.$emit("keyboardheightchange");
},
// 内容发生变化,进行处理
valueChange() {
const value = this.innerValue;
this.$nextTick(() => {
this.$emit("input", value);
// 标识value值的变化是由内部引起的
this.changeFromInner = true;
this.$emit("change", value);
// 尝试调用u-form的验证方法
uni.$u.formValidate(this, "change");
});
},
// 点击清除控件
onClear() {
this.innerValue = "";
this.$nextTick(() => {
this.valueChange();
this.$emit("clear");
});
},
/**
* 在安卓nvue上事件无法冒泡
* 在某些时间我们希望监听u-from-item的点击事件此时会导致点击u-form-item内的u-input后
* 无法触发u-form-item的点击事件这里通过手动调用u-form-item的方法进行触发
*/
clickHandler() {
// #ifdef APP-NVUE
if (uni.$u.os() === "android") {
const formItem = uni.$u.$parent.call(this, "u-form-item");
if (formItem) {
formItem.clickHandler();
}
}
// #endif
},
},
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-input {
@include flex(row);
align-items: center;
justify-content: space-between;
flex: 1;
&--radius,
&--square {
border-radius: 4px;
}
&--no-radius {
border-radius: 0;
}
&--circle {
border-radius: 100px;
}
&__content {
flex: 1;
@include flex(row);
align-items: center;
justify-content: space-between;
&__field-wrapper {
position: relative;
@include flex(row);
margin: 0;
flex: 1;
&__field {
line-height: 26px;
text-align: left;
color: $u-main-color;
height: 24px;
font-size: 15px;
flex: 1;
}
}
&__clear {
width: 20px;
height: 20px;
border-radius: 100px;
background-color: #c6c7cb;
@include flex(row);
align-items: center;
justify-content: center;
transform: scale(0.82);
margin-left: 4px;
}
&__subfix-icon {
margin-left: 4px;
}
&__prefix-icon {
margin-right: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,84 @@
export default {
props: {
// 键盘的类型number-数字键盘card-身份证键盘car-车牌号键盘
mode: {
type: String,
default: uni.$u.props.keyboard.mode
},
// 是否显示键盘的"."符号
dotDisabled: {
type: Boolean,
default: uni.$u.props.keyboard.dotDisabled
},
// 是否显示顶部工具条
tooltip: {
type: Boolean,
default: uni.$u.props.keyboard.tooltip
},
// 是否显示工具条中间的提示
showTips: {
type: Boolean,
default: uni.$u.props.keyboard.showTips
},
// 工具条中间的提示文字
tips: {
type: String,
default: uni.$u.props.keyboard.tips
},
// 是否显示工具条左边的"取消"按钮
showCancel: {
type: Boolean,
default: uni.$u.props.keyboard.showCancel
},
// 是否显示工具条右边的"完成"按钮
showConfirm: {
type: Boolean,
default: uni.$u.props.keyboard.showConfirm
},
// 是否打乱键盘按键的顺序
random: {
type: Boolean,
default: uni.$u.props.keyboard.random
},
// 是否开启底部安全区适配开启的话会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: uni.$u.props.keyboard.safeAreaInsetBottom
},
// 是否允许通过点击遮罩关闭键盘
closeOnClickOverlay: {
type: Boolean,
default: uni.$u.props.keyboard.closeOnClickOverlay
},
// 控制键盘的弹出与收起
show: {
type: Boolean,
default: uni.$u.props.keyboard.show
},
// 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
overlay: {
type: Boolean,
default: uni.$u.props.keyboard.overlay
},
// z-index值
zIndex: {
type: [String, Number],
default: uni.$u.props.keyboard.zIndex
},
// 取消按钮的文字
cancelText: {
type: String,
default: uni.$u.props.keyboard.cancelText
},
// 确认按钮的文字
confirmText: {
type: String,
default: uni.$u.props.keyboard.confirmText
},
// 输入一个中文后,是否自动切换到英文
autoChange: {
type: Boolean,
default: uni.$u.props.keyboard.autoChange
}
}
}

View File

@@ -0,0 +1,164 @@
<template>
<u-popup
:overlay="overlay"
:closeOnClickOverlay="closeOnClickOverlay"
mode="bottom"
:popup="false"
:show="show"
:safeAreaInsetBottom="safeAreaInsetBottom"
@close="popupClose"
:zIndex="zIndex"
:customStyle="{
backgroundColor: 'rgb(214, 218, 220)'
}"
>
<view class="u-keyboard">
<slot />
<view
class="u-keyboard__tooltip"
v-if="tooltip"
>
<view
hover-class="u-hover-class"
:hover-stay-time="100"
>
<text
class="u-keyboard__tooltip__item u-keyboard__tooltip__cancel"
v-if="showCancel"
@tap="onCancel"
>{{showCancel && cancelText}}</text>
</view>
<view>
<text
v-if="showTips"
class="u-keyboard__tooltip__item u-keyboard__tooltip__tips"
>{{tips ? tips : mode == 'number' ? '数字键盘' : mode == 'card' ? '身份证键盘' : '车牌号键盘'}}</text>
</view>
<view
hover-class="u-hover-class"
:hover-stay-time="100"
>
<text
v-if="showConfirm"
@tap="onConfirm"
class="u-keyboard__tooltip__item u-keyboard__tooltip__submit"
hover-class="u-hover-class"
>{{showConfirm && confirmText}}</text>
</view>
</view>
<template v-if="mode == 'number' || mode == 'card'">
<u-number-keyboard
:random="random"
@backspace="backspace"
@change="change"
:mode="mode"
:dotDisabled="dotDisabled"
></u-number-keyboard>
</template>
<template v-else>
<u-car-keyboard
:random="random"
:autoChange="autoChange"
@backspace="backspace"
@change="change"
></u-car-keyboard>
</template>
</view>
</u-popup>
</template>
<script>
import props from './props.js';
/**
* keyboard 键盘
* @description 此为uViw自定义的键盘面板内含了数字键盘车牌号键身份证号键盘3中模式都有可以打乱按键顺序的选项。
* @tutorial https://www.uviewui.com/components/keyboard.html
* @property {String} mode 键盘类型,见官网基本使用的说明 (默认 'number'
* @property {Boolean} dotDisabled 是否显示"."按键只在mode=number时有效 (默认 false
* @property {Boolean} tooltip 是否显示键盘顶部工具条 (默认 true
* @property {Boolean} showTips 是否显示工具条中间的提示 (默认 true
* @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
* @property {Boolean} showCancel 是否显示工具条左边的"取消"按钮 (默认 true
* @property {Boolean} showConfirm 是否显示工具条右边的"完成"按钮( 默认 true
* @property {Boolean} random 是否打乱键盘按键的顺序 (默认 false
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区适配 (默认 true
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩收起键盘 (默认 true
* @property {Boolean} show 控制键盘的弹出与收起(默认 false
* @property {Boolean} overlay 是否显示遮罩 (默认 true
* @property {String | Number} zIndex 弹出键盘的z-index值 (默认 1075
* @property {String} cancelText 取消按钮的文字 (默认 '取消'
* @property {String} confirmText 确认按钮的文字 (默认 '确认'
* @property {Object} customStyle 自定义样式,对象形式
* @event {Function} change 按键被点击(不包含退格键被点击)
* @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击
* @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击
* @event {Function} backspace 键盘退格键被点击
* @example <u-keyboard mode="number" v-model="show"></u-keyboard>
*/
export default {
name: "u-keyboard",
data() {
return {
}
},
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
methods: {
change(e) {
this.$emit('change', e);
},
// 键盘关闭
popupClose() {
this.$emit('close');
},
// 输入完成
onConfirm() {
this.$emit('confirm');
},
// 取消输入
onCancel() {
this.$emit('cancel');
},
// 退格键
backspace() {
this.$emit('backspace');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-keyboard {
&__tooltip {
@include flex;
justify-content: space-between;
background-color: #FFFFFF;
padding: 14px 12px;
&__item {
color: #333333;
flex: 1;
text-align: center;
font-size: 15px;
}
&__submit {
text-align: right;
color: $u-primary;
}
&__cancel {
text-align: left;
color: #888888;
}
&__tips {
color: $u-tips-color;
}
}
}
</style>

View File

@@ -0,0 +1,28 @@
export default {
props: {
// 激活部分的颜色
activeColor: {
type: String,
default: uni.$u.props.lineProgress.activeColor
},
inactiveColor: {
type: String,
default: uni.$u.props.lineProgress.color
},
// 进度百分比,数值
percentage: {
type: [String, Number],
default: uni.$u.props.lineProgress.inactiveColor
},
// 是否在进度条内部显示百分比的值
showText: {
type: Boolean,
default: uni.$u.props.lineProgress.showText
},
// 进度条的高度单位px
height: {
type: [String, Number],
default: uni.$u.props.lineProgress.height
}
}
}

View File

@@ -0,0 +1,144 @@
<template>
<view
class="u-line-progress"
:style="[$u.addStyle(customStyle)]"
>
<view
class="u-line-progress__background"
ref="u-line-progress__background"
:style="[{
backgroundColor: inactiveColor,
height: $u.addUnit(height),
}]"
>
</view>
<view
class="u-line-progress__line"
:style="[progressStyle]"
>
<slot>
<text v-if="showText && percentage >= 10" class="u-line-progress__text">{{innserPercentage + '%'}}</text>
</slot>
</view>
</view>
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* lineProgress 线型进度条
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
* @tutorial https://www.uviewui.com/components/lineProgress.html
* @property {String} activeColor 激活部分的颜色 ( 默认 '#19be6b' )
* @property {String} inactiveColor 背景色 ( 默认 '#ececec' )
* @property {String | Number} percentage 进度百分比,数值 ( 默认 0 )
* @property {Boolean} showText 是否在进度条内部显示百分比的值 ( 默认 true )
* @property {String | Number} height 进度条的高度单位px ( 默认 12 )
*
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
*/
export default {
name: "u-line-progress",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
lineWidth: 0,
}
},
watch: {
percentage(n) {
this.resizeProgressWidth()
}
},
computed: {
progressStyle() {
let style = {}
style.width = this.lineWidth
style.backgroundColor = this.activeColor
style.height = uni.$u.addUnit(this.height)
return style
},
innserPercentage() {
// 控制范围在0-100之间
return uni.$u.range(0, 100, this.percentage)
}
},
mounted() {
this.init()
},
methods: {
init() {
uni.$u.sleep(20).then(() => {
this.resizeProgressWidth()
})
},
getProgressWidth() {
// #ifndef APP-NVUE
return this.$uGetRect('.u-line-progress__background')
// #endif
// #ifdef APP-NVUE
// 返回一个promise
return new Promise(resolve => {
dom.getComponentRect(this.$refs['u-line-progress__background'], (res) => {
resolve(res.size)
})
})
// #endif
},
resizeProgressWidth() {
this.getProgressWidth().then(size => {
const {
width
} = size
// 通过设置的percentage值计算其所占总长度的百分比
this.lineWidth = width * this.innserPercentage / 100 + 'px'
})
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-line-progress {
align-items: stretch;
position: relative;
@include flex(row);
flex: 1;
overflow: hidden;
border-radius: 100px;
&__background {
background-color: #ececec;
border-radius: 100px;
flex: 1;
}
&__line {
position: absolute;
top: 0;
left: 0;
bottom: 0;
align-items: center;
@include flex(row);
color: #ffffff;
border-radius: 100px;
transition: width 0.5s ease;
justify-content: flex-end;
}
&__text {
font-size: 10px;
align-items: center;
text-align: right;
color: #FFFFFF;
margin-right: 5px;
transform: scale(0.9);
}
}
</style>

View File

@@ -0,0 +1,33 @@
export default {
props: {
color: {
type: String,
default: uni.$u.props.line.color
},
// 长度竖向时表现为高度横向时表现为长度可以为百分比带px单位的值等
length: {
type: [String, Number],
default: uni.$u.props.line.length
},
// 线条方向col-竖向row-横向
direction: {
type: String,
default: uni.$u.props.line.direction
},
// 是否显示细边框
hairline: {
type: Boolean,
default: uni.$u.props.line.hairline
},
// 线条与上下左右元素的间距,字符串形式,如"30px"、"20px 30px"
margin: {
type: [String, Number],
default: uni.$u.props.line.margin
},
// 是否虚线true-实线false-虚线
dashed: {
type: Boolean,
default: uni.$u.props.line.dashed
}
}
}

View File

@@ -0,0 +1,62 @@
<template>
<view
class="u-line"
:style="[lineStyle]"
>
</view>
</template>
<script>
import props from './props.js';
/**
* line 线条
* @description 此组件一般用于显示一根线条用于分隔内容块有横向和竖向两种模式且能设置0.5px线条,使用也很简单
* @tutorial https://www.uviewui.com/components/line.html
* @property {String} color 线条的颜色 ( 默认 '#d6d7d9' )
* @property {String | Number} length 长度竖向时表现为高度横向时表现为长度可以为百分比带px单位的值等 ( 默认 '100%' )
* @property {String} direction 线条的方向row-横向col-竖向 (默认 'row' )
* @property {Boolean} hairline 是否显示细线条 (默认 true )
* @property {String | Number} margin 线条与上下左右元素的间距,字符串形式,如"30px" (默认 0 )
* @property {Boolean} dashed 是否虚线true-虚线false-实线 (默认 false )
* @property {Object} customStyle 定义需要用到的外部样式
* @example <u-line color="red"></u-line>
*/
export default {
name: 'u-line',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
lineStyle() {
const style = {}
style.margin = this.margin
// 如果是水平线条边框高度为1px再通过transform缩小一半就是0.5px了
if (this.direction === 'row') {
// 此处采用兼容分开写兼容nvue的写法
style.borderBottomWidth = '1px'
style.borderBottomStyle = this.dashed ? 'dashed' : 'solid'
style.width = uni.$u.addUnit(this.length)
if (this.hairline) style.transform = 'scaleY(0.5)'
} else {
// 如果是竖向线条边框宽度为1px再通过transform缩小一半就是0.5px了
style.borderLeftWidth = '1px'
style.borderLeftStyle = this.dashed ? 'dashed' : 'solid'
style.height = uni.$u.addUnit(this.length)
if (this.hairline) style.transform = 'scaleX(0.5)'
}
style.borderColor = this.color
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-line {
/* #ifndef APP-NVUE */
vertical-align: middle;
/* #endif */
}
</style>

View File

@@ -0,0 +1,39 @@
export default {
props: {
// 文字颜色
color: {
type: String,
default: uni.$u.props.link.color
},
// 字体大小单位px
fontSize: {
type: [String, Number],
default: uni.$u.props.link.fontSize
},
// 是否显示下划线
underLine: {
type: Boolean,
default: uni.$u.props.link.underLine
},
// 要跳转的链接
href: {
type: String,
default: uni.$u.props.link.href
},
// 小程序中复制到粘贴板的提示语
mpTips: {
type: String,
default: uni.$u.props.link.mpTips
},
// 下划线颜色
lineColor: {
type: String,
default: uni.$u.props.link.lineColor
},
// 超链接的问题不使用slot形式传入是因为nvue下无法修改颜色
text: {
type: String,
default: uni.$u.props.link.text
}
}
}

View File

@@ -0,0 +1,83 @@
<template>
<text
class="u-link"
@tap.stop="openLink"
:style="[linkStyle, $u.addStyle(customStyle)]"
>{{text}}</text>
</template>
<script>
import props from './props.js';
/**
* link 超链接
* @description 该组件为超链接组件在不同平台有不同表现形式在APP平台会通过plus环境打开内置浏览器在小程序中把链接复制到粘贴板同时提示信息在H5中通过window.open打开链接。
* @tutorial https://www.uviewui.com/components/link.html
* @property {String} color 文字颜色 (默认 color['u-primary']
* @property {String Number} fontSize 字体大小单位px (默认 15
* @property {Boolean} underLine 是否显示下划线 (默认 false
* @property {String} href 跳转的链接要带上http(s)
* @property {String} mpTips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
* @property {String} lineColor 下划线颜色默认同color参数颜色
* @property {String} text 超链接的问题不使用slot形式传入是因为nvue下无法修改颜色
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
*/
export default {
name: "u-link",
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
computed: {
linkStyle() {
const style = {
color: this.color,
fontSize: uni.$u.addUnit(this.fontSize),
// line-height设置为比字体大小多2px
lineHeight: uni.$u.addUnit(uni.$u.getPx(this.fontSize) + 2),
textDecoration: this.underLine ? 'underline' : 'none'
}
// if (this.underLine) {
// style.borderBottomColor = this.lineColor || this.color
// style.borderBottomWidth = '1px'
// }
return style
}
},
methods: {
openLink() {
// #ifdef APP-PLUS
plus.runtime.openURL(this.href)
// #endif
// #ifdef H5
window.open(this.href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: this.href,
success: () => {
uni.hideToast();
this.$nextTick(() => {
uni.$u.toast(this.mpTips);
})
}
});
// #endif
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-link-line-height:1 !default;
.u-link {
/* #ifndef APP-NVUE */
line-height: $u-link-line-height;
/* #endif */
@include flex;
flex-wrap: wrap;
flex: 1;
}
</style>

View File

@@ -0,0 +1,9 @@
export default {
props: {
// 用于滚动到指定item
anchor: {
type: [String, Number],
default: uni.$u.props.listItem.anchor
}
}
}

View File

@@ -0,0 +1,116 @@
<template>
<!-- #ifdef APP-NVUE -->
<cell>
<!-- #endif -->
<view
class="u-list-item"
:ref="`u-list-item-${anchor}`"
:anchor="`u-list-item-${anchor}`"
:class="[`u-list-item-${anchor}`]"
>
<slot />
</view>
<!-- #ifdef APP-NVUE -->
</cell>
<!-- #endif -->
</template>
<script>
import props from './props.js';
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* List 列表
* @description 该组件为高性能列表组件
* @tutorial https://www.uviewui.com/components/list.html
* @property {String | Number} anchor 用于滚动到指定item
* @example <u-list-ite v-for="(item, index) in indexList" :key="index" ></u-list-item>
*/
export default {
name: 'u-list-item',
mixins: [uni.$u.mpMixin, uni.$u.mixin,props],
data() {
return {
// 节点信息
rect: {},
index: 0,
show: true,
sys: uni.$u.sys()
}
},
computed: {
},
inject: ['uList'],
watch: {
// #ifndef APP-NVUE
'uList.innerScrollTop'(n) {
const preLoadScreen = this.uList.preLoadScreen
const windowHeight = this.sys.windowHeight
if(n <= windowHeight * preLoadScreen) {
this.parent.updateOffsetFromChild(0)
} else if (this.rect.top <= n - windowHeight * preLoadScreen) {
this.parent.updateOffsetFromChild(this.rect.top)
}
}
// #endif
},
created() {
this.parent = {}
},
mounted() {
this.init()
},
methods: {
init() {
// 初始化数据
this.updateParentData()
this.index = this.parent.children.indexOf(this)
this.resize()
},
updateParentData() {
// 此方法在mixin中
this.getParentData('u-list')
},
resize() {
this.queryRect(`u-list-item-${this.anchor}`).then(size => {
const lastChild = this.parent.children[this.index - 1]
this.rect = size
const preLoadScreen = this.uList.preLoadScreen
const windowHeight = this.sys.windowHeight
// #ifndef APP-NVUE
if (lastChild) {
this.rect.top = lastChild.rect.top + lastChild.rect.height
}
if (size.top >= this.uList.innerScrollTop + (1 + preLoadScreen) * windowHeight) this.show =
false
// #endif
})
},
// 查询元素尺寸
queryRect(el) {
return new Promise(resolve => {
// #ifndef APP-NVUE
this.$uGetRect(`.${el}`).then(size => {
resolve(size)
})
// #endif
// #ifdef APP-NVUE
const ref = this.$refs[el]
dom.getComponentRect(ref, res => {
resolve(res.size)
})
// #endif
})
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-list-item {}
</style>

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