Merge branch 'develop' of https://gitee.com/likeadmin/likeadmin_java into develop

This commit is contained in:
TinyAnts
2022-09-07 16:19:57 +08:00
195 changed files with 31008 additions and 87 deletions

View File

@@ -2,7 +2,7 @@ import request from '@/utils/request'
// 页面装修详情
export function getDecoratePages(params: any) {
return request.get({ url: '/decorate/pages/detail', params })
return request.get({ url: '/decorate/pages/detail', params }, { ignoreCancelToken: true })
}
// 页面装修保存

View File

@@ -1,5 +1,5 @@
<template>
<div class="custom-link mt-[60px]">
<div class="custom-link mt-[30px]">
<div class="flex flex-wrap items-center">
自定义链接
<div class="ml-4 flex-1 min-w-[100px]">

View File

@@ -2,7 +2,7 @@
<div class="link flex">
<el-menu
:default-active="activeMenu"
class="w-[160px] min-h-[400px] link-menu"
class="w-[160px] min-h-[350px] link-menu"
@select="handleSelect"
>
<el-menu-item v-for="(item, index) in menus" :index="item.type" :key="index">
@@ -10,8 +10,16 @@
</el-menu-item>
</el-menu>
<div class="flex-1 pl-4">
<shop-pages v-model="activeLink" v-if="LinkTypeEnum.SHOP_PAGES == activeMenu" />
<custom-link v-model="activeLink" v-if="LinkTypeEnum.CUSTOM_LINK == activeMenu" />
<shop-pages
v-model="activeLink"
v-if="LinkTypeEnum.SHOP_PAGES == activeMenu"
@update:model-value="updateLink"
/>
<custom-link
v-model="activeLink"
v-if="LinkTypeEnum.CUSTOM_LINK == activeMenu"
@update:model-value="updateLink"
/>
</div>
</div>
</template>
@@ -24,36 +32,60 @@ import CustomLink from './custom-link.vue'
const props = defineProps({
modelValue: {
type: Object as PropType<Link>
type: Object as PropType<Link>,
required: true
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const activeLink = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const menus = ref([
{
name: '商城页面',
type: LinkTypeEnum.SHOP_PAGES
type: LinkTypeEnum.SHOP_PAGES,
link: {}
},
{
name: '自定义链接',
type: LinkTypeEnum.CUSTOM_LINK
type: LinkTypeEnum.CUSTOM_LINK,
link: {}
}
])
const activeLink = computed({
get() {
return menus.value.find((item) => item.type == activeMenu.value)?.link as Link
},
set(value) {
menus.value.forEach((item) => {
if (item.type == activeMenu.value) {
item.link = value
}
})
}
})
const activeMenu = ref<string>(LinkTypeEnum.SHOP_PAGES)
const handleSelect = (index: string) => {
activeMenu.value = index
}
const updateLink = (value: any) => {
emit('update:modelValue', value)
}
watch(
() => props.modelValue,
(value) => {
activeMenu.value = value.type
activeLink.value = value
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>

View File

@@ -18,7 +18,7 @@
</template>
<script lang="ts" setup>
import type { Link } from '.'
import { LinkTypeEnum, type Link } from '.'
import LinkContent from './index.vue'
import Popup from '@/components/popup/index.vue'
const props = defineProps({
@@ -31,14 +31,16 @@ const emit = defineEmits<{
}>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const activeLink = ref<Link>()
const activeLink = ref<Link>({ path: '', type: LinkTypeEnum.SHOP_PAGES })
const handleConfirm = () => {
emit('update:modelValue', activeLink.value)
}
watch(
() => props.modelValue,
(value) => {
activeLink.value = value as Link
if (value?.type) {
activeLink.value = value as Link
}
},
{
immediate: true

View File

@@ -2,7 +2,7 @@
<div class="shop-pages">
<div class="link-list flex flex-wrap">
<div
class="link-item border border-br px-5 py-[5px] rounded-[3px] cursor-pointer"
class="link-item border border-br px-5 py-[5px] rounded-[3px] cursor-pointer mr-[10px] mb-[10px]"
v-for="(item, index) in linkList"
:class="{ 'border-primary text-primary': modelValue.path == item.path }"
:key="index"
@@ -33,6 +33,16 @@ const linkList = ref([
path: '/pages/index/index',
name: '商城首页',
type: LinkTypeEnum.SHOP_PAGES
},
{
path: '/pages/news/news',
name: '文章资讯',
type: LinkTypeEnum.SHOP_PAGES
},
{
path: '/pages/user/user',
name: '个人中心',
type: LinkTypeEnum.SHOP_PAGES
}
])

View File

@@ -6,9 +6,9 @@
<div class="tabbar flex">
<div
class="tabbar-item flex flex-col justify-center items-center flex-1"
v-for="(item, index) in tabbar.data"
v-for="(item, index) in tabbar.list"
:key="index"
:style="{ color: tabbar.color }"
:style="{ color: tabbar.style.defaultColor }"
>
<img class="w-[22px] h-[22px]" :src="item.unselected" alt="" />
<div class="leading-3 text-[12px] mt-[4px]">{{ item.name }}</div>
@@ -29,7 +29,7 @@
<el-tab-pane label="导航图片" name="content">
<div class="mb-[18px]">
<del-wrap
v-for="(item, index) in tabbar.data"
v-for="(item, index) in tabbar.list"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
@@ -78,7 +78,7 @@
</del-wrap>
</div>
<el-form-item v-if="tabbar.data?.length < max" label-width="0">
<el-form-item v-if="tabbar.list?.length < max" label-width="0">
<el-button type="primary" @click="handleAdd">
添加导航
</el-button>
@@ -86,10 +86,16 @@
</el-tab-pane>
<el-tab-pane label="样式设置" name="styles">
<el-form-item label="默认颜色">
<color-picker class="max-w-[400px]" v-model="tabbar.color" />
<color-picker
class="max-w-[400px]"
v-model="tabbar.style.defaultColor"
/>
</el-form-item>
<el-form-item label="选中颜色">
<color-picker class="max-w-[400px]" v-model="tabbar.color" />
<color-picker
class="max-w-[400px]"
v-model="tabbar.style.selectedColor"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
@@ -108,8 +114,11 @@ import feedback from '@/utils/feedback'
const max = 5
const min = 2
const tabbar = reactive({
color: '',
data: [
style: {
defaultColor: '',
selectedColor: ''
},
list: [
{
name: '',
selected: '',
@@ -120,8 +129,8 @@ const tabbar = reactive({
})
const handleAdd = () => {
if (tabbar.data?.length < max) {
tabbar.data.push({
if (tabbar.list?.length < max) {
tabbar.list.push({
name: '',
selected: '',
unselected: '',
@@ -132,18 +141,19 @@ const handleAdd = () => {
}
}
const handleDelete = (index: number) => {
if (tabbar.data?.length <= min) {
if (tabbar.list?.length <= min) {
return feedback.msgError(`最少保留${min}`)
}
tabbar.data.splice(index, 1)
tabbar.list.splice(index, 1)
}
const getData = async () => {
const data = await getDecorateTabbar()
// tabbar.value = data
tabbar.list = data.list
tabbar.style = data.style
}
const setData = async () => {
// await setDecorateTabbar(tabbar.value)
await setDecorateTabbar(toRaw(tabbar))
getData()
feedback.msgSuccess('保存成功')
}

View File

@@ -1,3 +1,3 @@
# 请求域名
VITE_APP_BASE_URL='https://likeadmin-java.yixiangonline.com'
VITE_APP_BASE_URL='https://likeadmin-java-api.yixiangonline.com/'

View File

@@ -47,7 +47,6 @@
"@dcloudio/uni-quickapp-webview": "^3.0.0-alpha-3050520220824001",
"lodash-es": "^4.17.21",
"pinia": "^2.0.20",
"uview-plus": "^3.1.6",
"vconsole": "^3.14.6",
"vue": "^3.2.37",
"vue-i18n": "^9.2.2",

View File

@@ -13,5 +13,4 @@ onHide(() => {
</script>
<style lang="scss">
/* 注意要写在第一行同时给style标签加入lang="scss"属性 */
@import 'uview-plus/index.scss';
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*400px</div>
<del-wrap
v-for="(item, index) in content.data"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
>
<div class="bg-fill-light flex items-center w-full p-4 mt-4">
<material-picker v-model="item.image" upload-class="bg-body" />
<div class="ml-3 flex-1">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-model="item.link" />
</el-form-item>
</div>
</div>
</del-wrap>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import feedback from '@/utils/feedback'
import type { PropType } from 'vue'
import type options from './options'
const limit = 5
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {}
})
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
props.content.data.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,31 @@
<template>
<div class="banner">
<div class="banner-image">
<image-contain width="100%" height="200px" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const getImage = computed(() => {
const { data } = props.content
if (Array.isArray(data)) {
return data[0] ? data[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '首页轮播图',
name: 'banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="90px">
<el-form-item label="客服标题">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="服务时间">
<el-input class="w-[400px]" v-model="content.time" />
</el-form-item>
<el-form-item label="联系电话">
<el-input class="w-[400px]" v-model="content.mobile" />
</el-form-item>
<el-form-item label="客服二维码">
<div>
<material-picker v-model="content.qrcode" />
<div class="form-tips">建议图片尺寸200*200像素图片格式jpgpngjpeg</div>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="customer-service">
<image-contain width="140px" height="140px" :src="content.qrcode" alt="" />
<div class="text-[15px] mt-[7px] font-medium">{{ content.title }}</div>
<div class="text-[#666] mt-[20px]">服务时间{{ content.time }}</div>
<div class="text-[#666] mt-[7px]">客服电话{{ content.mobile }}</div>
<div
class="text-white text-[16px] rounded-[42px] bg-[#4173FF] w-full h-[42px] flex justify-center items-center mt-[50px]"
>
保存二维码图片
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped>
.customer-service {
margin: 10px 18px;
border-radius: 10px;
padding: 50px 55px 80px;
background: #fff;
@apply flex flex-col justify-center items-center;
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,11 @@
export default () => ({
title: '客服设置',
name: 'customer-service',
content: {
title: '添加客服二维码',
time: '',
mobile: '',
qrcode: ''
},
styles: {}
})

View File

@@ -0,0 +1,14 @@
const widgets: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true })
interface Widget {
attr: any
content: any
options: any
}
console.log(widgets)
const exportWidgets: Record<string, Widget> = {}
Object.keys(widgets).forEach((key) => {
const widgetName = key.replace(/^\.\/([\w-]+).*/gi, '$1')
exportWidgets[widgetName] = widgets[key]?.default
})
export default exportWidgets

View File

@@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="排版样式">
<el-radio-group v-model="content.style">
<el-radio :label="1">横排</el-radio>
<el-radio :label="2">竖排</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题名称">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<AddNav v-model="content.data" />
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
import AddNav from '../../add-nav.vue'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="my-service">
<div v-if="content.title" class="title px-[15px] py-[10px]">
<div>{{ content.title }}</div>
</div>
<div v-if="content.style == 1" class="flex flex-wrap pt-[20px] pb-[10px]">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex flex-col items-center w-1/4 mb-[15px]"
>
<image-contain width="26px" height="26px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
<div v-if="content.style == 2">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex items-center border-b border-[#e5e5e5] h-[50px] px-[12px]"
>
<image-contain width="24px" height="24px" :src="item.image" alt="" />
<div class="ml-[10px] flex-1">{{ item.name }}</div>
<div>
<icon name="el-icon-ArrowRight" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped>
.my-service {
margin: 10px 10px 0;
background-color: #fff;
border-radius: 7px;
.title {
border-bottom: 1px solid #e5e5e5;
font-size: 16px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,16 @@
export default () => ({
title: '我的服务',
name: 'my-service',
content: {
style: 1,
title: '我的服务',
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@@ -0,0 +1,36 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<div class="form-tips mb-4">最多可添加10个建议图片尺寸100px*100px</div>
<AddNav v-model="content.data" />
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
import AddNav from '../../add-nav.vue'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="nav pt-[15px] pb-[8px]">
<div class="flex flex-wrap">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex flex-col items-center w-1/5 mb-[15px]"
>
<image-contain width="41px" height="41px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped>
.nav {
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '导航菜单',
name: 'nav',
content: {
enabled: 1,
data: [
{
image: '',
name: '导航',
link: {}
}
]
},
styles: {}
})

View File

@@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="search">
<div class="search-con flex items-center px-[15px]">
<icon name="el-icon-Search" :size="17" />
<span class="ml-[5px]">请输入关键词搜索</span>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.search {
background-color: #fff;
padding: 7px 12px;
.search-con {
height: 100%;
height: 36px;
border-radius: 36px;
background: #f4f4f4;
color: #999999;
}
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,7 @@
export default () => ({
title: '搜索',
name: 'search',
disabled: 1,
content: {},
styles: {}
})

View File

@@ -0,0 +1,75 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*200px</div>
<del-wrap
v-for="(item, index) in content.data"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
>
<div class="bg-fill-light flex items-center w-full p-4 mt-4">
<material-picker v-model="item.image" upload-class="bg-body" />
<div class="ml-3 flex-1">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-model="item.link" />
</el-form-item>
</div>
</div>
</del-wrap>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import feedback from '@/utils/feedback'
import type { PropType } from 'vue'
import type options from './options'
const limit = 5
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {}
})
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
props.content.data.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,31 @@
<template>
<div class="banner mx-[10px] mt-[10px]">
<div class="banner-image">
<image-contain width="100%" height="100px" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const getImage = computed(() => {
const { data } = props.content
if (Array.isArray(data)) {
return data[0] ? data[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '个人中心广告图',
name: 'user-banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="user-info flex items-center px-[25px]">
<img src="./images/default_avatar.png" class="w-[60px] h-[60px]" alt="" />
<div class="text-white text-[18px] ml-[10px]">未登录</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.user-info {
background: url(./images/my_topbg.png);
height: 115px;
background-position: bottom;
background-size: 100% auto;
.search-con {
height: 100%;
height: 36px;
border-radius: 36px;
background: #f4f4f4;
color: #999999;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@@ -0,0 +1,7 @@
export default () => ({
title: '用户信息',
name: 'user-info',
disabled: 1,
content: {},
styles: {}
})

View File

@@ -53,7 +53,6 @@
},
"easycom": {
"custom": {
"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
}
}

View File

@@ -1,18 +1,11 @@
<template>
<view class="content">
<z-paging ref="paging" v-model="dataList">
<!-- 之后-vue3 -->
<template v-slot:top>
<view>我是固定在顶部的view</view>
</template>
</z-paging>
<navigator url="/pages/login/login" hover-class="navigator-hover">
<button type="default">跳转到新页面</button>
</navigator>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dataList = ref([])
</script>
<script setup lang="ts"></script>
<style></style>

View File

@@ -0,0 +1,46 @@
<template>
<view class="login">
<view>
<u-image src="" mode="widthFix" height="160" width="160" />
</view>
<view class="mt-4">这里是商城名称</view>
<view class="w-full mt-[60rpx]">
<u-form :model="formData" borderBottom>
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_user.png" />
<u-input
v-model="formData.mobile"
:border="false"
placeholder="请输入账号/手机号码"
/>
</u-form-item>
<u-form-item borderBottom>
<u-icon class="mr-2" :size="36" name="/static/images/icon_password.png" />
<u-input placeholder="请输入密码" :border="false" />
<view
class="border-l border-solid border-0 border-light pl-3 text-muted leading-4"
>
忘记密码
</view>
</u-form-item>
</u-form>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const formData = reactive({
mobile: ''
})
</script>
<style lang="scss">
page {
height: 100%;
}
.login {
@apply min-h-full flex flex-col items-center px-[40rpx] pt-[80rpx] box-border;
}
</style>

View File

@@ -1,20 +1,7 @@
import uviewPlus from 'uview-plus'
import { App } from 'vue'
import uView from '@/uni_modules/vk-uview-ui'
export default (app: App) => {
uni.$u.setConfig({
// 修改$u.config对象的属性
config: {
// 修改默认单位为rpx相当于执行 uni.$u.config.unit = 'rpx'
unit: 'rpx'
},
// 修改$u.props对象的属性
props: {
// 修改radio组件的size参数的默认值相当于执行 uni.$u.props.radio.size = 30
radio: {
size: 15
}
}
})
app.use(uviewPlus)
// 使用 uView UI
app.use(uView)
}

0
app/src/router/index.ts Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,2 +1,3 @@
@import './tailwind.css';
@import './public.scss';
@import '../uni_modules/vk-uview-ui/index.scss';

View File

@@ -0,0 +1,3 @@
// .input-placeholder {
// color: $u-info !important;
// }

0
app/src/styles/var.css Normal file
View File

View File

@@ -1,4 +1,4 @@
@import 'uview-plus/theme.scss';
@import '@/uni_modules/vk-uview-ui/theme.scss';
$u-main-color: #333333;
$u-content-color: #666666;
@@ -8,27 +8,30 @@ $u-border-color: #e5e5e5;
$u-bg-color: #f3f4f6;
$u-disabled-color: #c8c9cc;
$u-primary: #4173ff;
$u-primary-dark: #398ade;
$u-primary-disabled: #9acafc;
$u-primary-light: #ecf5ff;
$u-type-primary: #4173ff;
$u-type-primary-light: #ecf5ff;
$u-type-primary-disabled: #a0cfff;
$u-type-primary-dark: #2b85e4;
$u-warning: #f9ae3d;
$u-warning-dark: #f1a532;
$u-warning-disabled: #f9d39b;
$u-warning-light: #fdf6ec;
$u-type-warning: #ff9900;
$u-type-warning-disabled: #fcbd71;
$u-type-warning-dark: #f29100;
$u-type-warning-light: #fdf6ec;
$u-success: #5ac725;
$u-success-dark: #53c21d;
$u-success-disabled: #a9e08f;
$u-success-light: #f5fff0;
$u-type-success: #19be6b;
$u-type-success-disabled: #71d5a1;
$u-type-success-dark: #18b566;
$u-type-success-light: #dbf1e1;
$u-error: #f56c6c;
$u-error-dark: #e45656;
$u-error-disabled: #f7b2b2;
$u-error-light: #fef0f0;
$u-type-error: #fa3534;
$u-type-error-disabled: #fab6b6;
$u-type-error-dark: #dd6161;
$u-type-error-light: #fef0f0;
$u-info: #909399;
$u-info-dark: #767a82;
$u-info-disabled: #c4c6c9;
$u-info-light: #f4f4f5;
$u-type-info: #909399;
$u-type-info-disabled: #c8c9cc;
$u-type-info-dark: #82848a;
$u-type-info-light: #f4f4f5;
$u-form-item-height: 60rpx;
$u-form-item-border-color: #e5e5e5;

View File

@@ -0,0 +1,118 @@
## 1.3.122022-08-30
* 【优化】`u-keyboard` 组件内部细节。
## 1.3.112022-08-30
* 【修复】`u-subsection` 组件的 `list` 属性不支持动态修改的问题。
## 1.3.102022-07-30
* 【优化】上传组件部分细节
## 1.3.92022-07-07
* 【更新】省市区数据源
* 【优化】`u-subsection` 组件支持在右上角显示数字角标
```html
<template>
<u-subsection :list="list"></u-subsection>
</template>
```
```js
export default {
data() {
return {
list: [
{
name: '待发货',
num: 10
},
{
name: '待付款',
num: 5
},
{
name: '待评价',
num: 15
}
]
}
}
}
```
## 1.3.82022-06-13
* 【优化】组件 `u-icon`使之更方便的兼容第三方icon满足规则自动计算customPrefix
**规则如下:**
*`name` 中包含 `-icon-` 字符串时
*`vk-icon-goods`,则组件的 `customPrefix` 属性自动识别为 `vk-icon` `name`属性 自动识别为 `goods`
*`vk-2-icon-goods-list`,则组件的 `customPrefix` 属性自动识别为 `vk-2-icon` `name`属性 自动识别为 `goods-list`
## 1.3.72022-06-10
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall``vue3` 模式下的细节问题。
## 1.3.62022-06-10
* 【优化】组件 `u-action-sheet` `u-calendar` `u-dropdown-item` `u-field` `u-input` `u-keyboard` `u-modal` `u-radio-group` `u-rate` `u-search` `u-slider` `u-switch` `u-tabbar` `u-waterfall``vue3` 模式下的细节问题。
## 1.3.52022-05-28
* 【优化】组件 `u-mask` `u-popup` `u-select` `u-modal` `u-keyboard` `u-calendar` `u-action-sheet` `u-picker` 均新增 `blur` 属性可用于设置弹出遮罩的模糊度默认为0不模糊
* ![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/49b773a3-273f-4b1c-95e8-a42dcba1a53c.png)
## 1.3.42022-05-03
* 【修复】`u-tabs` 组件细节问题。
## 1.1.42022-03-22
* 【修复】`u-field` 组件 `arrowDirection` 属性无效的问题。
## 1.1.32022-03-21
* 【优化】部分细节。
## 1.1.22022-03-21
* 【优化】部分细节。
## 1.1.12022-03-17
* 【优化】部分细节。
## 1.1.02022-03-12
* 【重要】`u-picker` 组件新增 `regionDiscern` 方法 智能识别省市区街道地址
如将字符串 `浙江省杭州市西湖区希望路1333弄是啊我庭12号楼1203` 中识别为
```json
{
"province": {
"code": "330000",
"name": "浙江省"
},
"city": {
"code": "330100",
"name": "杭州市"
},
"area": {
"code": "330106",
"name": "西湖区"
},
"address": "龙井路1号",
"formatted_address": "浙江省杭州市西湖区龙井路1号"
}
```
而组件的 `addressDiscern` 方法还可以识别收货信息,如 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
## 1.0.132022-03-12
* 【优化】部分细节。
## 1.0.122022-03-09
* 【修复】`u-radio-group` 在 vue3 模式下,设置默认值可能会无效的问题。
## 1.0.112022-03-07
* 【优化】部分细节。
## 1.0.102022-03-05
* 【修复】`u-radio` 中的值相等的判断 == 改为 ===
* 【优化】部分注释的错别字。
## 1.0.92022-03-03
* 【修复】`u-parse` 在 vue3模式下编译到app无法正常显示的问题。
## 1.0.82022-02-26
* 【优化】`u-form` 组件新增2个属性 `inputAlign``clearable` 用于统一设置表单内所有 `u-input` 组件的对应属性默认值
* 【优化】更新城市数据源信息
## 1.0.72022-02-25
* 【重要】`u-picker` 组件新增 `addressDiscern` 方法 智能识别收货信息
如在 `张三 13888888888 上海市嘉定区希望路1333弄是啊我庭12号楼1203` 中识别姓名、手机号、地址(支持多种格式)
即使这样的字符串也能识别 `!!!!~~~$张三~~~上海市嘉定区希望路1333弄是啊我庭12号楼1203【【【【13888888888】`
## 1.0.62022-02-24
* 【优化】`u-form-item` 组件的 `prop` 属性支持 a.b 形式
## 1.0.52022-01-11
* 【修复】`u-sticky` 组件 在微信小程序中无法正常吸顶的问题
## 1.0.42021-12-31
* 【优化】`u-dropdown-item` 组件 0和"" 无法区分的问题。
* 【修复】`u-modal` 在Vue3版本中使用了mask-close-able属性无效的问题
## 1.0.32021-12-20
【优化】u-icon在微信小程序下可能会显示null字符串的问题
## 1.0.22021-12-09
* 1、【优化】`u-button` 组件新增 `timerId` 属性
* 之前的效果是所有按钮一定时间内只能点击1次`共用计算时间`导致点击按钮A后无法马上点击按钮B
* 优化的效果是每个按钮一定时间内只能点击1次`分开计算时间`)且支持设置相同的 timerId 来达到指定按钮 `共用计算时间`
## 1.0.12021-11-22
* 修复 u-parse 组件在微信小程序上的显示问题。
## 1.0.02021-11-18
uView Vue3.0 横空出世继承uView1.0意志再战江湖风云再起by vk 2021-11-18

View File

@@ -0,0 +1,246 @@
<template>
<u-popup
:blur="blur"
mode="bottom"
:border-radius="borderRadius"
:popup="false"
v-model="popupValue"
:maskCloseAble="maskCloseAble"
length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom"
@close="popupClose"
:z-index="uZIndex"
>
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
<text>{{ tips.text }}</text>
</view>
<block v-for="(item, index) in list" :key="index">
<view
@touchmove.stop.prevent
@tap="itemClick(index)"
:style="[itemStyle(index)]"
class="u-action-sheet-item u-line-1"
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
:hover-stay-time="150"
>
<text>{{ item[labelName] }}</text>
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">
{{ item.subText }}
</text>
</view>
</block>
<view class="u-gab" v-if="cancelBtn"></view>
<view
@touchmove.stop.prevent
class="u-actionsheet-cancel u-action-sheet-item"
hover-class="u-hover-class"
:hover-stay-time="150"
v-if="cancelBtn"
@tap="close"
>
{{ cancelText }}
</view>
</u-popup>
</template>
<script>
/**
* actionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI配置更加灵活所有平台都表现一致。
* @tutorial https://www.uviewui.com/components/actionSheet.html
* @property {Array<Object>} list 按钮的文字数组,见官方文档示例
* @property {Object} tips 顶部的提示文字,见官方文档示例
* @property {String} cancel-text 取消按钮的提示文字
* @property {Boolean} cancel-btn 是否显示底部的取消按钮默认true
* @property {Number String} border-radius 弹出部分顶部左右的圆角值单位rpx默认0
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭默认true
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配默认false
* @property {Number String} z-index z-index值默认1075
* @property {String} cancel-text 取消按钮的提示文字
* @event {Function} click 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
emits: ["update:modelValue", "input", "click", "close"],
props: {
// 通过双向绑定控制组件的弹出与收起
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// 点击遮罩是否可以关闭actionsheet
maskCloseAble: {
type: Boolean,
default: true
},
// 按钮的文字数组可以自定义颜色和字体大小字体单位为rpx
list: {
type: Array,
default() {
// 如下
// return [{
// text: '确定',
// color: '',
// fontSize: ''
// }]
return [];
}
},
// 顶部的提示文字
tips: {
type: Object,
default() {
return {
text: "",
color: "",
fontSize: "26"
};
}
},
// 底部的取消按钮
cancelBtn: {
type: Boolean,
default: true
},
// 是否开启底部安全区适配开启的话会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 弹出的顶部圆角值
borderRadius: {
type: [String, Number],
default: 0
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 取消按钮的文字提示
cancelText: {
type: String,
default: "取消"
},
// 自定义label属性名
labelName: {
type: String,
default: "text"
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 顶部提示的样式
tipsStyle() {
let style = {};
if (this.tips.color) style.color = this.tips.color;
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + "rpx";
return style;
},
// 操作项目的样式
itemStyle() {
return index => {
let style = {};
if (this.list[index].color) style.color = this.list[index].color;
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + "rpx";
// 选项被禁用的样式
if (this.list[index].disabled) style.color = "#c0c4cc";
return style;
};
},
uZIndex() {
// 如果用户有传递z-index值优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
data() {
return {
popupValue: false
};
},
watch: {
valueCom(v1, v2) {
this.popupValue = v1;
}
},
methods: {
// 点击取消按钮
close() {
// 发送input事件并不会作用于父组件而是要设置组件内部通过props传递的value参数
// 这是一个vue发送事件的特殊用法
this.popupClose();
this.$emit("close");
},
// 弹窗关闭
popupClose() {
this.$emit("input", false);
this.$emit("update:modelValue", false);
},
// 点击某一个item
itemClick(index) {
// disabled的项禁止点击
if (this.list[index].disabled) return;
this.$emit("click", index);
this.$emit("input", false);
this.$emit("update:modelValue", false);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-tips {
font-size: 26rpx;
text-align: center;
padding: 34rpx 0;
line-height: 1.5;
color: $u-tips-color;
}
.u-action-sheet-item {
@include vue-flex;
line-height: 1;
justify-content: center;
align-items: center;
font-size: 32rpx;
padding: 34rpx 0;
flex-direction: column;
}
.u-action-sheet-item__subtext {
font-size: 24rpx;
color: $u-tips-color;
margin-top: 20rpx;
}
.u-gab {
height: 12rpx;
background-color: rgb(234, 234, 236);
}
.u-actionsheet-cancel {
color: $u-main-color;
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<view class="u-alert-tips" v-if="show" :class="[
!show ? 'u-close-alert-tips': '',
type ? 'u-alert-tips--bg--' + type + '-light' : '',
type ? 'u-alert-tips--border--' + type + '-disabled' : '',
]" :style="{
backgroundColor: bgColor,
borderColor: borderColor
}">
<view class="u-icon-wrap">
<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
</view>
<view class="u-alert-content" @tap.stop="click">
<view class="u-alert-title" :style="[uTitleStyle]">
{{title}}
</view>
<view v-if="description" class="u-alert-desc" :style="[descStyle]">
{{description}}
</view>
</view>
<view class="u-icon-wrap">
<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
:size="22" class="u-close-icon" :style="{
top: description ? '18rpx' : '24rpx'
}"></u-icon>
</view>
<text v-if="closeAble && closeText" class="u-close-text" :style="{
top: description ? '18rpx' : '24rpx'
}">{{closeText}}</text>
</view>
</template>
<script>
/**
* alertTips 警告提示
* @description 警告提示,展现需要关注的信息
* @tutorial https://uviewui.com/components/alertTips.html
* @property {String} title 显示的标题文字
* @property {String} description 辅助性文字颜色比title浅一点字号也小一点可选
* @property {String} type 关闭按钮(默认为叉号icon图标)
* @property {String} icon 图标名称
* @property {Object} icon-style 图标的样式,对象形式
* @property {Object} title-style 标题的样式,对象形式
* @property {Object} desc-style 描述的样式,对象形式
* @property {String} close-able 用文字替代关闭图标close-able为true时有效
* @property {Boolean} show-icon 是否显示左边的辅助图标
* @property {Boolean} show 显示或隐藏组件
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
*/
export default {
name: 'u-alert-tips',
emits: ["click", "close"],
props: {
// 显示文字
title: {
type: String,
default: ''
},
// 主题success/warning/info/error
type: {
type: String,
default: 'warning'
},
// 辅助性文字
description: {
type: String,
default: ''
},
// 是否可关闭
closeAble: {
type: Boolean,
default: false
},
// 关闭按钮自定义文本
closeText: {
type: String,
default: ''
},
// 是否显示图标
showIcon: {
type: Boolean,
default: false
},
// 文字颜色如果定义了color值icon会失效
color: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 左边显示的icon
icon: {
type: String,
default: ''
},
// icon的样式
iconStyle: {
type: Object,
default() {
return {}
}
},
// 标题的样式
titleStyle: {
type: Object,
default() {
return {}
}
},
// 描述文字的样式
descStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
}
},
computed: {
uTitleStyle() {
let style = {};
// 如果有描述文字的话,标题进行加粗
style.fontWeight = this.description ? 500 : 'normal';
// 将用户传入样式对象和style合并传入的优先级比style高同属性会被覆盖
return this.$u.deepMerge(style, this.titleStyle);
},
uIcon() {
// 如果有设置icon名称就使用否则根据type主题推定一个默认的图标
return this.icon ? this.icon : this.$u.type2icon(this.type);
},
uIconType() {
// 如果有设置图标的样式优先使用没有的话则用type的样式
return Object.keys(this.iconStyle).length ? '' : this.type;
}
},
methods: {
// 点击内容
click() {
this.$emit('click');
},
// 点击关闭按钮
close() {
this.$emit('close');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-alert-tips {
@include vue-flex;
align-items: center;
padding: 16rpx 30rpx;
border-radius: 8rpx;
position: relative;
transition: all 0.3s linear;
border: 1px solid #fff;
&--bg--primary-light {
background-color: $u-type-primary-light;
}
&--bg--info-light {
background-color: $u-type-info-light;
}
&--bg--success-light {
background-color: $u-type-success-light;
}
&--bg--warning-light {
background-color: $u-type-warning-light;
}
&--bg--error-light {
background-color: $u-type-error-light;
}
&--border--primary-disabled {
border-color: $u-type-primary-disabled;
}
&--border--success-disabled {
border-color: $u-type-success-disabled;
}
&--border--error-disabled {
border-color: $u-type-error-disabled;
}
&--border--warning-disabled {
border-color: $u-type-warning-disabled;
}
&--border--info-disabled {
border-color: $u-type-info-disabled;
}
}
.u-close-alert-tips {
opacity: 0;
visibility: hidden;
}
.u-icon {
margin-right: 16rpx;
}
.u-alert-title {
font-size: 28rpx;
color: $u-main-color;
}
.u-alert-desc {
font-size: 26rpx;
text-align: left;
color: $u-content-color;
}
.u-close-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
}
.u-close-hover {
color: red;
}
.u-close-text {
font-size: 24rpx;
color: $u-tips-color;
position: absolute;
top: 20rpx;
right: 20rpx;
line-height: 1;
}
</style>

View File

@@ -0,0 +1,290 @@
<template>
<view class="content">
<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
<canvas
class="cropper"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
canvas-id="cropper"
id="cropper"
></canvas>
<canvas
class="cropper"
:disable-scroll="true"
:style="{
position: 'fixed',
top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
height: `${cropperOpt.height * cropperOpt.pixelRatio}`
}"
canvas-id="targetId"
id="targetId"
></canvas>
</view>
<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
<!-- #ifdef H5 -->
<view class="upload" @tap="uploadTap">选择图片</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="upload" @tap="uploadTap">重新选择</view>
<!-- #endif -->
<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
</view>
</view>
</template>
<script>
import WeCropper from './weCropper.js';
export default {
props: {
// 裁剪矩形框的样式其中可包含的属性为lineWidth-边框宽度(单位rpx)color: 边框颜色,
// mask-遮罩颜色一般设置为一个rgba的透明度如"rgba(0, 0, 0, 0.35)"
boundStyle: {
type: Object,
default() {
return {
lineWidth: 4,
borderColor: 'rgb(245, 245, 245)',
mask: 'rgba(0, 0, 0, 0.35)'
};
}
}
// // 裁剪框宽度单位rpx
// rectWidth: {
// type: [String, Number],
// default: 400
// },
// // 裁剪框高度单位rpx
// rectHeight: {
// type: [String, Number],
// default: 400
// },
// // 输出图片宽度单位rpx
// destWidth: {
// type: [String, Number],
// default: 400
// },
// // 输出图片高度单位rpx
// destHeight: {
// type: [String, Number],
// default: 400
// },
// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
// fileType: {
// type: String,
// default: 'jpg',
// },
// // 生成的图片质量
// // H5上无效目前不考虑使用此参数
// quality: {
// type: [Number, String],
// default: 1
// }
},
data() {
return {
// 底部导航的高度
bottomNavHeight: 50,
originWidth: 200,
width: 0,
height: 0,
cropperOpt: {
id: 'cropper',
targetId: 'targetCropper',
pixelRatio: 1,
width: 0,
height: 0,
scale: 2.5,
zoom: 8,
cut: {
x: (this.width - this.originWidth) / 2,
y: (this.height - this.originWidth) / 2,
width: this.originWidth,
height: this.originWidth
},
boundStyle: {
lineWidth: uni.upx2px(this.boundStyle.lineWidth),
mask: this.boundStyle.mask,
color: this.boundStyle.borderColor
}
},
// 裁剪框和输出图片的尺寸,高度默认等于宽度
// 输出图片宽度单位px
destWidth: 200,
// 裁剪框宽度单位px
rectWidth: 200,
// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
fileType: 'jpg',
src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
};
},
onLoad(option) {
let rectInfo = uni.getSystemInfoSync();
this.width = rectInfo.windowWidth;
this.height = rectInfo.windowHeight - this.bottomNavHeight;
this.cropperOpt.width = this.width;
this.cropperOpt.height = this.height;
this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
if (option.destWidth) this.destWidth = option.destWidth;
if (option.rectWidth) {
let rectWidth = Number(option.rectWidth);
this.cropperOpt.cut = {
x: (this.width - rectWidth) / 2,
y: (this.height - rectWidth) / 2,
width: rectWidth,
height: rectWidth
};
}
this.rectWidth = option.rectWidth;
if (option.fileType) this.fileType = option.fileType;
// 初始化
this.cropper = new WeCropper(this.cropperOpt)
.on('ready', ctx => {
// wecropper is ready for work!
})
.on('beforeImageLoad', ctx => {
// before picture loaded, i can do something
})
.on('imageLoad', ctx => {
// picture loaded
})
.on('beforeDraw', (ctx, instance) => {
// before canvas draw,i can do something
});
// 设置导航栏样式以免用户在page.json中没有设置为黑色背景
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000'
});
uni.chooseImage({
count: 1, // 默认9
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: res => {
this.src = res.tempFilePaths[0];
// 获取裁剪图片资源后给data添加src属性及其值
this.cropper.pushOrign(this.src);
}
});
},
methods: {
touchStart(e) {
this.cropper.touchStart(e);
},
touchMove(e) {
this.cropper.touchMove(e);
},
touchEnd(e) {
this.cropper.touchEnd(e);
},
getCropperImage(isPre = false) {
if(!this.src) return this.$u.toast('请先选择图片再裁剪');
let cropper_opt = {
destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
destWidth: Number(this.destWidth),
fileType: this.fileType
};
this.cropper.getCropperImage(cropper_opt, (path, err) => {
if (err) {
uni.showModal({
title: '温馨提示',
content: err.message
});
} else {
if (isPre) {
uni.previewImage({
current: '', // 当前显示图片的 http 链接
urls: [path] // 需要预览的图片 http 链接列表
});
} else {
uni.$emit('uAvatarCropper', path);
this.$u.route({
type: 'back'
});
}
}
});
},
uploadTap() {
const self = this;
uni.chooseImage({
count: 1, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: (res) => {
self.src = res.tempFilePaths[0];
// 获取裁剪图片资源后给data添加src属性及其值
self.cropper.pushOrign(this.src);
}
});
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.content {
background: rgba(255, 255, 255, 1);
}
.cropper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 11;
}
.cropper-buttons {
background-color: #000000;
color: #eee;
}
.cropper-wrapper {
position: relative;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: #000;
}
.cropper-buttons {
width: 100vw;
@include vue-flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
font-size: 28rpx;
}
.cropper-buttons .upload,
.cropper-buttons .getCropperImage {
width: 50%;
text-align: center;
}
.cropper-buttons .upload {
text-align: left;
padding-left: 50rpx;
}
.cropper-buttons .getCropperImage {
text-align: right;
padding-right: 50rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
<template>
<view class="u-avatar" :style="[wrapStyle]" @tap="click">
<image
@error="loadError"
:style="[imgStyle]"
class="u-avatar__img"
v-if="!uText && avatar"
:src="avatar"
:mode="imgMode"
></image>
<text
class="u-line-1"
v-else-if="uText"
:style="{
fontSize: '38rpx'
}"
>
{{ uText }}
</text>
<slot v-else></slot>
<view
class="u-avatar__sex"
v-if="showSex"
:class="['u-avatar__sex--' + sexIcon]"
:style="[uSexStyle]"
>
<u-icon :name="sexIcon" size="20"></u-icon>
</view>
<view class="u-avatar__level" v-if="showLevel" :style="[uLevelStyle]">
<u-icon :name="levelIcon" size="20"></u-icon>
</view>
</view>
</template>
<script>
let base64Avatar =
"data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
/**
* avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uviewui.com/components/avatar.html
* @property {String} bg-color 背景颜色,一般显示文字时用(默认#ffffff
* @property {String} src 头像路径,如加载失败,将会显示默认头像
* @property {String Number} size 头像尺寸,可以为指定字符串(large, default, mini)或者数值单位rpx默认default
* @property {String} mode 显示类型见上方说明默认circle
* @property {String} sex-icon 性别图标man-男woman-女默认man
* @property {String} level-icon 等级图标默认level
* @property {String} sex-bg-color 性别图标背景颜色
* @property {String} level-bg-color 等级图标背景颜色
* @property {String} show-sex 是否显示性别图标默认false
* @property {String} show-level 是否显示等级图标默认false
* @property {String} img-mode 头像图片的裁剪类型与uni的image组件的mode参数一致如效果达不到需求可尝试传widthFix值默认aspectFill
* @property {String} index 用户传递的标识符值如果是列表循环可穿v-for的index值
* @event {Function} click 头像被点击
* @example <u-avatar :src="src"></u-avatar>
*/
export default {
name: "u-avatar",
emits: ["click"],
props: {
// 背景颜色
bgColor: {
type: String,
default: "transparent"
},
// 头像路径
src: {
type: String,
default: ""
},
// 尺寸large-大default-中等mini-小如果为数值则单位为rpx
// 宽度等于高度
size: {
type: [String, Number],
default: "default"
},
// 头像模型square-带圆角方形circle-圆形
mode: {
type: String,
default: "circle"
},
// 文字内容
text: {
type: String,
default: ""
},
// 图片的裁剪模型
imgMode: {
type: String,
default: "aspectFill"
},
// 标识符
index: {
type: [String, Number],
default: ""
},
// 右上角性别角标man-男woman-女
sexIcon: {
type: String,
default: "man"
},
// 右下角的等级图标
levelIcon: {
type: String,
default: "level"
},
// 右下角等级图标背景颜色
levelBgColor: {
type: String,
default: ""
},
// 右上角性别图标的背景颜色
sexBgColor: {
type: String,
default: ""
},
// 是否显示性别图标
showSex: {
type: Boolean,
default: false
},
// 是否显示等级图标
showLevel: {
type: Boolean,
default: false
}
},
data() {
return {
error: false,
// 头像的地址因为如果加载错误需要赋值为默认图片props值无法修改所以需要一个中间值
avatar: this.src ? this.src : base64Avatar
};
},
watch: {
src(n) {
// 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
if (!n) {
// 如果传入null或者''或者undefined显示默认头像
this.avatar = base64Avatar;
this.error = true;
} else {
this.avatar = n;
this.error = false;
}
}
},
computed: {
wrapStyle() {
let style = {};
style.height =
this.size == "large"
? "120rpx"
: this.size == "default"
? "90rpx"
: this.size == "mini"
? "70rpx"
: this.size + "rpx";
style.width = style.height;
style.flex = `0 0 ${style.height}`;
style.backgroundColor = this.bgColor;
style.borderRadius = this.mode == "circle" ? "500px" : "5px";
if (this.text) style.padding = `0 6rpx`;
return style;
},
imgStyle() {
let style = {};
style.borderRadius = this.mode == "circle" ? "500px" : "5px";
return style;
},
// 取字符串的第一个字符
uText() {
return String(this.text)[0];
},
// 性别图标的自定义样式
uSexStyle() {
let style = {};
if (this.sexBgColor) style.backgroundColor = this.sexBgColor;
return style;
},
// 等级图标的自定义样式
uLevelStyle() {
let style = {};
if (this.levelBgColor) style.backgroundColor = this.levelBgColor;
return style;
}
},
methods: {
// 图片加载错误时,显示默认头像
loadError() {
this.error = true;
this.avatar = base64Avatar;
},
click() {
this.$emit("click", this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-avatar {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
border-radius: 10px;
position: relative;
&__img {
width: 100%;
height: 100%;
}
&__sex {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
top: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
&--man {
background-color: $u-type-primary;
}
&--woman {
background-color: $u-type-error;
}
&--none {
background-color: $u-type-warning;
}
}
&__level {
position: absolute;
width: 32rpx;
color: #ffffff;
height: 32rpx;
@include vue-flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
bottom: 5%;
z-index: 1;
right: -7%;
border: 1px #ffffff solid;
background-color: $u-type-warning;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
bottom: bottom + 'rpx',
right: right + 'rpx',
borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
zIndex: uZIndex,
opacity: opacity
}, customStyle]">
<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
<view class="u-back-top__content__tips">
{{tips}}
</view>
</view>
<slot v-else />
</view>
</template>
<script>
export default {
name: 'u-back-top',
props: {
// 返回顶部的形状circle-圆形square-方形
mode: {
type: String,
default: 'circle'
},
// 自定义图标
icon: {
type: String,
default: 'arrow-upward'
},
// 提示文字
tips: {
type: String,
default: ''
},
// 返回顶部滚动时间
duration: {
type: [Number, String],
default: 100
},
// 滚动距离
scrollTop: {
type: [Number, String],
default: 0
},
// 距离顶部多少距离显示单位rpx
top: {
type: [Number, String],
default: 400
},
// 返回顶部按钮到底部的距离单位rpx
bottom: {
type: [Number, String],
default: 200
},
// 返回顶部按钮到右边的距离单位rpx
right: {
type: [Number, String],
default: 40
},
// 层级
zIndex: {
type: [Number, String],
default: '9'
},
// 图标的样式,对象形式
iconStyle: {
type: Object,
default() {
return {
color: '#909399',
fontSize: '38rpx'
}
}
},
// 整个组件的样式
customStyle: {
type: Object,
default() {
return {}
}
}
},
watch: {
showBackTop(nVal, oVal) {
// 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
// 让组件有显示和消失的动画效果如果用v-if控制组件状态将无设置动画效果
if(nVal) {
this.uZIndex = this.zIndex;
this.opacity = 1;
} else {
this.uZIndex = -1;
this.opacity = 0;
}
}
},
computed: {
showBackTop() {
// 由于scrollTop为页面的滚动距离默认为px单位这里将用于传入的top(rpx)值
// 转为px用于比较如果滚动条到顶的距离大于设定的距离就显示返回顶部的按钮
return this.scrollTop > uni.upx2px(this.top);
},
},
data() {
return {
// 不透明度,为了让组件有一个显示和隐藏的过渡动画
opacity: 0,
// 组件的z-index值隐藏时设置为-1就会看不到
uZIndex: -1
}
},
methods: {
backToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-back-top {
width: 80rpx;
height: 80rpx;
position: fixed;
z-index: 9;
@include vue-flex;
flex-direction: column;
justify-content: center;
background-color: #E1E1E1;
color: $u-content-color;
align-items: center;
transition: opacity 0.4s;
&__content {
@include vue-flex;
flex-direction: column;
align-items: center;
&__tips {
font-size: 24rpx;
transform: scale(0.8);
line-height: 1;
}
}
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<view v-if="show" class="u-badge" :class="[
isDot ? 'u-badge-dot' : '',
size == 'mini' ? 'u-badge-mini' : '',
type ? 'u-badge--bg--' + type : ''
]" :style="[{
top: offset[0] + 'rpx',
right: offset[1] + 'rpx',
fontSize: fontSize + 'rpx',
position: absolute ? 'absolute' : 'static',
color: color,
backgroundColor: bgColor
}, boxStyle]"
>
{{showText}}
</view>
</template>
<script>
/**
* badge 角标
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://www.uviewui.com/components/badge.html
* @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+为0且show-zero为false时隐藏
* @property {Boolean} is-dot 不展示数字只有一个小点默认false
* @property {Boolean} absolute 组件是否绝对定位为true时offset参数才有效默认true
* @property {String Number} overflow-count 展示封顶的数字值默认99
* @property {String} type 使用预设的背景颜色默认error
* @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge默认false
* @property {String} size Badge的尺寸设为mini会得到小一号的Badge默认default
* @property {Array} offset 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值单位rpx。absolute为true时有效默认[20, 20]
* @property {String} color 字体颜色(默认#ffffff
* @property {String} bgColor 背景颜色优先级比type高如设置type参数会失效
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合优先级比offset高如设置offset参数会失效默认false
* @example <u-badge type="error" count="7"></u-badge>
*/
export default {
name: 'u-badge',
props: {
// primary,warning,success,error,info
type: {
type: String,
default: 'error'
},
// default, mini
size: {
type: String,
default: 'default'
},
//是否是圆点
isDot: {
type: Boolean,
default: false
},
// 显示的数值内容
count: {
type: [Number, String],
},
// 展示封顶的数字值
overflowCount: {
type: Number,
default: 99
},
// 当数值为 0 时,是否展示 Badge
showZero: {
type: Boolean,
default: false
},
// 位置偏移
offset: {
type: Array,
default: () => {
return [20, 20]
}
},
// 是否开启绝对定位开启了offset才会起作用
absolute: {
type: Boolean,
default: true
},
// 字体大小
fontSize: {
type: [String, Number],
default: '24'
},
// 字体演示
color: {
type: String,
default: '#ffffff'
},
// badge的背景颜色
bgColor: {
type: String,
default: ''
},
// 是否让badge组件的中心点和父组件右上角重合配置的话offset将会失效
isCenter: {
type: Boolean,
default: false
}
},
computed: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
let style = {};
if(this.isCenter) {
style.top = 0;
style.right = 0;
// Y轴-50%意味着badge向上移动了badge自身高度一半X轴50%,意味着向右移动了自身宽度一半
style.transform = "translateY(-50%) translateX(50%)";
} else {
style.top = this.offset[0] + 'rpx';
style.right = this.offset[1] + 'rpx';
style.transform = "translateY(0) translateX(0)";
}
// 如果尺寸为mini后接上scal()
if(this.size == 'mini') {
style.transform = style.transform + " scale(0.8)";
}
return style;
},
// isDot类型时不显示文字
showText() {
if(this.isDot) return '';
else {
if(this.count > this.overflowCount) return `${this.overflowCount}+`;
else return this.count;
}
},
// 是否显示组件
show() {
// 如果count的值为0并且showZero设置为false不显示组件
if(this.count == 0 && this.showZero == false) return false;
else return true;
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-badge {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
justify-content: center;
align-items: center;
line-height: 24rpx;
padding: 4rpx 8rpx;
border-radius: 100rpx;
z-index: 9;
&--bg--primary {
background-color: $u-type-primary;
}
&--bg--error {
background-color: $u-type-error;
}
&--bg--success {
background-color: $u-type-success;
}
&--bg--info {
background-color: $u-type-info;
}
&--bg--warning {
background-color: $u-type-warning;
}
}
.u-badge-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
line-height: 1;
}
.u-badge-mini {
transform: scale(0.8);
transform-origin: center center;
}
// .u-primary {
// background: $u-type-primary;
// color: #fff;
// }
// .u-error {
// background: $u-type-error;
// color: #fff;
// }
// .u-warning {
// background: $u-type-warning;
// color: #fff;
// }
// .u-success {
// background: $u-type-success;
// color: #fff;
// }
// .u-black {
// background: #585858;
// color: #fff;
// }
.u-info {
background-color: $u-type-info;
color: #fff;
}
</style>

View File

@@ -0,0 +1,602 @@
<template>
<button
id="u-wave-btn"
class="u-btn u-line-1 u-fix-ios-appearance"
:class="[
'u-size-' + size,
plain ? 'u-btn--' + type + '--plain' : '',
loading ? 'u-loading' : '',
shape == 'circle' ? 'u-round-circle' : '',
hairLine ? showHairLineBorder : 'u-btn--bold-border',
'u-btn--' + type,
disabled ? `u-btn--${type}--disabled` : '',
]"
:hover-start-time="Number(hoverStartTime)"
:hover-stay-time="Number(hoverStayTime)"
:disabled="disabled"
:form-type="formType"
:open-type="openType"
:app-parameter="appParameter"
:hover-stop-propagation="hoverStopPropagation"
:send-message-title="sendMessageTitle"
send-message-path="sendMessagePath"
:lang="lang"
:data-name="dataName"
:session-from="sessionFrom"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
@getphonenumber="getphonenumber"
@getuserinfo="getuserinfo"
@error="error"
@opensetting="opensetting"
@launchapp="launchapp"
:style="[customStyle, {
overflow: ripple ? 'hidden' : 'visible'
}]"
@tap.stop="click($event)"
:hover-class="getHoverClass"
:loading="loading"
>
<slot></slot>
<view
v-if="ripple"
class="u-wave-ripple"
:class="[waveActive ? 'u-wave-active' : '']"
:style="{
top: rippleTop + 'px',
left: rippleLeft + 'px',
width: fields.targetWidth + 'px',
height: fields.targetWidth + 'px',
'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
}"
></view>
</button>
</template>
<script>
/**
* button 按钮
* @description Button 按钮
* @tutorial https://www.uviewui.com/components/button.html
* @property {String} size 按钮的大小
* @property {Boolean} ripple 是否开启点击水波纹效果
* @property {String} ripple-bg-color 水波纹的背景色ripple为true时有效
* @property {String} type 按钮的样式类型
* @property {Boolean} plain 按钮是否镂空,背景色透明
* @property {Boolean} disabled 是否禁用
* @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
* @property {Boolean} shape 按钮外观形状,见文档说明
* @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花Android上为圆圈)
* @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
* @property {String} open-type 开放能力
* @property {String} data-name 额外传参参数用于小程序的data-xxx属性通过target.dataset.name获取
* @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
* @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
* @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
* @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
* @event {Function} click 按钮点击
* @event {Function} getphonenumber open-type="getPhoneNumber"时有效
* @event {Function} getuserinfo 用户点击该按钮时会返回获取到的用户信息从返回参数的detail中获取到的值同uni.getUserInfo
* @event {Function} error 当使用开放能力时,发生错误的回调
* @event {Function} opensetting 在打开授权设置页并关闭后回调
* @event {Function} launchapp 打开 APP 成功的回调
* @example <u-button>月落</u-button>
*/
export default {
name: 'u-button',
emits: ["click", "getphonenumber", "getuserinfo", "error", "opensetting", "launchapp"],
props: {
// 是否细边框
hairLine: {
type: Boolean,
default: true
},
// 按钮的预置样式defaultprimaryerrorwarningsuccess
type: {
type: String,
default: 'default'
},
// 按钮尺寸defaultmediummini
size: {
type: String,
default: 'default'
},
// 按钮形状circle两边为半圆square带圆角
shape: {
type: String,
default: 'square'
},
// 按钮是否镂空
plain: {
type: Boolean,
default: false
},
// 是否禁止状态
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 开放能力具体请看uniapp稳定关于button组件部分说明
// https://uniapp.dcloud.io/component/button
openType: {
type: String,
default: ''
},
// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
// 取值为submit提交表单reset重置表单
formType: {
type: String,
default: ''
},
// 打开 APP 时,向 APP 传递的参数open-type=launchApp时有效
// 只微信小程序、QQ小程序有效
appParameter: {
type: String,
default: ''
},
// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
hoverStopPropagation: {
type: Boolean,
default: false
},
// 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文。只微信小程序有效
lang: {
type: String,
default: 'en'
},
// 会话来源open-type="contact"时有效。只微信小程序有效
sessionFrom: {
type: String,
default: ''
},
// 会话内消息卡片标题open-type="contact"时有效
// 默认当前标题,只微信小程序有效
sendMessageTitle: {
type: String,
default: ''
},
// 会话内消息卡片点击跳转小程序路径open-type="contact"时有效
// 默认当前分享路径,只微信小程序有效
sendMessagePath: {
type: String,
default: ''
},
// 会话内消息卡片图片open-type="contact"时有效
// 默认当前页面截图,只微信小程序有效
sendMessageImg: {
type: String,
default: ''
},
// 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
// 用户点击后可以快速发送小程序消息open-type="contact"时有效
showMessageCard: {
type: Boolean,
default: false
},
// 手指按(触摸)按钮时按钮时的背景颜色
hoverBgColor: {
type: String,
default: ''
},
// 水波纹的背景颜色
rippleBgColor: {
type: String,
default: ''
},
// 是否开启水波纹效果
ripple: {
type: Boolean,
default: false
},
// 按下的类名
hoverClass: {
type: String,
default: ''
},
// 自定义样式,对象形式
customStyle: {
type: Object,
default() {
return {};
}
},
// 额外传参参数用于小程序的data-xxx属性通过target.dataset.name获取
dataName: {
type: String,
default: ''
},
// 节流,一定时间内只能触发一次
throttleTime: {
type: [String, Number],
default: 500
},
// 按住后多久出现点击态,单位毫秒
hoverStartTime: {
type: [String, Number],
default: 20
},
// 手指松开后点击态保留时间,单位毫秒
hoverStayTime: {
type: [String, Number],
default: 150
},
timerId: {
type: [String, Number]
},
},
computed: {
// 当没有传bgColor变量时按钮按下去的颜色类名
getHoverClass() {
// 如果开启水波纹效果则不启用hover-class效果
if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
let hoverClass = '';
hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
return hoverClass;
},
// 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
showHairLineBorder() {
if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
return '';
} else {
return 'u-hairline-border';
}
}
},
data() {
let btnTimerId = this.timerId || "button_" + Math.floor(Math.random() * 100000000 + 0);
return {
btnTimerId,
rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
fields: {}, // 波纹按钮节点信息
waveActive: false // 激活水波纹
};
},
methods: {
// 按钮点击
click(e) {
// 进行节流控制每this.throttle毫秒内只在开始处执行
this.$u.throttle(() => {
// 如果按钮时disabled和loading状态不触发水波纹效果
if (this.loading === true || this.disabled === true) return;
// 是否开启水波纹效果
if (this.ripple) {
// 每次点击时,移除上一次的类,再次添加,才能触发动画效果
this.waveActive = false;
this.$nextTick(function() {
this.getWaveQuery(e);
});
}
this.$emit('click', e);
}, this.throttleTime, true, this.btnTimerId);
},
// 查询按钮的节点信息
getWaveQuery(e) {
this.getElQuery().then(res => {
// 查询返回的是一个数组节点
let data = res[0];
// 查询不到节点信息,不操作
if (!data.width || !data.width) return;
// 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
// 最终的方形(变换后的圆形)才能覆盖整个按钮
data.targetWidth = data.height > data.width ? data.height : data.width;
if (!data.targetWidth) return;
this.fields = data;
let touchesX = '',
touchesY = '';
// #ifdef MP-BAIDU
touchesX = e.changedTouches[0].clientX;
touchesY = e.changedTouches[0].clientY;
// #endif
// #ifdef MP-ALIPAY
touchesX = e.detail.clientX;
touchesY = e.detail.clientY;
// #endif
// #ifndef MP-BAIDU || MP-ALIPAY
touchesX = e.touches[0].clientX;
touchesY = e.touches[0].clientY;
// #endif
// 获取触摸点相对于按钮上边和左边的x和y坐标原理是通过屏幕的触摸点touchesY减去按钮的上边界data.top
// 但是由于`transform-origin`默认是center所以这里再减去半径才是水波纹view应该的位置
// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
this.rippleTop = touchesY - data.top - data.targetWidth / 2;
this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
this.$nextTick(() => {
this.waveActive = true;
});
});
},
// 获取节点信息
getElQuery() {
return new Promise(resolve => {
let queryInfo = '';
// 获取元素节点信息请查看uniapp相关文档
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
queryInfo = uni.createSelectorQuery().in(this);
//#ifdef MP-ALIPAY
queryInfo = uni.createSelectorQuery();
//#endif
queryInfo.select('.u-btn').boundingClientRect();
queryInfo.exec(data => {
resolve(data);
});
});
},
// 下面为对接uniapp官方按钮开放能力事件回调的对接
getphonenumber(res) {
this.$emit('getphonenumber', res);
},
getuserinfo(res) {
this.$emit('getuserinfo', res);
},
error(res) {
this.$emit('error', res);
},
opensetting(res) {
this.$emit('opensetting', res);
},
launchapp(res) {
this.$emit('launchapp', res);
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-btn::after {
border: none;
}
.u-btn {
position: relative;
border: 0;
//border-radius: 10rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
// 避免边框某些场景可能被“裁剪”不能设置为hidden
overflow: visible;
line-height: 1;
@include vue-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0 40rpx;
z-index: 1;
box-sizing: border-box;
transition: all 0.15s;
&--bold-border {
border: 1px solid #ffffff;
}
&--default {
color: $u-content-color;
border-color: #c0c4cc;
background-color: #ffffff;
}
&--primary {
color: #ffffff;
border-color: $u-type-primary;
background-color: $u-type-primary;
}
&--success {
color: #ffffff;
border-color: $u-type-success;
background-color: $u-type-success;
}
&--error {
color: #ffffff;
border-color: $u-type-error;
background-color: $u-type-error;
}
&--warning {
color: #ffffff;
border-color: $u-type-warning;
background-color: $u-type-warning;
}
&--default--disabled {
color: #ffffff;
border-color: #e4e7ed;
background-color: #ffffff;
}
&--primary--disabled {
color: #ffffff!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-disabled!important;
}
&--success--disabled {
color: #ffffff!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-disabled!important;
}
&--error--disabled {
color: #ffffff!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-disabled!important;
}
&--warning--disabled {
color: #ffffff!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-disabled!important;
}
&--primary--plain {
color: $u-type-primary!important;
border-color: $u-type-primary-disabled!important;
background-color: $u-type-primary-light!important;
}
&--success--plain {
color: $u-type-success!important;
border-color: $u-type-success-disabled!important;
background-color: $u-type-success-light!important;
}
&--error--plain {
color: $u-type-error!important;
border-color: $u-type-error-disabled!important;
background-color: $u-type-error-light!important;
}
&--warning--plain {
color: $u-type-warning!important;
border-color: $u-type-warning-disabled!important;
background-color: $u-type-warning-light!important;
}
}
.u-hairline-border:after {
content: ' ';
position: absolute;
pointer-events: none;
// 设置为border-box意味着下面的scale缩小为0.5实际上缩小的是伪元素的内容border-box意味着内容不含border
box-sizing: border-box;
// 中心点作为变形(scale())的原点
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
left: 0;
top: 0;
width: 199.8%;
height: 199.7%;
-webkit-transform: scale(0.5, 0.5);
transform: scale(0.5, 0.5);
border: 1px solid currentColor;
z-index: 1;
}
.u-wave-ripple {
z-index: 0;
position: absolute;
border-radius: 100%;
background-clip: padding-box;
pointer-events: none;
user-select: none;
transform: scale(0);
opacity: 1;
transform-origin: center;
}
.u-wave-ripple.u-wave-active {
opacity: 0;
transform: scale(2);
transition: opacity 1s linear, transform 0.4s linear;
}
.u-round-circle {
border-radius: 100rpx;
}
.u-round-circle::after {
border-radius: 100rpx;
}
.u-loading::after {
background-color: hsla(0, 0%, 100%, 0.35);
}
.u-size-default {
font-size: 30rpx;
height: 80rpx;
line-height: 80rpx;
}
.u-size-medium {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 26rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0 80rpx;
}
.u-size-mini {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
width: auto;
font-size: 22rpx;
padding-top: 1px;
height: 50rpx;
line-height: 50rpx;
padding: 0 20rpx;
}
.u-primary-plain-hover {
color: #ffffff !important;
background: $u-type-primary-dark !important;
}
.u-default-plain-hover {
color: $u-type-primary-dark !important;
background: $u-type-primary-light !important;
}
.u-success-plain-hover {
color: #ffffff !important;
background: $u-type-success-dark !important;
}
.u-warning-plain-hover {
color: #ffffff !important;
background: $u-type-warning-dark !important;
}
.u-error-plain-hover {
color: #ffffff !important;
background: $u-type-error-dark !important;
}
.u-info-plain-hover {
color: #ffffff !important;
background: $u-type-info-dark !important;
}
.u-default-hover {
color: $u-type-primary-dark !important;
border-color: $u-type-primary-dark !important;
background-color: $u-type-primary-light !important;
}
.u-primary-hover {
background: $u-type-primary-dark !important;
color: #fff;
}
.u-success-hover {
background: $u-type-success-dark !important;
color: #fff;
}
.u-info-hover {
background: $u-type-info-dark !important;
color: #fff;
}
.u-warning-hover {
background: $u-type-warning-dark !important;
color: #fff;
}
.u-error-hover {
background: $u-type-error-dark !important;
color: #fff;
}
</style>

View File

@@ -0,0 +1,663 @@
<template>
<u-popup :blur="blur" closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="popupValue" length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
<view class="u-calendar">
<view class="u-calendar__header">
<view class="u-calendar__header__text" v-if="!$slots['tooltip']">
{{toolTip}}
</view>
<slot v-else name="tooltip" />
</view>
<view class="u-calendar__action u-flex u-row-center">
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
</view>
<view class="u-calendar__action__text">{{ showTitle }}</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
</view>
<view class="u-calendar__action__icon">
<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
</view>
</view>
<view class="u-calendar__week-day">
<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
</view>
<view class="u-calendar__content">
<!-- 前置空白部分 -->
<block v-for="(item, index) in weekdayArr" :key="index">
<view class="u-calendar__content__item"></view>
</block>
<view class="u-calendar__content__item" :class="{
'u-hover-class':openDisAbled(year,month,index+1),
'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
@tap="dateClick(index)">
<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
<view>{{ index + 1 }}</view>
</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
</view>
<view class="u-calendar__content__bg-month">{{month}}</view>
</view>
<view class="u-calendar__bottom">
<view class="u-calendar__bottom__choose">
<text>{{mode == 'date' ? activeDate : startDate}}</text>
<text v-if="endDate">{{endDate}}</text>
</view>
<view class="u-calendar__bottom__btn">
<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
</view>
</view>
</view>
</u-popup>
</template>
<script>
/**
* calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
* @tutorial http://uviewui.com/components/calendar.html
* @property {String} mode 选择日期的模式date-为单个日期range-为选择日期范围
* @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
* @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
* @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
* @property {String Number} max-year 可切换的最大年份(默认2050)
* @property {String Number} min-year 最小可选日期(默认1950)
* @property {String Number} min-date 可切换的最小年份(默认1950-01-01)
* @property {String Number} max-date 最大可选日期(默认当前日期)
* @property {String Number} 弹窗顶部左右两边的圆角值单位rpx(默认20)
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
* @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
* @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
* @property {String} color 日期字体的默认颜色(默认#303133)
* @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
* @property {String Number} z-index 弹出时的z-index值(默认10075)
* @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
* @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
* @property {String} range-color 选择范围内字体颜色(默认#2979ff)
* @property {String} start-text 起始日期底部的提示文字(默认 '开始')
* @property {String} end-text 结束日期底部的提示文字(默认 '结束')
* @property {String} btn-type 底部确定按钮的主题(默认 'primary')
* @property {String} toolTip 顶部提示文字如设置名为tooltip的slot此参数将失效(默认 '选择日期')
* @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
* @example <u-calendar v-model="show" :mode="mode"></u-calendar>
*/
export default {
name: 'u-calendar',
emits: ["update:modelValue", "input", "change"],
props: {
// 通过双向绑定控制组件的弹出与收起
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否允许通过点击遮罩关闭Picker
maskCloseAble: {
type: Boolean,
default: true
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 是否允许切换年份
changeYear: {
type: Boolean,
default: true
},
// 是否允许切换月份
changeMonth: {
type: Boolean,
default: true
},
// date-单个日期选择range-开始日期+结束日期选择
mode: {
type: String,
default: 'date'
},
// 可切换的最大年份
maxYear: {
type: [Number, String],
default: 2050
},
// 可切换的最小年份
minYear: {
type: [Number, String],
default: 1950
},
// 最小可选日期(不在范围内日期禁用不可选)
minDate: {
type: [Number, String],
default: '1950-01-01'
},
/**
* 最大可选日期
* 默认最大值为今天,之后的日期不可选
* 2030-12-31
* */
maxDate: {
type: [Number, String],
default: ''
},
// 弹窗顶部左右两边的圆角值
borderRadius: {
type: [String, Number],
default: 20
},
// 月份切换按钮箭头颜色
monthArrowColor: {
type: String,
default: '#606266'
},
// 年份切换按钮箭头颜色
yearArrowColor: {
type: String,
default: '#909399'
},
// 默认日期字体颜色
color: {
type: String,
default: '#303133'
},
// 选中|起始结束日期背景色
activeBgColor: {
type: String,
default: '#2979ff'
},
// 选中|起始结束日期字体颜色
activeColor: {
type: String,
default: '#ffffff'
},
// 范围内日期背景色
rangeBgColor: {
type: String,
default: 'rgba(41,121,255,0.13)'
},
// 范围内日期字体颜色
rangeColor: {
type: String,
default: '#2979ff'
},
// mode=range时生效起始日期自定义文案
startText: {
type: String,
default: '开始'
},
// mode=range时生效结束日期自定义文案
endText: {
type: String,
default: '结束'
},
//按钮样式类型
btnType: {
type: String,
default: 'primary'
},
// 当前选中日期带选中效果
isActiveCurrent: {
type: Boolean,
default: true
},
// 切换年月是否触发事件 mode=date时生效
isChange: {
type: Boolean,
default: false
},
// 是否显示右上角的关闭图标
closeable: {
type: Boolean,
default: true
},
// 顶部的提示文字
toolTip: {
type: String,
default: '选择日期'
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
},
},
data() {
return {
popupValue:false,
// 星期几,值为1-7
weekday: 1,
weekdayArr:[],
// 当前月有多少天
days: 0,
daysArr:[],
showTitle: '',
year: 2020,
month: 0,
day: 0,
startYear: 0,
startMonth: 0,
startDay: 0,
endYear: 0,
endMonth: 0,
endDay: 0,
today: '',
activeDate: '',
startDate: '',
endDate: '',
isStart: true,
min: null,
max: null,
weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
dataChange() {
return `${this.mode}-${this.minDate}-${this.maxDate}`;
},
uZIndex() {
// 如果用户有传递z-index值优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
watch: {
dataChange(val) {
this.init()
},
valueCom(v1, v2) {
this.popupValue = v1;
}
},
created() {
this.init()
},
methods: {
getColor(index, type) {
let color = type == 1 ? '' : this.color;
let day = index + 1
let date = `${this.year}-${this.month}-${day}`
let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
let start = this.startDate.replace(/\-/g, '/')
let end = this.endDate.replace(/\-/g, '/')
if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
color = type == 1 ? this.activeBgColor : this.activeColor;
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
color = type == 1 ? this.rangeBgColor : this.rangeColor;
}
return color;
},
init() {
let now = new Date();
this.year = now.getFullYear();
this.month = now.getMonth() + 1;
this.day = now.getDate();
this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
this.activeDate = this.today;
this.min = this.initDate(this.minDate);
this.max = this.initDate(this.maxDate || this.today);
this.startDate = "";
this.startYear = 0;
this.startMonth = 0;
this.startDay = 0;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.isStart = true;
this.changeData();
},
//日期处理
initDate(date) {
let fdate = date.split('-');
return {
year: Number(fdate[0] || 1920),
month: Number(fdate[1] || 1),
day: Number(fdate[2] || 1)
}
},
openDisAbled: function(year, month, day) {
let bool = true;
let date = `${year}/${month}/${day}`;
// let today = this.today.replace(/\-/g, '/');
let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
let timestamp = new Date(date).getTime();
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
bool = false;
}
return bool;
},
generateArray: function(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start);
},
formatNum: function(num) {
return num < 10 ? '0' + num : num + '';
},
//一个月有多少天
getMonthDay(year, month) {
let days = new Date(year, month, 0).getDate();
return days;
},
getWeekday(year, month) {
let date = new Date(`${year}/${month}/01 00:00:00`);
return date.getDay();
},
checkRange(year) {
let overstep = false;
if (year < this.minYear || year > this.maxYear) {
uni.showToast({
title: "日期超出范围啦~",
icon: 'none'
})
overstep = true;
}
return overstep;
},
changeMonthHandler(isAdd) {
if (isAdd) {
let month = this.month + 1;
let year = month > 12 ? this.year + 1 : this.year;
if (!this.checkRange(year)) {
this.month = month > 12 ? 1 : month;
this.year = year;
this.changeData();
}
} else {
let month = this.month - 1;
let year = month < 1 ? this.year - 1 : this.year;
if (!this.checkRange(year)) {
this.month = month < 1 ? 12 : month;
this.year = year;
this.changeData();
}
}
},
changeYearHandler(isAdd) {
let year = isAdd ? this.year + 1 : this.year - 1;
if (!this.checkRange(year)) {
this.year = year;
this.changeData();
}
},
changeData() {
this.days = this.getMonthDay(this.year, this.month);
this.daysArr=this.generateArray(1,this.days)
this.weekday = this.getWeekday(this.year, this.month);
this.weekdayArr=this.generateArray(1,this.weekday)
this.showTitle = `${this.year}${this.month}`;
if (this.isChange && this.mode == 'date') {
this.btnFix(true);
}
},
dateClick: function(day) {
day += 1;
if (!this.openDisAbled(this.year, this.month, day)) {
this.day = day;
let date = `${this.year}-${this.month}-${day}`;
if (this.mode == 'date') {
this.activeDate = date;
} else {
let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
if (this.isStart || compare) {
this.startDate = date;
this.startYear = this.year;
this.startMonth = this.month;
this.startDay = this.day;
this.endYear = 0;
this.endMonth = 0;
this.endDay = 0;
this.endDate = "";
this.activeDate = "";
this.isStart = false;
} else {
this.endDate = date;
this.endYear = this.year;
this.endMonth = this.month;
this.endDay = this.day;
this.isStart = true;
}
}
}
},
close() {
// 修改通过v-model绑定的父组件变量的值为false从而隐藏日历弹窗
this.$emit('input', false);
this.$emit("update:modelValue", false);
},
getWeekText(date) {
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
let week = date.getDay();
return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
},
btnFix(show) {
if (!show) {
this.close();
}
if (this.mode == 'date') {
let arr = this.activeDate.split('-')
let year = this.isChange ? this.year : Number(arr[0]);
let month = this.isChange ? this.month : Number(arr[1]);
let day = this.isChange ? this.day : Number(arr[2]);
//当前月有多少天
let days = this.getMonthDay(year, month);
let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
let weekText = this.getWeekText(result);
let isToday = false;
if (`${year}-${month}-${day}` == this.today) {
//今天
isToday = true;
}
this.$emit('change', {
year: year,
month: month,
day: day,
days: days,
result: result,
week: weekText,
isToday: isToday,
// switch: show //是否是切换年月操作
});
} else {
if (!this.startDate || !this.endDate) return;
let startMonth = this.formatNum(this.startMonth);
let startDay = this.formatNum(this.startDay);
let startDate = `${this.startYear}-${startMonth}-${startDay}`;
let startWeek = this.getWeekText(startDate)
let endMonth = this.formatNum(this.endMonth);
let endDay = this.formatNum(this.endDay);
let endDate = `${this.endYear}-${endMonth}-${endDay}`;
let endWeek = this.getWeekText(endDate);
this.$emit('change', {
startYear: this.startYear,
startMonth: this.startMonth,
startDay: this.startDay,
startDate: startDate,
startWeek: startWeek,
endYear: this.endYear,
endMonth: this.endMonth,
endDay: this.endDay,
endDate: endDate,
endWeek: endWeek
});
}
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-calendar {
color: $u-content-color;
&__header {
width: 100%;
box-sizing: border-box;
font-size: 30rpx;
background-color: #fff;
color: $u-main-color;
&__text {
margin-top: 30rpx;
padding: 0 60rpx;
@include vue-flex;
justify-content: center;
align-items: center;
}
}
&__action {
padding: 40rpx 0 40rpx 0;
&__icon {
margin: 0 16rpx;
}
&__text {
padding: 0 16rpx;
color: $u-main-color;
font-size: 32rpx;
line-height: 32rpx;
font-weight: bold;
}
}
&__week-day {
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
&__text {
flex: 1;
text-align: center;
}
}
&__content {
width: 100%;
@include vue-flex;
flex-wrap: wrap;
padding: 6px 0;
box-sizing: border-box;
background-color: #fff;
position: relative;
&--end-date {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&--start-date {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
&__item {
width: 14.2857%;
@include vue-flex;
align-items: center;
justify-content: center;
padding: 6px 0;
overflow: hidden;
position: relative;
z-index: 2;
&__inner {
height: 84rpx;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 32rpx;
position: relative;
border-radius: 50%;
&__desc {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
transform: scale(0.75);
transform-origin: center center;
position: absolute;
left: 0;
text-align: center;
bottom: 2rpx;
}
}
&__tips {
width: 100%;
font-size: 24rpx;
line-height: 24rpx;
position: absolute;
left: 0;
transform: scale(0.8);
transform-origin: center center;
text-align: center;
bottom: 8rpx;
z-index: 2;
}
}
&__bg-month {
position: absolute;
font-size: 130px;
line-height: 130px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #e4e7ed;
z-index: 1;
}
}
&__bottom {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #fff;
padding: 0 40rpx 30rpx;
box-sizing: border-box;
font-size: 24rpx;
color: $u-tips-color;
&__choose {
height: 50rpx;
}
&__btn {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<view class="u-keyboard" @touchmove.stop.prevent="() => {}">
<view class="u-keyboard-grids">
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
<view :hover-stay-time="100" @touchstart="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
v-for="(item, j) in group" :key="j">
{{ item }}
</view>
</view>
<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
hover-class="u-hover-class">
<u-icon :size="38" name="backspace" :bold="true"></u-icon>
</view>
<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @click="changeCarInputMode">
<text class="zh" :class="[!abc ? 'active' : 'inactive']"></text>
/
<text class="en" :class="[abc ? 'active' : 'inactive']"></text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "u-keyboard",
emits: ["change", "backspace"],
props: {
// 是否打乱键盘按键的顺序
random: {
type: Boolean,
default: false
}
},
data() {
return {
// 车牌输入时abc=true为输入车牌号码bac=false为输入省份中文简称
abc: false
};
},
computed: {
areaList() {
let data = [
'京',
'沪',
'粤',
'津',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'陕',
'蒙',
'吉',
'闽',
'贵',
'渝',
'川',
'青',
'琼',
'宁',
'挂',
'藏',
'港',
'澳',
'新',
'使',
'学'
];
let tmp = [];
// 打乱顺序
if (this.random) data = this.$u.randomArray(data);
// 切割成二维数组
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
},
EngKeyBoardList() {
let data = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
'Z',
'X',
'C',
'V',
'B',
'N',
'M'
];
let tmp = [];
if (this.random) data = this.$u.randomArray(data);
tmp[0] = data.slice(0, 10);
tmp[1] = data.slice(10, 20);
tmp[2] = data.slice(20, 30);
tmp[3] = data.slice(30, 36);
return tmp;
}
},
methods: {
// 点击键盘按钮
carInputClick(i, j) {
let value = '';
// 不同模式,获取不同数组的值
if (this.abc) value = this.EngKeyBoardList[i][j];
else value = this.areaList[i][j];
if(!this.abc) this.abc = true;
this.$emit('change', value);
if(this.vibrate) uni.vibrateShort();
},
// 修改汽车牌键盘的输入模式,中文|英文
changeCarInputMode() {
this.abc = !this.abc;
},
// 修改汽车牌键盘的输入模式,中文|英文
updateCarInputMode(abc) {
this.abc = abc;
},
// 点击退格键
backspaceClick() {
let count = 1;
this.backspaceFn(count);
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
this.timer = null;
this.timer = setInterval(() => {
count++;
this.backspaceFn(count);
}, 250);
},
backspaceFn(count){
this.$emit('backspace',count);
},
clearTimer() {
clearInterval(this.timer);
this.timer = null;
},
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-keyboard-grids {
background: rgb(215, 215, 217);
padding: 24rpx 0;
position: relative;
}
.u-keyboard-grids-item {
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-keyboard-grids-btn {
text-decoration: none;
width: 62rpx;
flex: 0 0 64rpx;
height: 80rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
font-size: 36rpx;
text-align: center;
line-height: 80rpx;
background-color: #fff;
margin: 8rpx 5rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx #888992;
font-weight: 500;
justify-content: center;
}
.u-carinput-hover {
background-color: rgb(185, 188, 195) !important;
}
.u-keyboard-back {
position: absolute;
width: 96rpx;
right: 22rpx;
bottom: 32rpx;
height: 80rpx;
background-color: rgb(185, 188, 195);
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
box-shadow: 0 2rpx 0rpx #888992;
}
.u-keyboard-change {
font-size: 24rpx;
box-shadow: 0 2rpx 0rpx #888992;
position: absolute;
width: 96rpx;
left: 22rpx;
line-height: 1;
bottom: 32rpx;
height: 80rpx;
background-color: #ffffff;
@include vue-flex;
align-items: center;
border-radius: 8rpx;
justify-content: center;
}
.u-keyboard-change .inactive.zh {
transform: scale(0.85) translateY(-10rpx);
}
.u-keyboard-change .inactive.en {
transform: scale(0.85) translateY(10rpx);
}
.u-keyboard-change .active {
color: rgb(237, 112, 64);
font-size: 30rpx;
}
.u-keyboard-change .zh {
transform: translateY(-10rpx);
}
.u-keyboard-change .en {
transform: translateY(10rpx);
}
</style>

View File

@@ -0,0 +1,300 @@
<template>
<view
class="u-card"
@tap.stop="click"
:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
:style="{
borderRadius: borderRadius + 'rpx',
margin: margin,
boxShadow: boxShadow
}"
>
<view
v-if="showHead"
class="u-card__head"
:style="[{padding: padding + 'rpx'}, headStyle]"
:class="{
'u-border-bottom': headBorderBottom
}"
@tap="headClick"
>
<view v-if="!$slots.head" class="u-flex u-row-between">
<view class="u-card__head--left u-flex u-line-1" v-if="title">
<image
:src="thumb"
class="u-card__head--left__thumb"
mode="aspectfull"
v-if="thumb"
:style="{
height: thumbWidth + 'rpx',
width: thumbWidth + 'rpx',
borderRadius: thumbCircle ? '100rpx' : '6rpx'
}"
></image>
<text
class="u-card__head--left__title u-line-1"
:style="{
fontSize: titleSize + 'rpx',
color: titleColor
}"
>
{{ title }}
</text>
</view>
<view class="u-card__head--right u-line-1" v-if="subTitle">
<text
class="u-card__head__title__text"
:style="{
fontSize: subTitleSize + 'rpx',
color: subTitleColor
}"
>
{{ subTitle }}
</text>
</view>
</view>
<slot name="head" v-else />
</view>
<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
<view
v-if="showFoot"
class="u-card__foot"
@tap="footClick"
:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
:class="{
'u-border-top': footBorderTop
}"
>
<slot name="foot" />
</view>
</view>
</template>
<script>
/**
* card 卡片
* @description 卡片组件一般用于多个列表条目,且风格统一的场景
* @tutorial https://www.uviewui.com/components/card.html
* @property {Boolean} full 卡片与屏幕两侧是否留空隙默认false
* @property {String} title 头部左边的标题
* @property {String} title-color 标题颜色(默认#303133
* @property {String | Number} title-size 标题字体大小单位rpx默认30
* @property {String} sub-title 头部右边的副标题
* @property {String} sub-title-color 副标题颜色(默认#909399
* @property {String | Number} sub-title-size 副标题字体大小默认26
* @property {Boolean} border 是否显示边框默认true
* @property {String | Number} index 用于标识点击了第几个卡片
* @property {String} box-shadow 卡片外围阴影字符串形式默认none
* @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"默认30rpx
* @property {String | Number} border-radius 卡片整体的圆角值单位rpx默认16
* @property {Object} head-style 头部自定义样式,对象形式
* @property {Object} body-style 中部自定义样式,对象形式
* @property {Object} foot-style 底部自定义样式,对象形式
* @property {Boolean} head-border-bottom 是否显示头部的下边框默认true
* @property {Boolean} foot-border-top 是否显示底部的上边框默认true
* @property {Boolean} show-head 是否显示头部默认true
* @property {Boolean} show-head 是否显示尾部默认true
* @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
* @property {String | Number} thumb-width 缩略图的宽度高等于宽单位rpx默认60
* @property {Boolean} thumb-circle 缩略图是否为圆形默认false
* @event {Function} click 整个卡片任意位置被点击时触发
* @event {Function} head-click 卡片头部被点击时触发
* @event {Function} body-click 卡片主体部分被点击时触发
* @event {Function} foot-click 卡片底部部分被点击时触发
* @example <u-card padding="30" title="card"></u-card>
*/
export default {
name: 'u-card',
emits: ["click", "head-click", "body-click", "foot-click"],
props: {
// 与屏幕两侧是否留空隙
full: {
type: Boolean,
default: false
},
// 标题
title: {
type: String,
default: ''
},
// 标题颜色
titleColor: {
type: String,
default: '#303133'
},
// 标题字体大小单位rpx
titleSize: {
type: [Number, String],
default: '30'
},
// 副标题
subTitle: {
type: String,
default: ''
},
// 副标题颜色
subTitleColor: {
type: String,
default: '#909399'
},
// 副标题字体大小单位rpx
subTitleSize: {
type: [Number, String],
default: '26'
},
// 是否显示外部边框只对full=false时有效(卡片与边框有空隙时)
border: {
type: Boolean,
default: true
},
// 用于标识点击了第几个
index: {
type: [Number, String, Object],
default: ''
},
// 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx""20rpx 20rpx 30rpx 30rpx"
margin: {
type: String,
default: '30rpx'
},
// card卡片的圆角
borderRadius: {
type: [Number, String],
default: '16'
},
// 头部自定义样式,对象形式
headStyle: {
type: Object,
default() {
return {};
}
},
// 主体自定义样式,对象形式
bodyStyle: {
type: Object,
default() {
return {};
}
},
// 底部自定义样式,对象形式
footStyle: {
type: Object,
default() {
return {};
}
},
// 头部是否下边框
headBorderBottom: {
type: Boolean,
default: true
},
// 底部是否有上边框
footBorderTop: {
type: Boolean,
default: true
},
// 标题左边的缩略图
thumb: {
type: String,
default: ''
},
// 缩略图宽高单位rpx
thumbWidth: {
type: [String, Number],
default: '60'
},
// 缩略图是否为圆形
thumbCircle: {
type: Boolean,
default: false
},
// 给headbodyfoot的内边距
padding: {
type: [String, Number],
default: '30'
},
// 是否显示头部
showHead: {
type: Boolean,
default: true
},
// 是否显示尾部
showFoot: {
type: Boolean,
default: true
},
// 卡片外围阴影,字符串形式
boxShadow: {
type: String,
default: 'none'
}
},
data() {
return {};
},
methods: {
click() {
this.$emit('click', this.index);
},
headClick() {
this.$emit('head-click', this.index);
},
bodyClick() {
this.$emit('body-click', this.index);
},
footClick() {
this.$emit('foot-click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-card {
position: relative;
overflow: hidden;
font-size: 28rpx;
background-color: #ffffff;
box-sizing: border-box;
&-full {
// 如果是与屏幕之间不留空隙应该设置左右边距为0
margin-left: 0 !important;
margin-right: 0 !important;
width: 100%;
}
&--border:after {
border-radius: 16rpx;
}
&__head {
&--left {
color: $u-main-color;
&__thumb {
margin-right: 16rpx;
}
&__title {
max-width: 400rpx;
}
}
&--right {
color: $u-tips-color;
margin-left: 6rpx;
}
}
&__body {
color: $u-content-color;
}
&__foot {
color: $u-tips-color;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<view class="u-cell-box">
<view class="u-cell-title" v-if="title" :style="[titleStyle]">
{{title}}
</view>
<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
<slot />
</view>
</view>
</template>
<script>
/**
* cellGroup 单元格父组件Group
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等。搭配u-cell-item
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 分组标题
* @property {Boolean} border 是否显示外边框默认true
* @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
* @example <u-cell-group title="设置喜好">
*/
export default {
name: "u-cell-group",
props: {
// 分组标题
title: {
type: String,
default: ''
},
// 是否显示分组list上下边框
border: {
type: Boolean,
default: true
},
// 分组标题的样式,对象形式,注意驼峰属性写法
// 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
titleStyle: {
type: Object,
default () {
return {};
}
}
},
data() {
return {
index: 0,
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell-box {
width: 100%;
}
.u-cell-title {
padding: 30rpx 32rpx 10rpx 32rpx;
font-size: 30rpx;
text-align: left;
color: $u-tips-color;
}
.u-cell-item-box {
background-color: #FFFFFF;
flex-direction: row;
}
</style>

View File

@@ -0,0 +1,317 @@
<template>
<view
@tap="click"
class="u-cell"
:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
hover-stay-time="150"
:hover-class="hoverClass"
:style="{
backgroundColor: bgColor
}"
>
<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
<view class="u-flex" v-else>
<slot name="icon"></slot>
</view>
<view
class="u-cell_title"
:style="[
{
width: titleWidth ? titleWidth + 'rpx' : 'auto'
},
titleStyle
]"
>
<block v-if="title !== ''">{{ title }}</block>
<slot name="title" v-else></slot>
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
<block v-if="label !== ''">{{ label }}</block>
<slot name="label" v-else></slot>
</view>
</view>
<view class="u-cell__value" :style="[valueStyle]">
<block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
<slot v-else></slot>
</view>
<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
<slot name="right-icon"></slot>
</view>
<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
</view>
</template>
<script>
/**
* cellItem 单元格Item
* @description cell单元格一般用于一组列表的情况比如个人中心页设置页等。搭配u-cell-group使用
* @tutorial https://www.uviewui.com/components/cell.html
* @property {String} title 左侧标题
* @property {String} icon 左侧图标名只支持uView内置图标见Icon 图标
* @property {Object} icon-style 左边图标的样式,对象形式
* @property {String} value 右侧内容
* @property {String} label 标题下方的描述信息
* @property {Boolean} border-bottom 是否显示cell的下边框默认true
* @property {Boolean} border-top 是否显示cell的上边框默认false
* @property {Boolean} center 是否使内容垂直居中默认false
* @property {String} hover-class 是否开启点击反馈none为无效果默认true
* // @property {Boolean} border-gap border-bottom为true时Cell列表中间的条目的下边框是否与左边有一个间隔默认true
* @property {Boolean} arrow 是否显示右侧箭头默认true
* @property {Boolean} required 箭头方向可选值默认right
* @property {Boolean} arrow-direction 是否显示左边表示必填的星号默认false
* @property {Object} title-style 标题样式,对象形式
* @property {Object} value-style 右侧内容样式,对象形式
* @property {Object} label-style 标题下方描述信息的样式,对象形式
* @property {String} bg-color 背景颜色默认transparent
* @property {String Number} index 用于在click事件回调中返回标识当前是第几个Item
* @property {String Number} title-width 标题的宽度单位rpx
* @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
*/
export default {
name: 'u-cell-item',
emits: ["click"],
props: {
// 左侧图标名称(只能uView内置图标)或者图标src
icon: {
type: String,
default: ''
},
// 左侧标题
title: {
type: [String, Number],
default: ''
},
// 右侧内容
value: {
type: [String, Number],
default: ''
},
// 标题下方的描述信息
label: {
type: [String, Number],
default: ''
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: true
},
// 是否显示上边框
borderTop: {
type: Boolean,
default: false
},
// 多个cell中中间的cell显示下划线时下划线是否给一个到左边的距离
// 1.4.0版本废除此参数默认边框由border-top和border-bottom提供此参数会造成干扰
// borderGap: {
// type: Boolean,
// default: true
// },
// 是否开启点击反馈即点击时cell背景为灰色none为无效果
hoverClass: {
type: String,
default: 'u-cell-hover'
},
// 是否显示右侧箭头
arrow: {
type: Boolean,
default: true
},
// 内容是否垂直居中
center: {
type: Boolean,
default: false
},
// 是否显示左边表示必填的星号
required: {
type: Boolean,
default: false
},
// 标题的宽度单位rpx
titleWidth: {
type: [Number, String],
default: ''
},
// 右侧箭头方向可选值right|up|down默认为right
arrowDirection: {
type: String,
default: 'right'
},
// 控制标题的样式
titleStyle: {
type: Object,
default() {
return {};
}
},
// 右侧显示内容的样式
valueStyle: {
type: Object,
default() {
return {};
}
},
// 描述信息的样式
labelStyle: {
type: Object,
default() {
return {};
}
},
// 背景颜色
bgColor: {
type: String,
default: 'transparent'
},
// 用于识别被点击的是第几个cell
index: {
type: [String, Number],
default: ''
},
// 是否使用lable插槽
useLabelSlot: {
type: Boolean,
default: false
},
// 左边图标的大小单位rpx只对传入icon字段时有效
iconSize: {
type: [Number, String],
default: 34
},
// 左边图标的样式,对象形式
iconStyle: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
};
},
computed: {
arrowStyle() {
let style = {};
if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
else style.transform = 'rotate(0deg)';
return style;
}
},
methods: {
click() {
this.$emit('click', this.index);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-cell {
@include vue-flex;
align-items: center;
position: relative;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
width: 100%;
padding: 26rpx 32rpx;
font-size: 28rpx;
line-height: 54rpx;
color: $u-content-color;
background-color: #fff;
text-align: left;
}
.u-cell_title {
font-size: 28rpx;
}
.u-cell__left-icon-wrap {
margin-right: 10rpx;
font-size: 32rpx;
}
.u-cell__right-icon-wrap {
margin-left: 10rpx;
color: #969799;
font-size: 28rpx;
}
.u-cell__left-icon-wrap,
.u-cell__right-icon-wrap {
@include vue-flex;
align-items: center;
height: 48rpx;
}
.u-cell-border:after {
position: absolute;
/* #ifndef APP-NVUE */
box-sizing: border-box;
content: ' ';
pointer-events: none;
border-bottom: 1px solid $u-border-color;
/* #endif */
right: 0;
left: 0;
top: 0;
transform: scaleY(0.5);
}
.u-cell-border {
position: relative;
}
.u-cell__label {
margin-top: 6rpx;
font-size: 26rpx;
line-height: 36rpx;
color: $u-tips-color;
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
}
.u-cell__value {
overflow: hidden;
text-align: right;
/* #ifndef APP-NVUE */
vertical-align: middle;
/* #endif */
color: $u-tips-color;
font-size: 26rpx;
}
.u-cell__title,
.u-cell__value {
flex: 1;
}
.u-cell--required {
/* #ifndef APP-NVUE */
overflow: visible;
/* #endif */
@include vue-flex;
align-items: center;
}
.u-cell--required:before {
position: absolute;
/* #ifndef APP-NVUE */
content: '*';
/* #endif */
left: 8px;
margin-top: 4rpx;
font-size: 14px;
color: $u-type-error;
}
.u-cell_right {
line-height: 1;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<view class="u-checkbox-group u-clearfix" :class="uFromData.inputAlign == 'right' ? 'flex-end' : ''"><slot></slot></view>
</template>
<script>
import Emitter from "../../libs/util/emitter.js";
/**
* checkboxGroup 开关选择器父组件Group
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} max 最多能选中多少个checkbox默认999
* @property {String Number} size 组件整体的大小单位rpx默认40
* @property {Boolean} disabled 是否禁用所有checkbox默认false
* @property {String Number} icon-size 图标大小单位rpx默认20
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
* @property {String} width 宽度,需带单位
* @property {String} width 宽度,需带单位
* @property {String} shape 外观形状shape-方形circle-圆形(默认circle)
* @property {Boolean} wrap 是否每个checkbox都换行默认false
* @property {String} active-color 选中时的颜色应用到所有子Checkbox组件默认#2979ff
* @event {Function} change 任一个checkbox状态发生变化时触发回调为一个对象
* @example <u-checkbox-group></u-checkbox-group>
*/
export default {
name: "u-checkbox-group",
emits: ["update:modelValue", "input", "change"],
mixins: [Emitter],
props: {
// 匹配某一个radio组件如果某个radio的name值等于此值那么这个radio就被会选中
value: {
type: [String, Number, Array, Boolean],
default: ""
},
modelValue: {
type: [String, Number, Array, Boolean],
default: ""
},
// 最多能选中多少个checkbox
max: {
type: [Number, String],
default: 999
},
// 所有选中项的 name
// value: {
// default: Array,
// default() {
// return []
// }
// },
// 是否禁用所有复选框
disabled: {
type: Boolean,
default: false
},
// 在表单内提交时的标识符
name: {
type: [Boolean, String],
default: ""
},
// 是否禁止点击提示语选中复选框
labelDisabled: {
type: Boolean,
default: false
},
// 形状square为方形circle为圆型
shape: {
type: String,
default: "square"
},
// 选中状态下的颜色
activeColor: {
type: String,
default: "#2979ff"
},
// 组件的整体大小
size: {
type: [String, Number],
default: 34
},
// 每个checkbox占u-checkbox-group的宽度
width: {
type: String,
default: "auto"
},
// 是否每个checkbox都换行
wrap: {
type: Boolean,
default: false
},
// 图标的大小单位rpx
iconSize: {
type: [String, Number],
default: 20
}
},
data() {
return {
values: [],
uFromData: {
inputAlign: "left"
}
};
},
created() {
// 如果将children定义在data中在微信小程序会造成循环引用而报错
this.children = [];
},
mounted() {
// 支付宝、头条小程序不支持provide/inject所以使用这个方法获取整个父组件在created定义避免循环应用
let parent = this.$u.$parent.call(this, "u-form");
if (parent) {
Object.keys(this.uFromData).map(key => {
this.uFromData[key] = parent[key];
});
}
},
methods: {
emitEvent(obj) {
let values = this.values || [];
if (obj.value) {
let index = values.indexOf(obj.name);
if (index === -1) {
values.push(obj.name);
}
} else {
let index = values.indexOf(obj.name);
if (index > -1) {
values.splice(index, 1);
}
}
this.$emit("change", values);
// 通过emit事件设置父组件通过v-model双向绑定的值
this.$emit("input", values);
this.$emit("update:modelValue", values);
// 发出事件用于在表单组件中嵌入checkbox的情况进行验证
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
setTimeout(() => {
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldChange", values);
}, 60);
},
_emitEvent(obj) {
let values = this.values || [];
if (obj.value) {
let index = values.indexOf(obj.name);
if (index === -1) {
values.push(obj.name);
}
} else {
let index = values.indexOf(obj.name);
if (index > -1) {
values.splice(index, 1);
}
}
//this.$emit("change", values);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox-group {
/* #ifndef MP || APP-NVUE */
display: inline-flex;
flex-wrap: wrap;
/* #endif */
}
.u-checkbox-group.flex-end {
/* #ifndef APP-NVUE */
display: inline-flex;
justify-content: flex-end;
flex-wrap: wrap;
/* #endif */
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<view class="u-checkbox" :style="[checkboxStyle]">
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
<u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor" />
</view>
<view
class="u-checkbox__label"
@tap="onClickLabel"
:style="{
fontSize: $u.addUnit(labelSize)
}"
>
<slot />
</view>
</view>
</template>
<script>
/**
* checkbox 复选框
* @description 该组件需要搭配checkboxGroup组件使用以便用户进行操作时获得当前复选框组的选中情况。
* @tutorial https://www.uviewui.com/components/checkbox.html
* @property {String Number} icon-size 图标大小单位rpx默认20
* @property {String Number} label-size label字体大小单位rpx默认28
* @property {String Number} name checkbox组件的标示符
* @property {String} shape 形状外观形状shape-方形circle-圆形(默认circle)
* @property {Boolean} disabled 是否禁用
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
* @property {String} active-color 选中时的颜色如设置CheckboxGroup的active-color将失效
* @event {Function} change 某个checkbox状态发生变化时触发回调为一个对象
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
*/
export default {
name: "u-checkbox",
emits: ["update:modelValue", "input", "change"],
props: {
// 是否为选中状态
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// checkbox的名称
name: {
type: [String, Number],
default: ""
},
// 形状square为方形circle为圆型
shape: {
type: String,
default: ""
},
// 是否禁用
disabled: {
type: [String, Boolean],
default: ""
},
// 是否禁止点击提示语选中复选框
labelDisabled: {
type: [String, Boolean],
default: ""
},
// 选中状态下的颜色如设置此值将会覆盖checkboxGroup的activeColor值
activeColor: {
type: String,
default: ""
},
// 图标的大小单位rpx
iconSize: {
type: [String, Number],
default: ""
},
// label的字体大小rpx单位
labelSize: {
type: [String, Number],
default: ""
},
// 组件的整体大小
size: {
type: [String, Number],
default: ""
}
},
data() {
return {
parentDisabled: false,
newParams: {}
};
},
created() {
// 支付宝小程序不支持provide/inject所以使用这个方法获取整个父组件在created定义避免循环应用
this.parent = this.$u.$parent.call(this, "u-checkbox-group");
// 如果存在u-checkbox-group将本组件的this塞进父组件的children中
this.parent && this.parent.children.push(this);
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 是否禁用如果父组件u-checkbox-group禁用的话将会忽略子组件的配置
isDisabled() {
return this.disabled !== "" ? this.disabled : this.parent ? this.parent.disabled : false;
},
// 是否禁用label点击
isLabelDisabled() {
return this.labelDisabled !== "" ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
},
// 组件尺寸对应size的值默认值为34rpx
checkboxSize() {
return this.size ? this.size : this.parent ? this.parent.size : 34;
},
// 组件的勾选图标的尺寸默认20
checkboxIconSize() {
return this.iconSize ? this.iconSize : this.parent ? this.parent.iconSize : 20;
},
// 组件选中激活时的颜色
elActiveColor() {
return this.activeColor ? this.activeColor : this.parent ? this.parent.activeColor : "primary";
},
// 组件的形状
elShape() {
return this.shape ? this.shape : this.parent ? this.parent.shape : "square";
},
iconStyle() {
let style = {};
// 既要判断是否手动禁用还要判断用户v-model绑定的值如果绑定为false那么也无法选中
if (this.elActiveColor && this.valueCom && !this.isDisabled) {
style.borderColor = this.elActiveColor;
style.backgroundColor = this.elActiveColor;
}
style.width = this.$u.addUnit(this.checkboxSize);
style.height = this.$u.addUnit(this.checkboxSize);
return style;
},
// checkbox内部的勾选图标如果选中状态为白色否则为透明色即可
iconColor() {
return this.valueCom ? "#ffffff" : "transparent";
},
iconClass() {
let classes = [];
classes.push("u-checkbox__icon-wrap--" + this.elShape);
if (this.valueCom == true) classes.push("u-checkbox__icon-wrap--checked");
if (this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled");
if (this.valueCom && this.isDisabled) classes.push("u-checkbox__icon-wrap--disabled--checked");
// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
return classes.join(" ");
},
checkboxStyle() {
let style = {};
if (this.parent && this.parent.width) {
style.width = this.parent.width;
// #ifdef MP
// 各家小程序因为它们特殊的编译结构使用float布局
style.float = "left";
// #endif
// #ifndef MP
// H5和APP使用flex布局
style.flex = `0 0 ${this.parent.width}`;
// #endif
}
if (this.parent && this.parent.wrap) {
style.width = "100%";
// #ifndef MP
// H5和APP使用flex布局将宽度设置100%,即可自动换行
style.flex = "0 0 100%";
// #endif
}
return style;
}
},
mounted() {
this._emitEvent();
},
watch: {
valueCom: {
handler: function(newVal, oldVal) {
this._emitEvent();
}
}
},
methods: {
_emitEvent() {
let value = this.valueCom;
let obj = {
value,
name: this.name
};
// 执行父组件u-checkbox-group的事件方法
if (this.parent && this.parent.emitEvent) this.parent._emitEvent(obj);
},
onClickLabel() {
if (!this.isLabelDisabled && !this.isDisabled) {
this.setValue();
}
},
toggle() {
if (!this.isDisabled) {
this.setValue();
}
},
emitEvent() {
let obj = {
value: !this.valueCom,
name: this.name
};
this.$emit("change", obj);
// 执行父组件u-checkbox-group的事件方法
if (this.parent && this.parent.emitEvent) this.parent.emitEvent(obj);
},
// 设置input的值这里通过input事件设置通过v-model绑定的组件的值
setValue() {
let value = this.valueCom;
// 判断是否超过了可选的最大数量
let checkedNum = 0;
if (this.parent && this.parent.children) {
// 只要父组件的某一个子元素的value为true就加1(已有的选中数量)
this.parent.children.map(val => {
if (val.value) checkedNum++;
});
}
// 如果原来为选中状态,那么可以取消
if (value == true) {
this.emitEvent();
this.$emit("input", !value);
this.$emit("update:modelValue", !value);
} else {
// 如果超出最多可选项,提示
if (this.parent && checkedNum >= this.parent.max) {
return this.$u.toast(`最多可选${this.parent.max}`);
}
// 如果原来为未选中状态需要选中的数量少于父组件中设置的max值才可以选中
this.emitEvent();
this.$emit("input", !value);
this.$emit("update:modelValue", !value);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-checkbox {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
overflow: hidden;
user-select: none;
line-height: 1.8;
&__icon-wrap {
color: $u-content-color;
flex: none;
display: -webkit-flex;
@include vue-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 42rpx;
height: 42rpx;
color: transparent;
text-align: center;
transition-property: color, border-color, background-color;
font-size: 20px;
border: 1px solid #c8c9cc;
transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题需要设置行高为0否则图标偏下
&__icon {
line-height: 0;
}
/* #endif */
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 6rpx;
}
&--checked {
color: #fff;
background-color: $u-type-primary;
border-color: $u-type-primary;
}
&--disabled {
background-color: #ebedf0;
border-color: #c8c9cc;
}
&--disabled--checked {
color: #c8c9cc !important;
}
}
&__label {
word-wrap: break-word;
margin-left: 10rpx;
margin-right: 24rpx;
color: $u-content-color;
font-size: 30rpx;
&--disabled {
color: #c8c9cc;
}
}
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<view
class="u-circle-progress"
:style="{
width: widthPx + 'px',
height: widthPx + 'px',
backgroundColor: bgColor
}"
>
<!-- 支付宝小程序不支持canvas-id属性必须用id属性 -->
<canvas
class="u-canvas-bg"
:canvas-id="elBgId"
:id="elBgId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<canvas
class="u-canvas"
:canvas-id="elId"
:id="elId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<slot></slot>
</view>
</template>
<script>
/**
* circleProgress 环形进度条
* @description 展示操作或任务的当前进度比如上传文件是一个圆形的进度条。注意此组件的percent值只能动态增加不能动态减少。
* @tutorial https://www.uviewui.com/components/circleProgress.html
* @property {String Number} percent 圆环进度百分比值为数值类型0-100
* @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b
* @property {String Number} width 整个圆环组件的宽度高度默认等于宽度值单位rpx默认200
* @property {String Number} border-width 圆环的边框宽度单位rpx默认14
* @property {String Number} duration 整个圆环执行一圈的时间单位ms默认呢1500
* @property {String} type 如设置active-color值将会失效
* @property {String} bg-color 整个组件背景颜色,默认为白色
* @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
*/
export default {
name: 'u-circle-progress',
props: {
// 圆环进度百分比值
percent: {
type: Number,
default: 0,
// 限制值在0到100之间
validator: val => {
return val >= 0 && val <= 100;
}
},
// 底部圆环的颜色(灰色的圆环)
inactiveColor: {
type: String,
default: '#ececec'
},
// 圆环激活部分的颜色
activeColor: {
type: String,
default: '#19be6b'
},
// 圆环线条的宽度单位rpx
borderWidth: {
type: [Number, String],
default: 14
},
// 整个圆形的宽度单位rpx
width: {
type: [Number, String],
default: 200
},
// 整个圆环执行一圈的时间单位ms
duration: {
type: [Number, String],
default: 1500
},
// 主题类型
type: {
type: String,
default: ''
},
// 整个圆环进度区域的背景色
bgColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
// #ifdef MP-WEIXIN
elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值否则会报错
elId: 'uCircleProgressElId',
// #endif
// #ifndef MP-WEIXIN
elBgId: this.$u.guid(), // 非微信端的时候需用动态的id否则一个页面多个圆形进度条组件数据会混乱
elId: this.$u.guid(),
// #endif
widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
startAngle: -Math.PI / 2, // canvas画圆的起始角度默认为3点钟方向定位到12点钟方向
progressContext: null, // 活动圆的canvas上下文
newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
};
},
watch: {
percent(nVal, oVal = 0) {
if (nVal > 100) nVal = 100;
if (nVal < 0) oVal = 0;
// 此值其实等于this.percent命名一个新
this.newPercent = nVal;
this.oldPercent = oVal;
setTimeout(() => {
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
// 将此值减少或者新增到新的百分比值
this.drawCircleByProgress(oVal);
}, 50);
}
},
created() {
// 赋值,用于加载后第一个画圆使用
this.newPercent = this.percent;
this.oldPercent = 0;
},
computed: {
// 有type主题时优先起作用
circleColor() {
if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
else return this.activeColor;
}
},
mounted() {
// 在h5端必须要做一点延时才起作用this.$nextTick()无效(HX2.4.7)
setTimeout(() => {
this.drawProgressBg();
this.drawCircleByProgress(this.oldPercent);
}, 50);
},
methods: {
drawProgressBg() {
let ctx = uni.createCanvasContext(this.elBgId, this);
ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
ctx.beginPath(); // 开始描绘路径
// 设置一个原点(110,110)半径为100的圆的路径到当前路径
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
ctx.stroke(); // 对路径进行描绘
ctx.draw();
},
drawCircleByProgress(progress) {
// 第一次操作进度环时将上下文保存到了this.data中直接使用即可
let ctx = this.progressContext;
if (!ctx) {
ctx = uni.createCanvasContext(this.elId, this);
this.progressContext = ctx;
}
// 表示进度的两端为圆形
ctx.setLineCap('round');
// 设置线条的宽度和颜色
ctx.setLineWidth(this.borderWidthPx);
ctx.setStrokeStyle(this.circleColor);
// 将总过渡时间除以100得出每修改百分之一进度所需的时间
let time = Math.floor(this.duration / 100);
// 结束角的计算依据为将2π分为100份乘以当前的进度值得出终止点的弧度值加起始角为整个圆从默认的
// 3点钟方向开始画图转为更好理解的12点钟方向开始作图这需要起始角和终止角同时加上this.startAngle值
let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
ctx.beginPath();
// 半径为整个canvas宽度的一半
let radius = this.widthPx / 2;
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
ctx.stroke();
ctx.draw();
// 如果变更后新值大于旧值,意味着增大了百分比
if (this.newPercent > this.oldPercent) {
// 每次递增百分之一
progress++;
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
if (progress > this.newPercent) return;
} else {
// 同理于上面
progress--;
if (progress < this.newPercent) return;
}
setTimeout(() => {
// 定时器每次操作间隔为time值为了让进度条有动画效果
this.drawCircleByProgress(progress);
}, time);
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-circle-progress {
position: relative;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
}
.u-canvas-bg {
position: absolute;
}
.u-canvas {
position: absolute;
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<view class="u-col" :class="[
'u-col-' + span
]" :style="{
padding: `0 ${Number(gutter)/2 + 'rpx'}`,
marginLeft: 100 / 12 * offset + '%',
flex: `0 0 ${100 / 12 * span}%`,
alignItems: uAlignItem,
justifyContent: uJustify,
textAlign: textAlign
}"
@tap="click">
<slot></slot>
</view>
</template>
<script>
/**
* col 布局单元格
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
* @tutorial https://www.uviewui.com/components/layout.html
* @property {String Number} span 栅格占据的列数总12等分默认0
* @property {String} text-align 文字水平对齐方式默认left
* @property {String Number} offset 分栏左边偏移计算方式与span相同默认0
* @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
*/
export default {
name: "u-col",
props: {
// 占父容器宽度的多少等分总分为12份
span: {
type: [Number, String],
default: 12
},
// 指定栅格左侧的间隔数(总12栏)
offset: {
type: [Number, String],
default: 0
},
// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
justify: {
type: String,
default: 'start'
},
// 垂直对齐方式可选值为top、center、bottom
align: {
type: String,
default: 'center'
},
// 文字对齐方式
textAlign: {
type: String,
default: 'left'
},
// 是否阻止事件传播
stop: {
type: Boolean,
default: true
}
},
data() {
return {
gutter: 20, // 给col添加间距左右边距各占一半从父组件u-row获取
}
},
created() {
this.parent = false;
},
mounted() {
// 获取父组件实例,并赋值给对应的参数
this.parent = this.$u.$parent.call(this, 'u-row');
if (this.parent) {
this.gutter = this.parent.gutter;
}
},
computed: {
uJustify() {
if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
else return this.justify;
},
uAlignItem() {
if (this.align == 'top') return 'flex-start';
if (this.align == 'bottom') return 'flex-end';
else return this.align;
}
},
methods: {
click(e) {
this.$emit('click');
}
}
}
</script>
<style lang="scss">
@import "../../libs/css/style.components.scss";
.u-col {
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
float: left;
/* #endif */
}
.u-col-0 {
width: 0;
}
.u-col-1 {
width: calc(100%/12);
}
.u-col-2 {
width: calc(100%/12 * 2);
}
.u-col-3 {
width: calc(100%/12 * 3);
}
.u-col-4 {
width: calc(100%/12 * 4);
}
.u-col-5 {
width: calc(100%/12 * 5);
}
.u-col-6 {
width: calc(100%/12 * 6);
}
.u-col-7 {
width: calc(100%/12 * 7);
}
.u-col-8 {
width: calc(100%/12 * 8);
}
.u-col-9 {
width: calc(100%/12 * 9);
}
.u-col-10 {
width: calc(100%/12 * 10);
}
.u-col-11 {
width: calc(100%/12 * 11);
}
.u-col-12 {
width: calc(100%/12 * 12);
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<view class="u-collapse-item" :style="[itemStyle]">
<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
<block v-if="!$slots['title-all']">
<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
isShow && activeStyle && !arrow ? activeStyle : '']">
{{ title }}
</view>
<slot v-else name="title" />
<view class="u-icon-wrap">
<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
class="u-arrow-down-icon" name="arrow-down"></u-icon>
</view>
</block>
<slot v-else name="title-all" />
</view>
<view class="u-collapse-body" :style="[{
height: isShow ? height + 'px' : '0'
}]">
<view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
/**
* collapseItem 手风琴Item
* @description 通过折叠面板收纳内容区域搭配u-collapse使用
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {String} title 面板标题
* @property {String Number} index 主要用于事件的回调标识那个Item被点击
* @property {Boolean} disabled 面板是否可以打开或收起默认false
* @property {Boolean} open 设置某个面板的初始状态是否打开默认false
* @property {String Number} name 唯一标识符如不设置默认用当前collapse-item的索引值
* @property {String} align 标题的对齐方式默认left
* @property {Object} active-style 不显示箭头时可以添加当前选择的collapse-item活动样式对象形式
* @event {Function} change 某个item被打开或者收起时触发
* @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
*/
export default {
name: "u-collapse-item",
emits: ["change"],
props: {
// 标题
title: {
type: String,
default: ''
},
// 标题的对齐方式
align: {
type: String,
default: 'left'
},
// 是否可以点击收起
disabled: {
type: Boolean,
default: false
},
// collapse显示与否
open: {
type: Boolean,
default: false
},
// 唯一标识符
name: {
type: [Number, String],
default: ''
},
//活动样式
activeStyle: {
type: Object,
default () {
return {}
}
},
// 标识当前为第几个
index: {
type: [String, Number],
default: ''
}
},
data() {
return {
isShow: false,
elId: this.$u.guid(),
height: 0, // body内容的高度
headStyle: {}, // 头部样式,对象形式
bodyStyle: {}, // 主体部分样式
itemStyle: {}, // 每个item的整体样式
arrowColor: '', // 箭头的颜色
hoverClass: '', // 头部按下时的效果样式类
arrow: true, // 是否显示右侧箭头
};
},
watch: {
open(val) {
this.isShow = val;
}
},
created() {
this.parent = false;
// 获取u-collapse的信息放在u-collapse是为了方便不用每个u-collapse-item写一遍
this.isShow = this.open;
},
methods: {
// 异步获取内容,或者动态修改了内容时,需要重新初始化
init() {
this.parent = this.$u.$parent.call(this, 'u-collapse');
if(this.parent) {
this.nameSync = this.name ? this.name : this.parent.childrens.length;
this.parent.childrens.push(this);
this.headStyle = this.parent.headStyle;
this.bodyStyle = this.parent.bodyStyle;
this.arrowColor = this.parent.arrowColor;
this.hoverClass = this.parent.hoverClass;
this.arrow = this.parent.arrow;
this.itemStyle = this.parent.itemStyle;
}
this.$nextTick(() => {
this.queryRect();
});
},
// 点击collapsehead头部
headClick() {
if (this.disabled) return;
if (this.parent && this.parent.accordion == true) {
this.parent.childrens.map(val => {
// 自身不设置为false因为后面有this.isShow = !this.isShow;处理了
if (this != val) {
val.isShow = false;
}
});
}
this.isShow = !this.isShow;
// 触发本组件的事件
this.$emit('change', {
index: this.index,
show: this.isShow
})
// 只有在打开时才发出事件
if (this.isShow) this.parent && this.parent.onChange();
this.$forceUpdate();
},
// 查询内容高度
queryRect() {
// $uGetRect为uView自带的节点查询简化方法详见文档介绍https://www.uviewui.com/js/getRect.html
// 组件内部一般用this.$uGetRect对外的为this.$u.getRect二者功能一致名称不同
this.$uGetRect('#' + this.elId).then(res => {
this.height = res.height;
})
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-collapse-head {
position: relative;
@include vue-flex;
justify-content: space-between;
align-items: center;
color: $u-main-color;
font-size: 30rpx;
line-height: 1;
padding: 24rpx 0;
text-align: left;
}
.u-collapse-title {
flex: 1;
overflow: hidden;
}
.u-arrow-down-icon {
transition: all 0.3s;
margin-right: 20rpx;
margin-left: 14rpx;
}
.u-arrow-down-icon-active {
transform: rotate(180deg);
transform-origin: center center;
}
.u-collapse-body {
overflow: hidden;
transition: all 0.3s;
}
.u-collapse-content {
overflow: hidden;
font-size: 28rpx;
color: $u-tips-color;
text-align: left;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view class="u-collapse">
<slot />
</view>
</template>
<script>
/**
* collapse 手风琴
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uviewui.com/components/collapse.html
* @property {Boolean} accordion 是否手风琴模式默认true
* @property {Boolean} arrow 是否显示标题右侧的箭头默认true
* @property {String} arrow-color 标题右侧箭头的颜色(默认#909399
* @property {Object} head-style 标题自定义样式,对象形式
* @property {Object} body-style 主体自定义样式,对象形式
* @property {String} hover-class 样式类名按下时有效默认u-hover-class
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式参数activeNames类型为String否则为Array)
* @example <u-collapse></u-collapse>
*/
export default {
name:"u-collapse",
emits: ["change"],
props: {
// 是否手风琴模式
accordion: {
type: Boolean,
default: true
},
// 头部的样式
headStyle: {
type: Object,
default () {
return {}
}
},
// 主体的样式
bodyStyle: {
type: Object,
default () {
return {}
}
},
// 每一个item的样式
itemStyle: {
type: Object,
default () {
return {}
}
},
// 是否显示右侧的箭头
arrow: {
type: Boolean,
default: true
},
// 箭头的颜色
arrowColor: {
type: String,
default: '#909399'
},
// 标题部分按压时的样式类,"none"为无效果
hoverClass: {
type: String,
default: 'u-hover-class'
}
},
created() {
this.childrens = []
},
data() {
return {
}
},
methods: {
// 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
init() {
this.childrens.forEach((vm, index) => {
vm.init();
})
},
// collapse item被点击由collapse item调用父组件方法
onChange() {
let activeItem = [];
this.childrens.forEach((vm, index) => {
if (vm.isShow) {
activeItem.push(vm.nameSync);
}
})
// 如果是手风琴模式只有一个匹配结果也即activeItem长度为1将其转为字符串
if (this.accordion) activeItem = activeItem.join('');
this.$emit('change', activeItem);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
</style>

View File

@@ -0,0 +1,238 @@
<template>
<view
class="u-notice-bar"
:style="{
background: computeBgColor,
padding: padding
}"
:class="[
type ? `u-type-${type}-light-bg` : ''
]"
>
<view class="u-icon-wrap">
<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
</view>
<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
<view
class="u-news-item u-line-1"
:style="[textStyle]"
@tap="click(index)"
:class="['u-type-' + type]"
>
{{ item }}
</view>
</swiper-item>
</swiper>
<view class="u-icon-wrap">
<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
</view>
</view>
</template>
<script>
export default {
emits: ["close", "getMore", "end"],
props: {
// 显示的内容,数组
list: {
type: Array,
default() {
return [];
}
},
// 显示的主题success|error|primary|info|warning
type: {
type: String,
default: 'warning'
},
// 是否显示左侧的音量图标
volumeIcon: {
type: Boolean,
default: true
},
// 是否显示右侧的右箭头图标
moreIcon: {
type: Boolean,
default: false
},
// 是否显示右侧的关闭图标
closeIcon: {
type: Boolean,
default: false
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 文字颜色,各图标也会使用文字颜色
color: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 滚动方向row-水平滚动column-垂直滚动
direction: {
type: String,
default: 'row'
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 字体大小单位rpx
fontSize: {
type: [Number, String],
default: 26
},
// 滚动一个周期的时间长单位ms
duration: {
type: [Number, String],
default: 2000
},
// 音量喇叭的大小
volumeSize: {
type: [Number, String],
default: 34
},
// 水平滚动时的滚动速度即每秒滚动多少rpx这有利于控制文字无论多少时都能有一个恒定的速度
speed: {
type: Number,
default: 160
},
// 水平滚动时,是否采用衔接形式滚动
isCircular: {
type: Boolean,
default: true
},
// 滚动方向horizontal-水平滚动vertical-垂直滚动
mode: {
type: String,
default: 'horizontal'
},
// 播放状态play-播放paused-暂停
playState: {
type: String,
default: 'play'
},
// 是否禁止用手滑动切换
// 目前HX2.6.11只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
disableTouch: {
type: Boolean,
default: true
},
// 通知的边距
padding: {
type: [Number, String],
default: '18rpx 24rpx'
}
},
computed: {
// 计算字体颜色如果没有自定义的就用uview主题颜色
computeColor() {
if (this.color) return this.color;
// 如果是无主题就默认使用content-color
else if(this.type == 'none') return '#606266';
else return this.type;
},
// 文字内容的样式
textStyle() {
let style = {};
if (this.color) style.color = this.color;
else if(this.type == 'none') style.color = '#606266';
style.fontSize = this.fontSize + 'rpx';
return style;
},
// 垂直或者水平滚动
vertical() {
if(this.mode == 'horizontal') return false;
else return true;
},
// 计算背景颜色
computeBgColor() {
if (this.bgColor) return this.bgColor;
else if(this.type == 'none') return 'transparent';
}
},
data() {
return {
// animation: false
};
},
methods: {
// 点击通告栏
click(index) {
this.$emit('click', index);
},
// 点击关闭按钮
close() {
this.$emit('close');
},
// 点击更多箭头按钮
getMore() {
this.$emit('getMore');
},
change(e) {
let index = e.detail.current;
if(index == this.list.length - 1) {
this.$emit('end');
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar {
width: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
padding: 18rpx 24rpx;
overflow: hidden;
}
.u-swiper {
font-size: 26rpx;
height: 32rpx;
@include vue-flex;
align-items: center;
flex: 1;
margin-left: 12rpx;
}
.u-swiper-item {
@include vue-flex;
align-items: center;
overflow: hidden;
}
.u-news-item {
overflow: hidden;
}
.u-right-icon {
margin-left: 12rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
.u-left-icon {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<view class="u-count-down">
<slot>
<text class="u-count-down__text" :style="customStyle">{{ formattedTime }}</text>
</slot>
</view>
</template>
<script>
import { isSameSecond, parseFormat, parseTimeData } from "./utils";
/**
* u-count-down 倒计时
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
* @tutorial https://uviewui.com/components/countDown.html
* @property {String | Number} timestamp 倒计时时长单位ms (默认 0
* @property {String} format 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒 (默认 'HH:mm:ss'
* @property {Boolean} autoStart 是否自动开始倒计时 (默认 true
* @event {Function} end 倒计时结束时触发
* @event {Function} change 倒计时变化时触发
* @event {Function} start 开始倒计时
* @event {Function} pause 暂停倒计时
* @event {Function} reset 重设倒计时,若 auto-start 为 true重设后会自动开始倒计时
* @example <u-count-down :timestamp="timestamp"></u-count-down>
*/
export default {
name: "u-count-down",
emits: ["change", "end", "finish"],
props: {
// 倒计时时长单位ms
timestamp: {
type: [String, Number],
default: 0
},
// 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒
format: {
type: String,
default: "DD:HH:mm:ss"
},
// 是否自动开始倒计时
autoStart: {
type: Boolean,
default: true
},
customStyle: {
type: [String, Object],
default: ""
}
},
data() {
return {
timer: null,
// 各单位(天,时,分等)剩余时间
timeData: parseTimeData(0),
// 格式化后的时间,如"03:23:21"
formattedTime: "0",
// 倒计时是否正在进行中
runing: false,
endTime: 0, // 结束的毫秒时间戳
remainTime: 0 // 剩余的毫秒时间
};
},
watch: {
timestamp(n) {
this.reset();
},
format(newVal, oldVal) {
this.pause();
this.start();
}
},
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.format.indexOf("SSS") > -1) {
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();
}
}, 30);
},
// 获取剩余的时间
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("end");
this.$emit("finish");
}
},
// 重置倒计时
reset() {
this.pause();
this.remainTime = this.timestamp;
this.setRemainTime(this.remainTime);
if (this.autoStart) {
this.start();
}
},
// 暂停倒计时
pause() {
this.runing = false;
this.clearTimeout();
},
// 清空定时器
clearTimeout() {
clearTimeout(this.timer);
this.timer = null;
}
},
// #ifndef VUE3
beforeDestroy() {
this.clearTimeout();
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.clearTimeout();
},
// #endif
};
</script>
<style lang="scss"></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,266 @@
<template>
<view
class="u-count-num"
:style="{
fontSize: fontSize + 'rpx',
fontWeight: bold ? 'bold' : 'normal',
color: color
}"
>
{{ displayValueCom }}
</view>
</template>
<script>
/**
* countTo 数字滚动
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
* @tutorial https://www.uviewui.com/components/countTo.html
* @property {String Number} nullVal 空值或NaN时显示的值默认 -
* @property {String Number} start-val 开始值
* @property {String Number} end-val 结束值
* @property {String Number} duration 滚动过程所需的时间单位ms默认2000
* @property {Boolean} autoplay 是否自动开始滚动默认true
* @property {String Number} decimals 要显示的小数位数见官网说明默认0
* @property {Boolean} use-easing 滚动结束时是否缓动结尾见官网说明默认true
* @property {String} separator 千位分隔符,见官网说明
* @property {String} color 字体颜色(默认#303133
* @property {String Number} font-size 字体大小单位rpx默认50
* @property {Boolean} bold 字体是否加粗默认false
* @event {Function} end 数值滚动到目标值时触发
* @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
*/
export default {
name: "u-count-to",
emits: ["end"],
props: {
// 没有值时显示
nullVal: {
type: [Number, String],
default: "-"
},
// 开始的数值默认从0增长到某一个数
startVal: {
type: [Number, String],
default: 0
},
// 要滚动的目标数值,必须
endVal: {
type: [Number, String],
default: 0,
required: true
},
// 滚动到目标数值的动画持续时间单位为毫秒ms
duration: {
type: [Number, String],
default: 2000
},
// 设置数值后是否自动开始滚动
autoplay: {
type: Boolean,
default: true
},
// 要显示的小数位数
decimals: {
type: [Number, String],
default: 0
},
// 是否在即将到达目标数值的时候,使用缓慢滚动的效果
useEasing: {
type: Boolean,
default: true
},
// 十进制分割
decimal: {
type: [Number, String],
default: "."
},
// 字体颜色
color: {
type: String,
default: "#303133"
},
// 字体大小
fontSize: {
type: [Number, String],
default: 50
},
// 是否加粗字体
bold: {
type: Boolean,
default: false
},
// 千位分隔符,类似金额的分割(¥23,321.05中的",")
separator: {
type: String,
default: ""
}
},
data() {
return {
localStartVal: this.startVal,
displayValue: this.formatNumber(this.startVal),
printVal: null,
paused: false, // 是否暂停
localDuration: Number(this.duration),
startTime: null, // 开始的时间
timestamp: null, // 时间戳
remaining: null, // 停留的时间
rAF: null,
lastTime: 0 // 上一次的时间
};
},
computed: {
countDown() {
return this.startVal > this.endVal;
},
displayValueCom() {
let str;
let { displayValue, nullVal } = this;
if (isNaN(displayValue)) {
str = nullVal;
} else {
str = displayValue;
}
return str;
}
},
watch: {
startVal() {
this.autoplay && this.start();
},
endVal() {
this.autoplay && this.start();
}
},
mounted() {
this.autoplay && this.start();
},
methods: {
easingFn(t, b, c, d) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
},
requestAnimationFrame(callback) {
const currTime = new Date().getTime();
// 为了使setTimteout的尽可能的接近每秒60帧的效果
const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
const id = setTimeout(() => {
callback(currTime + timeToCall);
}, timeToCall);
this.lastTime = currTime + timeToCall;
return id;
},
cancelAnimationFrame(id) {
clearTimeout(id);
},
// 开始滚动数字
start() {
this.localStartVal = this.startVal;
this.startTime = null;
this.localDuration = this.duration;
this.paused = false;
this.rAF = this.requestAnimationFrame(this.count);
},
// 暂定状态,重新再开始滚动;或者滚动状态下,暂停
reStart() {
if (this.paused) {
this.resume();
this.paused = false;
} else {
this.stop();
this.paused = true;
}
},
// 暂停
stop() {
this.cancelAnimationFrame(this.rAF);
},
// 重新开始(暂停的情况下)
resume() {
this.startTime = null;
this.localDuration = this.remaining;
this.localStartVal = this.printVal;
this.requestAnimationFrame(this.count);
},
// 重置
reset() {
this.startTime = null;
this.cancelAnimationFrame(this.rAF);
this.displayValue = this.formatNumber(this.startVal);
},
count(timestamp) {
if (!this.startTime) this.startTime = timestamp;
this.timestamp = timestamp;
const progress = timestamp - this.startTime;
this.remaining = this.localDuration - progress;
if (this.useEasing) {
if (this.countDown) {
this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
} else {
this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
}
} else {
if (this.countDown) {
this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
} else {
this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
}
}
if (this.countDown) {
this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
} else {
this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
}
this.displayValue = this.formatNumber(this.printVal);
if (progress < this.localDuration) {
this.rAF = this.requestAnimationFrame(this.count);
} else {
this.$emit("end");
}
},
// 判断是否数字
isNumber(val) {
return !isNaN(parseFloat(val));
},
formatNumber(num) {
// 将num转为Number类型因为其值可能为字符串数值调用toFixed会报错
num = Number(num);
num = num.toFixed(Number(this.decimals));
num += "";
const x = num.split(".");
let x1 = x[0];
const x2 = x.length > 1 ? this.decimal + x[1] : "";
const rgx = /(\d+)(\d{3})/;
if (this.separator && !this.isNumber(this.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, "$1" + this.separator + "$2");
}
}
return x1 + x2;
},
// #ifndef VUE3
destroyed() {
this.cancelAnimationFrame(this.rAF);
},
// #endif
// #ifdef VUE3
unmounted() {
this.cancelAnimationFrame(this.rAF);
}
// #endif
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<view class="u-divider" :style="{
height: height == 'auto' ? 'auto' : height + 'rpx',
backgroundColor: bgColor,
marginBottom: marginBottom + 'rpx',
marginTop: marginTop + 'rpx'
}" @tap="click">
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
<view v-if="useSlot" class="u-divider-text" :style="{
color: color,
fontSize: fontSize + 'rpx'
}"><slot /></view>
<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
</view>
</template>
<script>
/**
* divider 分割线
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
* @tutorial https://www.uviewui.com/components/divider.html
* @property {String Number} half-width 文字左或右边线条宽度数值或百分比数值时单位为rpx
* @property {String} border-color 线条颜色优先级高于type默认#dcdfe6
* @property {String} color 文字颜色(默认#909399
* @property {String Number} fontSize 字体大小单位rpx默认26
* @property {String} bg-color 整个divider的背景颜色默认呢#ffffff
* @property {String Number} height 整个divider的高度单位rpx默认40
* @property {String} type 将线条设置主题色默认primary
* @property {Boolean} useSlot 是否使用slot传入内容如果不传入中间不会有空隙默认true
* @property {String Number} margin-top 与前一个组件的距离单位rpx默认0
* @property {String Number} margin-bottom 与后一个组件的距离单位rpx0
* @event {Function} click divider组件被点击时触发
* @example <u-divider color="#fa3534">长河落日圆</u-divider>
*/
export default {
name: 'u-divider',
props: {
// 单一边divider横线的宽度(数值)单位rpx。或者百分比
halfWidth: {
type: [Number, String],
default: 150
},
// divider横线的颜色如设置
borderColor: {
type: String,
default: '#dcdfe6'
},
// 主题色可以是primary|info|success|warning|error之一值
type: {
type: String,
default: 'primary'
},
// 文字颜色
color: {
type: String,
default: '#909399'
},
// 文字大小单位rpx
fontSize: {
type: [Number, String],
default: 26
},
// 整个divider的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 整个divider的高度单位rpx
height: {
type: [Number, String],
default: 'auto'
},
// 上边距
marginTop: {
type: [String, Number],
default: 0
},
// 下边距
marginBottom: {
type: [String, Number],
default: 0
},
// 是否使用slot传入内容如果不用slot传入内容先的中间就不会有空隙
useSlot: {
type: Boolean,
default: true
}
},
computed: {
lineStyle() {
let style = {};
if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
else style.width = this.halfWidth + 'rpx';
// borderColor优先级高于type值
if(this.borderColor) style.borderColor = this.borderColor;
return style;
}
},
methods: {
click() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-divider {
width: 100%;
position: relative;
text-align: center;
@include vue-flex;
justify-content: center;
align-items: center;
overflow: hidden;
flex-direction: row;
}
.u-divider-line {
border-bottom: 1px solid $u-border-color;
transform: scale(1, 0.5);
transform-origin: center;
&--bordercolor--primary {
border-color: $u-type-primary;
}
&--bordercolor--success {
border-color: $u-type-success;
}
&--bordercolor--error {
border-color: $u-type-primary;
}
&--bordercolor--info {
border-color: $u-type-info;
}
&--bordercolor--warning {
border-color: $u-type-warning;
}
}
.u-divider-text {
white-space: nowrap;
padding: 0 16rpx;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<view
class="u-dropdown-item"
v-if="active"
@touchmove.stop.prevent="() => {}"
@tap.stop.prevent="() => {}"
>
<block v-if="!$slots.default && !$slots.$default">
<scroll-view
scroll-y="true"
:style="{
height: $u.addUnit(height)
}"
>
<view class="u-dropdown-item__options">
<u-cell-group>
<u-cell-item
@click="cellClick(item.value)"
:arrow="false"
:title="item.label"
v-for="(item, index) in options"
:key="index"
:title-style="{
color: value === item.value ? activeColor : inactiveColor
}"
>
<u-icon
v-if="valueCom === item.value"
name="checkbox-mark"
:color="activeColor"
size="32"
></u-icon>
</u-cell-item>
</u-cell-group>
</view>
</scroll-view>
</block>
<slot v-else />
</view>
</template>
<script>
/**
* dropdown-item 下拉菜单
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String | Number} v-model 双向绑定选项卡选择值
* @property {String} title 菜单项标题
* @property {Array[Object]} options 选项数据如果传入了默认slot此参数无效
* @property {Boolean} disabled 是否禁用此选项卡默认false
* @property {String | Number} duration 选项卡展开和收起的过渡时间单位ms默认300
* @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)默认auto
* @example <u-dropdown-item title="标题"></u-dropdown-item>
*/
export default {
name: "u-dropdown-item",
emits: ["update:modelValue", "input", "change"],
props: {
// 当前选中项的value值
value: {
type: [Number, String, Array],
default: ""
},
modelValue: {
type: [Number, String, Array],
default: ""
},
// 菜单项标题
title: {
type: [String, Number],
default: ""
},
// 选项数据如果传入了默认slot此参数无效
options: {
type: Array,
default() {
return [];
}
},
// 是否禁用此菜单项
disabled: {
type: Boolean,
default: false
},
// 下拉弹窗的高度
height: {
type: [Number, String],
default: "auto"
}
},
data() {
return {
active: false, // 当前项是否处于展开状态
activeColor: "#2979ff", // 激活时左边文字和右边对勾图标的颜色
inactiveColor: "#606266" // 未激活时左边文字和右边对勾图标的颜色
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 监听props是否发生了变化有些值需要传递给父组件u-dropdown无法双向绑定
propsChange() {
return `${this.title}-${this.disabled}`;
}
},
watch: {
propsChange(n) {
// 当值变化时通知父组件重新初始化让父组件执行每个子组件的init()方法
// 将所有子组件数据重新整理一遍
if (this.parent) this.parent.init();
}
},
created() {
// 父组件的实例
this.parent = false;
},
methods: {
init() {
// 获取父组件u-dropdown
let parent = this.$u.$parent.call(this, "u-dropdown");
if (parent) {
this.parent = parent;
// 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
this.activeColor = parent.activeColor;
this.inactiveColor = parent.inactiveColor;
// 将本组件的this放入到父组件的children数组中让父组件可以操作本(子)组件的方法和属性
// push进去前显判断是否已经存在了本实例因为在子组件内部数据变化时会通过父组件重新初始化子组件
let exist = parent.children.find(val => {
return this === val;
});
if (!exist) parent.children.push(this);
if (parent.children.length == 1) this.active = true;
// 父组件无法监听children的变化故将子组件的title传入父组件的menuList数组中
parent.menuList.push({
title: this.title,
disabled: this.disabled
});
}
},
// cell被点击
cellClick(value) {
// 修改通过v-model绑定的值
this.$emit("input", value);
this.$emit("update:modelValue", value);
// 通知父组件(u-dropdown)收起菜单
this.parent.close();
// 发出事件抛出当前勾选项的value
this.$emit("change", value);
}
},
mounted() {
this.init();
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>

View File

@@ -0,0 +1,299 @@
<template>
<view class="u-dropdown">
<view class="u-dropdown__menu" :style="{
height: $u.addUnit(height)
}" :class="{
'u-border-bottom': borderBottom
}">
<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
<view class="u-flex">
<text class="u-dropdown__menu__item__text" :style="{
color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
fontSize: $u.addUnit(titleSize)
}">{{item.title}}</text>
<view class="u-dropdown__menu__item__arrow" :class="{
'u-dropdown__menu__item__arrow--rotate': index === current
}">
<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
</view>
</view>
</view>
</view>
<view class="u-dropdown__content" :style="[contentStyle, {
transition: `opacity ${duration / 1000}s linear`,
top: $u.addUnit(height),
height: contentHeight + 'px'
}]"
@tap="maskClick" @touchmove.stop.prevent>
<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
<slot></slot>
</view>
<view class="u-dropdown__content__mask"></view>
</view>
</view>
</template>
<script>
/**
* dropdown 下拉菜单
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
* @tutorial http://uviewui.com/components/dropdown.html
* @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff
* @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266
* @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单默认true
* @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单默认true
* @property {String | Number} duration 选项卡展开和收起的过渡时间单位ms默认300
* @property {String | Number} height 标题菜单的高度单位任意默认80
* @property {String | Number} border-radius 菜单展开内容下方的圆角值单位任意默认0
* @property {Boolean} border-bottom 标题菜单是否显示下边框默认false
* @property {String | Number} title-size 标题的字体大小单位任意数值默认为rpx单位默认28
* @event {Function} open 下拉菜单被打开时触发
* @event {Function} close 下拉菜单被关闭时触发
* @example <u-dropdown></u-dropdown>
*/
export default {
name: 'u-dropdown',
emits: ["open", "close"],
props: {
// 菜单标题和选项的激活态颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 菜单标题和选项的未激活态颜色
inactiveColor: {
type: String,
default: '#606266'
},
// 点击遮罩是否关闭菜单
closeOnClickMask: {
type: Boolean,
default: true
},
// 点击当前激活项标题是否关闭菜单
closeOnClickSelf: {
type: Boolean,
default: true
},
// 过渡时间
duration: {
type: [Number, String],
default: 300
},
// 标题菜单的高度单位任意数值默认为rpx单位
height: {
type: [Number, String],
default: 80
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 标题的字体大小
titleSize: {
type: [Number, String],
default: 28
},
// 下拉出来的内容部分的圆角值
borderRadius: {
type: [Number, String],
default: 0
},
// 菜单右侧的icon图标
menuIcon: {
type: String,
default: 'arrow-down'
},
// 菜单右侧图标的大小
menuIconSize: {
type: [Number, String],
default: 26
}
},
data() {
return {
showDropdown: true, // 是否打开下来菜单,
menuList: [], // 显示的菜单
active: false, // 下拉菜单的状态
// 当前是第几个菜单处于激活状态小程序中此处不能写成false或者""否则后续将current赋值为0
// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
current: 99999,
// 外层内容的样式,初始时处于底层,且透明
contentStyle: {
zIndex: -1,
opacity: 0
},
// 让某个菜单保持高亮的状态
highlightIndex: 99999,
contentHeight: 0
}
},
computed: {
// 下拉出来部分的样式
popupStyle() {
let style = {};
// 进行Y轴位移展开状态时恢复原位。收齐状态时往上位移100%,进行隐藏
style.transform = `translateY(${this.active ? 0 : '-100%'})`
style['transition-duration'] = this.duration / 1000 + 's';
style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
return style;
}
},
created() {
// 引用所有子组件(u-dropdown-item)的this不能在data中声明变量否则在微信小程序会造成循环引用而报错
this.children = [];
},
mounted() {
this.getContentHeight();
},
methods: {
init() {
// 当某个子组件内容变化时触发父组件的init父组件再让每一个子组件重新初始化一遍
// 以保证数据的正确性
this.menuList = [];
this.children.map(child => {
child.init();
})
},
// 点击菜单
menuClick(index) {
// 判断是否被禁用
if (this.menuList[index].disabled) return;
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
if (index === this.current && this.closeOnClickSelf) {
this.close();
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
setTimeout(() => {
this.children[index].active = false;
}, this.duration)
return;
}
this.open(index);
},
// 打开下拉菜单
open(index) {
// 重置高亮索引,否则会造成多个菜单同时高亮
// this.highlightIndex = 9999;
// 展开时,设置下拉内容的样式
this.contentStyle = {
zIndex: 11,
}
// 标记展开状态以及当前展开项的索引
this.active = true;
this.current = index;
// 历遍所有的子元素将索引匹配的项标记为激活状态因为子元素是通过v-if控制切换的
// 之所以不是因display: none是因为nvue没有display这个属性
this.children.map((val, idx) => {
val.active = index == idx ? true : false;
})
this.$emit('open', this.current);
},
// 设置下拉菜单处于收起状态
close() {
this.$emit('close', this.current);
// 设置为收起状态同时current归位设置为空字符串
this.active = false;
this.current = 99999;
// 下拉内容的样式进行调整不透明度设置为0
this.contentStyle = {
zIndex: -1,
opacity: 0
}
},
// 点击遮罩
maskClick() {
// 如果不允许点击遮罩,直接返回
if (!this.closeOnClickMask) return;
this.close();
},
// 外部手动设置某个菜单高亮
highlight(index = undefined) {
this.highlightIndex = index !== undefined ? index : 99999;
},
// 获取下拉菜单内容的高度
getContentHeight() {
// 这里的原理为因为dropdown组件是相对定位的它的下拉出来的内容必须给定一个高度
// 才能让遮罩占满菜单一下,直到屏幕底部的高度
// this.$u.sys()为uView封装的获取设备信息的方法
let windowHeight = this.$u.sys().windowHeight;
this.$uGetRect('.u-dropdown__menu').then(res => {
// 这里获取的是dropdown的尺寸在H5上uniapp获取尺寸是有bug的(以前提出修复过后来又出现了此bug目前hx2.8.11版本)
// H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离但是元素的bottom值确是导航栏顶部到元素底部的距离
// 二者是互相矛盾的本质原因是H5端导航栏非原生uni的开发者大意造成
// 这里取菜单栏的botton值合理的不能用res.top否则页面会造成滚动
this.contentHeight = windowHeight - res.bottom;
})
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-dropdown {
flex: 1;
width: 100%;
position: relative;
&__menu {
@include vue-flex;
position: relative;
z-index: 11;
height: 80rpx;
&__item {
flex: 1;
@include vue-flex;
justify-content: center;
align-items: center;
&__text {
font-size: 28rpx;
color: $u-content-color;
}
&__arrow {
margin-left: 6rpx;
transition: transform .3s;
align-items: center;
@include vue-flex;
&--rotate {
transform: rotate(180deg);
}
}
}
}
&__content {
position: absolute;
z-index: 8;
width: 100%;
left: 0px;
bottom: 0;
overflow: hidden;
&__mask {
position: absolute;
z-index: 9;
background: rgba(0, 0, 0, .3);
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
&__popup {
position: relative;
z-index: 10;
transition: all 0.3s;
transform: translate3D(0, -100%, 0);
overflow: hidden;
}
}
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<view class="u-empty" v-if="show" :style="{
marginTop: marginTop + 'rpx'
}">
<u-icon
:name="src ? src : 'empty-' + mode"
:custom-style="iconStyle"
:label="text ? text : icons[mode]"
label-pos="bottom"
:label-color="color"
:label-size="fontSize"
:size="iconSize"
:color="iconColor"
margin-top="14"
></u-icon>
<view class="u-slot-wrap">
<slot name="bottom"></slot>
</view>
</view>
</template>
<script>
/**
* empty 内容为空
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
* @tutorial https://www.uviewui.com/components/empty.html
* @property {String} color 文字颜色(默认#c0c4cc
* @property {String} text 文字提示(默认“无内容”)
* @property {String} src 自定义图标路径如定义mode参数会失效
* @property {String Number} font-size 提示文字的大小单位rpx默认28
* @property {String} mode 内置的图标见官网说明默认data
* @property {String Number} img-width 图标的宽度单位rpx默认240
* @property {String} img-height 图标的高度单位rpx默认auto
* @property {String Number} margin-top 组件距离上一个元素之间的距离默认0
* @property {Boolean} show 是否显示组件默认true
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
*/
export default {
name: "u-empty",
props: {
// 图标路径
src: {
type: String,
default: ''
},
// 提示文字
text: {
type: String,
default: ''
},
// 文字颜色
color: {
type: String,
default: '#c0c4cc'
},
// 图标的颜色
iconColor: {
type: String,
default: '#c0c4cc'
},
// 图标的大小
iconSize: {
type: [String, Number],
default: 120
},
// 文字大小单位rpx
fontSize: {
type: [String, Number],
default: 26
},
// 选择预置的图标类型
mode: {
type: String,
default: 'data'
},
// 图标宽度单位rpx
imgWidth: {
type: [String, Number],
default: 120
},
// 图标高度单位rpx
imgHeight: {
type: [String, Number],
default: 'auto'
},
// 是否显示组件
show: {
type: Boolean,
default: true
},
// 组件距离上一个元素之间的距离
marginTop: {
type: [String, Number],
default: 0
},
iconStyle: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
icons: {
car: '购物车为空',
page: '页面不存在',
search: '没有搜索结果',
address: '没有收货地址',
wifi: '没有WiFi',
order: '订单为空',
coupon: '没有优惠券',
favor: '暂无收藏',
permission: '无权限',
history: '无历史记录',
news: '无新闻列表',
message: '消息列表为空',
list: '列表为空',
data: '数据为空'
},
// icons: [{
// icon: 'car',
// text: '购物车为空'
// },{
// icon: 'page',
// text: '页面不存在'
// },{
// icon: 'search',
// text: '没有搜索结果'
// },{
// icon: 'address',
// text: '没有收货地址'
// },{
// icon: 'wifi',
// text: '没有WiFi'
// },{
// icon: 'order',
// text: '订单为空'
// },{
// icon: 'coupon',
// text: '没有优惠券'
// },{
// icon: 'favor',
// text: '暂无收藏'
// },{
// icon: 'permission',
// text: '无权限'
// },{
// icon: 'history',
// text: '无历史记录'
// },{
// icon: 'news',
// text: '无新闻列表'
// },{
// icon: 'message',
// text: '消息列表为空'
// },{
// icon: 'list',
// text: '列表为空'
// },{
// icon: 'data',
// text: '数据为空'
// }],
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-empty {
@include vue-flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.u-image {
margin-bottom: 20rpx;
}
.u-slot-wrap {
@include vue-flex;
justify-content: center;
align-items: center;
margin-top: 20rpx;
}
</style>

View File

@@ -0,0 +1,402 @@
<template>
<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
<view class="u-label" :class="[required ? 'u-required' : '']" :style="{
justifyContent: justifyContent,
flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
}">
<view class="u-icon-wrap" v-if="icon">
<u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
</view>
<slot name="icon"></slot>
<text class="u-label-text" :class="[$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
</view>
<view class="fild-body">
<view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="valueCom"
:placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
:focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
@tap="fieldClick" />
<input
v-else
:style="[fieldStyle]"
:type="type"
class="u-flex-1 u-field__input-wrap"
:value="valueCom"
:password="password || type === 'password'"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
:maxlength="inputMaxlength"
:focus="focus"
:confirmType="confirmType"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
@confirm="onConfirm"
@tap="fieldClick"
/>
</view>
<u-icon :size="clearSize" v-if="clearable && valueCom != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
<view class="u-button-wrap"><slot name="right" /></view>
<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
</view>
</view>
<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
paddingLeft: labelWidth + 'rpx'
}">{{ errorMessage }}</view>
</view>
</template>
<script>
/**
* field 输入框
* @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的此外借助uView的picker和actionSheet组件可以快速实现上拉菜单时间地区选择等 为表单解决方案的利器。
* @tutorial https://www.uviewui.com/components/field.html
* @property {String} type 输入框的类型默认text
* @property {String} icon label左边的图标限uView的图标名称
* @property {Object} icon-style 左边图标的样式,对象形式
* @property {Boolean} right-icon 输入框右边的图标名称限uView的图标名称默认false
* @property {Boolean} required 是否必填,左边您显示红色"*"号默认false
* @property {String} label 输入框左边的文字提示
* @property {Boolean} password 是否密码输入方式(用点替换文字)type为text时有效默认false
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示)点击可清空输入框内容默认true
* @property {Number String} label-width label的宽度单位rpx默认130
* @property {String} label-align label的文字对齐方式默认left
* @property {Object} field-style 自定义输入框的样式,对象形式
* @property {Number | String} clear-size 清除图标的大小单位rpx默认30
* @property {String} input-align 输入框内容对齐方式默认left
* @property {Boolean} border-bottom 是否显示field的下边框默认true
* @property {Boolean} border-top 是否显示field的上边框默认false
* @property {String} icon-color 左边通过icon配置的图标的颜色默认#606266
* @property {Boolean} auto-height 是否自动增高输入区域type为textarea时有效默认true
* @property {String Boolean} error-message 显示的错误提示内容如果为空字符串或者false则不显示错误信息
* @property {String} placeholder 输入框的提示文字
* @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
* @property {Boolean} focus 是否自动获得焦点默认false
* @property {Boolean} fixed 如果type为textarea且在一个"position:fixed"的区域需要指明为true默认false
* @property {Boolean} disabled 是否不可输入默认false
* @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度默认140
* @property {String} confirm-type 设置键盘右下角按钮的文字仅在type="text"时生效默认done
* @event {Function} input 输入框内容发生变化时触发
* @event {Function} focus 输入框获得焦点时触发
* @event {Function} blur 输入框失去焦点时触发
* @event {Function} confirm 点击完成按钮时触发
* @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
* @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发这样设计是考虑到传递右边的图标一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
* @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
*/
export default {
name:"u-field",
emits: ["update:modelValue", "input", "focus", "blur", "confirm", "right-icon-click", "click"],
props: {
value: [Number, String],
modelValue: [Number, String],
icon: String,
rightIcon: String,
arrowDirection: {
type: String,
default: 'right'
},
required: Boolean,
label: String,
password: Boolean,
clearable: {
type: Boolean,
default: true
},
// 左边标题的宽度单位rpx
labelWidth: {
type: [Number, String],
default: 130
},
// 对齐方式left|center|right
labelAlign: {
type: String,
default: 'left'
},
inputAlign: {
type: String,
default: 'left'
},
iconColor: {
type: String,
default: '#606266'
},
autoHeight: {
type: Boolean,
default: true
},
errorMessage: {
type: [String, Boolean],
default: ''
},
placeholder: String,
placeholderStyle: String,
focus: Boolean,
fixed: Boolean,
type: {
type: String,
default: 'text'
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
confirmType: {
type: String,
default: 'done'
},
// lable的位置可选为 left-左边top-上边
labelPosition: {
type: String,
default: 'left'
},
// 输入框的自定义样式
fieldStyle: {
type: Object,
default() {
return {}
}
},
// 清除按钮的大小
clearSize: {
type: [Number, String],
default: 30
},
// lable左边的图标样式对象形式
iconStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示上边框
borderTop: {
type: Boolean,
default: false
},
// 是否显示下边框
borderBottom: {
type: Boolean,
default: true
},
// 是否自动去除两端的空格
trim: {
type: Boolean,
default: true
}
},
data() {
return {
focused: false,
itemIndex: 0,
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
inputWrapStyle() {
let style = {};
style.textAlign = this.inputAlign;
// 判断lable的位置如果是left的话让input左边两边有间隙
if(this.labelPosition == 'left') {
style.margin = `0 8rpx`;
} else {
// 如果lable是top的input的左边就没必要有间隙了
style.marginRight = `8rpx`;
}
return style;
},
rightIconStyle() {
let style = {};
let arrowDirectionObj = {
"top": "-90deg",
"bottom": "90deg",
"left": "180deg",
"right": "0deg",
};
let deg = arrowDirectionObj[this.arrowDirection] || "0deg";
style.transform = 'rotate('+deg+')';
return style;
},
labelStyle() {
let style = {};
if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
if(this.labelAlign == 'center') style.justifyContent = 'center';
if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
return style;
},
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
justifyContent() {
if(this.labelAlign == 'left') return 'flex-start';
if(this.labelAlign == 'center') return 'center';
if(this.labelAlign == 'right') return 'flex-end';
},
// 因为uniapp的input组件的maxlength组件必须要数值这里转为数值给用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength)
},
// label的位置
fieldInnerStyle() {
let style = {};
if(this.labelPosition == 'left') {
style.flexDirection = 'row';
} else {
style.flexDirection = 'column';
}
return style;
}
},
methods: {
onInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if(this.trim) value = this.$u.trim(value);
this.$emit('input', value);
this.$emit("update:modelValue", value);
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
// 最开始使用的是监听图标@touchstart事件自从hx2.8.4后,此方法在微信小程序出错
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件导致图标消失而无法点击这里做一个延时
setTimeout(() => {
this.focused = false;
}, 100)
this.$emit('blur', event);
},
onConfirm(e) {
this.$emit('confirm', e.detail.value);
},
onClear(event) {
this.$emit('input', '');
this.$emit("update:modelValue", '');
},
rightIconClick() {
this.$emit('right-icon-click');
this.$emit('click');
},
fieldClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-field {
font-size: 28rpx;
padding: 20rpx 28rpx;
text-align: left;
position: relative;
color: $u-main-color;
}
.u-field-inner {
@include vue-flex;
align-items: center;
}
.u-textarea-inner {
align-items: flex-start;
}
.u-textarea-class {
min-height: 96rpx;
width: auto;
font-size: 28rpx;
}
.fild-body {
@include vue-flex;
flex: 1;
align-items: center;
}
.u-arror-right {
margin-left: 8rpx;
}
.u-label-text {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
}
.u-label-left-gap {
margin-left: 6rpx;
}
.u-label-postion-top {
flex-direction: column;
align-items: flex-start;
}
.u-label {
width: 130rpx;
flex: 1 1 130rpx;
text-align: left;
position: relative;
@include vue-flex;
align-items: center;
}
.u-required::before {
content: '*';
position: absolute;
left: -16rpx;
font-size: 14px;
color: $u-type-error;
height: 9px;
line-height: 1;
}
.u-field__input-wrap {
position: relative;
overflow: hidden;
font-size: 28rpx;
height: 48rpx;
flex: 1;
width: auto;
}
.u-clear-icon {
@include vue-flex;
align-items: center;
}
.u-error-message {
color: $u-type-error;
font-size: 26rpx;
text-align: left;
}
.placeholder-style {
color: rgb(150, 151, 153);
}
.u-input-class {
font-size: 28rpx;
}
.u-button-wrap {
margin-left: 8rpx;
}
</style>

View File

@@ -0,0 +1,509 @@
<template>
<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
<view class="u-form-item__body" :style="{
flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
}">
<!-- 微信小程序中将一个参数设置空字符串结果会变成字符串"true" -->
<view class="u-form-item--left" :style="{
width: uLabelWidth,
flex: `0 0 ${uLabelWidth}`,
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
}">
<!-- 为了块对齐 -->
<view class="u-form-item--left__content" v-if="required || leftIcon || label">
<!-- nvue不支持伪元素before -->
<text v-if="required" class="u-form-item--left__content--required">*</text>
<view class="u-form-item--left__content__icon" v-if="leftIcon">
<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
</view>
<view class="u-form-item--left__content__label" :style="[elLabelStyle, {
'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
}]">
{{label}}
</view>
</view>
</view>
<view class="u-form-item--right u-flex">
<view class="u-form-item--right__content">
<view class="u-form-item--right__content__slot" :style="elLabelPosition == 'left' && elInputAlign == 'right' ? 'text-align:right;display: inline-block;line-height:initial;' : ''">
<slot />
</view>
<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
<slot name="right" />
</view>
</view>
</view>
</view>
<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
textAlign: elInputAlign == 'right' ? 'right' : 'left'
}">{{validateMessage}}</view>
</view>
</template>
<script>
import Emitter from '../../libs/util/emitter.js';
import schema from '../../libs/util/async-validator';
// 去除警告信息
schema.warning = function() {};
/**
* form-item 表单item
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等。
* @tutorial http://uviewui.com/components/form.html
* @property {String} label 左侧提示文字
* @property {Object} prop 表单域model对象的属性名在使用 validate、resetFields 方法的情况下,该属性是必填的
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
* @property {String} label-position 表单域提示文字的位置left-左侧top-上方
* @property {String Number} label-width 提示文字的宽度单位rpx默认90
* @property {Object} label-style lable的样式对象形式
* @property {String} label-align lable的对齐方式
* @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
* @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
* @property {Object} left-icon-style 左侧图标的样式,对象形式
* @property {Object} right-icon-style 右侧图标的样式,对象形式
* @property {Boolean} required 是否显示左边的"*"号这里仅起展示作用如需校验必填请通过rules配置必填规则(默认false)
* @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
*/
export default {
name: 'u-form-item',
mixins: [Emitter],
inject: {
uForm: {
default () {
return null
}
}
},
props: {
// input的label提示语
label: {
type: String,
default: ''
},
// 绑定的值
prop: {
type: String,
default: ''
},
// 是否显示表单域的下划线边框
borderBottom: {
type: [String, Boolean],
default: ''
},
// label的位置left-左边top-上边
labelPosition: {
type: String,
default: ''
},
// label的宽度单位rpx
labelWidth: {
type: [String, Number],
default: ''
},
// lable的样式对象形式
labelStyle: {
type: Object,
default () {
return {}
}
},
// lable字体的对齐方式
labelAlign: {
type: String,
default: ''
},
// 右侧图标
rightIcon: {
type: String,
default: ''
},
// 左侧图标
leftIcon: {
type: String,
default: ''
},
// 左侧图标的样式
leftIconStyle: {
type: Object,
default () {
return {}
}
},
// 左侧图标的样式
rightIconStyle: {
type: Object,
default () {
return {}
}
},
// 是否显示左边的必填星号只作显示用具体校验必填的逻辑请在rules中配置
required: {
type: Boolean,
default: false
},
inputAlign:{
type: String,
default: ''
},
},
data() {
return {
initialValue: '', // 存储的默认值
// isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置不再通过rules的规则自动生成
validateState: '', // 是否校验成功
validateMessage: '', // 校验失败的提示语
// 有错误时的提示方式message-提示信息border-如果input设置了边框变成呈红色
errorType: ['message'],
fieldValue: '', // 获取当前子组件input的输入的值
// 父组件的参数在computed计算中无法得知this.parent发生变化故将父组件的参数值放到data中
parentData: {
borderBottom: true,
labelWidth: 90,
labelPosition: 'left',
labelStyle: {},
labelAlign: 'left',
inputAlign: 'left',
}
};
},
watch: {
validateState(val) {
this.broadcastInputError();
},
// 监听u-form组件的errorType的变化
"uForm.errorType"(val) {
this.errorType = val;
this.broadcastInputError();
},
},
computed: {
// 计算后的label宽度由于需要多个判断故放到computed中
uLabelWidth() {
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true')意味着要将label的位置宽度设置为auto
return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
.elLabelWidth)) : '100%';
},
showError() {
return type => {
// 如果errorType数组中含有none或者toast提示类型
if (this.errorType.indexOf('none') >= 0) return false;
else if (this.errorType.indexOf(type) >= 0) return true;
else return false;
}
},
// label的宽度
elLabelWidth() {
// label默认宽度为90优先使用本组件的值如果没有(如果设置为0也算是配置了值依然起效)则用u-form的值
return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
.labelWidth :
90);
},
// label的样式
elLabelStyle() {
return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
{});
},
// label的位置左侧或者上方
elLabelPosition() {
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
'left');
},
// label的对齐方式
elLabelAlign() {
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
},
// label的下划线
elBorderBottom() {
// 子组件的borderBottom默认为空字符串如果不等于空字符串意味着子组件设置了值优先使用子组件的值
return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
true;
},
elInputAlign() {
return this.inputAlign ? this.inputAlign : (this.parentData.inputAlign ? this.parentData.inputAlign : 'left');
},
},
methods: {
broadcastInputError() {
// 子组件发出事件第三个参数为true或者falsetrue代表有错误
this.broadcast('u-input', 'onFormItemError', this.validateState === 'error' && this.showError('border'));
},
// 判断是否需要required校验
setRules() {
let that = this;
// 由于人性化考虑,必填"*"号通过props的required配置不再通过rules的规则自动生成
// 从父组件u-form拿到当前u-form-item需要验证 的规则
// let rules = this.getRules();
// if (rules.length) {
// this.isRequired = rules.some(rule => {
// // 如果有必填项就返回没有的话就是undefined
// return rule.required;
// });
// }
// #ifndef VUE3
// blur事件
this.$on('onFieldBlur', that.onFieldBlur);
// change事件
this.$on('onFieldChange', that.onFieldChange);
// #endif
// #ifdef VUE3
// #endif
},
// 从u-form的rules属性中取出当前u-form-item的校验规则
getRules() {
// 父组件的所有规则
let rules = this.parent.rules;
rules = rules ? rules[this.prop] : [];
// 保证返回的是一个数组形式
return [].concat(rules || []);
},
// blur事件时进行表单校验
onFieldBlur() {
this.validation('blur');
},
// change事件进行表单校验
onFieldChange() {
this.validation('change');
},
// 过滤出符合要求的rule规则
getFilteredRule(triggerType = '') {
let rules = this.getRules();
// 整体验证表单时triggerType为空字符串此时返回所有规则进行验证
if (!triggerType) return rules;
// 历遍判断规则是否有对应的事件比如blurchange触发等的事件
// 使用indexOf判断是因为某些时候设置的验证规则的trigger属性可能为多个比如['blur','change']
// 某些场景可能的判断规则可能不存在trigger属性故先判断是否存在此属性
return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
},
getData(dataObj, name, defaultValue) {
let newDataObj;
if (dataObj) {
newDataObj = JSON.parse(JSON.stringify(dataObj));
let k = "",
d = ".",
l = "[",
r = "]";
name = name.replace(/\s+/g, k) + d;
let tstr = k;
for (let i = 0; i < name.length; i++) {
let theChar = name.charAt(i);
if (theChar != d && theChar != l && theChar != r) {
tstr += theChar;
} else if (newDataObj) {
if (tstr != k) newDataObj = newDataObj[tstr];
tstr = k;
}
}
}
if (typeof newDataObj === "undefined" && typeof defaultValue !== "undefined") newDataObj = defaultValue;
return newDataObj;
},
setData(dataObj, name, value) {
// 通过正则表达式 查找路径数据
let dataValue;
if (typeof value === "object") {
dataValue = JSON.parse(JSON.stringify(value));
} else {
dataValue = value;
}
let regExp = new RegExp("([\\w$]+)|\\[(:\\d)\\]", "g");
const patten = name.match(regExp);
// 遍历路径 逐级查找 最后一级用于直接赋值
for (let i = 0; i < patten.length - 1; i++) {
let keyName = patten[i];
if (typeof dataObj[keyName] !== "object") dataObj[keyName] = {};
dataObj = dataObj[keyName];
}
// 最后一级
dataObj[patten[patten.length - 1]] = dataValue;
},
// 校验数据
validation(trigger, callback = () => {}) {
// 检验之间,先获取需要校验的值
//this.fieldValue = this.parent.model[this.prop];
// 修改支持a.b
this.fieldValue = this.getData(this.parent.model,this.prop);
// blur和change是否有当前方式的校验规则
let rules = this.getFilteredRule(trigger);
// 判断是否有验证规则如果没有规则也调用回调方法否则父组件u-form会因为
// 对count变量的统计错误而无法进入上一层的回调
if (!rules || rules.length === 0) {
return callback('');
}
// 设置当前的装填,标识为校验中
this.validateState = 'validating';
// 调用async-validator的方法
let validator = new schema({
[this.prop]: rules
});
validator.validate({
[this.prop]: this.fieldValue
}, {
firstFields: true
}, (errors, fields) => {
// 记录状态和报错信息
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
let field = errors ? errors[0].field : '';
// 调用回调方法
callback(this.validateMessage, {
state: this.validateState,
key: field,
msg: this.validateMessage
});
});
},
// 清空当前的u-form-item
resetField() {
//this.parent.model[this.prop] = this.initialValue;
this.setData(this.parent.model,this.prop,this.initialValue);
// 设置为`success`状态,只是为了清空错误标记
this.validateState = 'success';
}
},
// 组件创建完成时将当前实例保存到u-form中
mounted() {
// 支付宝、头条小程序不支持provide/inject所以使用这个方法获取整个父组件在created定义避免循环应用
this.parent = this.$u.$parent.call(this, 'u-form');
if (this.parent) {
// 历遍parentData中的属性将parent中的同名属性赋值给parentData
Object.keys(this.parentData).map(key => {
this.parentData[key] = this.parent[key];
});
// 如果没有传入prop或者uForm为空(如果u-form-input单独使用就不会有uForm注入),就不进行校验
if (this.prop) {
// 将本实例添加到父组件中
this.parent.fields.push(this);
this.errorType = this.parent.errorType;
// 设置初始值
this.initialValue = this.fieldValue;
// 添加表单校验,这里必须要写在$nextTick中因为u-form的rules是通过ref手动传入的
// 不在$nextTick中的话可能会造成执行此处代码时父组件还没通过ref把规则给u-form导致规则为空
this.$nextTick(() => {
this.setRules();
})
}
}
},
// #ifndef VUE3
// 组件销毁前将实例从u-form的缓存中移除
beforeDestroy() {
// 如果当前没有prop的话表示当前不要进行删除因为没有注入
if (this.parent && this.prop) {
this.parent.fields.map((item, index) => {
if (item === this) this.parent.fields.splice(index, 1);
})
}
},
// #endif
// #ifdef VUE3
beforeUnmount() {
// 如果当前没有prop的话表示当前不要进行删除因为没有注入
if (this.parent && this.prop) {
this.parent.fields.map((item, index) => {
if (item === this) this.parent.fields.splice(index, 1);
})
}
},
// #endif
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-form-item {
@include vue-flex;
// align-items: flex-start;
padding: 10rpx 0;
font-size: 28rpx;
color: $u-main-color;
box-sizing: border-box;
line-height: $u-form-item-height;
flex-direction: column;
&__border-bottom--error:after {
border-color: $u-type-error;
}
&__body {
@include vue-flex;
}
&--left {
@include vue-flex;
align-items: center;
&__content {
position: relative;
@include vue-flex;
align-items: center;
padding-right: 10rpx;
flex: 1;
&__icon {
margin-right: 8rpx;
}
&--required {
position: absolute;
left: -16rpx;
vertical-align: middle;
color: $u-type-error;
padding-top: 6rpx;
}
&__label {
@include vue-flex;
align-items: center;
flex: 1;
}
}
}
&--right {
flex: 1;
&__content {
@include vue-flex;
align-items: center;
flex: 1;
&__slot {
flex: 1;
/* #ifndef MP */
@include vue-flex;
align-items: center;
/* #endif */
}
&__icon {
margin-left: 10rpx;
color: $u-light-color;
font-size: 30rpx;
}
}
}
&__message {
font-size: 24rpx;
line-height: 24rpx;
color: $u-type-error;
margin-top: 12rpx;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<view class="u-form"><slot /></view>
</template>
<script>
/**
* form 表单
* @description 此组件一般用于表单场景可以配置Input输入框Select弹出框进行表单验证等。
* @tutorial http://uviewui.com/components/form.html
* @property {Object} model 表单数据对象
* @property {Boolean} border-bottom 是否显示表单域的下划线边框
* @property {String} label-position 表单域提示文字的位置left-左侧top-上方
* @property {String Number} label-width 提示文字的宽度单位rpx默认90
* @property {Object} label-style lable的样式对象形式
* @property {String} label-align lable的对齐方式
* @property {Object} rules 通过ref设置见官网说明
* @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
* @example <u-form :model="form" ref="uForm"></u-form>
*/
export default {
name: 'u-form',
props: {
// 当前form的需要验证字段的集合
model: {
type: Object,
default() {
return {};
}
},
// 验证规则
// rules: {
// type: [Object, Function, Array],
// default() {
// return {};
// }
// },
// 有错误时的提示方式message-提示信息border-如果input设置了边框变成呈红色
// border-bottom-下边框呈现红色none-无提示
errorType: {
type: Array,
default() {
return ['message', 'toast']
}
},
// 是否显示表单域的下划线边框
borderBottom: {
type: Boolean,
default: true
},
// label的位置left-左边top-上边
labelPosition: {
type: String,
default: 'left'
},
// label的宽度单位rpx
labelWidth: {
type: [String, Number],
default: 90
},
// lable字体的对齐方式
labelAlign: {
type: String,
default: 'left'
},
// lable的样式对象形式
labelStyle: {
type: Object,
default() {
return {}
}
},
// 表单内所有input的inputAlign属性的值
inputAlign: {
type: String,
default: 'left'
},
// 表单内所有input的clearable属性的值
clearable:{
type: Boolean,
default: true
}
},
provide() {
return {
uForm: this
};
},
data() {
return {
rules: {}
};
},
created() {
// 存储当前form下的所有u-form-item的实例
// 不能定义在data中否则微信小程序会造成循环引用而报错
this.fields = [];
},
methods: {
setRules(rules) {
this.rules = rules;
},
// 清空所有u-form-item组件的内容本质上是调用了u-form-item组件中的resetField()方法
resetFields() {
this.fields.map(field => {
field.resetField();
});
},
// 校验全部数据
validate(callback) {
return new Promise(resolve => {
// 对所有的u-form-item进行校验
let valid = true; // 默认通过
let count = 0; // 用于标记是否检查完毕
let errorArr = []; // 存放错误信息
let errorObjArr = []; // 存放错误信息对象
this.fields.map(field => {
// 调用每一个u-form-item实例的validation的校验方法
field.validation('', (errorMsg, errObj) => {
// 如果任意一个u-form-item校验不通过就意味着整个表单不通过
if (errorMsg) {
valid = false;
errorArr.push(errorMsg);
errorObjArr.push(errObj)
}
// 当历遍了所有的u-form-item时调用promise的then方法
if (++count === this.fields.length) {
resolve(valid, errorObjArr[0]); // 进入promise的then方法
// 判断是否设置了toast的提示方式只提示最前面的表单域的第一个错误信息
if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
this.$u.toast(errorArr[0]);
}
// 调用回调方法
if (typeof callback == 'function') callback(valid, errorObjArr[0]);
}
});
});
});
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
</style>

View File

@@ -0,0 +1,52 @@
<template>
<u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
<view class="u-update-content">
<rich-text :nodes="content"></rich-text>
</view>
</u-modal>
</template>
<script>
export default {
data() {
return {
show: false,
content: `
1. 修复badge组件的size参数无效问题<br>
2. 新增Modal模态框组件<br>
3. 新增压窗屏组件可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
4. 修复键盘组件在微信小程序上遮罩无效的问题
`,
}
},
onReady() {
this.show = true;
},
methods: {
cancel() {
this.closeModal();
},
confirm() {
this.closeModal();
},
closeModal() {
uni.navigateBack();
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-full-content {
background-color: #00C777;
}
.u-update-content {
font-size: 26rpx;
color: $u-content-color;
line-height: 1.7;
padding: 30rpx;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<view class="u-gap" :style="[gapStyle]"></view>
</template>
<script>
/**
* gap 间隔槽
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
* @tutorial https://www.uviewui.com/components/gap.html
* @property {String} bg-color 背景颜色(默认#f3f4f6
* @property {String Number} height 分割槽高度单位rpx默认30
* @property {String Number} margin-top 与前一个组件的距离单位rpx默认0
* @property {String Number} margin-bottom 与后一个组件的距离单位rpx0
* @example <u-gap height="80" bg-color="#bbb"></u-gap>
*/
export default {
name: "u-gap",
props: {
bgColor: {
type: String,
default: 'transparent ' // 背景透明
},
// 高度
height: {
type: [String, Number],
default: 30
},
// 与上一个组件的距离
marginTop: {
type: [String, Number],
default: 0
},
// 与下一个组件的距离
marginBottom: {
type: [String, Number],
default: 0
},
},
computed: {
gapStyle() {
return {
backgroundColor: this.bgColor,
height: this.height + 'rpx',
marginTop: this.marginTop + 'rpx',
marginBottom: this.marginBottom + 'rpx'
};
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
</style>

View File

@@ -0,0 +1,127 @@
<template>
<view class="u-grid-item" :hover-class="parentData.hoverClass"
:hover-stay-time="200" @tap="click" :style="{
background: bgColor,
width: width,
}">
<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
<slot />
</view>
</view>
</template>
<script>
/**
* gridItem 提示
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge)或者图标等也可以扩展为左右滑动的轮播形式。搭配u-grid使用
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String} bg-color 宫格的背景颜色(默认#ffffff
* @property {String Number} index 点击宫格时,返回的值
* @property {Object} custom-style 自定义样式,对象形式
* @event {Function} click 点击宫格触发
* @example <u-grid-item></u-grid-item>
*/
export default {
name: "u-grid-item",
emits: ["click"],
props: {
// 背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 点击时返回的index
index: {
type: [Number, String],
default: ''
},
// 自定义样式,对象形式
customStyle: {
type: Object,
default() {
return {
padding: '30rpx 0'
}
}
}
},
data() {
return {
parentData: {
hoverClass: '', // 按下去的时候,是否显示背景灰色
col: 3, // 父组件划分的宫格数
border: true, // 是否显示边框,根据父组件决定
}
};
},
created() {
// 父组件的实例
this.updateParentData();
// this.parent在updateParentData()中定义
this.parent.children.push(this);
},
computed: {
// 每个grid-item的宽度
width() {
return 100 / Number(this.parentData.col) + '%';
},
},
methods: {
// 获取父组件的参数
updateParentData() {
// 此方法写在mixin中
this.getParentData('u-grid');
},
click() {
this.$emit('click', this.index);
this.parent && this.parent.click(this.index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-grid-item {
box-sizing: border-box;
background: #fff;
@include vue-flex;
align-items: center;
justify-content: center;
position: relative;
flex-direction: column;
/* #ifdef MP */
position: relative;
float: left;
/* #endif */
}
.u-grid-item-hover {
background: #f7f7f7 !important;
}
.u-grid-marker-box {
position: absolute;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
line-height: 0;
}
.u-grid-marker-wrap {
position: absolute;
}
.u-grid-item-box {
padding: 30rpx 0;
@include vue-flex;
align-items: center;
justify-content: center;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
</template>
<script>
/**
* grid 宫格布局
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
* @tutorial https://www.uviewui.com/components/grid.html
* @property {String Number} col 宫格的列数默认3
* @property {Boolean} border 是否显示宫格的边框默认true
* @property {Boolean} hover-class 点击宫格的时候是否显示按下的灰色背景默认false
* @event {Function} click 点击宫格触发
* @example <u-grid :col="3" @click="click"></u-grid>
*/
export default {
name: 'u-grid',
emits: ["click"],
props: {
// 分成几列
col: {
type: [Number, String],
default: 3
},
// 是否显示边框
border: {
type: Boolean,
default: true
},
// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
align: {
type: String,
default: 'left'
},
// 宫格按压时的样式类,"none"为无效果
hoverClass: {
type: String,
default: 'u-hover-class'
}
},
data() {
return {
index: 0,
}
},
watch: {
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if(this.children.length) {
this.children.map(child => {
// 判断子组件(u-radio)如果有updateParentData方法的话就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(child.updateParentData) == 'function' && child.updateParentData();
})
}
},
},
created() {
// 如果将children定义在data中在微信小程序会造成循环引用而报错
this.children = [];
},
computed: {
// 计算父组件的值是否发生变化
parentData() {
return [this.hoverClass, this.col, this.size, this.border];
},
// 宫格对齐方式
gridStyle() {
let style = {};
switch(this.align) {
case 'left':
style.justifyContent = 'flex-start';
break;
case 'center':
style.justifyContent = 'center';
break;
case 'right':
style.justifyContent = 'flex-end';
break;
default: style.justifyContent = 'flex-start';
};
return style;
}
},
methods: {
click(index) {
this.$emit('click', index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-grid {
width: 100%;
/* #ifdef MP */
position: relative;
box-sizing: border-box;
overflow: hidden;
/* #endif */
/* #ifndef MP */
@include vue-flex;
flex-wrap: wrap;
align-items: center;
/* #endif */
}
</style>

View File

@@ -0,0 +1,369 @@
<template>
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
<view
v-else
class="u-icon__icon"
:class="customClass"
:style="[iconStyle]"
:hover-class="hoverClass"
@touchstart="touchstart"
>
<text
v-if="showDecimalIcon"
:style="[decimalIconStyle]"
:class="decimalIconClass"
:hover-class="hoverClass"
class="u-icon__decimal"
></text>
</view>
<!-- 这里进行空字符串判断如果仅仅是v-if="label"可能会出现传递0的时候结果也无法显示微信小程序不传值默认为null故需要增加null的判断 -->
<text
v-if="label !== '' && label !== null"
class="u-icon__label"
:style="{
color: labelColor,
fontSize: $u.addUnit(labelSize),
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0
}"
>
{{ label }}
</text>
</view>
</template>
<script>
/**
* icon 图标
* @description 基于字体的图标集,包含了大多数常见场景的图标。
* @tutorial https://www.uviewui.com/components/icon.html
* @property {String} name 图标名称,见示例图标集
* @property {String} color 图标颜色默认inherit
* @property {String | Number} size 图标字体大小单位rpx默认32
* @property {String | Number} label-size label字体大小单位rpx默认28
* @property {String} label 图标右侧的label文字默认28
* @property {String} label-pos label文字相对于图标的位置只能right或bottom默认right
* @property {String} label-color label字体颜色默认#606266
* @property {Object} custom-style icon的样式对象形式
* @property {String} custom-prefix 自定义字体图标库时,需要写上此值
* @property {String | Number} margin-left label在右侧时与图标的距离单位rpx默认6
* @property {String | Number} margin-top label在下方时与图标的距离单位rpx默认6
* @property {String | Number} margin-bottom label在上方时与图标的距离单位rpx默认6
* @property {String | Number} margin-right label在左侧时与图标的距离单位rpx默认6
* @property {String} label-pos label相对于图标的位置只能right或bottom默认right
* @property {String} index 一个用于区分多个图标的值点击图标时通过click事件传出
* @property {String} hover-class 图标按下去的样式类用法同uni的view组件的hover-class参数详情见官网
* @property {String} width 显示图片小图标时的宽度
* @property {String} height 显示图片小图标时的高度
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {Boolean} show-decimal-icon 是否为DecimalIcon
* @property {String} inactive-color 背景颜色可接受主题色仅Decimal时有效
* @property {String | Number} percent 显示的百分比仅Decimal时有效
* @event {Function} click 点击图标时触发
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
*/
export default {
name: "u-icon",
emits: ["click", "touchstart"],
props: {
// 图标类名
name: {
type: String,
default: ""
},
// 图标颜色,可接受主题色
color: {
type: String,
default: ""
},
// 字体大小单位rpx
size: {
type: [Number, String],
default: "inherit"
},
// 是否显示粗体
bold: {
type: Boolean,
default: false
},
// 点击图标的时候传递事件出去的index用于区分点击了哪一个
index: {
type: [Number, String],
default: ""
},
// 触摸图标时的类名
hoverClass: {
type: String,
default: ""
},
// 自定义扩展前缀,方便用户扩展自己的图标库
customPrefix: {
type: String,
default: "uicon"
},
// 图标右边或者下面的文字
label: {
type: [String, Number],
default: ""
},
// label的位置只能右边或者下边
labelPos: {
type: String,
default: "right"
},
// label的大小
labelSize: {
type: [String, Number],
default: "28"
},
// label的颜色
labelColor: {
type: String,
default: "#606266"
},
// label与图标的距离(横向排列)
marginLeft: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginTop: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginRight: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginBottom: {
type: [String, Number],
default: "6"
},
// 图片的mode
imgMode: {
type: String,
default: "widthFix"
},
// 自定义样式
customStyle: {
type: Object,
default() {
return {};
}
},
// 用于显示图片小图标时,图片的宽度
width: {
type: [String, Number],
default: ""
},
// 用于显示图片小图标时,图片的高度
height: {
type: [String, Number],
default: ""
},
// 用于解决某些情况下,让图标垂直居中的用途
top: {
type: [String, Number],
default: 0
},
// 是否为DecimalIcon
showDecimalIcon: {
type: Boolean,
default: false
},
// 背景颜色可接受主题色仅Decimal时有效
inactiveColor: {
type: String,
default: "#ececec"
},
// 显示的百分比仅Decimal时有效
percent: {
type: [Number, String],
default: "50"
}
},
computed: {
customClass() {
let classes = [];
let { customPrefix, name } = this;
let index = name.indexOf("-icon-");
if (index > -1) {
customPrefix = name.substring(0, index + 5);
classes.push(name);
} else {
classes.push(`${customPrefix}-${name}`);
}
// uView的自定义图标类名为u-iconfont
if (customPrefix === "uicon") {
classes.push("u-iconfont");
} else {
classes.push(customPrefix);
}
// 主题色,通过类配置
if (
this.showDecimalIcon &&
this.inactiveColor &&
this.$u.config.type.includes(this.inactiveColor)
) {
classes.push("u-icon__icon--" + this.inactiveColor);
} else if (this.color && this.$u.config.type.includes(this.color))
classes.push("u-icon__icon--" + this.color);
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(" ");
//#endif
return classes;
},
iconStyle() {
let style = {};
style = {
fontSize: this.size == "inherit" ? "inherit" : this.$u.addUnit(this.size),
fontWeight: this.bold ? "bold" : "normal",
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
top: this.$u.addUnit(this.top)
};
// 非主题色值时,才当作颜色值
if (
this.showDecimalIcon &&
this.inactiveColor &&
!this.$u.config.type.includes(this.inactiveColor)
) {
style.color = this.inactiveColor;
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
return style;
},
// 判断传入的name属性是否图片路径只要带有"/"均认为是图片形式
isImg() {
return this.name.indexOf("/") !== -1;
},
imgStyle() {
let style = {};
// 如果设置width和height属性则优先使用否则使用size属性
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size);
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size);
return style;
},
decimalIconStyle() {
let style = {};
style = {
fontSize: this.size == "inherit" ? "inherit" : this.$u.addUnit(this.size),
fontWeight: this.bold ? "bold" : "normal",
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
top: this.$u.addUnit(this.top),
width: this.percent + "%"
};
// 非主题色值时,才当作颜色值
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
return style;
},
decimalIconClass() {
let classes = [];
classes.push(this.customPrefix + "-" + this.name);
// uView的自定义图标类名为u-iconfont
if (this.customPrefix == "uicon") {
classes.push("u-iconfont");
} else {
classes.push(this.customPrefix);
}
// 主题色,通过类配置
if (this.color && this.$u.config.type.includes(this.color))
classes.push("u-icon__icon--" + this.color);
else classes.push("u-icon__icon--primary");
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(" ");
//#endif
return classes;
}
},
methods: {
click() {
this.$emit("click", this.index);
},
touchstart() {
this.$emit("touchstart", this.index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
@import "../../iconfont.css";
.u-icon {
display: inline-flex;
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
position: relative;
&--primary {
color: $u-type-primary;
}
&--success {
color: $u-type-success;
}
&--error {
color: $u-type-error;
}
&--warning {
color: $u-type-warning;
}
&--info {
color: $u-type-info;
}
}
&__decimal {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
}
&__img {
height: auto;
will-change: transform;
}
&__label {
line-height: 1;
}
}
</style>

View File

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

View File

@@ -0,0 +1,89 @@
<template>
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸所以在外面套一个"壳" -->
<view>
<view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
<view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
<slot v-if="useSlot" />
<block v-else>
<text>{{ index }}</text>
</block>
</view>
</view>
</view>
</template>
<script>
/**
* indexAnchor 索引列表锚点
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
* @property {Boolean} use-slot 是否使用自定义内容的插槽默认false
* @property {String Number} index 索引字符如果定义了use-slot此参数自动失效
* @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
* @event {Function} default 锚点位置显示内容,默认为索引字符
* @example <u-index-anchor :index="item" />
*/
export default {
name: "u-index-anchor",
props: {
useSlot: {
type: Boolean,
default: false
},
index: {
type: String,
default: ''
},
customStyle: {
type: Object,
default () {
return {}
}
}
},
data() {
return {
active: false,
wrapperStyle: {},
anchorStyle: {}
}
},
created() {
this.parent = false;
},
mounted() {
this.parent = this.$u.$parent.call(this, 'u-index-list');
if(this.parent) {
this.parent.children.push(this);
this.parent.updateData();
}
},
computed: {
customAnchorStyle() {
return Object.assign(this.anchorStyle, this.customStyle);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-index-anchor {
box-sizing: border-box;
padding: 14rpx 24rpx;
color: #606266;
width: 100%;
font-weight: 500;
font-size: 28rpx;
line-height: 1.2;
background-color: rgb(245, 245, 245);
}
.u-index-anchor--active {
right: 0;
left: 0;
color: #2979ff;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,315 @@
<template>
<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸所以在外面套一个"壳" -->
<view>
<view class="u-index-bar">
<slot />
<view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
<view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
:data-index="index">
{{ item }}
</view>
</view>
<view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
zIndex: alertZIndex
}">
<text>{{indexList[touchmoveIndex]}}</text>
</view>
</view>
</view>
</template>
<script>
var indexList = function() {
var indexList = [];
var charCodeOfA = 'A'.charCodeAt(0);
for (var i = 0; i < 26; i++) {
indexList.push(String.fromCharCode(charCodeOfA + i));
}
return indexList;
};
/**
* indexList 索引列表
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
* @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
* @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
* @property {Array} index-list 索引字符列表数组默认A-Z
* @property {Number String} z-index 锚点吸顶时的层级默认965
* @property {Boolean} sticky 是否开启锚点自动吸顶默认true
* @property {Number String} offset-top 锚点自动吸顶时与顶部的距离默认0
* @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff
* @event {Function} select 选中右边索引字符时触发
* @example <u-index-list :scrollTop="scrollTop"></u-index-list>
*/
export default {
name: "u-index-list",
props: {
sticky: {
type: Boolean,
default: true
},
zIndex: {
type: [Number, String],
default: ''
},
scrollTop: {
type: [Number, String],
default: 0,
},
offsetTop: {
type: [Number, String],
default: 0
},
indexList: {
type: Array,
default () {
return indexList()
}
},
activeColor: {
type: String,
default: '#2979ff'
}
},
created() {
// #ifdef H5
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
// #endif
// #ifndef H5
this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
// #endif
// 只能在created生命周期定义children如果在data定义会因为循环引用而报错
this.children = [];
},
data() {
return {
activeAnchorIndex: 0,
showSidebar: true,
// children: [],
touchmove: false,
touchmoveIndex: 0,
}
},
watch: {
scrollTop() {
this.updateData()
}
},
computed: {
// 弹出toast的z-index值
alertZIndex() {
return this.$u.zIndex.toast;
}
},
methods: {
updateData() {
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.showSidebar = !!this.children.length;
this.setRect().then(() => {
this.onScroll();
});
}, 0);
},
setRect() {
return Promise.all([
this.setAnchorsRect(),
this.setListRect(),
this.setSiderbarRect()
]);
},
setAnchorsRect() {
return Promise.all(this.children.map((anchor, index) => anchor
.$uGetRect('.u-index-anchor-wrapper')
.then((rect) => {
Object.assign(anchor, {
height: rect.height,
top: rect.top
});
})));
},
setListRect() {
return this.$uGetRect('.u-index-bar').then((rect) => {
Object.assign(this, {
height: rect.height,
top: rect.top + this.scrollTop
});
});
},
setSiderbarRect() {
return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
this.sidebar = {
height: rect.height,
top: rect.top
};
});
},
getActiveAnchorIndex() {
const {
children
} = this;
const {
sticky
} = this;
for (let i = this.children.length - 1; i >= 0; i--) {
const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
const reachTop = sticky ? preAnchorHeight : 0;
if (reachTop >= children[i].top) {
return i;
}
}
return -1;
},
onScroll() {
const {
children = []
} = this;
if (!children.length) {
return;
}
const {
sticky,
stickyOffsetTop,
zIndex,
scrollTop,
activeColor
} = this;
const active = this.getActiveAnchorIndex();
this.activeAnchorIndex = active;
if (sticky) {
let isActiveAnchorSticky = false;
if (active !== -1) {
isActiveAnchorSticky =
children[active].top <= 0;
}
children.forEach((item, index) => {
if (index === active) {
let wrapperStyle = '';
let anchorStyle = {
color: `${activeColor}`
};
if (isActiveAnchorSticky) {
wrapperStyle = {
height: `${children[index].height}px`
};
anchorStyle = {
position: 'fixed',
top: `${stickyOffsetTop}px`,
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
color: `${activeColor}`
};
}
item.active = active;
item.wrapperStyle = wrapperStyle;
item.anchorStyle = anchorStyle;
} else if (index === active - 1) {
const currentAnchor = children[index];
const currentOffsetTop = currentAnchor.top;
const targetOffsetTop = index === children.length - 1 ?
this.top :
children[index + 1].top;
const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
const translateY = parentOffsetHeight - currentAnchor.height;
const anchorStyle = {
position: 'relative',
transform: `translate3d(0, ${translateY}px, 0)`,
zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
color: `${activeColor}`
};
item.active = active;
item.anchorStyle = anchorStyle;
} else {
item.active = false;
item.anchorStyle = '';
item.wrapperStyle = '';
}
});
}
},
onTouchMove(event) {
this.touchmove = true;
const sidebarLength = this.children.length;
const touch = event.touches[0];
const itemHeight = this.sidebar.height / sidebarLength;
let clientY = 0;
clientY = touch.clientY;
let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
if (index < 0) {
index = 0;
} else if (index > sidebarLength - 1) {
index = sidebarLength - 1;
}
this.touchmoveIndex = index;
this.scrollToAnchor(index);
},
onTouchStop() {
this.touchmove = false;
this.scrollToAnchorIndex = null;
},
scrollToAnchor(index) {
if (this.scrollToAnchorIndex === index) {
return;
}
this.scrollToAnchorIndex = index;
const anchor = this.children.find((item) => item.index === this.indexList[index]);
if (anchor) {
this.$emit('select', anchor.index);
uni.pageScrollTo({
duration: 0,
scrollTop: anchor.top + this.scrollTop
});
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-index-bar {
position: relative
}
.u-index-bar__sidebar {
position: fixed;
top: 50%;
right: 0;
@include vue-flex;
flex-direction: column;
text-align: center;
transform: translateY(-50%);
user-select: none;
z-index: 99;
}
.u-index-bar__index {
font-weight: 500;
padding: 8rpx 18rpx;
font-size: 22rpx;
line-height: 1
}
.u-indexed-list-alert {
position: fixed;
width: 120rpx;
height: 120rpx;
right: 90rpx;
top: 50%;
margin-top: -60rpx;
border-radius: 24rpx;
font-size: 50rpx;
color: #fff;
background-color: rgba(0, 0, 0, 0.65);
@include vue-flex;
justify-content: center;
align-items: center;
padding: 0;
z-index: 9999999;
}
.u-indexed-list-alert text {
line-height: 50rpx;
}
</style>

View File

@@ -0,0 +1,451 @@
<template>
<view
class="u-input"
:class="{
'u-input--border': border,
'u-input--error': validateState
}"
:style="{
padding: padding ? padding : `0 ${border ? 20 : 0}rpx`,
borderColor: borderColor,
textAlign: inputAlignCom,
backgroundColor: backgroundColor,
}"
@tap.stop="inputClick"
>
<textarea
v-if="type == 'textarea'"
class="u-input__input u-input__textarea"
:style="[getStyle]"
:value="defaultValue"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
:maxlength="inputMaxlength"
:fixed="fixed"
:focus="focus"
:autoHeight="autoHeight"
:selection-end="uSelectionEnd"
:selection-start="uSelectionStart"
:cursor-spacing="getCursorSpacing"
@input="handleInput"
@blur="handleBlur"
@focus="onFocus"
@confirm="onConfirm"
/>
<input
v-else
class="u-input__input"
:type="type == 'password' ? 'text' : type"
:style="[getStyle]"
:value="defaultValue"
:password="type == 'password' && !showPassword"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled || type === 'select'"
:maxlength="inputMaxlength"
:focus="focus"
:confirmType="confirmType"
:cursor-spacing="getCursorSpacing"
:selection-end="uSelectionEnd"
:selection-start="uSelectionStart"
@focus="onFocus"
@blur="handleBlur"
@input="handleInput"
@confirm="onConfirm"
/>
<view class="u-input__right-icon u-flex">
<view
class="u-input__right-icon__clear u-input__right-icon__item"
@tap="onClear"
v-if="clearableCom && valueCom != '' && focused"
>
<u-icon size="32" name="close-circle-fill" color="#c0c4cc" />
</view>
<view
class="u-input__right-icon__clear u-input__right-icon__item"
v-if="passwordIcon && type == 'password'"
>
<u-icon
size="32"
:name="!showPassword ? 'eye' : 'eye-fill'"
color="#c0c4cc"
@click="showPassword = !showPassword"
/>
</view>
<view
class="u-input__right-icon--select u-input__right-icon__item"
v-if="type == 'select'"
:class="{
'u-input__right-icon--select--reverse': selectOpen
}"
>
<u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
</view>
</view>
</view>
</template>
<script>
import Emitter from "../../libs/util/emitter.js";
/**
* input 输入框
* @description 此组件为一个输入框默认没有边框和样式是专门为配合表单组件u-form而设计的利用它可以快速实现表单验证输入内容下拉选择等功能。
* @tutorial http://uviewui.com/components/input.html
* @property {String} type 模式选择,见官网说明
* @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
* @property {} v-model 用于双向绑定输入框的值
* @property {String} input-align 输入框文字的对齐方式(默认left)
* @property {String} placeholder placeholder显示值(默认 '请输入内容')
* @property {Boolean} disabled 是否禁用输入框(默认false)
* @property {String Number} maxlength 输入框的最大可输入长度(默认140)
* @property {String Number} selection-start 光标起始位置自动聚焦时有效需与selection-end搭配使用默认-1
* @property {String Number} maxlength 光标结束位置自动聚焦时有效需与selection-start搭配使用默认-1
* @property {String Number} cursor-spacing 指定光标与键盘的距离单位px(默认0)
* @property {String} placeholderStyle placeholder的样式字符串形式如"color: red;"(默认 "color: #c0c4cc;")
* @property {String} confirm-type 设置键盘右下角按钮的文字仅在type为text时生效(默认done)
* @property {Object} custom-style 自定义输入框的样式,对象形式
* @property {Boolean} focus 是否自动获得焦点(默认false)
* @property {Boolean} fixed 如果type为textarea且在一个"position:fixed"的区域需要指明为true(默认false)
* @property {Boolean} password-icon type为password时是否显示右侧的密码查看图标(默认true)
* @property {Boolean} border 是否显示边框(默认false)
* @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
* @property {Boolean} auto-height 是否自动增高输入区域type为textarea时有效(默认true)
* @property {String Number} height 高度单位rpx(text类型时为70textarea时为100)
* @example <u-input v-model="value" :type="type" :border="border" />
*/
export default {
name: "u-input",
emits: ["update:modelValue", "input", "change", "blur", "focus", "click", "touchstart"],
mixins: [Emitter],
props: {
value: {
type: [String, Number],
default: ""
},
modelValue: {
type: [String, Number],
default: ""
},
// 输入框的类型textareatextnumber
type: {
type: String,
default: "text"
},
inputAlign: {
type: String,
default: ""
},
placeholder: {
type: String,
default: "请输入内容"
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
placeholderStyle: {
type: String,
default: "color: #c0c4cc;"
},
confirmType: {
type: String,
default: "done"
},
// 输入框的自定义样式
customStyle: {
type: Object,
default() {
return {};
}
},
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
fixed: {
type: Boolean,
default: false
},
// 是否自动获得焦点
focus: {
type: Boolean,
default: false
},
// 密码类型时,是否显示右侧的密码图标
passwordIcon: {
type: Boolean,
default: true
},
// input|textarea是否显示边框
border: {
type: Boolean,
default: false
},
// 输入框的边框颜色
borderColor: {
type: String,
default: "#dcdfe6"
},
autoHeight: {
type: Boolean,
default: true
},
// type=select时旋转右侧的图标标识当前处于打开还是关闭select的状态
// open-打开close-关闭
selectOpen: {
type: Boolean,
default: false
},
// 高度单位rpx
height: {
type: [Number, String],
default: ""
},
// 是否可清空
clearable: {
type: [Boolean, String],
},
// 指定光标与键盘的距离,单位 px
cursorSpacing: {
type: [Number, String],
default: 0
},
// 光标起始位置自动聚焦时有效需与selection-end搭配使用
selectionStart: {
type: [Number, String],
default: -1
},
// 光标结束位置自动聚焦时有效需与selection-start搭配使用
selectionEnd: {
type: [Number, String],
default: -1
},
// 是否自动去除两端的空格
trim: {
type: Boolean,
default: true
},
// 是否显示键盘上方带有”完成“按钮那一栏
showConfirmbar: {
type: Boolean,
default: true
},
// input的背景色
backgroundColor: {
type: String,
},
// input的padding
padding: {
type: String,
},
},
data() {
return {
defaultValue: "",
inputHeight: 70, // input的高度
textareaHeight: 100, // textarea的高度
validateState: false, // 当前input的验证状态用于错误时边框是否改为红色
focused: false, // 当前是否处于获得焦点的状态
showPassword: false, // 是否预览密码
lastValue: "" ,// 用于头条小程序,判断@input中前后的值是否发生了变化因为头条中文下按下键没有输入内容也会触发@input时间
uForm:{
inputAlign: "",
clearable: ""
}
};
},
watch: {
valueCom(nVal, oVal) {
this.defaultValue = nVal;
// 当值发生变化且为select类型时(此时input被设置为disabled不会触发@input事件),模拟触发@input事件
if (nVal != oVal && this.type == "select")
this.handleInput({
detail: {
value: nVal
}
});
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
inputAlignCom(){
return this.inputAlign || this.uForm.inputAlign || "left";
},
clearableCom(){
if (typeof this.clearable == "boolean") return this.clearable;
if (typeof this.uForm.clearable == "boolean") return this.uForm.clearable;
return true;
},
// 因为uniapp的input组件的maxlength组件必须要数值这里转为数值给用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength);
},
getStyle() {
let style = {};
// 如果没有自定义高度就根据type为input还是textare来分配一个默认的高度
style.minHeight = this.height
? this.height + "rpx"
: this.type == "textarea"
? this.textareaHeight + "rpx"
: this.inputHeight + "rpx";
style = Object.assign(style, this.customStyle);
return style;
},
//
getCursorSpacing() {
return Number(this.cursorSpacing);
},
// 光标起始位置
uSelectionStart() {
return String(this.selectionStart);
},
// 光标结束位置
uSelectionEnd() {
return String(this.selectionEnd);
}
},
created() {
// 监听u-form-item发出的错误事件将输入框边框变红色
// #ifndef VUE3
this.$on("onFormItemError", this.onFormItemError);
// #endif
this.defaultValue = this.valueCom;
},
mounted() {
let parent = this.$u.$parent.call(this, 'u-form');
if (parent) {
Object.keys(this.uForm).map(key => {
this.uForm[key] = parent[key];
});
}
},
methods: {
/**
* change 事件
* @param event
*/
handleInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if (this.trim) value = this.$u.trim(value);
// vue 原生的方法 return 出去
this.$emit("input", value);
this.$emit("update:modelValue", value);
// 当前model 赋值
this.defaultValue = value;
// 过一个生命周期再发送事件给u-form-item否则this.$emit('input')更新了父组件的值,但是微信小程序上
// 尚未更新到u-form-item导致获取的值为空从而校验混论
// 这里不能延时时间太短或者使用this.$nextTick否则在头条上会造成混乱
setTimeout(() => {
// 头条小程序由于自身bug导致中文下每按下一个键(尚未完成输入),都会触发一次@input导致错误这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$u.trim(value) == this.lastValue) return;
this.lastValue = value;
// #endif
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldChange", value);
}, 40);
},
/**
* blur 事件
* @param event
*/
handleBlur(event) {
// 最开始使用的是监听图标@touchstart事件自从hx2.8.4后,此方法在微信小程序出错
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件导致图标消失而无法点击这里做一个延时
setTimeout(() => {
this.focused = false;
}, 100);
// vue 原生的方法 return 出去
this.$emit("blur", event.detail.value);
setTimeout(() => {
// 头条小程序由于自身bug导致中文下每按下一个键(尚未完成输入),都会触发一次@input导致错误这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$u.trim(value) == this.lastValue) return;
this.lastValue = value;
// #endif
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldBlur", event.detail.value);
}, 40);
},
onFormItemError(status) {
this.validateState = status;
},
onFocus(event) {
this.focused = true;
this.$emit("focus");
},
onConfirm(e) {
this.$emit("confirm", e.detail.value);
},
onClear(event) {
this.$emit("input", "");
this.$emit("update:modelValue", "");
},
inputClick() {
this.$emit("click");
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-input {
position: relative;
flex: 1;
@include vue-flex;
&__input {
//height: $u-form-item-height;
font-size: 28rpx;
color: $u-main-color;
flex: 1;
}
&__textarea {
width: auto;
font-size: 28rpx;
color: $u-main-color;
padding: 10rpx 0;
line-height: normal;
flex: 1;
}
&--border {
border-radius: 6rpx;
border-radius: 4px;
border: 1px solid $u-form-item-border-color;
}
&--error {
border-color: $u-type-error !important;
}
&__right-icon {
&__item {
margin-left: 10rpx;
}
&--select {
transition: transform 0.4s;
&--reverse {
transform: rotate(-180deg);
}
}
}
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<u-popup
class=""
:blur="blur"
:mask="mask"
:maskCloseAble="maskCloseAble"
mode="bottom"
:popup="false"
v-model="popupValue"
length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom"
@close="popupClose"
:zIndex="uZIndex"
>
<slot />
<view class="u-tooltip" v-if="tooltip">
<view
class="u-tooltip-item u-tooltip-cancel"
hover-class="u-tooltip-cancel-hover"
@tap="onCancel"
>
{{ cancelBtn ? cancelText : "" }}
</view>
<view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
{{
tips ? tips : mode == "number" ? "数字键盘" : mode == "card" ? "身份证键盘" : "车牌号键盘"
}}
</view>
<view
v-if="confirmBtn"
@tap="onConfirm"
class="u-tooltip-item u-tooltips-submit"
hover-class="u-tooltips-submit-hover"
>
{{ confirmBtn ? confirmText : "" }}
</view>
</view>
<block v-if="mode == 'number' || mode == 'card'">
<u-number-keyboard
:random="random"
@backspace="backspace"
@change="change"
:mode="mode"
:dotEnabled="dotEnabled"
></u-number-keyboard>
</block>
<block v-else>
<u-car-keyboard
ref="uCarKeyboard"
:random="random"
@backspace="backspace"
@change="change"
></u-car-keyboard>
</block>
</u-popup>
</template>
<script>
/**
* keyboard 键盘
* @description 此为uViw自定义的键盘面板内含了数字键盘车牌号键身份证号键盘3中模式都有可以打乱按键顺序的选项。
* @tutorial https://www.uviewui.com/components/keyboard.html
* @property {String} mode 键盘类型见官网基本使用的说明默认number
* @property {Boolean} dot-enabled 是否显示"."按键只在mode=number时有效默认true
* @property {Boolean} tooltip 是否显示键盘顶部工具条默认true
* @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
* @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮默认true
* @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮默认true
* @property {Boolean} mask 是否显示遮罩默认true
* @property {String} confirm-text 确认按钮的文字
* @property {String} cancel-text 取消按钮的文字
* @property {Number String} z-index 弹出键盘的z-index值默认1075
* @property {Boolean} random 是否打乱键盘按键的顺序默认false
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配默认false
* @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘默认true
* @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",
emits: ["update:modelValue", "input", "change", "cancel", "confirm", "backspace"],
props: {
// 通过双向绑定控制键盘的弹出与收起
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// 键盘的类型number-数字键盘card-身份证键盘car-车牌号键盘
mode: {
type: String,
default: "number"
},
// 是否显示键盘的"."符号
dotEnabled: {
type: Boolean,
default: true
},
// 是否显示顶部工具条
tooltip: {
type: Boolean,
default: true
},
// 是否显示工具条中间的提示
showTips: {
type: Boolean,
default: true
},
// 工具条中间的提示文字
tips: {
type: String,
default: ""
},
// 是否显示工具条左边的"取消"按钮
cancelBtn: {
type: Boolean,
default: true
},
// 是否显示工具条右边的"完成"按钮
confirmBtn: {
type: Boolean,
default: true
},
// 是否打乱键盘按键的顺序
random: {
type: Boolean,
default: false
},
// 是否开启底部安全区适配开启的话会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否允许通过点击遮罩关闭键盘
maskCloseAble: {
type: Boolean,
default: true
},
// 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
mask: {
type: Boolean,
default: true
},
// z-index值
zIndex: {
type: [Number, String],
default: ""
},
// 取消按钮的文字
cancelText: {
type: String,
default: "取消"
},
// 确认按钮的文字
confirmText: {
type: String,
default: "确认"
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
}
},
data() {
return {
popupValue: false
};
},
watch: {
valueCom:{
immediate: true,
handler(v1, v2) {
this.popupValue = v1;
}
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
methods: {
change(e) {
this.$emit("change", e);
},
// 键盘关闭
popupClose() {
// 通过发送input这个特殊的事件名可以修改父组件传给props的value的变量也即双向绑定
this.$emit("input", false);
this.$emit("update:modelValue", false);
},
// 输入完成
onConfirm() {
this.popupClose();
this.$emit("confirm");
},
// 取消输入
onCancel() {
this.popupClose();
this.$emit("cancel");
},
// 退格键
backspace(count) {
this.$emit("backspace", count);
},
changeCarInputMode() {
if (this.$refs.uCarKeyboard) this.$refs.uCarKeyboard.changeCarInputMode();
},
updateCarInputMode(abcKey) {
if (this.$refs.uCarKeyboard) this.$refs.uCarKeyboard.updateCarInputMode(abcKey);
}
// 关闭键盘
// close() {
// this.show = false;
// },
// // 打开键盘
// open() {
// this.show = true;
// }
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-keyboard {
position: relative;
z-index: 1003;
}
.u-tooltip {
@include vue-flex;
justify-content: space-between;
}
.u-tooltip-item {
color: #333333;
flex: 0 0 33.333333%;
text-align: center;
padding: 20rpx 10rpx;
font-size: 28rpx;
}
.u-tooltips-submit {
text-align: right;
flex-grow: 1;
flex-wrap: 0;
padding-right: 40rpx;
color: $u-type-primary;
}
.u-tooltip-cancel {
text-align: left;
flex-grow: 1;
flex-wrap: 0;
padding-left: 40rpx;
color: #888888;
}
.u-tooltips-submit-hover {
color: $u-type-success;
}
.u-tooltip-cancel-hover {
color: #333333;
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<view class="u-wrap" :style="{
opacity: Number(opacity),
borderRadius: borderRadius + 'rpx',
// 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
transition: `opacity ${time / 1000}s ease-in-out`
}"
:class="'u-lazy-item-' + elIndex">
<view :class="'u-lazy-item-' + elIndex">
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" v-if="!isError" class="u-lazy-item"
:src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded" @error="loadError" @tap="clickImg"></image>
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" class="u-lazy-item error" v-else :src="errorImg"
:mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image>
</view>
</view>
</template>
<script>
/**
* lazyLoad 懒加载
* @description 懒加载使用的场景为页面有很多图片时APP会同时加载所有的图片导致页面卡顿各个位置的图片出现前后不一致等.
* @tutorial https://www.uviewui.com/components/lazyLoad.html
* @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
* @property {String} image 图片路径
* @property {String} loading-img 预加载时的占位图
* @property {String} error-img 图片加载出错时的占位图
* @property {String} threshold 触发加载时的位置,见上方说明,单位 rpx默认300
* @property {String Number} duration 图片加载成功时淡入淡出时间单位ms默认
* @property {String} effect 图片加载成功时淡入淡出的css动画效果默认ease-in-out
* @property {Boolean} is-effect 图片加载成功时是否启用淡入淡出效果默认true
* @property {String Number} border-radius 图片圆角值单位rpx默认0
* @property {String Number} height 图片高度注意实际高度可能受img-mode参数影响默认450
* @property {String Number} mg-mode 图片的裁剪模式详见image组件裁剪模式默认widthFix
* @event {Function} click 点击图片时触发
* @event {Function} load 图片加载成功时触发
* @event {Function} error 图片加载失败时触发
* @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
*/
export default {
name: 'u-lazy-load',
emits: ["click", "load", "error"],
props: {
index: {
type: [Number, String]
},
// 要显示的图片
image: {
type: String,
default: ''
},
// 图片裁剪模式
imgMode: {
type: String,
default: 'widthFix'
},
// 占位图片路径
loadingImg: {
type: String,
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
},
// 加载失败的错误占位图
errorImg: {
type: String,
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162trcPDw5ubm7i4uNzc3Obm5s3NzaGhoeHh4cjIyKenp9LS0r29vbKysuvr67sDMEkAAAlpSURBVHja7NzpYqMgAIVRUVHc8/5PO66R1WAbOzX97q+ZtDEpR0AWTR7kVyWhCAAhgABCAAGEAAIIAQQQAggBBBACCCAEEEAIIIAQQAgggBBAACGAAEIAAYQAQgABhAACCAEEEAIIIAQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAsqeX5QWHKIcs/Ptl03lfL4zDFPWfBGmSpPn+IZzSH5KkCL5B+n+oklwz6Iz//R2QzFOabzhEmiRirAmZt/bl0w/dpMbLqeeo4wEdpC7zR5WAPKziHKtO7ql+ReKvIa9BxgNaL5ZtEkpeAGIVp5jKJa09xVo9vgSSzQcszdYvmOqjQNSQ6pHK6rO1n1Xj32788miwHLaZz1Tl9i/yayDlYJ/60/+lp8GSY7OY1B8E4p55bWmfquFk22GLuUUxi78cX+m+BjL2GLkhMrV+/muS6Sfic0CEp5T1Yu2OQdTzsKV0MJV73KVjroyTffxfuv5Tf3fd6iLT9wz8YdVHgUzF2Is9/Xhi5sYJqP1w/GUpjOiHVbaI0w2L+pg3GZzvtokcgHxWDXHaiy78l3sPke01qphamT5c+dqyeAGSumdL/mkggauTam0e3L/mPEiqtzKDbl0Z1Wn8xOa4ySo8X/7TQIJnY/seEKWf12UmC72CKP9xYjr19RPT7NNA+oMO+R0gwmlotAry+C6I0f59ch8yXVQOr0BKYcXt1IUYRyCt+Ur9HGsrQKI79WY9sY9ARPKlzFOFdb41ioD8b5Bp+mqeeRKAxINkESBFGpOpKhgv9OuYpH8A8l4Qa3qp60Kl2/k+rG2sWafuuyCBafb2j4JkgZUob3nWcmicpkxEgmTLLGejTxnWSWCi8lPmsk6DlIHFJv24ojiYyYoGacwL8zXTLEAVaDI/Ybb3NIgKDSv2oXpmHkvNs+PTpMASEdlk7fOZeRk37fwJ6yGnQarQsGIfqqcvx43rTOXY6jf7uKXdRzdLDRPbjIrx1cIj3Kr4KyBFezzgUGuR5893qkOQ19fR2uVBaU+r16LphJNOiatK7PeBZK/Kb+tUn71rcQjSvARpghfH/yG/D2RetTuI3N5QrMWdP46brP7FmKZ//CGQ9At9SL01DLkzY/Vs8Z97fQZ7gelw7jHqCz+/Wile5J4g3Vc79eb5a6oLSue+Ve83gaSv2jp5PxCzjzwFUm9zw9MllSMil1kS4d2E9SaQ1xNo9wMxx0+nQNLnew/WDHvveMAHYm08mofl3TFI/8pD3Q6kMAv6DIi2jTCwRJUvNdDYrrJum9oHhusCbWALonwxBRk1vXMnEGWuT5wAmfYuVGUYpJ7fUZujCN92hvzwWlrFgxSfANKb10DxIMbShnfrynyZZV30imA7P43ArXXHbvBVkTCIuGy25AdBrHmNeBCpL214QdLp9LZarG3IMWrmW0ehtuO7F2PS09UcgqS3B7FKPhpknrStD0HGF/vQRne37LwLG8EbHT4WxN7/Fg0yD9Yr/3br4nnstA+0Il6QxzdBmg8A6a2/IRbkcK9h/uzV8zywF/oSkOyageCPglRWgcWClHnEzs9q/t/SENVXgFijlsq3VtXdCsRp4qObrLLLgjuzSq3fX89ZZW6AfxNIzF6X9FYgThN/fk093KkvHX/hbWd+DqS/FUhlf+G3gohEXzVs3g9iDluWoaW8fL73QhB34u+tIHIf19nLuF4Q98a09Eynnl56q+ePgEhnX+dbQOp6H5XnJ0ACd8dFgkwf12nTOTcEqd2pom+CFF02TIPw6dKmrLS5qOtBpo8b5quUtrwrSGbuqPkeSJqllTFHO02NPxdMrm+y5LKdWyWXjw4vA5nGEtnjuyCFyHqNYvEolzmASm3zK1Eg5zr13lhqV1tlksnVw8Pkwgri7O07AVKLJkutRYw87bPlRpBpNXE8xGb+fhBlvEGrGPLqViu5sILIx9dAmqF1705sxF4M8+R8P5dOdQwi12fMnATpjJ2JSt/POIvU9wPJEs/jduJAjLvU0yFT0i64Yb1bsVi79dA4pEy3TzoHMq2O7Re4vXm5O9+l290NpE4CU+YRIMNye2iaqbVS2AUnn2fsekthYKReVNutVedA5juttyIXrT38mOds+ps9DWhwL7GWc61/DVKPzVN9UHDarf1icU98IOU8tm6L031Iq63t1tKzj3fe/FCpO4F0/i0Z2+yvA1KeGBjqj1qYx8/zoxpKZ1Yl367I1k+sfcft/QPy9csXy/32qX1qLZsrryG5BGQaRj0vc/b7N54XXq293TCLB5HO42Fy517obW19b+qjl3CHp0fdLJcWvmdy1etESi/uAdJrs1hTaUklHuW8qSDdC3UfXVR5cnD3rAFSSqtFb7z7eapErx7rC739jCXfbK3aWiipjXo8UbmxXPa7QQq9R289j2Gr88N7Ag5AlHPRKc37pNZv0CZtX1tVMG6rm8qW1/KlCgQvcMss933ybwXZz3dReW5yce4ByZtHFIhwT9kmjxg8BzbKDUe1PB9edBJqSN7/KM1LmqyuMZ5BpeTUw1aD/uDI0relPfSHa/Wn8Pxq1BNfxy/h3IdwOJqIKumb9CHvTqMefyY82RoQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAGEAAIIAQQQAgggBBBACCAEEEAIIIAQQAAhgABCACGAAEIAAYQAAggBBBACCAEEEAIIIAQQQAggfyL/BBgA8PgLdH0TBtkAAAAASUVORK5CYII='
},
// 图片进入可见区域前多少像素时单位rpx开始加载图片
// 负数为图片超出屏幕底部多少距离后触发懒加载,正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上)
threshold: {
type: [Number, String],
default: 100
},
// 淡入淡出动画的过渡时间
duration: {
type: [Number, String],
default: 500
},
// 渡效果的速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显
// linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
effect: {
type: String,
default: 'ease-in-out'
},
// 是否使用过渡效果
isEffect: {
type: Boolean,
default: true
},
// 圆角值
borderRadius: {
type: [Number, String],
default: 0
},
// 图片高度单位rpx
height: {
type: [Number, String],
default: '450'
}
},
data() {
return {
isShow: false,
opacity: 1,
time: this.duration,
loadStatus: '', // 默认是懒加载中的状态
isError: false, // 图片加载失败
elIndex: this.$u.guid()
}
},
computed: {
// 将threshold从rpx转为px
getThreshold() {
// 先取绝对值因为threshold可能是负数最后根据this.threshold是正数或者负数重新还原
let thresholdPx = uni.upx2px(Math.abs(this.threshold));
return this.threshold < 0 ? -thresholdPx : thresholdPx;
},
// 计算图片的高度可能为auto带%,或者直接数值
imgHeight() {
return this.$u.addUnit(this.height);
}
},
created() {
// 由于一些特殊原因不能将此变量放到data中定义
this.observer = {};
},
watch: {
isShow(nVal) {
// 如果是不开启过渡效果,直接返回
if (!this.isEffect) return;
this.time = 0;
// 原来opacity为1(不透明,是为了显示占位图)改成0(透明,意味着该元素显示的是背景颜色,默认的白色)再改成1是为了获得过渡效果
this.opacity = 0;
// 延时30ms否则在浏览器H5过渡效果无效
setTimeout(() => {
this.time = this.duration;
this.opacity = 1;
}, 30)
},
// 图片路径发生变化时需要重新标记一些变量否则会一直卡在某一个状态比如isError
image(n) {
if(!n) {
// 如果传入null或者''或者undefined标记为错误状态
this.isError = true;
} else {
this.init();
this.isError = false;
}
}
},
methods: {
// 用于重新初始化
init() {
this.isError = false;
this.loadStatus = '';
},
// 点击图片触发的事件,loadlazy-还是懒加载中状态loading-图片正在加载loaded-图片加加载完成
clickImg() {
let whichImg = '';
// 如果isShow为false意味着图片还没开始加载点击的只能是最开始的占位图
if (this.isShow == false) whichImg = 'lazyImg';
// 如果isError为true意味着图片加载失败这是只剩下错误的占位图所以点击的只能是错误占位图
// 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
else if (this.isError == true) whichImg = 'errorImg';
// 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
else whichImg = 'realImg';
// 只通知当前图片的index
this.$emit('click', this.index);
},
// 图片加载完成事件可能是加载占位图时触发也可能是加载真正的图片完成时触发通过isShow区分
imgLoaded() {
// 占位图加载完成
if (this.loadStatus == '') {
this.loadStatus = 'lazyed';
}
// 真正的图片加载完成
else if (this.loadStatus == 'lazyed') {
this.loadStatus = 'loaded';
this.$emit('load', this.index);
}
},
// 错误的图片加载完成
errorImgLoaded() {
this.$emit('error', this.index);
},
// 图片加载失败
loadError() {
this.isError = true;
},
disconnectObserver(observerName) {
const observer = this[observerName];
observer && observer.disconnect();
},
},
// #ifndef VUE3
// 组件销毁前将实例从u-form的缓存中移除
beforeDestroy() {
// 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
//observer.disconnect();
},
// #endif
// #ifdef VUE3
beforeUnmount() {
},
// #endif
mounted() {
// 此uOnReachBottom事件由mixin.js发出目的是让页面到底时保证所有图片都进行加载做到绝对稳定且可靠
this.$nextTick(() => {
uni.$once('uOnReachBottom', () => {
if (!this.isShow) this.isShow = true;
});
})
// mounted的时候不一定挂载了这个元素延时30ms否则会报错或者不报错但是也没有效果
setTimeout(() => {
// 这里是组件内获取布局状态不能用uni.createIntersectionObserver而必须用this.createIntersectionObserver
this.disconnectObserver('contentObserver');
const contentObserver = uni.createIntersectionObserver(this);
// 要理解这里怎么计算的,请看这个:
// https://blog.csdn.net/qq_25324335/article/details/83687695
contentObserver.relativeToViewport({
bottom: this.getThreshold,
}).observe('.u-lazy-item-' + this.elIndex, (res) => {
if (res.intersectionRatio > 0) {
// 懒加载状态改变
this.isShow = true;
// 如果图片已经加载,去掉监听,减少性能的消耗
this.disconnectObserver('contentObserver');
}
})
this.contentObserver = contentObserver;
}, 30)
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-wrap {
background-color: #eee;
overflow: hidden;
}
.u-lazy-item {
width: 100%;
// 骗系统开启硬件加速
transform: transition3d(0, 0, 0);
// 防止图片加载“闪一下”
will-change: transform;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<view class="u-progress" :style="{
borderRadius: round ? '100rpx' : 0,
height: height + 'rpx',
backgroundColor: inactiveColor
}">
<view :class="[
type ? `u-type-${type}-bg` : '',
striped ? 'u-striped' : '',
striped && stripedActive ? 'u-striped-active' : ''
]" class="u-active" :style="[progressStyle]">
<slot v-if="$slots.default || $slots.$default" />
<block v-else-if="showPercent">
{{percent + '%'}}
</block>
</view>
</view>
</template>
<script>
/**
* lineProgress 线型进度条
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
* @tutorial https://www.uviewui.com/components/lineProgress.html
* @property {String Number} percent 进度条百分比值为数值类型0-100
* @property {Boolean} round 进度条两端是否为半圆默认true
* @property {String} type 如设置active-color值将会失效
* @property {String} active-color 进度条激活部分的颜色(默认#19be6b
* @property {String} inactive-color 进度条的底色(默认#ececec
* @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值默认true
* @property {String Number} height 进度条的高度单位rpx默认28
* @property {Boolean} striped 是否显示进度条激活部分的条纹默认false
* @property {Boolean} striped-active 条纹是否具有动态效果默认false
* @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
*/
export default {
name: "u-line-progress",
props: {
// 两端是否显示半圆形
round: {
type: Boolean,
default: true
},
// 主题颜色
type: {
type: String,
default: ''
},
// 激活部分的颜色
activeColor: {
type: String,
default: '#19be6b'
},
inactiveColor: {
type: String,
default: '#ececec'
},
// 进度百分比,数值
percent: {
type: Number,
default: 0
},
// 是否在进度条内部显示百分比的值
showPercent: {
type: Boolean,
default: true
},
// 进度条的高度单位rpx
height: {
type: [Number, String],
default: 28
},
// 是否显示条纹
striped: {
type: Boolean,
default: false
},
// 条纹是否显示活动状态
stripedActive: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
computed: {
progressStyle() {
let style = {};
style.width = this.percent + '%';
if(this.activeColor) style.backgroundColor = this.activeColor;
return style;
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-progress {
overflow: hidden;
height: 15px;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
width: 100%;
border-radius: 100rpx;
}
.u-active {
width: 0;
height: 100%;
align-items: center;
@include vue-flex;
justify-items: flex-end;
justify-content: space-around;
font-size: 20rpx;
color: #ffffff;
transition: all 0.4s ease;
}
.u-striped {
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 39px 39px;
}
.u-striped-active {
animation: progress-stripes 2s linear infinite;
}
@keyframes progress-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 39px 0;
}
}
</style>

View File

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

View File

@@ -0,0 +1,89 @@
<template>
<text class="u-link" @tap.stop="openLink" :style="{
color: color,
fontSize: fontSize + 'rpx',
borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none',
paddingBottom: underLine ? '0rpx' : '0'
}">
<slot></slot>
</text>
</template>
<script>
/**
* link 超链接
* @description 该组件为超链接组件在不同平台有不同表现形式在APP平台会通过plus环境打开内置浏览器在小程序中把链接复制到粘贴板同时提示信息在H5中通过window.open打开链接。
* @tutorial https://www.uviewui.com/components/link.html
* @property {String} color 文字颜色(默认#606266
* @property {String Number} font-size 字体大小单位rpx默认28
* @property {Boolean} under-line 是否显示下划线默认false
* @property {String} href 跳转的链接要带上http(s)
* @property {String} line-color 下划线颜色默认同color参数颜色
* @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
* @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
*/
export default {
name: "u-link",
props: {
// 文字颜色
color: {
type: String,
default: '#2979ff'
},
// 字体大小单位rpx
fontSize: {
type: [String, Number],
default: 28
},
// 是否显示下划线
underLine: {
type: Boolean,
default: false
},
// 要跳转的链接
href: {
type: String,
default: ''
},
// 小程序中复制到粘贴板的提示语
mpTips: {
type: String,
default: '链接已复制,请在浏览器打开'
},
// 下划线颜色
lineColor: {
type: String,
default: ''
}
},
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(() => {
this.$u.toast(this.mpTips);
})
}
});
// #endif
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-link {
line-height: 1;
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<view class="u-loading-page">
</view>
</template>
<script>
export default {
props: {
},
data() {
return {
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,103 @@
<template>
<view v-if="show" class="u-loading" :class="mode == 'circle' ? 'u-loading-circle' : 'u-loading-flower'" :style="[cricleStyle]">
</view>
</template>
<script>
/**
* loading 加载动画
* @description 警此组件为一个小动画目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。
* @tutorial https://www.uviewui.com/components/loading.html
* @property {String} mode 模式选择见官网说明默认circle
* @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#c7c7c7
* @property {String Number} size 加载图标的大小单位rpx默认34
* @property {Boolean} show 是否显示动画默认true
* @example <u-loading mode="circle"></u-loading>
*/
export default {
name: "u-loading",
props: {
// 动画的类型
mode: {
type: String,
default: 'circle'
},
// 动画的颜色
color: {
type: String,
default: '#c7c7c7'
},
// 加载图标的大小单位rpx
size: {
type: [String, Number],
default: '34'
},
// 是否显示动画
show: {
type: Boolean,
default: true
}
},
computed: {
// 加载中圆圈动画的样式
cricleStyle() {
let style = {};
style.width = this.size + 'rpx';
style.height = this.size + 'rpx';
if (this.mode == 'circle') style.borderColor = `#e4e4e4 #e4e4e4 #e4e4e4 ${this.color ? this.color : '#c7c7c7'}`;
return style;
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-loading-circle {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
vertical-align: middle;
width: 28rpx;
height: 28rpx;
background: 0 0;
border-radius: 50%;
border: 2px solid;
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
animation: u-circle 1s linear infinite;
}
.u-loading-flower {
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
-webkit-animation: a 1s steps(12) infinite;
animation: u-flower 1s steps(12) infinite;
background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
background-size: 100%;
}
@keyframes u-flower {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
@-webkit-keyframes u-circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<view class="u-load-more-wrap" :style="{
backgroundColor: bgColor,
marginBottom: marginBottom + 'rpx',
marginTop: marginTop + 'rpx',
height: $u.addUnit(height)
}">
<u-line color="#d4d4d4" length="50"></u-line>
<!-- 加载中和没有更多的状态才显示两边的横线 -->
<view :class="status == 'loadmore' || status == 'nomore' ? 'u-more' : ''" class="u-load-more-inner">
<view class="u-loadmore-icon-wrap">
<u-loading class="u-loadmore-icon" :color="iconColor" :mode="iconType == 'circle' ? 'circle' : 'flower'" :show="status == 'loading' && icon"></u-loading>
</view>
<!-- 如果没有更多的状态下显示内容为dot粗点加载特定样式 -->
<view class="u-line-1" :style="[loadTextStyle]" :class="[(status == 'nomore' && isDot == true) ? 'u-dot-text' : 'u-more-text']" @tap="loadMore">
{{ showText }}
</view>
</view>
<u-line color="#d4d4d4" length="50"></u-line>
</view>
</template>
<script>
/**
* loadmore 加载更多
* @description 此组件一般用于标识页面底部加载数据时的状态。
* @tutorial https://www.uviewui.com/components/loadMore.html
* @property {String} status 组件状态默认loadmore
* @property {String} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff
* @property {Boolean} icon 加载中时是否显示图标默认true
* @property {String} icon-type 加载中时的图标类型默认circle
* @property {String} icon-color icon-type为circle时有效加载中的动画图标的颜色默认#b7b7b7
* @property {Boolean} is-dot status为nomore时内容显示为一个"●"默认false
* @property {String} color 字体颜色(默认#606266
* @property {String Number} margin-top 到上一个相邻元素的距离
* @property {String Number} margin-bottom 到下一个相邻元素的距离
* @property {Object} load-text 自定义显示的文字,见上方说明示例
* @event {Function} loadmore status为loadmore时点击组件会发出此事件
* @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" />
*/
export default {
name: "u-loadmore",
emits: ["loadmore"],
props: {
// 组件背景色
bgColor: {
type: String,
default: 'transparent'
},
// 是否显示加载中的图标
icon: {
type: Boolean,
default: true
},
// 字体大小
fontSize: {
type: String,
default: '28'
},
// 字体颜色
color: {
type: String,
default: '#606266'
},
// 组件状态loadmore-加载前的状态loading-加载中的状态nomore-没有更多的状态
status: {
type: String,
default: 'loadmore'
},
// 加载中状态的图标flower-花朵状图标circle-圆圈状图标
iconType: {
type: String,
default: 'circle'
},
// 显示的文字
loadText: {
type: Object,
default () {
return {
loadmore: '加载更多',
loading: '正在加载...',
nomore: '没有更多了'
}
}
},
// 在“没有更多”状态下,是否显示粗点
isDot: {
type: Boolean,
default: false
},
// 加载中显示圆圈动画时,动画的颜色
iconColor: {
type: String,
default: '#b7b7b7'
},
// 上边距
marginTop: {
type: [String, Number],
default: 0
},
// 下边距
marginBottom: {
type: [String, Number],
default: 0
},
// 高度单位rpx
height: {
type: [String, Number],
default: 'auto'
}
},
data() {
return {
// 粗点
dotText: "●"
}
},
computed: {
// 加载的文字显示的样式
loadTextStyle() {
return {
color: this.color,
fontSize: this.fontSize + 'rpx',
position: 'relative',
zIndex: 1,
backgroundColor: this.bgColor,
// 如果是加载中状态,动画和文字需要距离近一点
}
},
// 加载中圆圈动画的样式
cricleStyle() {
return {
borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}`
}
},
// 加载中花朵动画形式
// 动画由base64图片生成暂不支持修改
flowerStyle() {
return {
}
},
// 显示的提示文字
showText() {
let text = '';
if(this.status == 'loadmore') text = this.loadText.loadmore;
else if(this.status == 'loading') text = this.loadText.loading;
else if(this.status == 'nomore' && this.isDot) text = this.dotText;
else text = this.loadText.nomore;
return text;
}
},
methods: {
loadMore() {
// 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
if(this.status == 'loadmore') this.$emit('loadmore');
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
/* #ifdef MP */
// 在mp.scss中赋予了u-line为flex: 1这里需要一个明确的长度所以重置掉它
// 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用
u-line {
flex: none;
}
/* #endif */
.u-load-more-wrap {
@include vue-flex;
justify-content: center;
align-items: center;
}
.u-load-more-inner {
@include vue-flex;
justify-content: center;
align-items: center;
padding: 0 12rpx;
}
.u-more {
position: relative;
@include vue-flex;
justify-content: center;
}
.u-dot-text {
font-size: 28rpx;
}
.u-loadmore-icon-wrap {
margin-right: 8rpx;
}
.u-loadmore-icon {
@include vue-flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle, filterStyle]" @tap="click" @touchmove.stop.prevent="() => {}" :class="{
'u-mask-zoom': zoom,
'u-mask-show': show
}">
<slot />
</view>
</template>
<script>
/**
* mask 遮罩
* @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
* @tutorial https://www.uviewui.com/components/mask.html
* @property {Boolean} show 是否显示遮罩默认false
* @property {String Number} z-index z-index 层级默认1070
* @property {Object} custom-style 自定义样式对象,见上方说明
* @property {String Number} duration 动画时长单位毫秒默认300
* @property {Boolean} zoom 是否使用scale对遮罩进行缩放默认true
* @property {Boolean} mask-click-able 遮罩是否可点击为false时点击不会发送click事件默认true
* @event {Function} click mask-click-able为true时点击遮罩发送此事件
* @example <u-mask :show="show" @click="show = false"></u-mask>
*/
export default {
name: "u-mask",
emits: ["click"],
props: {
// 是否显示遮罩
show: {
type: Boolean,
default: false
},
// 层级z-index
zIndex: {
type: [Number, String],
default: ''
},
// 用户自定义样式
customStyle: {
type: Object,
default () {
return {}
}
},
// 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放
zoom: {
type: Boolean,
default: true
},
// 遮罩的过渡时间单位为ms
duration: {
type: [Number, String],
default: 300
},
// 是否可以通过点击遮罩进行关闭
maskClickAble: {
type: Boolean,
default: true
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
},
},
data() {
return {
zoomStyle: {
transform: ''
},
scale: 'scale(1.2, 1.2)'
}
},
watch: {
show(n) {
if(n && this.zoom) {
// 当展示遮罩的时候设置scale为1达到缩小(原来为1.2)的效果
this.zoomStyle.transform = 'scale(1, 1)';
} else if(!n && this.zoom) {
// 当隐藏遮罩的时候设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果
this.zoomStyle.transform = this.scale;
}
}
},
computed: {
maskStyle() {
let style = {};
style.backgroundColor = "rgba(0, 0, 0, 0.6)";
if(this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask;
else style.zIndex = -1;
style.transition = `all ${this.duration / 1000}s ease-in-out`;
// 判断用户传递的对象是否为空,不为空就进行合并
if (Object.keys(this.customStyle).length) style = {
...style,
...this.customStyle
};
return style;
},
filterStyle() {
let { blur } = this;
let style = {};
if (blur) {
style.backdropFilter = `blur(${blur}rpx)`;
}
return style;
}
},
methods: {
click() {
if (!this.maskClickAble) return;
this.$emit('click');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transition: transform 0.3s;
}
.u-mask-show {
opacity: 1;
}
.u-mask-zoom {
transform: scale(1.2, 1.2);
}
</style>

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