完善生产查询与登录权限配置
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* actionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
*/
|
||||
export const ActionSheetItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 标题 */
|
||||
text: { type: String, default: '' },
|
||||
/** 描述 */
|
||||
subText: { type: String, default: '' },
|
||||
/** 边距 */
|
||||
padding: { type: [Number, String] as PropType<number | string>, default: '34rpx 0' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: () => getColor('mainColor') },
|
||||
/** 字体大小 */
|
||||
fontSize: { type: [String, Number], default: '32rpx' },
|
||||
/** 是否禁用 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 是否异步关闭 */
|
||||
asyncClose: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type ActionSheetItemProps = ExtractPropTypes<typeof ActionSheetItemProps>;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-action-sheet-item u-line-1"
|
||||
:class="[!isLast ? 'u-border-bottom' : '', disabled ? 'u-action-sheet-item--disabled' : '', customClass]"
|
||||
:style="$u.toStyle(itemStyle, customStyle)"
|
||||
@click="handleClick"
|
||||
@touchmove.stop.prevent
|
||||
hover-class="u-hover-class"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<slot>
|
||||
<text>{{ text }}</text>
|
||||
<text class="u-action-sheet-item__subtext u-line-1" v-if="subText">{{ subText }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-action-sheet-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ActionSheetItemProps } from './types';
|
||||
import { $u, useChildren } from '../../libs';
|
||||
|
||||
const props = defineProps(ActionSheetItemProps);
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const { parent, parentExposed, childIndex, emitToParent } = useChildren('u-action-sheet-item', 'u-action-sheet');
|
||||
|
||||
const isLast = computed(() => {
|
||||
return (childIndex?.value ?? 0) + 1 >= (parent?.value?.getChildren()?.length ?? 0);
|
||||
});
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
style.color = props.color || parentExposed?.value?.props?.color;
|
||||
style.fontSize = $u.addUnit(props.fontSize || parentExposed?.value?.props?.fontSize);
|
||||
style.padding = $u.addUnit(props.padding);
|
||||
if (props.disabled) style.color = 'var(--u-light-color)';
|
||||
return style;
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
if (props.disabled) return;
|
||||
emit('click', childIndex?.value || 0);
|
||||
if (!props.asyncClose && !parentExposed?.value?.props?.asyncClose) {
|
||||
emitToParent('itemClick', childIndex?.value || 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.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--disabled {
|
||||
color: $u-light-color;
|
||||
}
|
||||
|
||||
.u-action-sheet-item__subtext {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ActionSheetItem, ActionSheetTips } from '../../types/global';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* actionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
*/
|
||||
export const ActionSheetProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 点击遮罩是否可以关闭actionSheet */
|
||||
maskCloseAble: { type: Boolean, default: true },
|
||||
/** 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx */
|
||||
list: {
|
||||
type: Array as unknown as PropType<ActionSheetItem[]>,
|
||||
default: () => []
|
||||
},
|
||||
/** 顶部的提示文字 */
|
||||
tips: {
|
||||
type: Object as unknown as PropType<ActionSheetTips>,
|
||||
default: () => ({ text: '', color: getColor('tipsColor'), fontSize: '26rpx' })
|
||||
},
|
||||
/** 底部的取消按钮 */
|
||||
cancelBtn: { type: Boolean, default: true },
|
||||
/** 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 */
|
||||
safeAreaInsetBottom: { type: Boolean, default: false },
|
||||
/** 通过双向绑定控制组件的弹出与收起 */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 弹出的顶部圆角值 */
|
||||
borderRadius: { type: [String, Number], default: 0 },
|
||||
/** 弹出的z-index值 */
|
||||
zIndex: { type: [String, Number], default: 0 },
|
||||
/** 取消按钮的文字提示 */
|
||||
cancelText: { type: String, default: '' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: () => getColor('mainColor') },
|
||||
/** 字体大小 */
|
||||
fontSize: { type: [String, Number], default: '32rpx' },
|
||||
/** 是否异步关闭 */
|
||||
asyncClose: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type ActionSheetProps = ExtractPropTypes<typeof ActionSheetProps>;
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<u-popup
|
||||
mode="bottom"
|
||||
:border-radius="borderRadius"
|
||||
:popup="false"
|
||||
v-model="popupValue"
|
||||
:maskCloseAble="maskCloseAble"
|
||||
length="auto"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom"
|
||||
@close="popupClose"
|
||||
:z-index="uZIndex"
|
||||
:custom-class="customClass"
|
||||
:custom-style="customStyle"
|
||||
>
|
||||
<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
|
||||
{{ tips.text }}
|
||||
</view>
|
||||
<block v-if="list && list.length > 0" v-for="(item, index) in list" :key="index">
|
||||
<view
|
||||
@touchmove.stop.prevent
|
||||
@tap="itemClick(index)"
|
||||
:style="[itemStyle(index)]"
|
||||
class="u-action-sheet-item u-line-1"
|
||||
:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<text>{{ item.text }}</text>
|
||||
<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{ item.subText }}</text>
|
||||
</view>
|
||||
</block>
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<view class="u-gab" v-if="cancelBtn"> </view>
|
||||
<view
|
||||
@touchmove.stop.prevent
|
||||
class="u-action-sheet-cancel u-action-sheet-item"
|
||||
hover-class="u-hover-class"
|
||||
:hover-stay-time="150"
|
||||
v-if="cancelBtn"
|
||||
@tap="close"
|
||||
>
|
||||
{{ getCancelText }}
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-action-sheet',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $u, useParent, useLocale } from '../..';
|
||||
import { ActionSheetProps } from './types';
|
||||
import uPopup from '../u-popup/u-popup.vue';
|
||||
|
||||
/**
|
||||
* actionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
* @tutorial https://uviewpro.cn/zh/components/actionSheet.html
|
||||
* @property {Array<{ text: string; subText?: string; color?: string; fontSize?: string; disabled?: boolean }>} list 按钮的文字数组,见官方文档示例
|
||||
* @property {{text: string; color?: string; fontSize?: string}} 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>
|
||||
*/
|
||||
|
||||
const props = defineProps(ActionSheetProps);
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'click', 'close']);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
useParent('u-action-sheet');
|
||||
|
||||
const popupValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
// 国际化计算属性
|
||||
const getCancelText = computed(() => props.cancelText || t('uActionSheet.cancelText'));
|
||||
|
||||
// 顶部提示的样式
|
||||
const tipsStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
style.color = String(props.tips?.color || $u.color.tipsColor);
|
||||
style.fontSize = $u.addUnit(String(props.tips?.fontSize || '26rpx'));
|
||||
return style;
|
||||
});
|
||||
|
||||
// 操作项目的样式
|
||||
const itemStyle = (index: number) => {
|
||||
let style: Record<string, string> = {};
|
||||
style.color = String(props.list[index]?.color || props.color);
|
||||
style.fontSize = $u.addUnit(String(props.list[index]?.fontSize || props.fontSize));
|
||||
// 选项被禁用的样式
|
||||
if (props.list[index]?.disabled) style.color = String($u.color.lightColor || '');
|
||||
return style;
|
||||
};
|
||||
|
||||
// 计算z-index
|
||||
const uZIndex = computed(() => {
|
||||
// 如果用户有传递z-index值,优先使用
|
||||
return props.zIndex ? props.zIndex : $u.zIndex.popup;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击取消按钮
|
||||
*/
|
||||
function close() {
|
||||
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
|
||||
// 这是一个vue发送事件的特殊用法
|
||||
popupClose();
|
||||
emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗关闭
|
||||
*/
|
||||
function popupClose() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击某一个item
|
||||
* @param index 选项索引
|
||||
*/
|
||||
function itemClick(index: number) {
|
||||
// disabled的项禁止点击
|
||||
if (props.list[index]?.disabled) return;
|
||||
emit('click', index);
|
||||
if (props.asyncClose) return;
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
props,
|
||||
close,
|
||||
itemClick
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-tips {
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
padding: 34rpx 0;
|
||||
line-height: 1;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.u-action-sheet-item {
|
||||
@include vue-flex;
|
||||
line-height: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
padding: 34rpx 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.u-action-sheet-item__subtext {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.u-gab {
|
||||
height: 12rpx;
|
||||
background-color: rgb(234, 234, 236);
|
||||
}
|
||||
|
||||
.u-action-sheet-cancel {
|
||||
color: $u-main-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ThemeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* alertTips 警告提示
|
||||
* @description 警告提示,展现需要关注的信息
|
||||
*/
|
||||
export const AlertTipsProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 显示文字 */
|
||||
title: { type: String, default: '' },
|
||||
/** 主题,success/warning/info/error */
|
||||
type: { type: String as PropType<ThemeType>, 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: () => ({}) },
|
||||
/** 标题的样式 */
|
||||
titleStyle: { type: Object, default: () => ({}) },
|
||||
/** 描述文字的样式 */
|
||||
descStyle: { type: Object, default: () => ({}) }
|
||||
};
|
||||
|
||||
export type AlertTipsProps = ExtractPropTypes<typeof AlertTipsProps>;
|
||||
@@ -0,0 +1,239 @@
|
||||
<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' : '',
|
||||
customClass
|
||||
]"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: borderColor
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon
|
||||
v-if="showIcon"
|
||||
:name="uIconName"
|
||||
:size="description ? 40 : 32"
|
||||
:color="uIconType"
|
||||
:custom-style="iconStyle"
|
||||
custom-class="u-tips-icon"
|
||||
></u-icon>
|
||||
</view>
|
||||
<view class="u-alert-content" @tap.stop="onClick">
|
||||
<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="onClose"
|
||||
v-if="closeAble && !closeText"
|
||||
hoverClass="u-type-error-hover-color"
|
||||
name="close"
|
||||
color="var(--u-tips-color)"
|
||||
:size="22"
|
||||
:custom-style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}"
|
||||
custom-class="u-close-icon"
|
||||
></u-icon>
|
||||
</view>
|
||||
<text
|
||||
v-if="closeAble && closeText"
|
||||
class="u-close-text"
|
||||
:style="{
|
||||
top: description ? '18rpx' : '24rpx'
|
||||
}"
|
||||
>
|
||||
{{ closeText }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-alert-tips',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import { AlertTipsProps } from './types';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* alertTips 警告提示
|
||||
* @description 警告提示,展现需要关注的信息
|
||||
* @tutorial https://uviewpro.cn/zh/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 点击关闭按钮时触发
|
||||
*/
|
||||
|
||||
const props = defineProps(AlertTipsProps);
|
||||
|
||||
const emit = defineEmits(['click', 'close']);
|
||||
|
||||
/**
|
||||
* 标题样式,合并加粗和用户自定义样式
|
||||
*/
|
||||
const uTitleStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
// 如果有描述文字的话,标题进行加粗
|
||||
style.fontWeight = props.description ? 500 : 'normal';
|
||||
// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
|
||||
return $u.deepMerge(style, props.titleStyle);
|
||||
});
|
||||
|
||||
/**
|
||||
* 图标名称,优先使用用户传入,否则根据type主题推定默认图标
|
||||
*/
|
||||
const uIconName = computed(() => {
|
||||
// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
|
||||
return props.icon ? props.icon : $u.type2icon(props.type as any);
|
||||
});
|
||||
|
||||
/**
|
||||
* 图标类型,优先使用iconStyle,否则用type
|
||||
*/
|
||||
const uIconType = computed(() => {
|
||||
// 如果有设置图标的样式,优先使用,没有的话,则用type的样式
|
||||
return Object.keys(props.iconStyle).length ? '' : props.type;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击内容
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click');
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击关闭按钮
|
||||
*/
|
||||
function onClose() {
|
||||
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 var(--u-white-color);
|
||||
|
||||
&--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;
|
||||
}
|
||||
|
||||
:deep(.u-tips-icon) {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
:deep(.u-close-icon) {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.u-close-alert-tips {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.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-hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.u-close-text {
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { AvatarCropperBoundStyle } from '../../types/global';
|
||||
|
||||
export const AvatarCropperProps = {
|
||||
/** 裁剪矩形框的样式 */
|
||||
boundStyle: {
|
||||
type: Object as PropType<AvatarCropperBoundStyle>,
|
||||
default: (): AvatarCropperBoundStyle => ({
|
||||
lineWidth: 4,
|
||||
borderColor: 'rgb(245, 245, 245)',
|
||||
mask: 'rgba(0, 0, 0, 0.35)'
|
||||
})
|
||||
}
|
||||
// 其他 props 可参考注释,后续如需开放直接补充
|
||||
// rectWidth: { type: [String, Number], default: 400 },
|
||||
// rectHeight: { type: [String, Number], default: 400 },
|
||||
// destWidth: { type: [String, Number], default: 400 },
|
||||
// destHeight: { type: [String, Number], default: 400 },
|
||||
// fileType: { type: String, default: 'jpg' },
|
||||
// quality: { type: [Number, String], default: 1 }
|
||||
};
|
||||
|
||||
export type AvatarCropperProps = ExtractPropTypes<typeof AvatarCropperProps>;
|
||||
@@ -0,0 +1,299 @@
|
||||
<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">{{ t('avatarCropper.select') }}</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="upload" @tap="uploadTap">{{ t('avatarCropper.reselect') }}</view>
|
||||
<!-- #endif -->
|
||||
<view class="getCropperImage" @tap="() => getCropperImage(false)">{{ t('avatarCropper.confirm') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-avatar-cropper',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { $u, useLocale } from '../..';
|
||||
// 兼容 UMD/ESM 导入 weCropper.js
|
||||
import WeCropper from './weCropper';
|
||||
import { AvatarCropperProps } from './types';
|
||||
|
||||
/**
|
||||
* 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
|
||||
* mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
|
||||
*/
|
||||
const props = defineProps(AvatarCropperProps);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* 组合式API变量声明
|
||||
* 保留所有说明注释
|
||||
*/
|
||||
const bottomNavHeight = ref(50); // 底部导航的高度
|
||||
const originWidth = ref(200);
|
||||
const width = ref(0);
|
||||
const height = ref(0);
|
||||
const src = ref(''); // 选择的图片路径,用于在点击确定时,判断是否选择了图片
|
||||
const destWidth = ref(200); // 输出图片宽度,单位px
|
||||
const rectWidth = ref(200); // 裁剪框宽度,单位px
|
||||
const fileType = ref('jpg'); // 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
|
||||
|
||||
// cropperOpt 需响应式,且类型安全
|
||||
const cropperOpt = reactive({
|
||||
id: 'cropper',
|
||||
targetId: 'targetCropper',
|
||||
pixelRatio: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scale: 2.5,
|
||||
zoom: 8,
|
||||
cut: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: originWidth.value,
|
||||
height: originWidth.value
|
||||
},
|
||||
boundStyle: {
|
||||
lineWidth: 0,
|
||||
mask: '',
|
||||
color: ''
|
||||
}
|
||||
});
|
||||
|
||||
let cropper: any = null; // WeCropper 实例,类型 any,建议后续补充类型声明
|
||||
|
||||
/**
|
||||
* 组件挂载时初始化 cropper
|
||||
* 保留所有说明注释
|
||||
*/
|
||||
onMounted(() => {
|
||||
// 获取系统信息
|
||||
const rectInfo = uni.getSystemInfoSync();
|
||||
width.value = rectInfo.windowWidth;
|
||||
height.value = rectInfo.windowHeight - bottomNavHeight.value;
|
||||
cropperOpt.width = width.value;
|
||||
cropperOpt.height = height.value;
|
||||
cropperOpt.pixelRatio = rectInfo.pixelRatio;
|
||||
|
||||
// 初始化裁剪框
|
||||
cropperOpt.cut = {
|
||||
x: (width.value - originWidth.value) / 2,
|
||||
y: (height.value - originWidth.value) / 2,
|
||||
width: originWidth.value,
|
||||
height: originWidth.value
|
||||
};
|
||||
cropperOpt.boundStyle = {
|
||||
lineWidth: uni.upx2px(props.boundStyle.lineWidth),
|
||||
mask: props.boundStyle.mask,
|
||||
color: props.boundStyle.borderColor
|
||||
};
|
||||
|
||||
// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: 'var(--u-white-color)',
|
||||
backgroundColor: 'var(--u-bg-black)'
|
||||
});
|
||||
|
||||
// 初始化 cropper 实例
|
||||
cropper = new WeCropper(cropperOpt)
|
||||
.on('ready', (ctx: any) => {
|
||||
// wecropper is ready for work!
|
||||
})
|
||||
.on('beforeImageLoad', (ctx: any) => {
|
||||
// before picture loaded, i can do something
|
||||
})
|
||||
.on('imageLoad', (ctx: any) => {
|
||||
// picture loaded
|
||||
})
|
||||
.on('beforeDraw', (ctx: any, instance: any) => {
|
||||
// before canvas draw,i can do something
|
||||
});
|
||||
|
||||
// 选择图片
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: (res: UniApp.ChooseImageSuccessCallbackResult) => {
|
||||
src.value = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
cropper.pushOrign(src.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 触摸开始事件
|
||||
* @param e 事件对象
|
||||
*/
|
||||
function touchStart(e: any) {
|
||||
cropper && cropper.touchStart(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动事件
|
||||
* @param e 事件对象
|
||||
*/
|
||||
function touchMove(e: any) {
|
||||
cropper && cropper.touchMove(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸结束事件
|
||||
* @param e 事件对象
|
||||
*/
|
||||
function touchEnd(e: any) {
|
||||
cropper && cropper.touchEnd(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取裁剪后的图片
|
||||
* @param isPre 是否预览
|
||||
*/
|
||||
function getCropperImage(isPre = false) {
|
||||
if (!src.value) return $u.toast(t('avatarCropper.noSelect'));
|
||||
|
||||
const cropper_opt = {
|
||||
destHeight: Number(destWidth.value), // uni.canvasToTempFilePath要求这些参数为数值
|
||||
destWidth: Number(destWidth.value),
|
||||
fileType: fileType.value
|
||||
};
|
||||
cropper.getCropperImage(cropper_opt, (path: string, err: any) => {
|
||||
if (err) {
|
||||
uni.showModal({
|
||||
title: t('avatarCropper.modalTitle'),
|
||||
content: err.message
|
||||
});
|
||||
} else {
|
||||
if (isPre) {
|
||||
uni.previewImage({
|
||||
current: '', // 当前显示图片的 http 链接
|
||||
urls: [path] // 需要预览的图片 http 链接列表
|
||||
});
|
||||
} else {
|
||||
uni.$emit('uAvatarCropper', path);
|
||||
$u.route({
|
||||
type: 'back'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新选择图片
|
||||
*/
|
||||
function uploadTap() {
|
||||
uni.chooseImage({
|
||||
count: 1, // 默认9
|
||||
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
|
||||
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
|
||||
success: (res: UniApp.ChooseImageSuccessCallbackResult) => {
|
||||
src.value = res.tempFilePaths[0];
|
||||
// 获取裁剪图片资源后,给data添加src属性及其值
|
||||
cropper.pushOrign(src.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
</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: var(--u-bg-black);
|
||||
color: var(--u-divider-color);
|
||||
}
|
||||
|
||||
.cropper-wrapper {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: var(--u-bg-black);
|
||||
}
|
||||
|
||||
.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>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* we-cropper 类型声明文件
|
||||
* 适用于 Vue3 + TypeScript 项目中 weCropper.js 的类型提示
|
||||
*/
|
||||
|
||||
export interface WeCropperOptions {
|
||||
id?: string;
|
||||
targetId?: string;
|
||||
pixelRatio?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
scale?: number;
|
||||
zoom?: number;
|
||||
cut?: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
boundStyle?: {
|
||||
lineWidth?: number;
|
||||
borderColor?: string;
|
||||
mask?: string;
|
||||
color?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type WeCropperEvent = (ctx: any, instance?: any) => void;
|
||||
|
||||
export interface WeCropperInstance {
|
||||
pushOrign(src: string): Promise<void>;
|
||||
pushOrigin(src: string): Promise<void>;
|
||||
removeImage(): Promise<void>;
|
||||
getCropperBase64(done?: (base64: string) => void): void;
|
||||
getCropperImage(opt: any, fn: (path: string, err: any) => void): void;
|
||||
update(): void;
|
||||
touchStart(e: any): void;
|
||||
touchMove(e: any): void;
|
||||
touchEnd(e: any): void;
|
||||
on(event: 'ready' | 'beforeImageLoad' | 'imageLoad' | 'beforeDraw', fn: WeCropperEvent): WeCropperInstance;
|
||||
// 其它动态方法
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// WeCropper 类型声明文件
|
||||
declare class WeCropper {
|
||||
constructor(params: any);
|
||||
|
||||
// 事件方法
|
||||
on(event: string, callback: (ctx?: any) => void): WeCropper;
|
||||
|
||||
// 其他方法
|
||||
pushOrign(src: string): void;
|
||||
getCropperImage(callback?: (tempFilePath: string) => void): void;
|
||||
|
||||
// 属性
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default WeCropper;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ImgMode } from '../../types/global';
|
||||
|
||||
// u-avatar 组件 props
|
||||
export const AvatarProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 背景颜色 */
|
||||
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 as PropType<ImgMode>, default: 'aspectFill' },
|
||||
/** 标识符 */
|
||||
index: { type: [String, Number], default: '' },
|
||||
/** 右上角性别角标,man-男,woman-女 */
|
||||
sexIcon: { type: String as PropType<'man' | 'woman'>, 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 }
|
||||
};
|
||||
|
||||
export type AvatarProps = ExtractPropTypes<typeof AvatarProps>;
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<view class="u-avatar" :class="customClass" :style="$u.toStyle(wrapStyle, customStyle)" @tap="onClick">
|
||||
<image
|
||||
@error="onLoadError"
|
||||
: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 lang="ts">
|
||||
export default {
|
||||
name: 'u-avatar',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { AvatarProps } from './types';
|
||||
import { $u } from '../..';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
const base64Avatar =
|
||||
'data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z';
|
||||
|
||||
/**
|
||||
* avatar 头像
|
||||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
|
||||
* @tutorial https://uviewpro.cn/zh/components/avatar.html
|
||||
* @property {String} bg-color 背景颜色,一般显示文字时用(默认var(--u-bg-white))
|
||||
* @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>
|
||||
*/
|
||||
const props = defineProps(AvatarProps);
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// 头像的地址,因为如果加载错误,需要赋值为默认图片,props值无法修改,所以需要一个中间值
|
||||
const avatar = ref(props.src ? props.src : base64Avatar);
|
||||
const error = ref(false);
|
||||
|
||||
// 用户可能会在头像加载失败时,再次修改头像值,所以需要重新赋值
|
||||
watch(
|
||||
() => props.src,
|
||||
n => {
|
||||
if (!n) {
|
||||
// 如果传入null或者'',或者undefined,显示默认头像
|
||||
avatar.value = base64Avatar;
|
||||
error.value = true;
|
||||
} else {
|
||||
avatar.value = n;
|
||||
error.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 头像外层样式
|
||||
*/
|
||||
const wrapStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
style.height =
|
||||
props.size === 'large'
|
||||
? '120rpx'
|
||||
: props.size === 'default'
|
||||
? '90rpx'
|
||||
: props.size === 'mini'
|
||||
? '70rpx'
|
||||
: props.size + 'rpx';
|
||||
style.width = style.height;
|
||||
style.flex = `0 0 ${style.height}`;
|
||||
style.backgroundColor = props.bgColor;
|
||||
style.borderRadius = props.mode === 'circle' ? '500px' : '5px';
|
||||
if (props.text) style.padding = '0 6rpx';
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 图片样式
|
||||
*/
|
||||
const imgStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
style.borderRadius = props.mode === 'circle' ? '500px' : '5px';
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 取字符串的第一个字符
|
||||
*/
|
||||
const uText = computed(() => {
|
||||
return String(props.text)[0];
|
||||
});
|
||||
|
||||
/**
|
||||
* 性别图标的自定义样式
|
||||
*/
|
||||
const uSexStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
if (props.sexBgColor) style.backgroundColor = props.sexBgColor;
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 等级图标的自定义样式
|
||||
*/
|
||||
const uLevelStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
if (props.levelBgColor) style.backgroundColor = props.levelBgColor;
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 图片加载错误时,显示默认头像
|
||||
*/
|
||||
function onLoadError() {
|
||||
error.value = true;
|
||||
avatar.value = base64Avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像被点击
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click', props.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: var(--u-white-color);
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
top: 5%;
|
||||
z-index: 1;
|
||||
right: -7%;
|
||||
border: 1px var(--u-white-color) 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: var(--u-white-color);
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
bottom: 5%;
|
||||
z-index: 1;
|
||||
right: -7%;
|
||||
border: 1px var(--u-white-color) solid;
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { Shape } from '../../types/global';
|
||||
|
||||
// u-back-top 组件 props
|
||||
export const BackTopProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 返回顶部的形状,circle-圆形,square-方形 */
|
||||
mode: { type: String as PropType<Shape>, 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 as PropType<Record<string, any>>,
|
||||
default: () => ({
|
||||
color: 'var(--u-tips-color)',
|
||||
fontSize: '38rpx'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export type BackTopProps = ExtractPropTypes<typeof BackTopProps>;
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<view
|
||||
@tap="backToTop"
|
||||
class="u-back-top"
|
||||
:class="['u-back-top--mode--' + mode, customClass]"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
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">
|
||||
<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 lang="ts">
|
||||
export default {
|
||||
name: 'u-back-top',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, useSlots } from 'vue';
|
||||
import { BackTopProps } from './types';
|
||||
import { $u } from '../../';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* back-top 返回顶部按钮
|
||||
* @description 页面滚动时显示的返回顶部按钮
|
||||
* @property {String} mode 返回顶部的形状,circle-圆形,square-方形
|
||||
* @property {String} icon 自定义图标
|
||||
* @property {String} tips 提示文字
|
||||
* @property {Number|String} duration 返回顶部滚动时间
|
||||
* @property {Number|String} scrollTop 滚动距离
|
||||
* @property {Number|String} top 距离顶部多少距离显示,单位rpx
|
||||
* @property {Number|String} bottom 返回顶部按钮到底部的距离,单位rpx
|
||||
* @property {Number|String} right 返回顶部按钮到右边的距离,单位rpx
|
||||
* @property {Number|String} zIndex 层级
|
||||
* @property {Object} iconStyle 图标的样式,对象形式
|
||||
* @property {Object} customStyle 整个组件的样式
|
||||
*/
|
||||
const props = defineProps(BackTopProps);
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
// 不透明度,为了让组件有一个显示和隐藏的过渡动画
|
||||
const opacity = ref(0);
|
||||
// 组件的z-index值,隐藏时设置为-1,就会看不到
|
||||
const uZIndex = ref(-1);
|
||||
|
||||
/**
|
||||
* 计算是否显示返回顶部按钮
|
||||
* 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
|
||||
* 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
|
||||
*/
|
||||
const showBackTop = computed(() => {
|
||||
// uni.upx2px 用于将 rpx 转为 px
|
||||
return (
|
||||
Number(props.scrollTop) >
|
||||
(typeof uni !== 'undefined' && uni.upx2px ? uni.upx2px(Number(props.top)) : Number(props.top))
|
||||
);
|
||||
// #else
|
||||
// return Number(props.scrollTop) > Number(props.top)
|
||||
});
|
||||
|
||||
// 监听组件的显示与隐藏状态,修改组件的层级和不透明度
|
||||
// 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
|
||||
watch(showBackTop, nVal => {
|
||||
if (nVal) {
|
||||
uZIndex.value = Number(props.zIndex);
|
||||
opacity.value = 1;
|
||||
} else {
|
||||
uZIndex.value = -1;
|
||||
opacity.value = 0;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 返回顶部方法
|
||||
*/
|
||||
function backToTop() {
|
||||
// uni.pageScrollTo 用于页面滚动到顶部
|
||||
if (typeof uni !== 'undefined' && uni.pageScrollTo) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: Number(props.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: var(--u-divider-color);
|
||||
color: $u-content-color;
|
||||
align-items: center;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
&__content {
|
||||
@include vue-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__tips {
|
||||
font-size: 24rpx;
|
||||
transform: scale(0.8);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { BadgeSize, ThemeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* badge 角标类型定义
|
||||
* @description 供 u-badge 组件 props 使用
|
||||
*/
|
||||
|
||||
export const BadgeProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 使用预设的背景颜色 primary,warning,success,error,info */
|
||||
type: { type: String as PropType<ThemeType>, default: 'error' },
|
||||
/** Badge的尺寸,default, mini */
|
||||
size: { type: String as PropType<BadgeSize>, default: 'default' },
|
||||
/** 是否是圆点 */
|
||||
isDot: { type: Boolean, default: false },
|
||||
/** 显示的数值内容 */
|
||||
count: { type: [Number, String], default: undefined },
|
||||
/** 展示封顶的数字值 */
|
||||
overflowCount: { type: Number, default: 99 },
|
||||
/** 当数值为 0 时,是否展示 Badge */
|
||||
showZero: { type: Boolean, default: false },
|
||||
/** 位置偏移 [number, number] */
|
||||
offset: { type: Array as unknown as PropType<[number, number]>, default: () => [20, 20] },
|
||||
/** 是否开启绝对定位,开启了offset才会起作用 */
|
||||
absolute: { type: Boolean, default: true },
|
||||
/** 字体大小 */
|
||||
fontSize: { type: [String, Number], default: '24' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: 'var(--u-white-color)' },
|
||||
/** badge的背景颜色 */
|
||||
bgColor: { type: String, default: '' },
|
||||
/** 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效 */
|
||||
isCenter: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type BadgeProps = ExtractPropTypes<typeof BadgeProps>;
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
class="u-badge"
|
||||
:class="[
|
||||
isDot ? 'u-badge-dot' : '',
|
||||
size === 'mini' ? 'u-badge-mini' : '',
|
||||
type ? 'u-badge--bg--' + type : '',
|
||||
customClass
|
||||
]"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
top: (offset as number[])[0] + 'rpx',
|
||||
right: (offset as number[])[1] + 'rpx',
|
||||
fontSize: fontSize + 'rpx',
|
||||
position: absolute ? 'absolute' : 'static',
|
||||
color: color,
|
||||
backgroundColor: bgColor
|
||||
},
|
||||
boxStyle,
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ showText }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-badge',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { BadgeProps } from './types';
|
||||
import { $u } from '../..';
|
||||
|
||||
/**
|
||||
* badge 角标
|
||||
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
|
||||
* @tutorial https://uviewpro.cn/zh/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 字体颜色(默认var(--u-white-color))
|
||||
* @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
|
||||
* @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
|
||||
* @example <u-badge type="error" count="7"></u-badge>
|
||||
*/
|
||||
|
||||
const props = defineProps(BadgeProps);
|
||||
|
||||
/**
|
||||
* 计算 badge 的定位和变换样式
|
||||
*/
|
||||
const boxStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
if (props.isCenter) {
|
||||
style.top = 0;
|
||||
style.right = 0;
|
||||
// Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
|
||||
style.transform = 'translateY(-50%) translateX(50%)';
|
||||
} else {
|
||||
style.top = (props.offset as number[])[0] + 'rpx';
|
||||
style.right = (props.offset as number[])[1] + 'rpx';
|
||||
style.transform = 'translateY(0) translateX(0)';
|
||||
}
|
||||
// 如果尺寸为mini,后接上scale()
|
||||
if (props.size === 'mini') {
|
||||
style.transform = style.transform + ' scale(0.8)';
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算显示的文本内容
|
||||
*/
|
||||
const showText = computed(() => {
|
||||
if (props.isDot) return '';
|
||||
else {
|
||||
if (Number(props.count) > props.overflowCount) return `${props.overflowCount}+`;
|
||||
else return props.count;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否显示组件
|
||||
*/
|
||||
const show = computed(() => {
|
||||
// 如果count的值为0,并且showZero设置为false,不显示组件
|
||||
if (Number(props.count) === 0 && props.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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ButtonFormType, ButtonOpenType, ButtonScope, ButtonSize, ButtonType, Shape } from '../../types/global';
|
||||
|
||||
/**
|
||||
* button 按钮类型定义
|
||||
* @description 供 u-button 组件 props 使用
|
||||
*/
|
||||
|
||||
export const ButtonProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 按钮文本 */
|
||||
text: { type: String, default: '' },
|
||||
/** 是否细边框 */
|
||||
hairLine: { type: Boolean, default: true },
|
||||
/** 按钮的预置样式,default,primary,error,warning,success */
|
||||
type: { type: String as PropType<ButtonType>, default: 'default' },
|
||||
/** 按钮尺寸,default,large, medium=small,mini */
|
||||
size: { type: String as PropType<ButtonSize>, default: 'default' },
|
||||
/** 按钮形状,circle(两边为半圆),square(带圆角) */
|
||||
shape: { type: String as PropType<Shape>, default: 'square' },
|
||||
/** 按钮是否镂空 */
|
||||
plain: { type: Boolean, default: false },
|
||||
/** 是否禁止状态 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 是否加载中 */
|
||||
loading: { type: Boolean, default: false },
|
||||
/** 支付宝小程序,当 open-type 为 getAuthorize 时有效 */
|
||||
scope: { type: String as PropType<ButtonScope>, default: '' },
|
||||
/** 开放能力,具体请看uniapp稳定关于button组件部分说明 */
|
||||
openType: { type: String as PropType<ButtonOpenType>, default: '' },
|
||||
/** 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件 */
|
||||
formType: { type: String as PropType<ButtonFormType>, default: '' },
|
||||
/** 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 */
|
||||
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: '' },
|
||||
/** 是否显示会话内消息卡片,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: '' },
|
||||
/** 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取 */
|
||||
dataName: { type: String, default: '' },
|
||||
/** 节流,一定时间内只能触发一次 */
|
||||
throttleTime: { type: [String, Number], default: 0 },
|
||||
/** 按住后多久出现点击态,单位毫秒 */
|
||||
hoverStartTime: { type: [String, Number], default: 20 },
|
||||
/** 手指松开后点击态保留时间,单位毫秒 */
|
||||
hoverStayTime: { type: [String, Number], default: 150 }
|
||||
};
|
||||
|
||||
export type ButtonProps = ExtractPropTypes<typeof ButtonProps>;
|
||||
@@ -0,0 +1,608 @@
|
||||
<template>
|
||||
<!-- prettier-ignore -->
|
||||
<button
|
||||
id="u-wave-btn"
|
||||
class="u-btn u-line-1 u-fix-ios-appearance"
|
||||
data-eventsync="true"
|
||||
: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` : '',
|
||||
customClass
|
||||
]"
|
||||
:hover-start-time="Number(hoverStartTime)"
|
||||
:hover-stay-time="Number(hoverStayTime)"
|
||||
:disabled="disabled"
|
||||
:form-type="(formType as any)"
|
||||
:open-type="disabled || loading ? undefined : openType"
|
||||
:app-parameter="appParameter"
|
||||
:hover-stop-propagation="hoverStopPropagation"
|
||||
:send-message-title="sendMessageTitle"
|
||||
:send-message-path="sendMessagePath"
|
||||
:lang="(lang as any)"
|
||||
:data-name="dataName"
|
||||
:session-from="sessionFrom"
|
||||
:send-message-img="sendMessageImg"
|
||||
:show-message-card="showMessageCard"
|
||||
@getAuthorize="getAuthorize"
|
||||
@getuserinfo="getuserinfo"
|
||||
@contact="contact"
|
||||
@getphonenumber="getphonenumber"
|
||||
@error="error"
|
||||
@launchapp="launchapp"
|
||||
@opensetting="opensetting"
|
||||
@chooseavatar="chooseavatar"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
overflow: ripple ? 'hidden' : 'visible'
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
@tap.stop="click($event)"
|
||||
:hover-class="getHoverClass"
|
||||
:loading="loading"
|
||||
>
|
||||
<slot>{{ props.text }}</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 lang="ts">
|
||||
export default {
|
||||
name: 'u-button',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { $u } from '../../';
|
||||
import { ButtonProps } from './types';
|
||||
|
||||
/**
|
||||
* button 按钮
|
||||
* @description Button 按钮
|
||||
* @tutorial https://uviewpro.cn/zh/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 {String} 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 对按钮的自定义样式,对象形式,见文档说明
|
||||
* @property {String} app-parameter 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
|
||||
* @property {Boolean} hover-stop-propagation 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
|
||||
* @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
|
||||
* @property {String} session-from 会话来源,open-type="contact"时有效。只微信小程序有效
|
||||
* @property {String} send-message-title 会话内消息卡片标题,open-type="contact"时有效
|
||||
* @property {String} send-message-path 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
|
||||
* @property {String} send-message-img 会话内消息卡片图片,open-type="contact"时有效
|
||||
* @property {Boolean} show-message-card 是否显示会话内消息卡片,open-type="contact"时有效
|
||||
* @property {Number|String} throttle-time 节流,一定时间内只能触发一次,单位毫秒
|
||||
* @property {String} scope 支付宝小程序,当 open-type 为 getAuthorize 时有效。可选值:'phoneNumber' | 'userInfo'
|
||||
* @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 成功的回调
|
||||
* @event {Function} contact 客服消息回调
|
||||
* @event {Function} chooseavatar 头像选择回调
|
||||
* @event {Function} agreeprivacyauthorization 用户点击允许授权回调
|
||||
* @example <u-button>月落</u-button>
|
||||
*/
|
||||
|
||||
const emit = defineEmits([
|
||||
'click',
|
||||
'getuserinfo',
|
||||
'contact',
|
||||
'getphonenumber',
|
||||
'error',
|
||||
'launchapp',
|
||||
'opensetting',
|
||||
'chooseavatar',
|
||||
'agreeprivacyauthorization'
|
||||
]);
|
||||
|
||||
const props = defineProps(ButtonProps);
|
||||
|
||||
/**
|
||||
* 按钮水波纹相关状态
|
||||
*/
|
||||
const rippleTop = ref(0); // 水波纹的起点Y坐标到按钮上边界的距离
|
||||
const rippleLeft = ref(0); // 水波纹起点X坐标到按钮左边界的距离
|
||||
const fields = ref<Record<string, any>>({}); // 波纹按钮节点信息
|
||||
const waveActive = ref(false); // 激活水波纹
|
||||
|
||||
/**
|
||||
* 当没有传bgColor变量时,按钮按下去的颜色类名
|
||||
* @returns {string}
|
||||
*/
|
||||
const getHoverClass = computed(() => {
|
||||
// 如果开启水波纹效果,则不启用hover-class效果
|
||||
if (props.loading || props.disabled || props.ripple) return '';
|
||||
// 如果用户传了 hoverClass,优先使用用户的
|
||||
if (props.hoverClass) return props.hoverClass;
|
||||
let hoverClass = '';
|
||||
hoverClass = props.plain ? 'u-' + props.type + '-plain-hover' : 'u-' + props.type + '-hover';
|
||||
return hoverClass;
|
||||
});
|
||||
|
||||
/**
|
||||
* 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
|
||||
* @returns {string}
|
||||
*/
|
||||
const showHairLineBorder = computed(() => {
|
||||
if (['primary', 'success', 'error', 'warning'].indexOf(props.type) >= 0 && !props.plain) {
|
||||
return '';
|
||||
} else {
|
||||
return 'u-hairline-border';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 按钮点击
|
||||
* @param e 事件对象
|
||||
* @emits click
|
||||
*/
|
||||
function click(e: any) {
|
||||
if (Number(props.throttleTime)) {
|
||||
$u.throttle(() => {
|
||||
clickAction(e);
|
||||
}, Number(props.throttleTime));
|
||||
} else {
|
||||
clickAction(e);
|
||||
}
|
||||
}
|
||||
|
||||
function clickAction(e: any) {
|
||||
// 如果按钮时disabled和loading状态,不触发水波纹效果
|
||||
if (props.loading === true || props.disabled === true) return;
|
||||
// 是否开启水波纹效果
|
||||
if (props.ripple) {
|
||||
// 每次点击时,移除上一次的类,再次添加,才能触发动画效果
|
||||
waveActive.value = false;
|
||||
nextTick(() => {
|
||||
getWaveQuery(e);
|
||||
});
|
||||
}
|
||||
emit('click', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询按钮的节点信息
|
||||
* @param e 事件对象
|
||||
*/
|
||||
function getWaveQuery(e: any) {
|
||||
getElQuery().then((res: any[]) => {
|
||||
// 查询返回的是一个数组节点
|
||||
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;
|
||||
fields.value = data;
|
||||
let touchesX = '',
|
||||
touchesY = '';
|
||||
// #ifdef MP-BAIDU
|
||||
// @ts-ignore
|
||||
touchesX = e.changedTouches[0].clientX;
|
||||
touchesY = e.changedTouches[0].clientY;
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
// @ts-ignore
|
||||
touchesX = e.detail.clientX;
|
||||
touchesY = e.detail.clientY;
|
||||
// #endif
|
||||
// #ifndef MP-BAIDU || MP-ALIPAY
|
||||
// @ts-ignore
|
||||
touchesX = e.touches[0].clientX;
|
||||
touchesY = e.touches[0].clientY;
|
||||
// #endif
|
||||
// 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
|
||||
// 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
|
||||
// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
|
||||
rippleTop.value = Number(touchesY) - data.top - data.targetWidth / 2;
|
||||
rippleLeft.value = Number(touchesX) - data.left - data.targetWidth / 2;
|
||||
nextTick(() => {
|
||||
waveActive.value = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点信息
|
||||
* @returns {Promise<any[]>}
|
||||
*/
|
||||
function getElQuery(): Promise<any[]> {
|
||||
return new Promise(resolve => {
|
||||
let queryInfo: any = '';
|
||||
// 获取元素节点信息,请查看uniapp相关文档
|
||||
// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
|
||||
// @ts-ignore
|
||||
queryInfo = uni.createSelectorQuery().in(null);
|
||||
// #ifdef MP-ALIPAY
|
||||
// @ts-ignore
|
||||
queryInfo = uni.createSelectorQuery();
|
||||
// #endif
|
||||
queryInfo.select('.u-btn').boundingClientRect();
|
||||
queryInfo.exec((data: any) => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 下面为对接uniapp官方按钮开放能力事件回调的对接
|
||||
/**
|
||||
* open-type="getPhoneNumber"时有效
|
||||
*/
|
||||
function getphonenumber(event: any) {
|
||||
emit('getphonenumber', event);
|
||||
}
|
||||
/**
|
||||
* 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
|
||||
*/
|
||||
function getuserinfo(event: any) {
|
||||
emit('getuserinfo', event);
|
||||
}
|
||||
/**
|
||||
* 当使用开放能力时,发生错误的回调
|
||||
*/
|
||||
function error(event: any) {
|
||||
emit('error', event);
|
||||
}
|
||||
/**
|
||||
* 在打开授权设置页并关闭后回调
|
||||
*/
|
||||
function opensetting(event: any) {
|
||||
emit('opensetting', event);
|
||||
}
|
||||
/**
|
||||
* 打开 APP 成功的回调
|
||||
*/
|
||||
function launchapp(event: any) {
|
||||
emit('launchapp', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝小程序授权
|
||||
* @param event
|
||||
*/
|
||||
function getAuthorize(event: any) {
|
||||
if (props.scope === 'phoneNumber') {
|
||||
getphonenumber(event);
|
||||
} else if (props.scope === 'userInfo') {
|
||||
getuserinfo(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服消息回调
|
||||
* @param event
|
||||
*/
|
||||
function contact(event: any) {
|
||||
emit('contact', event);
|
||||
}
|
||||
/**
|
||||
* 头像选择回调
|
||||
* @param event
|
||||
*/
|
||||
function chooseavatar(event: any) {
|
||||
emit('chooseavatar', event);
|
||||
}
|
||||
/**
|
||||
* 用户点击允许授权回调
|
||||
* @param event
|
||||
*/
|
||||
function agreeprivacyauthorization(event: any) {
|
||||
emit('agreeprivacyauthorization', event);
|
||||
}
|
||||
</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 var(--u-white-color);
|
||||
}
|
||||
|
||||
&--default {
|
||||
color: $u-content-color;
|
||||
border-color: var(--u-light-color);
|
||||
background-color: var(--u-bg-white);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
color: var(--u-white-color);
|
||||
border-color: $u-type-primary;
|
||||
background-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: var(--u-white-color);
|
||||
border-color: $u-type-success;
|
||||
background-color: $u-type-success;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: var(--u-white-color);
|
||||
border-color: $u-type-error;
|
||||
background-color: $u-type-error;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: var(--u-white-color);
|
||||
border-color: $u-type-warning;
|
||||
background-color: $u-type-warning;
|
||||
}
|
||||
|
||||
&--default--disabled {
|
||||
color: var(--u-white-color);
|
||||
border-color: var(--u-border-color);
|
||||
background-color: var(--u-bg-white);
|
||||
}
|
||||
|
||||
&--primary--disabled {
|
||||
color: var(--u-white-color) !important;
|
||||
border-color: $u-type-primary-disabled !important;
|
||||
background-color: $u-type-primary-disabled !important;
|
||||
}
|
||||
|
||||
&--success--disabled {
|
||||
color: var(--u-white-color) !important;
|
||||
border-color: $u-type-success-disabled !important;
|
||||
background-color: $u-type-success-disabled !important;
|
||||
}
|
||||
|
||||
&--error--disabled {
|
||||
color: var(--u-white-color) !important;
|
||||
border-color: $u-type-error-disabled !important;
|
||||
background-color: $u-type-error-disabled !important;
|
||||
}
|
||||
|
||||
&--warning--disabled {
|
||||
color: var(--u-white-color) !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-large {
|
||||
font-size: 36rpx;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
}
|
||||
|
||||
.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-small {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
width: auto;
|
||||
font-size: 24rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.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: var(--u-white-color) !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: var(--u-white-color) !important;
|
||||
background: $u-type-success-dark !important;
|
||||
}
|
||||
|
||||
.u-warning-plain-hover {
|
||||
color: var(--u-white-color) !important;
|
||||
background: $u-type-warning-dark !important;
|
||||
}
|
||||
|
||||
.u-error-plain-hover {
|
||||
color: var(--u-white-color) !important;
|
||||
background: $u-type-error-dark !important;
|
||||
}
|
||||
|
||||
.u-info-plain-hover {
|
||||
color: var(--u-white-color) !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: var(--u-white-color);
|
||||
}
|
||||
|
||||
.u-success-hover {
|
||||
background: $u-type-success-dark !important;
|
||||
color: var(--u-white-color);
|
||||
}
|
||||
|
||||
.u-info-hover {
|
||||
background: $u-type-info-dark !important;
|
||||
color: var(--u-white-color);
|
||||
}
|
||||
|
||||
.u-warning-hover {
|
||||
background: $u-type-warning-dark !important;
|
||||
color: var(--u-white-color);
|
||||
}
|
||||
|
||||
.u-error-hover {
|
||||
background: $u-type-error-dark !important;
|
||||
color: var(--u-white-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { CalendarChangeDate, CalendarChangeRange, CalendarMode, ThemeType } from '../../types/global';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* calendar 日历类型定义
|
||||
* @description 供 u-calendar 组件 props 使用
|
||||
*/
|
||||
export const CalendarProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否开启底部安全区适配 */
|
||||
safeAreaInsetBottom: { type: Boolean, default: false },
|
||||
/** 是否允许通过点击遮罩关闭Picker */
|
||||
maskCloseAble: { type: Boolean, default: true },
|
||||
/** 通过双向绑定控制组件的弹出与收起 */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 弹出的z-index值 */
|
||||
zIndex: { type: [String, Number], default: 0 },
|
||||
/** 是否允许切换年份 */
|
||||
changeYear: { type: Boolean, default: true },
|
||||
/** 是否允许切换月份 */
|
||||
changeMonth: { type: Boolean, default: true },
|
||||
/** date-单个日期选择,range-开始日期+结束日期选择 */
|
||||
mode: { type: String as PropType<CalendarMode>, default: 'date' },
|
||||
/** 可切换的最大年份 */
|
||||
maxYear: { type: [Number, String], default: 2050 },
|
||||
/** 可切换的最小年份 */
|
||||
minYear: { type: [Number, String], default: 1950 },
|
||||
/** 最小可选日期(不在范围内日期禁用不可选) */
|
||||
minDate: { type: [Number, String], default: '1950-01-01' },
|
||||
/** 最大可选日期,默认最大值为今天,之后的日期不可选 */
|
||||
maxDate: { type: [Number, String], default: '' },
|
||||
/** 弹窗顶部左右两边的圆角值 */
|
||||
borderRadius: { type: [String, Number], default: 20 },
|
||||
/** 月份切换按钮箭头颜色 */
|
||||
monthArrowColor: { type: String, default: 'var(--u-content-color)' },
|
||||
/** 年份切换按钮箭头颜色 */
|
||||
yearArrowColor: { type: String, default: 'var(--u-tips-color)' },
|
||||
/** 默认日期字体颜色 */
|
||||
color: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 选中|起始结束日期背景色 */
|
||||
activeBgColor: { type: String, default: () => getColor('primary') },
|
||||
/** 选中|起始结束日期字体颜色 */
|
||||
activeColor: { type: String, default: 'var(--u-white-color)' },
|
||||
/** 范围内日期背景色 */
|
||||
rangeBgColor: { type: String, default: 'var(--u-type-primary-light)' },
|
||||
/** 范围内日期字体颜色 */
|
||||
rangeColor: { type: String, default: () => getColor('primary') },
|
||||
/** mode=range时生效,起始日期自定义文案 */
|
||||
startText: { type: String, default: '' },
|
||||
/** mode=range时生效,结束日期自定义文案 */
|
||||
endText: { type: String, default: '' },
|
||||
/** 按钮样式类型 */
|
||||
btnType: { type: String as PropType<ThemeType>, default: 'primary' },
|
||||
/** 当前选中日期带选中效果 */
|
||||
isActiveCurrent: { type: Boolean, default: true },
|
||||
/** 初始化时是否默认选中今天,优先级低于 defaultDate/startDate/endDate */
|
||||
defaultSelectToday: { type: Boolean, default: true },
|
||||
/** 切换年月是否触发事件 mode=date时生效 */
|
||||
isChange: { type: Boolean, default: false },
|
||||
/** 是否显示右上角的关闭图标 */
|
||||
closeable: { type: Boolean, default: true },
|
||||
/** 顶部的提示文字 */
|
||||
toolTip: { type: String, default: '' },
|
||||
/** 是否显示农历 */
|
||||
showLunar: { type: Boolean, default: false },
|
||||
/** 是否在页面中显示 */
|
||||
isPage: { type: Boolean, default: false },
|
||||
/** 默认选中的日期,mode=date时生效,格式:2024-01-01 */
|
||||
defaultDate: { type: String, default: '' },
|
||||
/** 默认选中的开始日期,mode=range时生效,格式:2024-01-01 */
|
||||
startDate: { type: String, default: '' },
|
||||
/** 默认选中的结束日期,mode=range时生效,格式:2024-01-01 */
|
||||
endDate: { type: String, default: '' },
|
||||
/** 是否只读,只读模式下禁止点击选择日期 */
|
||||
readonly: { type: Boolean, default: false },
|
||||
/** 已打卡日期列表,格式:['2024-01-01', '2024-01-02'] */
|
||||
checkedDates: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 打卡日期背景色 */
|
||||
checkedBgColor: { type: String, default: 'var(--u-type-warning)' },
|
||||
/** 打卡日期字体颜色 */
|
||||
checkedColor: { type: String, default: 'var(--u-white-color)' },
|
||||
/** 今日是否已打卡 */
|
||||
todayChecked: { type: Boolean, default: false },
|
||||
/** 今日已打卡背景色 */
|
||||
todayCheckedBgColor: { type: String, default: 'var(--u-type-success)' },
|
||||
/** 未打卡日期背景色 */
|
||||
uncheckedBgColor: { type: String, default: 'var(--u-light-color)' },
|
||||
/** 未打卡日期字体颜色 */
|
||||
uncheckedColor: { type: String, default: 'var(--u-white-color)' },
|
||||
/** 是否启用打卡签到模式(未打卡日期显示灰色) */
|
||||
checkinMode: { type: Boolean, default: false },
|
||||
/** 节假日列表,格式:['2024-01-01', '2024-01-02'] */
|
||||
holidays: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 节假日文字颜色 */
|
||||
holidayColor: { type: String, default: 'var(--u-type-error)' },
|
||||
/** 加班日列表,格式:['2024-01-06', '2024-01-07'] */
|
||||
workdays: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 加班日文字颜色 */
|
||||
workdayColor: { type: String, default: 'var(--u-type-primary)' },
|
||||
/** 节日配置,格式:{ '2024-04-04': '清明节', '2024-04-01': '愚人节' } */
|
||||
festivals: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
|
||||
/** 节日文字颜色 */
|
||||
festivalColor: { type: String, default: 'var(--u-type-primary)' },
|
||||
/** 是否显示内置节日(中国传统节日) */
|
||||
showFestival: { type: Boolean, default: false },
|
||||
/** 是否启用自定义日期内容插槽(微信小程序需要显式声明) */
|
||||
useDateSlot: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type CalendarProps = ExtractPropTypes<typeof CalendarProps>;
|
||||
|
||||
export type CalendarEmits = {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'input', value: boolean): void;
|
||||
(e: 'change', value: CalendarChangeDate | CalendarChangeRange): void;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-car-keyboard 车牌号键盘类型定义
|
||||
* @description 供 u-car-keyboard 组件 props 使用
|
||||
*/
|
||||
export const CarKeyboardProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否打乱键盘按键的顺序 */
|
||||
random: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type CarKeyboardProps = ExtractPropTypes<typeof CarKeyboardProps>;
|
||||
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<view class="u-keyboard" :class="customClass" :style="$u.toStyle(customStyle)" @touchmove.stop.prevent="() => {}">
|
||||
<view class="u-keyboard-grids">
|
||||
<view>
|
||||
<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
|
||||
<view
|
||||
:hover-stay-time="100"
|
||||
@tap="carInputClick(i, j)"
|
||||
hover-class="u-carinput-hover"
|
||||
class="u-keyboard-grids-btn"
|
||||
v-for="(item, j) in group"
|
||||
:key="j"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
@touchstart="backspaceClick"
|
||||
@touchend="clearTimer"
|
||||
:hover-stay-time="100"
|
||||
class="u-keyboard-back"
|
||||
hover-class="u-hover-class"
|
||||
>
|
||||
<u-icon :size="38" name="backspace" :bold="true"></u-icon>
|
||||
</view>
|
||||
<view
|
||||
:hover-stay-time="100"
|
||||
class="u-keyboard-change"
|
||||
hover-class="u-carinput-hover"
|
||||
@tap="changeCarInputMode"
|
||||
>
|
||||
<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
|
||||
/
|
||||
<text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-car-keyboard',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import { CarKeyboardProps } from './types';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* u-car-keyboard 车牌号键盘
|
||||
* @description 车牌号输入专用键盘,支持省份简称和字母数字切换,支持按键顺序打乱。
|
||||
* @property {Boolean} random 是否打乱键盘按键的顺序(默认false)
|
||||
* @event {Function} change 按键被点击
|
||||
* @event {Function} backspace 退格键被点击
|
||||
*/
|
||||
|
||||
const props = defineProps(CarKeyboardProps);
|
||||
const emit = defineEmits(['change', 'backspace']);
|
||||
|
||||
// 车牌输入时,abc=true为输入车牌号码,abc=false为输入省份中文简称
|
||||
const abc = ref(false);
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const areaList = computed(() => {
|
||||
let data = [
|
||||
'京',
|
||||
'沪',
|
||||
'粤',
|
||||
'津',
|
||||
'冀',
|
||||
'豫',
|
||||
'云',
|
||||
'辽',
|
||||
'黑',
|
||||
'湘',
|
||||
'皖',
|
||||
'鲁',
|
||||
'苏',
|
||||
'浙',
|
||||
'赣',
|
||||
'鄂',
|
||||
'桂',
|
||||
'甘',
|
||||
'晋',
|
||||
'陕',
|
||||
'蒙',
|
||||
'吉',
|
||||
'闽',
|
||||
'贵',
|
||||
'渝',
|
||||
'川',
|
||||
'青',
|
||||
'琼',
|
||||
'宁',
|
||||
'挂',
|
||||
'藏',
|
||||
'港',
|
||||
'澳',
|
||||
'新',
|
||||
'使',
|
||||
'学'
|
||||
];
|
||||
let tmp: string[][] = [];
|
||||
// 打乱顺序
|
||||
if (props.random) {
|
||||
data = $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;
|
||||
});
|
||||
|
||||
const EngKeyBoardList = computed(() => {
|
||||
// prettier-ignore
|
||||
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: (string | number)[][] = [];
|
||||
if (props.random) {
|
||||
data = $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;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击键盘按钮
|
||||
*/
|
||||
function carInputClick(i: number, j: number) {
|
||||
let value = '';
|
||||
// 不同模式,获取不同数组的值
|
||||
if (abc.value) value = String(EngKeyBoardList.value[i][j]);
|
||||
else value = String(areaList.value[i][j]);
|
||||
emit('change', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改汽车牌键盘的输入模式,中文|英文
|
||||
*/
|
||||
function changeCarInputMode() {
|
||||
abc.value = !abc.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击退格键
|
||||
*/
|
||||
function backspaceClick() {
|
||||
emit('backspace');
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
emit('backspace');
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) clearInterval(timer);
|
||||
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: var(--u-bg-white);
|
||||
margin: 8rpx 5rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 0rpx var(--u-tips-color);
|
||||
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 var(--u-tips-color);
|
||||
}
|
||||
|
||||
.u-keyboard-change {
|
||||
font-size: 24rpx;
|
||||
box-shadow: 0 2rpx 0rpx var(--u-tips-color);
|
||||
position: absolute;
|
||||
width: 96rpx;
|
||||
left: 22rpx;
|
||||
line-height: 1;
|
||||
bottom: 32rpx;
|
||||
height: 80rpx;
|
||||
background-color: var(--u-bg-white);
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
border-radius: 8rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-keyboard-change .inactive.zh {
|
||||
transform: scale(0.85) translateY(-10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .inactive.en {
|
||||
transform: scale(0.85) translateY(10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .active {
|
||||
color: rgb(237, 112, 64);
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.u-keyboard-change .zh {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
|
||||
.u-keyboard-change .en {
|
||||
transform: translateY(10rpx);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* card 卡片类型定义
|
||||
* @description 供 u-card 组件 props 使用
|
||||
*/
|
||||
export type CardIndex = string | number | Record<string, any>;
|
||||
export type CardStyle = Record<string, any>;
|
||||
|
||||
export const CardProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 与屏幕两侧是否留空隙 */
|
||||
full: { type: Boolean, default: false },
|
||||
/** 标题 */
|
||||
title: { type: String, default: '' },
|
||||
/** 标题颜色 */
|
||||
titleColor: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 标题字体大小,单位rpx */
|
||||
titleSize: { type: [Number, String], default: '30' },
|
||||
/** 副标题 */
|
||||
subTitle: { type: String, default: '' },
|
||||
/** 副标题颜色 */
|
||||
subTitleColor: { type: String, default: 'var(--u-tips-color)' },
|
||||
/** 副标题字体大小,单位rpx */
|
||||
subTitleSize: { type: [Number, String], default: '26' },
|
||||
/** 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时) */
|
||||
border: { type: Boolean, default: false },
|
||||
/** 用于标识点击了第几个 */
|
||||
index: { type: [String, Number, Object] as PropType<CardIndex>, default: '' },
|
||||
/** 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx" */
|
||||
margin: { type: String, default: '30rpx' },
|
||||
/** card卡片的圆角 */
|
||||
borderRadius: { type: [Number, String], default: '16' },
|
||||
/** 头部自定义样式,对象形式 */
|
||||
headStyle: { type: Object as PropType<CardStyle>, default: () => ({}) },
|
||||
/** 主体自定义样式,对象形式 */
|
||||
bodyStyle: { type: Object as PropType<CardStyle>, default: () => ({}) },
|
||||
/** 底部自定义样式,对象形式 */
|
||||
footStyle: { type: Object as PropType<CardStyle>, default: () => ({}) },
|
||||
/** 头部是否下边框 */
|
||||
headBorderBottom: { type: Boolean, default: true },
|
||||
/** 底部是否有上边框 */
|
||||
footBorderTop: { type: Boolean, default: true },
|
||||
/** 标题左边的缩略图 */
|
||||
thumb: { type: String, default: '' },
|
||||
/** 缩略图宽高,单位rpx */
|
||||
thumbWidth: { type: [String, Number], default: '60' },
|
||||
/** 缩略图是否为圆形 */
|
||||
thumbCircle: { type: Boolean, default: false },
|
||||
/** 给head,body,foot的内边距 */
|
||||
padding: { type: [String, Number], default: '30' },
|
||||
/** 是否显示头部 */
|
||||
showHead: { type: Boolean, default: true },
|
||||
/** 是否显示尾部 */
|
||||
showFoot: { type: Boolean, default: true },
|
||||
/** 卡片外围阴影,字符串形式 */
|
||||
boxShadow: { type: String, default: 'none' }
|
||||
};
|
||||
|
||||
export type CardProps = ExtractPropTypes<typeof CardProps>;
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-card"
|
||||
@tap.stop="onClick"
|
||||
:class="[{ 'u-border': border, 'u-card-full': full, 'u-card--border': Number(borderRadius) > 0 }, customClass]"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
borderRadius: borderRadius + 'rpx',
|
||||
margin: margin,
|
||||
boxShadow: boxShadow
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<view
|
||||
v-if="showHead"
|
||||
class="u-card__head"
|
||||
:style="[{ padding: padding + 'rpx' }, headStyle]"
|
||||
:class="{
|
||||
'u-border-bottom': headBorderBottom
|
||||
}"
|
||||
@tap="onHeadClick"
|
||||
>
|
||||
<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="aspectFill"
|
||||
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="onBodyClick" class="u-card__body" :style="[{ padding: padding + 'rpx' }, bodyStyle]">
|
||||
<slot />
|
||||
<slot name="body" />
|
||||
</view>
|
||||
<view
|
||||
v-if="showFoot"
|
||||
class="u-card__foot"
|
||||
@tap="onFootClick"
|
||||
:style="[{ padding: slots.foot ? padding + 'rpx' : 0 }, footStyle]"
|
||||
:class="{
|
||||
'u-border-top': footBorderTop
|
||||
}"
|
||||
>
|
||||
<slot name="foot" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-card',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from 'vue';
|
||||
import { CardProps } from './types';
|
||||
import { $u } from '../..';
|
||||
|
||||
/**
|
||||
* card 卡片
|
||||
* @description 卡片组件一般用于多个列表条目,且风格统一的场景
|
||||
* @tutorial https://uviewpro.cn/zh/components/card.html
|
||||
* @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
|
||||
* @property {String} title 头部左边的标题
|
||||
* @property {String} title-color 标题颜色(默认var(--u-main-color))
|
||||
* @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
|
||||
* @property {String} sub-title 头部右边的副标题
|
||||
* @property {String} sub-title-color 副标题颜色(默认var(--u-tips-color))
|
||||
* @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-foot 是否显示尾部(默认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>
|
||||
*/
|
||||
const props = defineProps(CardProps);
|
||||
|
||||
const emit = defineEmits(['click', 'head-click', 'body-click', 'foot-click']);
|
||||
const slots = useSlots();
|
||||
|
||||
/**
|
||||
* 整个卡片任意位置被点击时触发
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click', props.index);
|
||||
}
|
||||
/**
|
||||
* 卡片头部被点击时触发
|
||||
*/
|
||||
function onHeadClick() {
|
||||
emit('head-click', props.index);
|
||||
}
|
||||
/**
|
||||
* 卡片主体部分被点击时触发
|
||||
*/
|
||||
function onBodyClick() {
|
||||
emit('body-click', props.index);
|
||||
}
|
||||
/**
|
||||
* 卡片底部部分被点击时触发
|
||||
*/
|
||||
function onFootClick() {
|
||||
emit('foot-click', props.index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
background-color: var(--u-bg-white);
|
||||
box-sizing: border-box;
|
||||
|
||||
&-full {
|
||||
// 如果是与屏幕之间不留空隙,应该设置左右边距为0
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--border:after {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__head {
|
||||
&--left {
|
||||
color: $u-main-color;
|
||||
|
||||
&__thumb {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
max-width: 400rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&--right {
|
||||
color: $u-tips-color;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
color: $u-content-color;
|
||||
}
|
||||
|
||||
&__foot {
|
||||
color: $u-tips-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* cell-group 组件 props 类型定义
|
||||
* @description 供 u-cell-group 组件 props 使用
|
||||
*/
|
||||
|
||||
export const CellGroupProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 分组标题 */
|
||||
title: { type: String, default: '' },
|
||||
/** 是否显示分组list上下边框 */
|
||||
border: { type: Boolean, default: true },
|
||||
/** 分组标题的样式,对象形式,注意驼峰属性写法 */
|
||||
titleStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) }
|
||||
};
|
||||
|
||||
export type CellGroupProps = ExtractPropTypes<typeof CellGroupProps>;
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<view class="u-cell-box" :class="customClass" :style="$u.toStyle(customStyle)">
|
||||
<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 lang="ts">
|
||||
export default {
|
||||
name: 'u-cell-group',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CellGroupProps } from './types';
|
||||
import { $u } from '../..';
|
||||
|
||||
/**
|
||||
* cellGroup 单元格父组件Group
|
||||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
|
||||
* @tutorial https://uviewpro.cn/zh/components/cell.html
|
||||
* @property {String} title 分组标题
|
||||
* @property {Boolean} border 是否显示外边框(默认true)
|
||||
* @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
|
||||
* @example <u-cell-group title="设置喜好">
|
||||
*/
|
||||
|
||||
defineProps(CellGroupProps);
|
||||
</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: var(--u-bg-white);
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { CellItemArrowDirection } from '../../types/global';
|
||||
|
||||
/**
|
||||
* cell-item 组件 props 类型定义
|
||||
* @description 供 u-cell-item 组件 props 使用
|
||||
*/
|
||||
export type CellItemIndex = string | number;
|
||||
export type CellItemStyle = Record<string, any>;
|
||||
|
||||
export const CellItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 左侧图标名称(只能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背景为灰色,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 as PropType<CellItemArrowDirection>, default: 'right' },
|
||||
/** 控制标题的样式 */
|
||||
titleStyle: { type: Object as PropType<CellItemStyle>, default: () => ({}) },
|
||||
/** 右侧显示内容的样式 */
|
||||
valueStyle: { type: Object as PropType<CellItemStyle>, default: () => ({}) },
|
||||
/** 描述信息的样式 */
|
||||
labelStyle: { type: Object as PropType<CellItemStyle>, default: () => ({}) },
|
||||
/** 背景颜色 */
|
||||
bgColor: { type: String, default: 'transparent' },
|
||||
/** 用于识别被点击的是第几个cell */
|
||||
index: { type: [String, Number] as PropType<CellItemIndex>, default: '' },
|
||||
/** 是否使用label插槽 */
|
||||
useLabelSlot: { type: Boolean, default: false },
|
||||
/** 左边图标的大小,单位rpx,只对传入icon字段时有效 */
|
||||
iconSize: { type: [Number, String], default: 34 },
|
||||
/** 左边图标的样式,对象形式 */
|
||||
iconStyle: { type: Object as PropType<CellItemStyle>, default: () => ({}) }
|
||||
};
|
||||
|
||||
export type CellItemProps = ExtractPropTypes<typeof CellItemProps>;
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<view
|
||||
@tap="onClick"
|
||||
class="u-cell"
|
||||
:class="[
|
||||
{
|
||||
'u-border-bottom': borderBottom,
|
||||
'u-border-top': borderTop,
|
||||
'u-col-center': center,
|
||||
'u-cell--required': required
|
||||
},
|
||||
customClass
|
||||
]"
|
||||
hover-stay-time="150"
|
||||
:hover-class="hoverClass"
|
||||
:style="$u.toStyle({ backgroundColor: bgColor }, customStyle)"
|
||||
>
|
||||
<view v-if="icon" class="u-cell__left-icon-wrap">
|
||||
<u-icon :size="iconSize" :name="icon" :custom-style="iconStyle"></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="u-flex" v-else>
|
||||
<slot name="icon"></slot>
|
||||
</view>
|
||||
<view class="u-cell_title" :style="[{ width: titleWidth ? titleWidth + 'rpx' : 'auto' }, titleStyle]">
|
||||
<template v-if="title !== ''">{{ title }}</template>
|
||||
<slot name="title" v-else></slot>
|
||||
<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
|
||||
<template v-if="label !== ''">{{ label }}</template>
|
||||
<slot name="label" v-else></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-cell__value" :style="[valueStyle]">
|
||||
<template v-if="value !== ''">{{ value }}</template>
|
||||
<slot v-else></slot>
|
||||
</view>
|
||||
<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
|
||||
<slot name="right-icon"></slot>
|
||||
</view>
|
||||
<view v-if="arrow" class="u-icon-wrap u-cell__right-icon-wrap">
|
||||
<u-icon name="arrow-right" :style="[arrowStyle]"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-cell-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue';
|
||||
import { CellItemProps } from './types';
|
||||
import { $u } from '../..';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* cellItem 单元格Item
|
||||
* @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
|
||||
* @tutorial https://uviewpro.cn/zh/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>
|
||||
*/
|
||||
|
||||
const emit = defineEmits<{ (e: 'click', index: string | number): void }>();
|
||||
|
||||
const props = defineProps(CellItemProps);
|
||||
|
||||
const $slots = useSlots();
|
||||
|
||||
/**
|
||||
* 箭头样式
|
||||
*/
|
||||
const arrowStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
if (props.arrowDirection === 'up') style.transform = 'rotate(-90deg)';
|
||||
else if (props.arrowDirection === 'down') style.transform = 'rotate(90deg)';
|
||||
else style.transform = 'rotate(0deg)';
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click', props.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: var(--u-bg-white);
|
||||
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: var(--u-tips-color);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.u-cell__left-icon-wrap,
|
||||
.u-cell__right-icon-wrap {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.u-cell-border:after {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
content: ' ';
|
||||
pointer-events: none;
|
||||
border-bottom: 1px solid $u-border-color;
|
||||
/* #endif */
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.u-cell-border {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u-cell__label {
|
||||
margin-top: 6rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
color: $u-tips-color;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-wrap: break-word;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-cell__value {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
/* #ifndef APP-NVUE */
|
||||
vertical-align: middle;
|
||||
/* #endif */
|
||||
color: $u-tips-color;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.u-cell__title,
|
||||
.u-cell__value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.u-cell--required {
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: visible;
|
||||
/* #endif */
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-cell--required:before {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
content: '*';
|
||||
/* #endif */
|
||||
left: 8px;
|
||||
margin-top: 4rpx;
|
||||
font-size: 14px;
|
||||
color: $u-type-error;
|
||||
}
|
||||
|
||||
.u-cell_right {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { Shape } from '../../types/global';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* checkbox-group 复选框组类型定义
|
||||
* @description 供 u-checkbox-group 组件 props 使用
|
||||
*/
|
||||
|
||||
export const CheckboxGroupProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 绑定值,选中的复选框name组成的数组 */
|
||||
modelValue: { type: Array as PropType<(string | number)[]>, default: undefined },
|
||||
/** 最多能选中多少个checkbox */
|
||||
max: { type: Number, default: 999 },
|
||||
/** 是否禁用所有复选框 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 在表单内提交时的标识符 */
|
||||
name: { type: [Boolean, String], default: '' },
|
||||
/** 是否禁止点击提示语选中复选框 */
|
||||
labelDisabled: { type: Boolean, default: false },
|
||||
/** 形状,square为方形,circle为原型 */
|
||||
shape: { type: String as PropType<Shape>, default: 'square' },
|
||||
/** 选中状态下的颜色 */
|
||||
activeColor: { type: String, default: () => getColor('primary') },
|
||||
/** 组件的整体大小,单位rpx,默认34rpx */
|
||||
size: { type: [String, Number], default: '' },
|
||||
/** 每个checkbox占u-checkbox-group的宽度 */
|
||||
width: { type: String, default: 'auto' },
|
||||
/** 是否每个checkbox都换行 */
|
||||
wrap: { type: Boolean, default: false },
|
||||
/** 图标的大小,单位rpx,默认20rpx */
|
||||
iconSize: { type: [String, Number], default: '' }
|
||||
};
|
||||
|
||||
export type CheckboxGroupProps = ExtractPropTypes<typeof CheckboxGroupProps>;
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<view class="u-checkbox-group u-clearfix" :class="customClass" :style="$u.toStyle(customStyle)">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-checkbox-group',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, nextTick, onMounted } from 'vue';
|
||||
import { $u, useParent, useChildren, useDebounce } from '../..';
|
||||
import { CheckboxGroupProps } from './types';
|
||||
|
||||
/**
|
||||
* checkboxGroup 开关选择器父组件Group
|
||||
* @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
|
||||
* @tutorial https://uviewpro.cn/zh/components/checkbox.html
|
||||
* @property {Array} modelValue 绑定值,选中的复选框name组成的数组(支持v-model双向绑定)
|
||||
* @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} shape 外观形状,shape-方形,circle-圆形(默认circle)
|
||||
* @property {Boolean} wrap 是否每个checkbox都换行(默认false)
|
||||
* @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认主题色primary)
|
||||
* @event {Function} change 任一个checkbox状态发生变化时触发,回调为选中的name数组
|
||||
* @example <u-checkbox-group v-model="selectedValues">
|
||||
* <u-checkbox name="apple">苹果</u-checkbox>
|
||||
* <u-checkbox name="banana">香蕉</u-checkbox>
|
||||
* </u-checkbox-group>
|
||||
*/
|
||||
const props = defineProps(CheckboxGroupProps);
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 使用父组件Hook
|
||||
const { children, broadcast } = useParent('u-checkbox-group');
|
||||
const { emitToParent } = useChildren('u-checkbox-group', 'u-form-item');
|
||||
const { debounce } = useDebounce(1);
|
||||
|
||||
/**
|
||||
* 根据modelValue设置子组件状态
|
||||
*/
|
||||
function syncChildrenSelection() {
|
||||
if (!children || children.length === 0 || !props.modelValue) return;
|
||||
const modelValueSet = new Set(props.modelValue);
|
||||
children.forEach((child: any) => {
|
||||
const childValue = child.getExposed?.()?.value;
|
||||
const shouldBeChecked = modelValueSet.has(childValue);
|
||||
const isCurrentlyChecked = child.getExposed?.()?.isChecked.value;
|
||||
|
||||
if (shouldBeChecked !== isCurrentlyChecked) {
|
||||
child.getExposed?.()?.setChecked({ checked: shouldBeChecked });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听modelValue变化,同步子组件状态
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
syncChildrenSelection();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 派发 change 事件和表单校验
|
||||
*/
|
||||
function emitEvent() {
|
||||
debounce(() => {
|
||||
// 收集所有选中的 name
|
||||
let values: any[] = [];
|
||||
children.forEach((child: any) => {
|
||||
if (child.getExposed?.()?.isChecked.value) {
|
||||
values.push(child.getExposed?.()?.value);
|
||||
}
|
||||
});
|
||||
emit('change', values);
|
||||
emit('update:modelValue', values);
|
||||
setTimeout(() => {
|
||||
emitToParent('onFormChange', values);
|
||||
}, 60);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选/全不选方法
|
||||
*/
|
||||
function setAllChecked(checked: boolean) {
|
||||
if (props.disabled) {
|
||||
console.warn('u-checkbox-group已禁用,无法操作');
|
||||
return;
|
||||
}
|
||||
broadcast('setChecked', { checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中的值
|
||||
*/
|
||||
function getSelectedValues() {
|
||||
return children
|
||||
.filter(child => child.getExposed?.()?.isChecked.value)
|
||||
.map(child => child.getExposed?.()?.name)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证选择是否超过最大数量
|
||||
*/
|
||||
function validateSelection() {
|
||||
const selectedCount = children.filter(child => child.getExposed?.()?.isChecked.value).length;
|
||||
if (props.max && selectedCount >= props.max) {
|
||||
$u.toast(`超过最大选择数量: ${props.max}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
syncChildrenSelection();
|
||||
});
|
||||
});
|
||||
|
||||
// 使用defineExpose暴露给外部
|
||||
defineExpose({
|
||||
// props
|
||||
props,
|
||||
|
||||
// 方法
|
||||
emitEvent,
|
||||
setAllChecked,
|
||||
getSelectedValues,
|
||||
validateSelection,
|
||||
syncChildrenSelection,
|
||||
|
||||
// 计算属性
|
||||
selectedCount: computed(() => children.filter(child => child.getExposed?.()?.isChecked.value).length),
|
||||
isFull: computed(() => {
|
||||
const selectedCount = children.filter(child => child.getExposed?.()?.isChecked.value).length;
|
||||
return props.max && selectedCount >= props.max;
|
||||
}),
|
||||
isEmpty: computed(() => children.filter(child => child.getExposed?.()?.isChecked.value).length === 0),
|
||||
// 工具方法
|
||||
getChildrenCount: () => children.length
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-checkbox-group {
|
||||
/* #ifndef MP || APP-NVUE */
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { Shape, SizeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* checkbox 复选框类型定义
|
||||
* @description 供 u-checkbox 组件 props 使用
|
||||
*/
|
||||
export type CheckboxValue = string | number | boolean;
|
||||
|
||||
export const CheckboxProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** checkbox的标签 */
|
||||
label: { type: String, default: '' },
|
||||
/** checkbox的值 */
|
||||
name: { type: [String, Number], default: '' },
|
||||
/** checkbox的值,不传递时为name的属性 */
|
||||
value: { type: [String, Number], default: '' },
|
||||
/** 形状,square为方形,circle为原型 */
|
||||
shape: { type: String as PropType<Shape>, default: '' },
|
||||
/** 是否为选中状态 */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 是否禁用 */
|
||||
disabled: { type: [String, Boolean], default: '' },
|
||||
/** 是否禁止点击提示语选中复选框 */
|
||||
labelDisabled: { type: [String, Boolean], default: '' },
|
||||
/** 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值 */
|
||||
activeColor: { type: String, default: '' },
|
||||
/** 图标的大小,单位rpx */
|
||||
iconSize: { type: [String, Number], default: '' },
|
||||
/** label的字体大小,rpx单位 */
|
||||
labelSize: { type: [String, Number], default: '' },
|
||||
/** 组件的整体大小 */
|
||||
size: { type: [String, Number] as PropType<SizeType | string | number>, default: '' }
|
||||
};
|
||||
|
||||
export type CheckboxProps = ExtractPropTypes<typeof CheckboxProps>;
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="u-checkbox" :style="$u.toStyle(checkboxStyle, customStyle)" :class="customClass">
|
||||
<view class="u-checkbox__icon-wrap" @tap="toggle" :class="iconClass" :style="$u.toStyle(iconStyle)">
|
||||
<u-icon name="checkbox-mark" :size="checkboxIconSize" :color="iconColor" />
|
||||
</view>
|
||||
<view
|
||||
class="u-checkbox__label"
|
||||
@tap="onClickLabel"
|
||||
:style="{
|
||||
fontSize: labelFontSize
|
||||
}"
|
||||
>
|
||||
<slot>
|
||||
{{ label }}
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-checkbox',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { $u, useChildren } from '../..';
|
||||
import { CheckboxProps } from './types';
|
||||
import type { SizeType } from '../../types/global';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* checkbox 复选框
|
||||
* @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
|
||||
* @tutorial https://uviewpro.cn/zh/components/checkbox.html
|
||||
* @property {String Number} icon-size 图标大小,单位rpx(默认20)
|
||||
* @property {String Number} label-size label字体大小,单位rpx(默认28)
|
||||
* @property {String Number} name checkbox组件的标示符
|
||||
* @property {String} shape 形状,见官网说明(默认circle)
|
||||
* @property {Boolean} disabled 是否禁用
|
||||
* @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
|
||||
* @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
|
||||
* @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
|
||||
* @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
|
||||
*/
|
||||
|
||||
const props = defineProps(CheckboxProps);
|
||||
const emit = defineEmits(['change', 'update:modelValue']);
|
||||
// checkbox 是否选中,true/false
|
||||
const checkedValue = ref(props.modelValue);
|
||||
// 使用子组件Hook
|
||||
const { parentExposed } = useChildren('u-checkbox', 'u-checkbox-group');
|
||||
const { parentExposed: formExposed } = useChildren('u-checkbox', 'u-form');
|
||||
|
||||
// checkbox 的value值,id
|
||||
const checkboxValue = computed(() => {
|
||||
if (props.value !== '') return props.value;
|
||||
return props.name;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal: boolean) => {
|
||||
checkedValue.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => checkedValue.value,
|
||||
(newVal: boolean) => {
|
||||
emit('update:modelValue', newVal);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 根据 size 定义不同的配置
|
||||
const sizeConfig = {
|
||||
small: {
|
||||
size: 28,
|
||||
fontSize: 24,
|
||||
iconSize: 16
|
||||
},
|
||||
default: {
|
||||
size: 34,
|
||||
fontSize: 28,
|
||||
iconSize: 20
|
||||
},
|
||||
large: {
|
||||
size: 40,
|
||||
fontSize: 32,
|
||||
iconSize: 24
|
||||
}
|
||||
};
|
||||
|
||||
// 获取实际使用的 size 值(优先级:props.size > u-form.size)
|
||||
const actualSize = computed(() => {
|
||||
// 优先使用 props 的 size 属性
|
||||
if (props.size !== '') {
|
||||
return String(props.size);
|
||||
}
|
||||
// 次优先:使用 u-checkbox-group 的 size 属性
|
||||
if (parentExposed.value?.props?.size) {
|
||||
return String(parentExposed.value.props.size);
|
||||
}
|
||||
// 最后:使用 u-form 的 size 属性(u-form 的 size 只支持预设值)
|
||||
if (formExposed.value?.props?.size) {
|
||||
return String(formExposed.value.props.size);
|
||||
}
|
||||
// 默认值
|
||||
return 'default';
|
||||
});
|
||||
|
||||
// 判断实际使用的 size 是否在预设配置中
|
||||
const isInSizeConfig = computed(() => actualSize.value in sizeConfig);
|
||||
|
||||
// 获取预设 size(用于查找 sizeConfig 配置,如图标大小、高度等)
|
||||
const presetSize = computed(() => {
|
||||
return (isInSizeConfig.value ? actualSize.value : 'default') as SizeType;
|
||||
});
|
||||
|
||||
// 获取当前尺寸配置
|
||||
const currentSizeConfig = computed(() => sizeConfig[presetSize.value]);
|
||||
|
||||
// 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
|
||||
const isDisabled = computed(() => {
|
||||
return props.disabled !== '' ? props.disabled : (parentExposed.value?.props?.disabled ?? false);
|
||||
});
|
||||
|
||||
// 是否禁用label点击
|
||||
const isLabelDisabled = computed(() => {
|
||||
return props.labelDisabled !== '' ? props.labelDisabled : (parentExposed.value?.props?.labelDisabled ?? false);
|
||||
});
|
||||
|
||||
// 组件尺寸,对应size的值,默认值为34rpx
|
||||
const checkboxSize = computed(() => currentSizeConfig.value.size);
|
||||
|
||||
// 组件的勾选图标的尺寸,默认20rpx
|
||||
const checkboxIconSize = computed(() => {
|
||||
if (props.iconSize) {
|
||||
return props.iconSize;
|
||||
}
|
||||
if (parentExposed.value?.props?.iconSize) {
|
||||
return parentExposed.value?.props?.iconSize;
|
||||
}
|
||||
if (isInSizeConfig.value) {
|
||||
return currentSizeConfig.value.iconSize;
|
||||
}
|
||||
return 20;
|
||||
});
|
||||
|
||||
// label字体大小,默认28rpx
|
||||
const labelFontSize = computed(() => {
|
||||
if (isInSizeConfig.value) {
|
||||
return $u.addUnit(currentSizeConfig.value.fontSize);
|
||||
}
|
||||
return $u.addUnit(props.labelSize);
|
||||
});
|
||||
|
||||
// 组件选中激活时的颜色
|
||||
const elActiveColor = computed(() => {
|
||||
return props.activeColor ? props.activeColor : (parentExposed.value?.props?.activeColor ?? 'primary');
|
||||
});
|
||||
|
||||
// 组件的形状
|
||||
const elShape = computed(() => {
|
||||
return props.shape ? props.shape : (parentExposed.value?.props?.shape ?? 'square');
|
||||
});
|
||||
|
||||
// 图标样式
|
||||
const iconStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
if (elActiveColor.value && checkedValue.value && !isDisabled.value) {
|
||||
style.borderColor = elActiveColor.value;
|
||||
style.backgroundColor = elActiveColor.value;
|
||||
}
|
||||
style.width = $u.addUnit(checkboxSize.value);
|
||||
style.height = $u.addUnit(checkboxSize.value);
|
||||
return style;
|
||||
});
|
||||
|
||||
// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
|
||||
const iconColor = computed(() => {
|
||||
return checkedValue.value ? 'var(--u-white-color)' : 'transparent';
|
||||
});
|
||||
|
||||
const iconClass = computed(() => {
|
||||
let classes: string[] = [];
|
||||
classes.push('u-checkbox__icon-wrap--' + elShape.value);
|
||||
if (checkedValue.value == true) classes.push('u-checkbox__icon-wrap--checked');
|
||||
if (isDisabled.value) classes.push('u-checkbox__icon-wrap--disabled');
|
||||
if (checkedValue.value && isDisabled.value) classes.push('u-checkbox__icon-wrap--disabled--checked');
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
const checkboxStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
if (parentExposed.value?.props?.width) {
|
||||
style.width = parentExposed.value.props.width;
|
||||
// #ifdef MP
|
||||
style.float = 'left';
|
||||
// #endif
|
||||
// #ifndef MP
|
||||
style.flex = `0 0 ${parentExposed.value.props.width}`;
|
||||
// #endif
|
||||
}
|
||||
if (parentExposed.value?.props?.wrap) {
|
||||
style.width = '100%';
|
||||
// #ifndef MP
|
||||
style.flex = '0 0 100%';
|
||||
// #endif
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击label
|
||||
*/
|
||||
function onClickLabel() {
|
||||
if (!isLabelDisabled.value && !isDisabled.value) {
|
||||
setValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击icon
|
||||
*/
|
||||
function toggle() {
|
||||
if (!isDisabled.value) {
|
||||
setValue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发change事件
|
||||
*/
|
||||
function emitEvent() {
|
||||
emit('change', !checkedValue.value);
|
||||
// 通知父组件状态变化
|
||||
setTimeout(() => {
|
||||
if (parentExposed.value?.emitEvent) {
|
||||
parentExposed.value.emitEvent();
|
||||
}
|
||||
}, 80);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通过v-model绑定的组件的值
|
||||
*/
|
||||
function setValue() {
|
||||
// 判断是否超过了可选的最大数量
|
||||
if (checkedValue.value == true) {
|
||||
emitEvent();
|
||||
checkedValue.value = false;
|
||||
} else {
|
||||
if (parentExposed?.value?.validateSelection && !parentExposed?.value?.validateSelection()) {
|
||||
return;
|
||||
}
|
||||
emitEvent();
|
||||
checkedValue.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置组件的modelValue值
|
||||
function setChecked(data: any) {
|
||||
if (!isDisabled.value) {
|
||||
const needEmit = checkedValue.value !== data.checked;
|
||||
checkedValue.value = data.checked;
|
||||
if (needEmit) {
|
||||
emitEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
isChecked: computed(() => checkedValue.value),
|
||||
label: props.label,
|
||||
name: props.name,
|
||||
value: checkboxValue.value,
|
||||
setValue,
|
||||
emitEvent,
|
||||
props,
|
||||
setChecked
|
||||
});
|
||||
</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 var(--u-border-color);
|
||||
transition-duration: 0.2s;
|
||||
/* #ifdef MP-TOUTIAO */
|
||||
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
|
||||
line-height: 0;
|
||||
/* #endif */
|
||||
|
||||
&--circle {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&--square {
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
color: var(--u-white-color);
|
||||
background-color: $u-type-primary;
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: var(--u-bg-gray-light);
|
||||
border-color: var(--u-border-color);
|
||||
}
|
||||
|
||||
&--disabled--checked {
|
||||
color: var(--u-bg-gray-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
word-wrap: break-word;
|
||||
margin-left: 10rpx;
|
||||
margin-right: 24rpx;
|
||||
color: $u-content-color;
|
||||
font-size: 30rpx;
|
||||
|
||||
&--disabled {
|
||||
color: var(--u-bg-gray-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ThemeType } from '../../types/global';
|
||||
import { $u } from '../../libs';
|
||||
|
||||
/**
|
||||
* circleProgress 环形进度条 Props
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
|
||||
*/
|
||||
export const CircleProgressProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 圆环进度百分比值 */
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator: (val: number) => val >= 0 && val <= 100
|
||||
},
|
||||
/** 底部圆环的颜色(灰色的圆环) */
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: () => $u.getColor('dividerColor')
|
||||
},
|
||||
/** 圆环激活部分的颜色 */
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: 'var(--u-type-success)'
|
||||
},
|
||||
/** 圆环线条的宽度,单位rpx */
|
||||
borderWidth: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 14
|
||||
},
|
||||
/** 整个圆形的宽度,单位rpx */
|
||||
width: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 200
|
||||
},
|
||||
/** 整个圆环执行一圈的时间,单位ms */
|
||||
duration: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 1500
|
||||
},
|
||||
/** 主题类型 */
|
||||
type: {
|
||||
type: String as PropType<ThemeType>,
|
||||
default: ''
|
||||
},
|
||||
/** 整个圆环进度区域的背景色 */
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'var(--u-bg-white)'
|
||||
}
|
||||
};
|
||||
|
||||
export type CircleProgressProps = ExtractPropTypes<typeof CircleProgressProps>;
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-circle-progress"
|
||||
:class="customClass"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px',
|
||||
backgroundColor: bgColor
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-TOUTIAO -->
|
||||
<canvas
|
||||
class="u-canvas-bg"
|
||||
type="2d"
|
||||
:canvas-id="elBgId"
|
||||
:id="elBgId"
|
||||
:style="{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px'
|
||||
}"
|
||||
></canvas>
|
||||
<canvas
|
||||
class="u-canvas"
|
||||
type="2d"
|
||||
:canvas-id="elId"
|
||||
:id="elId"
|
||||
:style="{
|
||||
width: widthPx + 'px',
|
||||
height: widthPx + 'px'
|
||||
}"
|
||||
></canvas>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN || MP-TOUTIAO -->
|
||||
<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>
|
||||
<!-- #endif -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-circle-progress',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, getCurrentInstance, onBeforeMount } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import { CircleProgressProps } from './types';
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
import { canvas2d } from '../../libs/util/canvas-2d';
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* circleProgress 环形进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
|
||||
* @tutorial https://uviewpro.cn/zh/components/circleProgress.html
|
||||
* @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
|
||||
* @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认var(--u-divider-color))
|
||||
* @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认var(--u-type-success))
|
||||
* @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="var(--u-type-primary)" :percent="80"></u-circle-progress>
|
||||
*/
|
||||
const props = defineProps(CircleProgressProps);
|
||||
|
||||
let elBgId = $u.guid(); // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
|
||||
let elId = $u.guid();
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
// elBgId = 'uCircleProgressBgId'; // 微信小程序中不能使用$u.guid()形式动态生成id值,否则会报错
|
||||
// elId = 'uCircleProgressElId';
|
||||
// #endif
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pixelRatio = ref<number>(1); // 像素比
|
||||
|
||||
// 存储 MP-WEIXIN 下通过 selectorQuery 获取到的 canvas node,方便在绘制前清空
|
||||
const canvasNodeMap = new Map<string, any>();
|
||||
|
||||
const widthPx = computed(() =>
|
||||
typeof uni !== 'undefined' && uni.upx2px ? uni.upx2px(Number(props.width)) : Number(props.width)
|
||||
);
|
||||
const borderWidthPx = computed(() =>
|
||||
typeof uni !== 'undefined' && uni.upx2px ? uni.upx2px(Number(props.borderWidth)) : Number(props.borderWidth)
|
||||
);
|
||||
const startAngle = -Math.PI / 2; // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
|
||||
const progressContext = ref<any>(null); // 活动圆的canvas上下文
|
||||
const newPercent = ref(props.percent); // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
|
||||
const oldPercent = ref(0); // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
|
||||
|
||||
/**
|
||||
* 有type主题时,优先起作用
|
||||
*/
|
||||
const circleColor = computed(() => {
|
||||
if (props.type && ['success', 'error', 'info', 'primary', 'warning'].includes(props.type)) {
|
||||
return $u.color[props.type as keyof typeof $u.color] as string;
|
||||
}
|
||||
return props.activeColor;
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
pixelRatio.value = uni.getSystemInfoSync().pixelRatio;
|
||||
});
|
||||
|
||||
// 监听percent变化,动态绘制进度
|
||||
watch(
|
||||
() => props.percent,
|
||||
(nVal, oVal = 0) => {
|
||||
let next = nVal > 100 ? 100 : nVal;
|
||||
let prev = oVal < 0 ? 0 : oVal;
|
||||
newPercent.value = next;
|
||||
oldPercent.value = prev;
|
||||
setTimeout(() => {
|
||||
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
|
||||
// 将此值减少或者新增到新的百分比值
|
||||
drawCircleByProgress(prev);
|
||||
}, 50);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 赋值,用于加载后第一个画圆使用
|
||||
newPercent.value = props.percent;
|
||||
oldPercent.value = 0;
|
||||
// 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
|
||||
setTimeout(() => {
|
||||
drawProgressBg();
|
||||
drawCircleByProgress(oldPercent.value);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取canvas上下文
|
||||
*/
|
||||
function getContext(canvasId: any) {
|
||||
return new Promise<UniApp.CanvasContext>(resolve => {
|
||||
let ctx = null;
|
||||
// #ifndef MP-WEIXIN || MP-TOUTIAO
|
||||
ctx = uni.createCanvasContext(canvasId, instance);
|
||||
resolve(ctx);
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
uni.createSelectorQuery()
|
||||
.in(instance?.proxy)
|
||||
.select(`#${canvasId}`)
|
||||
.node(res => {
|
||||
if (res && res.node) {
|
||||
const canvas = res.node;
|
||||
ctx = canvas2d(canvas.getContext('2d') as CanvasRenderingContext2D);
|
||||
canvas.width = widthPx.value * pixelRatio.value;
|
||||
canvas.height = widthPx.value * pixelRatio.value;
|
||||
ctx.scale(pixelRatio.value, pixelRatio.value);
|
||||
// 存储 canvas node,后续绘制时用于清空画布
|
||||
canvasNodeMap.set(canvasId, canvas);
|
||||
resolve(ctx);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制底部灰色圆环
|
||||
*/
|
||||
async function drawProgressBg() {
|
||||
const ctx = await getContext(elBgId);
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
// 清空背景画布(如果可用)以确保绘制一致性
|
||||
try {
|
||||
if (typeof ctx.clearRect === 'function') {
|
||||
const node = canvasNodeMap.get(elBgId);
|
||||
const w = node ? node.width : widthPx.value * pixelRatio.value;
|
||||
const h = node ? node.height : widthPx.value * pixelRatio.value;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
ctx.setLineWidth(borderWidthPx.value); // 设置圆环宽度
|
||||
ctx.setStrokeStyle(props.inactiveColor); // 线条颜色
|
||||
ctx.beginPath(); // 开始描绘路径
|
||||
const radius = widthPx.value / 2; // 设置一个原点(110,110),半径为100的圆的路径到当前路径
|
||||
ctx.arc(radius, radius, radius - borderWidthPx.value, 0, 2 * Math.PI, false);
|
||||
ctx.stroke(); // 对路径进行描绘
|
||||
ctx.draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制进度圆环
|
||||
* @param progress 当前进度
|
||||
*/
|
||||
async function drawCircleByProgress(progress: number) {
|
||||
// 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
|
||||
let ctx = progressContext.value;
|
||||
if (!ctx) {
|
||||
ctx = await getContext(elId);
|
||||
progressContext.value = ctx;
|
||||
}
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO
|
||||
// 清空进度画布,避免旧的更大进度残留,导致无法降低的视觉错误
|
||||
try {
|
||||
if (typeof ctx.clearRect === 'function') {
|
||||
const node = canvasNodeMap.get(elId);
|
||||
const w = node ? node.width : widthPx.value * pixelRatio.value;
|
||||
const h = node ? node.height : widthPx.value * pixelRatio.value;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// #endif
|
||||
// 表示进度的两端为圆形
|
||||
ctx.setLineCap('round');
|
||||
// 设置线条的宽度和颜色
|
||||
ctx.setLineWidth(borderWidthPx.value);
|
||||
ctx.setStrokeStyle(circleColor.value);
|
||||
// 将总过渡时间除以100,得出每修改百分之一进度所需的时间
|
||||
let time = Math.floor(Number(props.duration) / 100);
|
||||
// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
|
||||
// 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
|
||||
let endAngle = ((2 * Math.PI) / 100) * progress + startAngle;
|
||||
ctx.beginPath();
|
||||
// 半径为整个canvas宽度的一半
|
||||
let radius = widthPx.value / 2;
|
||||
ctx.arc(radius, radius, radius - borderWidthPx.value, startAngle, endAngle, false);
|
||||
ctx.stroke();
|
||||
ctx.draw();
|
||||
// 如果变更后新值大于旧值,意味着增大了百分比
|
||||
if (newPercent.value > oldPercent.value) {
|
||||
// 每次递增百分之一
|
||||
progress++;
|
||||
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
|
||||
if (progress > newPercent.value) return;
|
||||
} else {
|
||||
// 同理于上面
|
||||
progress--;
|
||||
if (progress < newPercent.value) return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
// 定时器,每次操作间隔为time值,为了让进度条有动画效果
|
||||
drawCircleByProgress(progress);
|
||||
}, time);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
.u-circle-progress {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.u-canvas-bg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.u-canvas {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-city-select 城市选择器 Props
|
||||
* @description 用于选择省、市、区三级行政区域,支持回显和自定义初始值。
|
||||
*/
|
||||
export const CitySelectProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 控制弹窗显示与隐藏(v-model) */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 默认选中的省市区名称数组 */
|
||||
defaultRegion: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 默认选中的省市区编码数组 */
|
||||
areaCode: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 是否允许点击遮罩关闭弹窗 */
|
||||
maskCloseAble: { type: Boolean, default: true },
|
||||
/** 弹窗层级 */
|
||||
zIndex: { type: [String, Number] as PropType<string | number>, default: 0 }
|
||||
};
|
||||
|
||||
export type CitySelectProps = ExtractPropTypes<typeof CitySelectProps>;
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<u-popup
|
||||
v-model="popupValue"
|
||||
:class="customClass"
|
||||
:style="$u.toStyle(customStyle)"
|
||||
mode="bottom"
|
||||
:popup="false"
|
||||
:mask="true"
|
||||
:closeable="true"
|
||||
:safe-area-inset-bottom="true"
|
||||
close-icon-color="var(--u-white-color)"
|
||||
:z-index="uZIndex"
|
||||
:maskCloseAble="props.maskCloseAble"
|
||||
@close="close"
|
||||
>
|
||||
<u-tabs
|
||||
v-if="popupValue"
|
||||
:list="genTabsList"
|
||||
:is-scroll="true"
|
||||
:current="tabsIndex"
|
||||
@change="tabsChange"
|
||||
ref="tabs"
|
||||
></u-tabs>
|
||||
<view class="area-box">
|
||||
<view class="u-flex" :class="{ change: isChange }">
|
||||
<!-- 省 -->
|
||||
<view class="area-item">
|
||||
<view class="u-padding-10 u-bg-gray" style="height: 100%">
|
||||
<scroll-view :scroll-y="true" style="height: 100%">
|
||||
<u-cell-group>
|
||||
<u-cell-item
|
||||
v-for="(item, index) in provincesList"
|
||||
:title="item.label"
|
||||
:arrow="false"
|
||||
:index="index"
|
||||
:key="index"
|
||||
@click="provinceChange(index)"
|
||||
>
|
||||
<template v-if="isChooseP && province === index" #right-icon>
|
||||
<u-icon size="34" name="checkbox-mark"></u-icon>
|
||||
</template>
|
||||
</u-cell-item>
|
||||
</u-cell-group>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 市 -->
|
||||
<view class="area-item">
|
||||
<view class="u-padding-10 u-bg-gray" style="height: 100%">
|
||||
<scroll-view :scroll-y="true" style="height: 100%">
|
||||
<u-cell-group v-if="isChooseP">
|
||||
<u-cell-item
|
||||
v-for="(item, index) in citys"
|
||||
:title="item.label"
|
||||
:arrow="false"
|
||||
:index="index"
|
||||
:key="index"
|
||||
@click="cityChange(index)"
|
||||
>
|
||||
<template v-if="isChooseC && city === index" #right-icon>
|
||||
<u-icon size="34" name="checkbox-mark"></u-icon>
|
||||
</template>
|
||||
</u-cell-item>
|
||||
</u-cell-group>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 区 -->
|
||||
<view class="area-item">
|
||||
<view class="u-padding-10 u-bg-gray" style="height: 100%">
|
||||
<scroll-view :scroll-y="true" style="height: 100%">
|
||||
<u-cell-group v-if="isChooseC">
|
||||
<u-cell-item
|
||||
v-for="(item, index) in areas"
|
||||
:title="item.label"
|
||||
:arrow="false"
|
||||
:index="index"
|
||||
:key="index"
|
||||
@click="areaChange(index)"
|
||||
>
|
||||
<template v-if="isChooseA && area === index" #right-icon>
|
||||
<u-icon size="34" name="checkbox-mark"></u-icon>
|
||||
</template>
|
||||
</u-cell-item>
|
||||
</u-cell-group>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-city-select',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import provinces from '../../libs/util/province';
|
||||
import citysData from '../../libs/util/city';
|
||||
import areasData from '../../libs/util/area';
|
||||
import { CitySelectProps } from './types';
|
||||
import { $u } from '../..';
|
||||
import uCellGroup from '../u-cell-group/u-cell-group.vue';
|
||||
import uCellItem from '../u-cell-item/u-cell-item.vue';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
import uPopup from '../u-popup/u-popup.vue';
|
||||
import uTabs from '../u-tabs/u-tabs.vue';
|
||||
|
||||
/**
|
||||
* u-city-select 城市选择器
|
||||
* @description 用于选择省、市、区三级行政区域,支持回显和自定义初始值。
|
||||
* @property {Boolean} modelValue 控制弹窗显示与隐藏(v-model)
|
||||
* @property {Array} defaultRegion 默认选中的省市区名称数组,如 ['山东省', '青岛市', '崂山区']
|
||||
* @property {Array} areaCode 默认选中的省市区编码数组,如 ['440000', '440100', '440106']
|
||||
* @property {Boolean} maskCloseAble 是否允许点击遮罩关闭弹窗(默认 true)
|
||||
* @property {String|Number} zIndex 弹窗层级(默认 0,自动适配)
|
||||
* @event update:modelValue v-model 绑定值变化时触发
|
||||
* @event city-change 选择省市区后触发,返回选中的省市区对象
|
||||
* @example <u-city-select v-model="show" :defaultRegion="['山东省', '青岛市', '崂山区']" @city-change="onChange"></u-city-select>
|
||||
*/
|
||||
const props = defineProps(CitySelectProps);
|
||||
|
||||
// emits 定义
|
||||
const emit = defineEmits(['update:modelValue', 'city-change']);
|
||||
|
||||
// 省市区选择相关响应式数据
|
||||
const cityValue = ref('');
|
||||
const isChooseP = ref(false); // 是否已选省
|
||||
const province = ref(0); // 省下标
|
||||
const provincesList = ref(provinces);
|
||||
const isChooseC = ref(false); // 是否已选市
|
||||
const city = ref(0); // 市下标
|
||||
const citys = ref(citysData[0]);
|
||||
const isChooseA = ref(false); // 是否已选区
|
||||
const area = ref(0); // 区下标
|
||||
const areas = ref(areasData[0][0]);
|
||||
const tabsIndex = ref(0);
|
||||
|
||||
// v-model 双向绑定
|
||||
const popupValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
// 是否切换到区级
|
||||
const isChange = computed(() => tabsIndex.value > 1);
|
||||
|
||||
// 顶部 tab 列表
|
||||
const genTabsList = computed(() => {
|
||||
const tabsList = [{ name: '请选择' }];
|
||||
if (isChooseP.value) {
|
||||
tabsList[0].name = provincesList.value[province.value].label;
|
||||
tabsList[1] = { name: '请选择' };
|
||||
}
|
||||
if (isChooseC.value) {
|
||||
tabsList[1].name = citys.value[city.value].label;
|
||||
tabsList[2] = { name: '请选择' };
|
||||
}
|
||||
if (isChooseA.value) {
|
||||
tabsList[2].name = areas.value[area.value].label;
|
||||
}
|
||||
return tabsList;
|
||||
});
|
||||
|
||||
// z-index 计算
|
||||
const uZIndex = computed(() => (props.zIndex ? props.zIndex : (uni as any).$u?.zIndex?.popup || 1075));
|
||||
|
||||
// 初始化选中项
|
||||
function init() {
|
||||
if (props.areaCode.length === 3) {
|
||||
setProvince('', props.areaCode[0]);
|
||||
setCity('', props.areaCode[1]);
|
||||
setArea('', props.areaCode[2]);
|
||||
} else if (props.defaultRegion.length === 3) {
|
||||
setProvince(props.defaultRegion[0], '');
|
||||
setCity(props.defaultRegion[1], '');
|
||||
setArea(props.defaultRegion[2], '');
|
||||
}
|
||||
}
|
||||
|
||||
// 选中省
|
||||
function setProvince(label = '', value = '') {
|
||||
provincesList.value.forEach((v, k) => {
|
||||
if (value ? v.value == value : v.label == label) {
|
||||
provinceChange(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 选中市
|
||||
function setCity(label = '', value = '') {
|
||||
citys.value.forEach((v, k) => {
|
||||
if (value ? v.value == value : v.label == label) {
|
||||
cityChange(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 选中区
|
||||
function setArea(label = '', value = '') {
|
||||
areas.value.forEach((v, k) => {
|
||||
if (value ? v.value == value : v.label == label) {
|
||||
isChooseA.value = true;
|
||||
area.value = k;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function close() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
// tab 切换
|
||||
function tabsChange(index: number) {
|
||||
tabsIndex.value = index;
|
||||
}
|
||||
// 省点击
|
||||
function provinceChange(index: number) {
|
||||
isChooseP.value = true;
|
||||
isChooseC.value = false;
|
||||
isChooseA.value = false;
|
||||
province.value = index;
|
||||
citys.value = citysData[index];
|
||||
tabsIndex.value = 1;
|
||||
}
|
||||
// 市点击
|
||||
function cityChange(index: number) {
|
||||
isChooseC.value = true;
|
||||
isChooseA.value = false;
|
||||
city.value = index;
|
||||
areas.value = areasData[province.value][index];
|
||||
tabsIndex.value = 2;
|
||||
}
|
||||
// 区点击
|
||||
function areaChange(index: number) {
|
||||
isChooseA.value = true;
|
||||
area.value = index;
|
||||
const result = {
|
||||
province: provincesList.value[province.value],
|
||||
city: citys.value[city.value],
|
||||
area: areas.value[area.value]
|
||||
};
|
||||
emit('city-change', result);
|
||||
close();
|
||||
}
|
||||
|
||||
// 挂载时初始化
|
||||
onMounted(init);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.area-box {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: 800rpx;
|
||||
|
||||
> view {
|
||||
width: 150%;
|
||||
transition: transform 0.3s ease-in-out 0s;
|
||||
transform: translateX(0);
|
||||
|
||||
&.change {
|
||||
transform: translateX(-33.3333333%);
|
||||
}
|
||||
}
|
||||
|
||||
.area-item {
|
||||
width: 33.3333333%;
|
||||
height: 800rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { AlignType, JustifyType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-col 布局单元格 Props
|
||||
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
|
||||
* @property {String|Number} span 栅格占据的列数,总12等分(默认12)
|
||||
* @property {String|Number} offset 分栏左边偏移,计算方式与span相同(默认0)
|
||||
* @property {JustifyType} justify 水平排列方式,可选值为start、end、center、around、between(默认start)
|
||||
* @property {AlignType} align 垂直对齐方式,可选值为top、center、bottom(默认center)
|
||||
* @property {String} textAlign 文字对齐方式(默认left)
|
||||
* @property {Boolean} stop 是否阻止事件传播(默认true)
|
||||
*/
|
||||
|
||||
export const ColProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 占父容器宽度的多少等分,总分为12份 */
|
||||
span: { type: [Number, String] as PropType<number | string>, default: 12 },
|
||||
/** 指定栅格左侧的间隔数(总12栏) */
|
||||
offset: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 水平排列方式 */
|
||||
justify: { type: String as PropType<JustifyType>, default: 'start' },
|
||||
/** 垂直对齐方式 */
|
||||
align: { type: String as PropType<AlignType>, default: 'center' },
|
||||
/** 文字对齐方式 */
|
||||
textAlign: { type: String, default: 'left' },
|
||||
/** 是否阻止事件传播 */
|
||||
stop: { type: Boolean, default: true }
|
||||
};
|
||||
|
||||
export type ColProps = ExtractPropTypes<typeof ColProps>;
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-col"
|
||||
:class="['u-col-' + span, customClass]"
|
||||
:style="$u.toStyle(colStyle, customStyle)"
|
||||
@tap="onClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-col',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ColProps } from './types';
|
||||
import { $u, useChildren } from '../../';
|
||||
|
||||
/**
|
||||
* col 布局单元格
|
||||
* @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
|
||||
* @tutorial https://uviewpro.cn/zh/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>
|
||||
*/
|
||||
|
||||
const emit = defineEmits<{ (e: 'click'): void }>();
|
||||
|
||||
const props = defineProps(ColProps);
|
||||
|
||||
const { parentExposed } = useChildren('u-col', 'u-row');
|
||||
|
||||
/**
|
||||
* gutter 给col添加间距,左右边距各占一半,从父组件u-row获取
|
||||
*/
|
||||
const gutter = computed(() => {
|
||||
return parentExposed?.value?.props?.gutter ?? 20;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算水平排列方式
|
||||
* @returns {string}
|
||||
*/
|
||||
const uJustify = computed(() => {
|
||||
if (props.justify === 'end' || props.justify === 'start') return 'flex-' + props.justify;
|
||||
else if (props.justify === 'around' || props.justify === 'between') return 'space-' + props.justify;
|
||||
else return props.justify;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算垂直对齐方式
|
||||
* @returns {string}
|
||||
*/
|
||||
const uAlignItem = computed(() => {
|
||||
if (props.align === 'top') return 'flex-start';
|
||||
if (props.align === 'bottom') return 'flex-end';
|
||||
else return props.align;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算样式对象
|
||||
*/
|
||||
const colStyle = computed<any>(() => ({
|
||||
padding: `0 ${Number(gutter.value) / 2}px`,
|
||||
marginLeft: `${(100 / 12) * Number(props.offset)}%`,
|
||||
flex: `0 0 ${(100 / 12) * Number(props.span)}%`,
|
||||
alignItems: uAlignItem.value,
|
||||
justifyContent: uJustify.value,
|
||||
textAlign: props.textAlign
|
||||
}));
|
||||
|
||||
/**
|
||||
* 点击事件
|
||||
* @param e 事件对象
|
||||
*/
|
||||
function onClick(e: Event) {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-col {
|
||||
/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
|
||||
float: left;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.u-col-0 {
|
||||
width: 0;
|
||||
}
|
||||
.u-col-1 {
|
||||
width: calc(100% / 12);
|
||||
}
|
||||
.u-col-2 {
|
||||
width: calc(100% / 12 * 2);
|
||||
}
|
||||
.u-col-3 {
|
||||
width: calc(100% / 12 * 3);
|
||||
}
|
||||
.u-col-4 {
|
||||
width: calc(100% / 12 * 4);
|
||||
}
|
||||
.u-col-5 {
|
||||
width: calc(100% / 12 * 5);
|
||||
}
|
||||
.u-col-6 {
|
||||
width: calc(100% / 12 * 6);
|
||||
}
|
||||
.u-col-7 {
|
||||
width: calc(100% / 12 * 7);
|
||||
}
|
||||
.u-col-8 {
|
||||
width: calc(100% / 12 * 8);
|
||||
}
|
||||
.u-col-9 {
|
||||
width: calc(100% / 12 * 9);
|
||||
}
|
||||
.u-col-10 {
|
||||
width: calc(100% / 12 * 10);
|
||||
}
|
||||
.u-col-11 {
|
||||
width: calc(100% / 12 * 11);
|
||||
}
|
||||
.u-col-12 {
|
||||
width: calc(100% / 12 * 12);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { TextAlign } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-collapse-item 手风琴Item Props
|
||||
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
|
||||
*/
|
||||
export const CollapseItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 标题 */
|
||||
title: { type: String, default: '' },
|
||||
/** 标题的对齐方式 */
|
||||
align: { type: String as PropType<TextAlign>, default: 'left' },
|
||||
/** 是否可以点击收起 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** collapse显示与否 */
|
||||
open: { type: Boolean, default: false },
|
||||
/** 唯一标识符 */
|
||||
name: { type: [Number, String] as PropType<number | string>, default: '' },
|
||||
/** 活动样式 */
|
||||
activeStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 标识当前为第几个 */
|
||||
index: { type: [String, Number] as PropType<string | number>, default: '' }
|
||||
};
|
||||
|
||||
export type CollapseItemProps = ExtractPropTypes<typeof CollapseItemProps>;
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<view class="u-collapse-item" :style="$u.toStyle(itemStyle, customStyle)" :class="customClass">
|
||||
<view
|
||||
:hover-stay-time="200"
|
||||
class="u-collapse-head"
|
||||
@tap.stop="headClick"
|
||||
:hover-class="hoverClass"
|
||||
:style="headStyle"
|
||||
>
|
||||
<template v-if="!slots['title-all']">
|
||||
<view v-if="!slots['title']" class="u-collapse-title u-line-1" :style="titleStyle">
|
||||
{{ title }}
|
||||
</view>
|
||||
<slot v-else name="title" />
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon v-if="showArrow" :color="arrowColor" :name="isShow ? 'arrow-up' : 'arrow-down'" />
|
||||
</view>
|
||||
</template>
|
||||
<slot v-else name="title-all" />
|
||||
</view>
|
||||
|
||||
<view class="u-collapse-body" :style="{ height: isShow ? height : '0' }">
|
||||
<view class="u-collapse-content" :id="elId" :style="bodyStyle">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-collapse-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, useSlots, getCurrentInstance, nextTick, computed } from 'vue';
|
||||
import { $u, useChildren } from '../..';
|
||||
import { CollapseItemProps } from './types';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* collapseItem 手风琴Item
|
||||
* @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
|
||||
* @tutorial https://uviewpro.cn/zh/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>
|
||||
*/
|
||||
const props = defineProps(CollapseItemProps);
|
||||
const emit = defineEmits(['change']);
|
||||
const slots = useSlots();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const isShow = ref(false);
|
||||
const elId = ref('');
|
||||
const height = ref('0');
|
||||
const headStyle = ref<Record<string, any>>({});
|
||||
const bodyStyle = ref<Record<string, any>>({});
|
||||
const itemStyle = ref<Record<string, any>>({});
|
||||
const arrowColor = ref('');
|
||||
const hoverClass = ref('');
|
||||
|
||||
// 使用通信库的子组件Hook
|
||||
const { childId, parentExposed } = useChildren('u-collapse-item', 'u-collapse');
|
||||
|
||||
// 计算属性
|
||||
const showArrow = computed(() => {
|
||||
return parentExposed.value?.props ? parentExposed.value.props.arrow : true;
|
||||
});
|
||||
|
||||
const titleStyle = computed(() => {
|
||||
let style = { textAlign: props.align ? props.align : 'left' };
|
||||
|
||||
if (isShow.value && props.activeStyle && !showArrow.value) {
|
||||
style = $u.deepMerge(style, props.activeStyle);
|
||||
}
|
||||
return $u.toStyle(style);
|
||||
});
|
||||
|
||||
// 获取唯一标识符
|
||||
const itemName = computed(() => {
|
||||
// 优先级:name > index > childId
|
||||
if (props.name !== undefined && props.name !== '') {
|
||||
return props.name;
|
||||
} else if (props.index !== undefined && props.index !== '') {
|
||||
return props.index;
|
||||
} else {
|
||||
return childId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置显示状态
|
||||
*/
|
||||
function setShowState(show: boolean) {
|
||||
if (isShow.value !== show) {
|
||||
isShow.value = show;
|
||||
|
||||
// 如果展开,需要重新计算高度
|
||||
if (show) {
|
||||
nextTick(() => {
|
||||
queryRect();
|
||||
});
|
||||
}
|
||||
|
||||
// 本地触发change事件
|
||||
emit('change', {
|
||||
index: props.index,
|
||||
name: itemName.value,
|
||||
show: isShow.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取内容,或者动态修改了内容时,需要重新初始化
|
||||
*/
|
||||
function init() {
|
||||
if (parentExposed.value?.props) {
|
||||
headStyle.value = parentExposed.value.props.headStyle || {};
|
||||
bodyStyle.value = parentExposed.value.props.bodyStyle || {};
|
||||
arrowColor.value = parentExposed.value.props.arrowColor || 'var(--u-tips-color)';
|
||||
hoverClass.value = parentExposed.value.props.hoverClass || 'u-hover-class';
|
||||
itemStyle.value = parentExposed.value.props.itemStyle || {};
|
||||
}
|
||||
|
||||
elId.value = $u.guid();
|
||||
nextTick(() => {
|
||||
queryRect();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击collapse head头部
|
||||
*/
|
||||
function headClick() {
|
||||
if (props.disabled) return;
|
||||
// 通知父组件状态变化
|
||||
parentExposed?.value?.onChange(itemName.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询内容高度
|
||||
*/
|
||||
function queryRect() {
|
||||
$u.getRect('#' + elId.value, instance)
|
||||
.then((res: any) => {
|
||||
if (res && res.height) {
|
||||
height.value = res.height + 'px';
|
||||
}
|
||||
// #ifdef MP-TOUTIAO
|
||||
if (isShow.value) {
|
||||
height.value = 'auto';
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.warn('queryRect error:', err);
|
||||
height.value = 'auto';
|
||||
});
|
||||
}
|
||||
|
||||
// 单选
|
||||
function openSingle(data: any) {
|
||||
// 只有目标项展开,其他都关闭
|
||||
const shouldShow = data.targetName === itemName.value;
|
||||
setShowState(shouldShow);
|
||||
}
|
||||
|
||||
// 关闭所有
|
||||
function closeAll() {
|
||||
setShowState(false);
|
||||
}
|
||||
|
||||
// 多选
|
||||
function setMultiple(data: any) {
|
||||
const shouldShow = data.targetNames.includes(itemName.value);
|
||||
setShowState(shouldShow);
|
||||
}
|
||||
|
||||
// 切换单个
|
||||
function toggleSingle(data: any) {
|
||||
// 只有目标项才切换状态
|
||||
if (data.targetName === itemName.value) {
|
||||
setShowState(!isShow.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 关键修复:根据 open 属性设置初始状态
|
||||
setShowState(props.open);
|
||||
// 初始化
|
||||
init();
|
||||
});
|
||||
|
||||
// 监听 open 属性变化
|
||||
watch(
|
||||
() => props.open,
|
||||
newVal => {
|
||||
setShowState(newVal);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听父组件exposed的变化
|
||||
watch(
|
||||
parentExposed,
|
||||
newExposed => {
|
||||
if (newExposed) {
|
||||
init();
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
init,
|
||||
isShow,
|
||||
elId,
|
||||
height,
|
||||
headStyle,
|
||||
bodyStyle,
|
||||
itemStyle,
|
||||
arrowColor,
|
||||
hoverClass,
|
||||
itemName: itemName.value,
|
||||
queryRect,
|
||||
setShowState,
|
||||
openSingle,
|
||||
closeAll,
|
||||
setMultiple,
|
||||
toggleSingle
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-collapse-head {
|
||||
position: relative;
|
||||
@include vue-flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: $u-main-color;
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
padding: 24rpx 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.u-collapse-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-arrow-down-icon {
|
||||
transition: all 0.3s;
|
||||
margin-right: 20rpx;
|
||||
margin-left: 14rpx;
|
||||
}
|
||||
|
||||
.u-arrow-down-icon-active {
|
||||
transform: rotate(180deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.u-collapse-body {
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.u-collapse-content {
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: $u-tips-color;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-collapse 手风琴 Props
|
||||
* @description 通过折叠面板收纳内容区域
|
||||
* @property {Boolean} accordion 是否手风琴模式(默认true)
|
||||
* @property {Object} headStyle 标题自定义样式,对象形式
|
||||
* @property {Object} bodyStyle 主体自定义样式,对象形式
|
||||
* @property {Object} itemStyle 每一个item的样式,对象形式
|
||||
* @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
|
||||
* @property {String} arrowColor 标题右侧箭头的颜色(默认var(--u-tips-color))
|
||||
* @property {String} hoverClass 样式类名,按下时有效(默认u-hover-class)
|
||||
*/
|
||||
export const CollapseProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否手风琴模式 */
|
||||
accordion: { type: Boolean, default: true },
|
||||
/** 头部的样式 */
|
||||
headStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 主体的样式 */
|
||||
bodyStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 每一个item的样式 */
|
||||
itemStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 是否显示右侧的箭头 */
|
||||
arrow: { type: Boolean, default: true },
|
||||
/** 箭头的颜色 */
|
||||
arrowColor: { type: String, default: 'var(--u-tips-color)' },
|
||||
/** 标题部分按压时的样式类,"none"为无效果 */
|
||||
hoverClass: { type: String, default: 'u-hover-class' }
|
||||
};
|
||||
|
||||
export type CollapseProps = ExtractPropTypes<typeof CollapseProps>;
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<view class="u-collapse" :style="$u.toStyle(customStyle)" :class="customClass">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-collapse',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue';
|
||||
import { CollapseProps } from './types';
|
||||
import { $u, useParent } from '../../';
|
||||
|
||||
/**
|
||||
* collapse 手风琴
|
||||
* @description 通过折叠面板收纳内容区域
|
||||
* @tutorial https://uviewpro.cn/zh/components/collapse.html
|
||||
* @property {Boolean} accordion 是否手风琴模式(默认true)
|
||||
* @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
|
||||
* @property {String} arrow-color 标题右侧箭头的颜色(默认var(--u-tips-color))
|
||||
* @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>
|
||||
*/
|
||||
const props = defineProps(CollapseProps);
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
// 使用通信库的父组件Hook
|
||||
const { children, broadcast, getChildrenExposed } = useParent('u-collapse');
|
||||
|
||||
// 当前激活的面板 - 只在手风琴模式下使用
|
||||
const activeName = ref<string | number>('');
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 初始化:收集所有open为true的项
|
||||
setTimeout(() => {
|
||||
initializeActiveName();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化activeName - 找到第一个open为true的项
|
||||
*/
|
||||
function initializeActiveName() {
|
||||
if (props.accordion) {
|
||||
// 手风琴模式下,取第一个open为true的项作为初始激活项
|
||||
const childrenExposed = getChildrenExposed();
|
||||
const openChild = childrenExposed.find(child => child.exposed.isShow === true);
|
||||
if (openChild) {
|
||||
activeName.value = openChild.exposed.itemName || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* collapse item被点击,由collapse item调用父组件方法
|
||||
*/
|
||||
function onChange(name: string | number) {
|
||||
if (props.accordion) {
|
||||
// 手风琴模式
|
||||
const childrenExposed = getChildrenExposed();
|
||||
const targetChild = childrenExposed.find(child => child.exposed.itemName === name);
|
||||
if (targetChild?.exposed.isShow.value === true) {
|
||||
// 目标项当前是展开状态,点击后要关闭它
|
||||
activeName.value = '';
|
||||
broadcast('closeAll', {});
|
||||
} else {
|
||||
// 目标项当前是关闭状态,点击后要展开它并关闭其他
|
||||
activeName.value = name;
|
||||
broadcast('openSingle', { targetName: name });
|
||||
}
|
||||
} else {
|
||||
// 非手风琴模式 - 只通知目标项切换状态
|
||||
broadcast('toggleSingle', { targetName: name });
|
||||
}
|
||||
|
||||
// 收集当前所有展开的项
|
||||
let currentActiveNames: (string | number)[] = [];
|
||||
if (props.accordion) {
|
||||
currentActiveNames = activeName.value === 0 || activeName.value ? [activeName.value] : [];
|
||||
} else {
|
||||
// 对于非手风琴模式,我们不知道所有项的状态
|
||||
currentActiveNames = [];
|
||||
children.forEach(child => {
|
||||
if (child.getExposed().isShow.value) {
|
||||
currentActiveNames.push(child.getExposed().itemName);
|
||||
}
|
||||
});
|
||||
}
|
||||
currentActiveNames.length > 0 && emit('change', props.accordion ? activeName.value || '' : currentActiveNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置激活的面板
|
||||
*/
|
||||
function setActiveNames(names: string | number | (string | number)[]) {
|
||||
if (props.accordion) {
|
||||
// 手风琴模式
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
activeName.value = name || '';
|
||||
if (name) {
|
||||
broadcast('openSingle', { targetName: name });
|
||||
} else {
|
||||
broadcast('closeAll', {});
|
||||
}
|
||||
} else {
|
||||
// 非手风琴模式
|
||||
const namesArray = Array.isArray(names) ? names : [names];
|
||||
broadcast('setMultiple', { targetNames: namesArray });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开所有面板
|
||||
*/
|
||||
function openAll() {
|
||||
if (props.accordion) {
|
||||
console.warn('手风琴模式下不能打开所有面板');
|
||||
return;
|
||||
}
|
||||
const childrenExposed = getChildrenExposed();
|
||||
const allNames = childrenExposed.map(child => child.exposed.itemName).filter(Boolean);
|
||||
broadcast('setMultiple', { targetNames: allNames });
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有面板
|
||||
*/
|
||||
function closeAll() {
|
||||
broadcast('closeAll', {});
|
||||
if (props.accordion) {
|
||||
activeName.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新初始化,用于动态内容变化
|
||||
*/
|
||||
function init() {
|
||||
const childrenExposed = getChildrenExposed();
|
||||
childrenExposed.forEach(child => {
|
||||
if (child.exposed.init) {
|
||||
child.exposed.init();
|
||||
}
|
||||
});
|
||||
|
||||
// 重新初始化activeName
|
||||
setTimeout(() => {
|
||||
initializeActiveName();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// 使用defineExpose暴露给外部
|
||||
defineExpose({
|
||||
// props
|
||||
props,
|
||||
|
||||
// 状态
|
||||
activeName,
|
||||
|
||||
// 方法
|
||||
onChange,
|
||||
setActiveNames,
|
||||
openAll,
|
||||
closeAll,
|
||||
init,
|
||||
|
||||
// 计算属性
|
||||
childrenCount: () => children.length
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { Direction, PlayState, ScrollDirection, ThemeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-column-notice 通告栏 Props
|
||||
*/
|
||||
export const ColumnNoticeProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 显示的内容,数组 */
|
||||
list: { type: Array as PropType<string[]>, default: () => [] },
|
||||
/** 显示的主题,success|error|primary|info|warning */
|
||||
type: { type: String as PropType<ThemeType | 'none'>, 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 as PropType<ScrollDirection>, default: 'row' },
|
||||
/** 是否显示 */
|
||||
show: { type: Boolean, default: true },
|
||||
/** 字体大小,单位rpx */
|
||||
fontSize: { type: [Number, String] as PropType<number | string>, default: 26 },
|
||||
/** 滚动一个周期的时间长,单位ms */
|
||||
duration: { type: [Number, String] as PropType<number | string>, default: 2000 },
|
||||
/** 音量喇叭的大小 */
|
||||
volumeSize: { type: [Number, String] as PropType<number | string>, default: 34 },
|
||||
/** 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度 */
|
||||
speed: { type: Number, default: 160 },
|
||||
/** 水平滚动时,是否采用衔接形式滚动 */
|
||||
isCircular: { type: Boolean, default: true },
|
||||
/** 滚动方向,horizontal-水平滚动,vertical-垂直滚动 */
|
||||
mode: { type: String as PropType<Direction>, default: 'horizontal' },
|
||||
/** 播放状态,play-播放,paused-暂停 */
|
||||
playState: { type: String as PropType<PlayState>, default: 'play' },
|
||||
/** 是否禁止用手滑动切换 */
|
||||
disableTouch: { type: Boolean, default: true },
|
||||
/** 通知的边距 */
|
||||
padding: { type: [Number, String] as PropType<number | string>, default: '18rpx 24rpx' }
|
||||
};
|
||||
|
||||
export type ColumnNoticeProps = ExtractPropTypes<typeof ColumnNoticeProps>;
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-notice-bar"
|
||||
:style="$u.toStyle({ background: computeBgColor, padding: props.padding }, customStyle)"
|
||||
:class="[props.type ? `u-type-${props.type}-light-bg` : '', customClass]"
|
||||
>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon
|
||||
class="u-left-icon"
|
||||
v-if="props.volumeIcon"
|
||||
name="volume-fill"
|
||||
:size="props.volumeSize"
|
||||
:color="computeColor"
|
||||
></u-icon>
|
||||
</view>
|
||||
<swiper
|
||||
:disable-touch="props.disableTouch"
|
||||
@change="change"
|
||||
:autoplay="props.autoplay && props.playState === 'play'"
|
||||
:vertical="vertical"
|
||||
circular
|
||||
:interval="props.duration"
|
||||
class="u-swiper"
|
||||
>
|
||||
<swiper-item v-for="(item, index) in props.list" :key="index" class="u-swiper-item">
|
||||
<view
|
||||
class="u-news-item u-line-1"
|
||||
:style="textStyle"
|
||||
@tap="click(index)"
|
||||
:class="['u-type-' + props.type]"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="u-icon-wrap">
|
||||
<u-icon
|
||||
@click="getMore"
|
||||
class="u-right-icon"
|
||||
v-if="props.moreIcon"
|
||||
name="arrow-right"
|
||||
:size="26"
|
||||
:color="computeColor"
|
||||
></u-icon>
|
||||
<u-icon
|
||||
@click="close"
|
||||
class="u-right-icon"
|
||||
v-if="props.closeIcon"
|
||||
name="close"
|
||||
:size="24"
|
||||
:color="computeColor"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-column-notice',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ColumnNoticeProps } from './types';
|
||||
import { $u } from '../../';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* u-column-notice 通告栏
|
||||
* @property {Array} list 显示的内容,数组
|
||||
* @property {String} type 显示的主题,success|error|primary|info|warning
|
||||
* @property {Boolean} volumeIcon 是否显示左侧的音量图标
|
||||
* @property {Boolean} moreIcon 是否显示右侧的右箭头图标
|
||||
* @property {Boolean} closeIcon 是否显示右侧的关闭图标
|
||||
* @property {Boolean} autoplay 是否自动播放
|
||||
* @property {String} color 文字颜色,各图标也会使用文字颜色
|
||||
* @property {String} bgColor 背景颜色
|
||||
* @property {String} direction 滚动方向,row-水平滚动,column-垂直滚动
|
||||
* @property {Boolean} show 是否显示
|
||||
* @property {String|Number} fontSize 字体大小,单位rpx
|
||||
* @property {String|Number} duration 滚动一个周期的时间长,单位ms
|
||||
* @property {String|Number} volumeSize 音量喇叭的大小
|
||||
* @property {Number} speed 水平滚动时的滚动速度,即每秒滚动多少rpx
|
||||
* @property {Boolean} isCircular 水平滚动时,是否采用衔接形式滚动
|
||||
* @property {String} mode 滚动方向,horizontal-水平滚动,vertical-垂直滚动
|
||||
* @property {String} playState 播放状态,play-播放,paused-暂停
|
||||
* @property {Boolean} disableTouch 是否禁止用手滑动切换
|
||||
* @property {String|Number} padding 通知的边距
|
||||
*/
|
||||
|
||||
const props = defineProps(ColumnNoticeProps);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', index: number): void;
|
||||
(e: 'close'): void;
|
||||
(e: 'getMore'): void;
|
||||
(e: 'end'): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* 计算字体颜色,如果没有自定义的,就用uview主题颜色
|
||||
*/
|
||||
const computeColor = computed(() => {
|
||||
if (props.color) return props.color;
|
||||
// 如果是无主题,就默认使用content-color
|
||||
else if (props.type === 'none') return 'var(--u-content-color)';
|
||||
else return props.type;
|
||||
});
|
||||
|
||||
/**
|
||||
* 文字内容的样式
|
||||
*/
|
||||
const textStyle = computed(() => {
|
||||
const style: Record<string, any> = {};
|
||||
if (props.color) style.color = props.color;
|
||||
else if (props.type === 'none') style.color = 'var(--u-content-color)';
|
||||
style.fontSize = props.fontSize + 'rpx';
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 垂直或者水平滚动
|
||||
*/
|
||||
const vertical = computed(() => {
|
||||
return props.mode !== 'horizontal';
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算背景颜色
|
||||
*/
|
||||
const computeBgColor = computed(() => {
|
||||
if (props.bgColor) return props.bgColor;
|
||||
else if (props.type === 'none') return 'transparent';
|
||||
else return '';
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击通告栏
|
||||
*/
|
||||
function click(index: number) {
|
||||
emit('click', index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击关闭按钮
|
||||
*/
|
||||
function close() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击更多箭头按钮
|
||||
*/
|
||||
function getMore() {
|
||||
emit('getMore');
|
||||
}
|
||||
|
||||
/**
|
||||
* swiper 切换事件
|
||||
*/
|
||||
function change(e: { detail: { current: number } }) {
|
||||
const index = e.detail.current;
|
||||
if (index === props.list.length - 1) {
|
||||
emit('end');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-notice-bar {
|
||||
width: 100%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 18rpx 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-swiper {
|
||||
font-size: 26rpx;
|
||||
height: 32rpx;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.u-swiper-item {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-news-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-right-icon {
|
||||
margin-left: 12rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-left-icon {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { config, configProvider } from '../../libs';
|
||||
import type { Theme, DarkMode } from '../../types/global';
|
||||
|
||||
export const ConfigProviderProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 主题风格,可选值:
|
||||
* - 'light': 强制亮色模式(默认)
|
||||
* - 'dark': 强制暗黑模式
|
||||
* - 'auto': 自动跟随系统设置
|
||||
*/
|
||||
darkMode: {
|
||||
type: String as PropType<DarkMode>,
|
||||
default: () => configProvider.getDarkMode()
|
||||
},
|
||||
/**
|
||||
* 当前主题名称(用于多主题切换)
|
||||
*/
|
||||
currentTheme: {
|
||||
type: String,
|
||||
default: () => configProvider.getCurrentTheme()?.name ?? config.defaultTheme
|
||||
},
|
||||
/**
|
||||
* 自定义主题列表
|
||||
*/
|
||||
themes: {
|
||||
type: Array as PropType<Theme[]>,
|
||||
default: () => configProvider.getThemes()
|
||||
},
|
||||
/**
|
||||
* 国际化配置:传入 locale 列表(数组)并可指定默认语言 currentLocale
|
||||
*/
|
||||
locales: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
currentLocale: {
|
||||
type: String,
|
||||
default: () => configProvider.getCurrentLocale()?.name ?? ''
|
||||
}
|
||||
};
|
||||
|
||||
export type ConfigProviderProps = ExtractPropTypes<typeof ConfigProviderProps>;
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<view :class="['u-config-provider', `u-theme-${darkMode}`, customClass]" :style="mergedThemeStyle">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* u-config-provider
|
||||
*
|
||||
* 说明(简要):
|
||||
* - 初始化场景(推荐在应用入口执行):使用 `useTheme().initTheme(themes)` 进行一次性初始化与全局设置(例如在 `main.ts` 或 `App.vue` 中)。
|
||||
* - 组件/页面场景(推荐):使用 `useTheme()` 组合式函数在组件内部读取响应式 `currentTheme`、`themes`、`darkMode` 并通过 `setTheme()` 和 `setDarkMode()` 切换主题/模式。
|
||||
*
|
||||
* 该组件的行为:
|
||||
* - 如果在挂载时传入 `themes`,会调用 `configProvider.initTheme(themes)`
|
||||
* - 如果传入 `currentTheme`,会优先设置当前主题
|
||||
* - 如果传入 `darkMode`,会设置当前的暗黑模式状态,同时在 document 上添加 `u-theme-dark` 或 `u-theme-light` 类名
|
||||
*
|
||||
* 详尽说明请参考:`docs/config-provider-usage.md`
|
||||
*/
|
||||
export default {
|
||||
name: 'u-config-provider',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from 'vue';
|
||||
import { ConfigProviderProps } from './types';
|
||||
import { $u, configProvider } from '../../libs';
|
||||
import { useTheme } from '../../libs/hooks/useTheme';
|
||||
import { useLocale } from '../../libs/hooks/useLocale';
|
||||
|
||||
const props = defineProps(ConfigProviderProps);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'theme-change': [themeName: string];
|
||||
'mode-change': [mode: string];
|
||||
}>();
|
||||
|
||||
const { initTheme } = useTheme();
|
||||
const { initLocales, setLocale, getLocales, getCurrentLocale } = useLocale();
|
||||
|
||||
const bootstrapTheme = () => {
|
||||
// 初始化主题
|
||||
try {
|
||||
// 如果已经初始化过主题,不再重复初始化,只更新 props 相关配置
|
||||
const existingThemes = configProvider.getThemes();
|
||||
if (existingThemes.length > 0) {
|
||||
// 已初始化,只在值不同时才更新(避免覆盖用户之前的设置)
|
||||
if (props.currentTheme && props.currentTheme !== configProvider.getCurrentTheme()?.name) {
|
||||
configProvider.setTheme(props.currentTheme as string);
|
||||
}
|
||||
if (props.darkMode && props.darkMode !== configProvider.getDarkMode()) {
|
||||
configProvider.setDarkMode(props.darkMode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 未初始化,进行初始化
|
||||
if (props.themes && props.themes.length) {
|
||||
configProvider.initTheme(props.themes, props.currentTheme as any);
|
||||
} else {
|
||||
// 使用 useTheme 的 initTheme,它会处理默认主题
|
||||
initTheme(undefined, props.currentTheme as any);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[u-config-provider] init theme failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
const bootstrapLocale = () => {
|
||||
// 初始化国际化
|
||||
try {
|
||||
const existingLocales = getLocales();
|
||||
if (existingLocales.length > 0) {
|
||||
if (props.currentLocale && props.currentLocale !== getCurrentLocale()?.name) {
|
||||
setLocale(props.currentLocale as string);
|
||||
}
|
||||
} else {
|
||||
if (props.locales && props.locales.length) {
|
||||
initLocales(props.locales, props.currentLocale as any);
|
||||
} else {
|
||||
initLocales(undefined, props.currentLocale as any);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[u-config-provider] init locales failed', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 当传入自定义 themes 时,初始化全局 configProvider(覆盖已有)
|
||||
onMounted(() => {
|
||||
bootstrapTheme();
|
||||
bootstrapLocale();
|
||||
});
|
||||
|
||||
// 监听外部 props 变化(如果上层修改 prop)
|
||||
watch(
|
||||
() => props.themes,
|
||||
val => {
|
||||
// 如果传入的 themes 来自于 configProvider 自身(常见于模板中使用 useTheme() 直接透传),
|
||||
// 那么对其做深度监听会在我们内部更新主题对象时触发该回调,进而再次调用 init 导致循环更新。
|
||||
// 为避免该情况,先做简单保护:当传入对象正是 configProvider.themesRef.value 时不重复初始化。
|
||||
if (val && val.length && val !== configProvider.themesRef.value) {
|
||||
configProvider.initTheme(val, props.currentTheme as any);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.currentTheme,
|
||||
val => {
|
||||
if (val) {
|
||||
configProvider.setTheme(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.darkMode,
|
||||
val => {
|
||||
if (val && val !== configProvider.getDarkMode()) {
|
||||
configProvider.setDarkMode(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 locales prop 变化
|
||||
watch(
|
||||
() => props.locales,
|
||||
val => {
|
||||
if (val && val.length) {
|
||||
initLocales(val, props.currentLocale as any);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.currentLocale,
|
||||
val => {
|
||||
if (val) {
|
||||
setLocale(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听全局主题变更并触发事件
|
||||
watch(
|
||||
() => configProvider.currentThemeRef.value,
|
||||
(val, oldVal) => {
|
||||
if (val && val.name !== (oldVal as any)?.name) {
|
||||
emit('theme-change', val.name);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听暗黑模式变更并触发事件
|
||||
watch(
|
||||
() => configProvider.darkModeRef.value,
|
||||
(val, oldVal) => {
|
||||
if (val && val !== oldVal) {
|
||||
emit('mode-change', val);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 计算合并样式(作为局部 fallback),configProvider 已经会把变量注入到 document 上
|
||||
const mergedThemeStyle = computed(() => {
|
||||
return $u.toStyle(configProvider.cssVarsRef.value, props.customStyle);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-count-down 倒计时 Props
|
||||
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
|
||||
*/
|
||||
export const CountDownProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 倒计时的时间,秒为单位 */
|
||||
timestamp: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 是否自动开始倒计时 */
|
||||
autoplay: { type: Boolean, default: true },
|
||||
/** 分隔符 */
|
||||
separator: { type: String, default: 'colon' },
|
||||
/** 分隔符的大小,单位rpx */
|
||||
separatorSize: { type: [Number, String] as PropType<number | string>, default: 30 },
|
||||
/** 分隔符颜色 */
|
||||
separatorColor: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 字体大小,单位rpx */
|
||||
fontSize: { type: [Number, String] as PropType<number | string>, default: 30 },
|
||||
/** 背景颜色 */
|
||||
bgColor: { type: String, default: 'var(--u-bg-white)' },
|
||||
/** 数字框高度,单位rpx */
|
||||
height: { type: [Number, String] as PropType<number | string>, default: 'auto' },
|
||||
/** 是否显示数字框 */
|
||||
showBorder: { type: Boolean, default: false },
|
||||
/** 边框颜色 */
|
||||
borderColor: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 是否显示秒 */
|
||||
showSeconds: { type: Boolean, default: true },
|
||||
/** 是否显示分钟 */
|
||||
showMinutes: { type: Boolean, default: true },
|
||||
/** 是否显示小时 */
|
||||
showHours: { type: Boolean, default: true },
|
||||
/** 是否显示“天” */
|
||||
showDays: { type: Boolean, default: true },
|
||||
/** 当"天"的部分为0时,不显示 */
|
||||
hideZeroDay: { type: Boolean, default: false }
|
||||
};
|
||||
|
||||
export type CountDownProps = ExtractPropTypes<typeof CountDownProps>;
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<view class="u-countdown" :class="customClass" :style="$u.toStyle(customStyle)">
|
||||
<view
|
||||
class="u-countdown-item"
|
||||
:style="$u.toStyle(itemStyle)"
|
||||
v-if="props.showDays && (props.hideZeroDay || (!props.hideZeroDay && d != '00'))"
|
||||
>
|
||||
<view class="u-countdown-time" :style="$u.toStyle(letterStyle)">
|
||||
{{ d }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{
|
||||
fontSize: props.separatorSize + 'rpx',
|
||||
color: props.separatorColor,
|
||||
paddingBottom: props.separator == 'colon' ? '4rpx' : 0
|
||||
}"
|
||||
v-if="props.showDays && (props.hideZeroDay || (!props.hideZeroDay && d != '00'))"
|
||||
>
|
||||
{{ props.separator == 'colon' && props.showHours ? ':' : t('uCountDown.day') }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="$u.toStyle(itemStyle)" v-if="props.showHours">
|
||||
<view class="u-countdown-time" :style="{ fontSize: props.fontSize + 'rpx', color: props.color }">
|
||||
{{ h }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{
|
||||
fontSize: props.separatorSize + 'rpx',
|
||||
color: props.separatorColor,
|
||||
paddingBottom: props.separator == 'colon' ? '4rpx' : 0
|
||||
}"
|
||||
v-if="props.showHours"
|
||||
>
|
||||
{{ props.separator == 'colon' && props.showMinutes ? ':' : t('uCountDown.hour') }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="$u.toStyle(itemStyle)" v-if="props.showMinutes">
|
||||
<view class="u-countdown-time" :style="{ fontSize: props.fontSize + 'rpx', color: props.color }">
|
||||
{{ i }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{
|
||||
fontSize: props.separatorSize + 'rpx',
|
||||
color: props.separatorColor,
|
||||
paddingBottom: props.separator == 'colon' ? '4rpx' : 0
|
||||
}"
|
||||
v-if="props.showMinutes"
|
||||
>
|
||||
{{ props.separator == 'colon' && props.showSeconds ? ':' : t('uCountDown.minute') }}
|
||||
</view>
|
||||
<view class="u-countdown-item" :style="$u.toStyle(itemStyle)" v-if="props.showSeconds">
|
||||
<view class="u-countdown-time" :style="{ fontSize: props.fontSize + 'rpx', color: props.color }">
|
||||
{{ s }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-countdown-colon"
|
||||
:style="{ fontSize: props.separatorSize + 'rpx', color: props.separatorColor, paddingBottom: 0 }"
|
||||
v-if="props.showSeconds && props.separator == 'zh'"
|
||||
>
|
||||
{{ t('uCountDown.second') }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-count-down',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { CountDownProps } from './types';
|
||||
import { $u, useLocale } from '../../';
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* countDown 倒计时
|
||||
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
|
||||
* @tutorial https://uviewpro.cn/zh/components/countDown.html
|
||||
* @property {String | Number} timestamp 倒计时,单位为秒
|
||||
* @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
|
||||
* @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
|
||||
* @property {String | Number} separator-size 分隔符的字体大小,单位rpx(默认30)
|
||||
* @property {String} separator-color 分隔符的颜色(默认var(--u-main-color))
|
||||
* @property {String | Number} font-size 倒计时字体大小,单位rpx(默认30)
|
||||
* @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
|
||||
* @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
|
||||
* @property {String} border-color 数字边框的颜色(默认var(--u-main-color))
|
||||
* @property {String} bg-color 倒计时数字的背景颜色(默认var(--u-bg-white))
|
||||
* @property {String} color 倒计时数字的颜色(默认var(--u-main-color))
|
||||
* @property {String | Number} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
|
||||
* @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
|
||||
* @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
|
||||
* @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
|
||||
* @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
|
||||
* @event {Function} end 倒计时结束
|
||||
* @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
|
||||
* @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['end', 'change']);
|
||||
|
||||
const props = defineProps(CountDownProps);
|
||||
|
||||
const d = ref('00'); // 天
|
||||
const h = ref('00'); // 小时
|
||||
const i = ref('00'); // 分钟
|
||||
const s = ref('00'); // 秒
|
||||
const timer = ref<number | ReturnType<typeof setInterval> | null>(null); // 定时器
|
||||
const seconds = ref(0);
|
||||
|
||||
/**
|
||||
* 倒计时item的样式,item为分别的时分秒部分的数字
|
||||
*/
|
||||
const itemStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
if (props.height) {
|
||||
style.height = props.height + 'rpx';
|
||||
style.width = props.height + 'rpx';
|
||||
}
|
||||
if (props.showBorder) {
|
||||
style.borderStyle = 'solid';
|
||||
style.borderColor = props.borderColor;
|
||||
style.borderWidth = '1px';
|
||||
}
|
||||
if (props.bgColor) {
|
||||
style.backgroundColor = props.bgColor;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 倒计时数字的样式
|
||||
*/
|
||||
const letterStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
if (props.fontSize) style.fontSize = props.fontSize + 'rpx';
|
||||
if (props.color) style.color = props.color;
|
||||
return style;
|
||||
});
|
||||
|
||||
// 监听时间戳的变化
|
||||
watch(
|
||||
() => props.timestamp,
|
||||
(newVal, oldVal) => {
|
||||
// 如果倒计时间发生变化,清除定时器,重新开始倒计时
|
||||
clearTimer();
|
||||
start();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 如果自动倒计时
|
||||
if (props.autoplay && props.timestamp) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 倒计时
|
||||
*/
|
||||
function start() {
|
||||
// 避免可能出现的倒计时重叠情况
|
||||
clearTimer();
|
||||
if (Number(props.timestamp) <= 0) return;
|
||||
seconds.value = Number(props.timestamp);
|
||||
formatTime(seconds.value);
|
||||
timer.value = setInterval(() => {
|
||||
seconds.value--;
|
||||
// 发出change事件
|
||||
emit('change', seconds.value);
|
||||
if (seconds.value < 0) {
|
||||
end();
|
||||
return;
|
||||
}
|
||||
formatTime(seconds.value);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
function formatTime(sec: number) {
|
||||
// 小于等于0的话,结束倒计时
|
||||
if (sec <= 0) end();
|
||||
let dayNum = Math.floor(sec / (60 * 60 * 24));
|
||||
let hourNum = Math.floor(sec / (60 * 60)) - dayNum * 24;
|
||||
// showHour为需要显示的小时
|
||||
let showHour: string;
|
||||
// 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
|
||||
// hour为给后面计算秒和分等用的(基于显示天的前提下计算)
|
||||
if (props.showDays) {
|
||||
showHour = hourNum < 10 ? '0' + hourNum : String(hourNum);
|
||||
} else {
|
||||
// 如果不显示天数,将“天”部分的时间折算到小时中去
|
||||
// 如果小于10,在前面补上一个"0"
|
||||
const hourTotal = Math.floor(sec / (60 * 60));
|
||||
showHour = hourTotal < 10 ? '0' + hourTotal : String(hourTotal);
|
||||
}
|
||||
let minuteNum = Math.floor(sec / 60) - hourNum * 60 - dayNum * 24 * 60;
|
||||
let secondNum = Math.floor(sec) - dayNum * 24 * 60 * 60 - hourNum * 60 * 60 - minuteNum * 60;
|
||||
const minute = minuteNum < 10 ? '0' + minuteNum : String(minuteNum);
|
||||
const second = secondNum < 10 ? '0' + secondNum : String(secondNum);
|
||||
const day = dayNum < 10 ? '0' + dayNum : String(dayNum);
|
||||
d.value = day;
|
||||
h.value = showHour;
|
||||
i.value = minute;
|
||||
s.value = second;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止倒计时
|
||||
*/
|
||||
function end() {
|
||||
clearTimer();
|
||||
emit('end', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除定时器
|
||||
*/
|
||||
function clearTimer() {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
start,
|
||||
end
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-countdown {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.u-countdown-item {
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rpx;
|
||||
border-radius: 6rpx;
|
||||
white-space: nowrap;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.u-countdown-time {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.u-countdown-colon {
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
padding: 0 5rpx;
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.u-countdown-scale {
|
||||
transform: scale(0.9);
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-count-to 数字滚动 Props
|
||||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
|
||||
*/
|
||||
export const CountToProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 开始的数值,默认从0增长到某一个数 */
|
||||
startVal: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 要滚动的目标数值,必须 */
|
||||
endVal: { type: [Number, String] as PropType<number | string>, default: 0, required: true },
|
||||
/** 滚动到目标数值的动画持续时间,单位为毫秒(ms) */
|
||||
duration: { type: [Number, String] as PropType<number | string>, default: 2000 },
|
||||
/** 设置数值后是否自动开始滚动 */
|
||||
autoplay: { type: Boolean, default: true },
|
||||
/** 要显示的小数位数 */
|
||||
decimals: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 是否在即将到达目标数值的时候,使用缓慢滚动的效果 */
|
||||
useEasing: { type: Boolean, default: true },
|
||||
/** 十进制分割符号 */
|
||||
decimal: { type: [Number, String] as PropType<number | string>, default: '.' },
|
||||
/** 字体颜色 */
|
||||
color: { type: String, default: 'var(--u-main-color)' },
|
||||
/** 字体大小 */
|
||||
fontSize: { type: [Number, String] as PropType<number | string>, default: 50 },
|
||||
/** 是否加粗字体 */
|
||||
bold: { type: Boolean, default: false },
|
||||
/** 千位分隔符,类似金额的分割(¥23,321.05中的",") */
|
||||
separator: { type: String, default: '' }
|
||||
};
|
||||
|
||||
export type CountToProps = ExtractPropTypes<typeof CountToProps>;
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-count-num"
|
||||
:class="customClass"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
fontSize: props.fontSize + 'rpx',
|
||||
fontWeight: props.bold ? 'bold' : 'normal',
|
||||
color: props.color
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ displayValue }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-count-to',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { CountToProps } from './types';
|
||||
import { $u } from '../../';
|
||||
|
||||
/**
|
||||
* countTo 数字滚动
|
||||
* @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
|
||||
* @tutorial https://uviewpro.cn/zh/components/countTo.html
|
||||
* @property {String | Number} start-val 开始值
|
||||
* @property {String | Number} end-val 结束值
|
||||
* @property {String | Number} duration 滚动过程所需的时间,单位ms(默认2000)
|
||||
* @property {Boolean} autoplay 是否自动开始滚动(默认true)
|
||||
* @property {String | Number} decimals 要显示的小数位数,见官网说明(默认0)
|
||||
* @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
|
||||
* @property {String} separator 千位分隔符,见官网说明
|
||||
* @property {String} color 字体颜色(默认var(--u-main-color))
|
||||
* @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>
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['end']);
|
||||
|
||||
const props = defineProps(CountToProps);
|
||||
|
||||
const localStartVal = ref(Number(props.startVal));
|
||||
const displayValue = ref(formatNumber(props.startVal));
|
||||
const printVal = ref<number | null>(null);
|
||||
const paused = ref(false); // 是否暂停
|
||||
const localDuration = ref(Number(props.duration));
|
||||
const startTime = ref<number | null>(null); // 开始的时间
|
||||
const timestamp = ref<number | null>(null); // 时间戳
|
||||
const remaining = ref<number | null>(null); // 停留的时间
|
||||
const rAF = ref<number | null>(null);
|
||||
const lastTime = ref(0); // 上一次的时间
|
||||
|
||||
/**
|
||||
* 是否倒计时
|
||||
*/
|
||||
const countDown = computed(() => Number(props.startVal) > Number(props.endVal));
|
||||
|
||||
watch(
|
||||
() => props.startVal,
|
||||
() => {
|
||||
if (props.autoplay) start();
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.endVal,
|
||||
() => {
|
||||
if (props.autoplay) start();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoplay) start();
|
||||
});
|
||||
|
||||
/**
|
||||
* 缓动函数
|
||||
*/
|
||||
function easingFn(t: number, b: number, c: number, d: number): number {
|
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* requestAnimationFrame polyfill
|
||||
*/
|
||||
function requestAnimationFrame(callback: (ts: number) => void): number {
|
||||
const currTime = new Date().getTime();
|
||||
// 为了使setTimteout的尽可能的接近每秒60帧的效果
|
||||
const timeToCall = Math.max(0, 16 - (currTime - lastTime.value));
|
||||
const id = setTimeout(() => {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
lastTime.value = currTime + timeToCall;
|
||||
return id as unknown as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消动画帧
|
||||
*/
|
||||
function cancelAnimationFrame(id: number | null) {
|
||||
if (id) clearTimeout(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始滚动数字
|
||||
*/
|
||||
function start() {
|
||||
localStartVal.value = Number(props.startVal);
|
||||
startTime.value = null;
|
||||
localDuration.value = Number(props.duration);
|
||||
paused.value = false;
|
||||
rAF.value = requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停/恢复滚动
|
||||
* 暂定状态,重新再开始滚动;或者滚动状态下,暂停
|
||||
*/
|
||||
function reStart() {
|
||||
if (paused.value) {
|
||||
resume();
|
||||
paused.value = false;
|
||||
} else {
|
||||
stop();
|
||||
paused.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
function stop() {
|
||||
cancelAnimationFrame(rAF.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新开始(暂停的情况下)
|
||||
*/
|
||||
function resume() {
|
||||
startTime.value = null;
|
||||
localDuration.value = remaining.value || Number(props.duration);
|
||||
localStartVal.value = printVal.value || Number(props.startVal);
|
||||
rAF.value = requestAnimationFrame(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function reset() {
|
||||
startTime.value = null;
|
||||
cancelAnimationFrame(rAF.value);
|
||||
displayValue.value = formatNumber(props.startVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字滚动动画主逻辑
|
||||
*/
|
||||
function count(ts: number) {
|
||||
if (!startTime.value) startTime.value = ts;
|
||||
timestamp.value = ts;
|
||||
const progress = ts - (startTime.value || 0);
|
||||
remaining.value = localDuration.value - progress;
|
||||
let val: number;
|
||||
if (props.useEasing) {
|
||||
if (countDown.value) {
|
||||
val =
|
||||
localStartVal.value -
|
||||
easingFn(progress, 0, localStartVal.value - Number(props.endVal), localDuration.value);
|
||||
} else {
|
||||
val = easingFn(
|
||||
progress,
|
||||
localStartVal.value,
|
||||
Number(props.endVal) - localStartVal.value,
|
||||
localDuration.value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (countDown.value) {
|
||||
val = localStartVal.value - (localStartVal.value - Number(props.endVal)) * (progress / localDuration.value);
|
||||
} else {
|
||||
val = localStartVal.value + (Number(props.endVal) - localStartVal.value) * (progress / localDuration.value);
|
||||
}
|
||||
}
|
||||
if (countDown.value) {
|
||||
val = val < Number(props.endVal) ? Number(props.endVal) : val;
|
||||
} else {
|
||||
val = val > Number(props.endVal) ? Number(props.endVal) : val;
|
||||
}
|
||||
printVal.value = val;
|
||||
displayValue.value = formatNumber(val);
|
||||
if (progress < localDuration.value) {
|
||||
rAF.value = requestAnimationFrame(count);
|
||||
} else {
|
||||
emit('end');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否数字
|
||||
*/
|
||||
function isNumber(val: unknown): boolean {
|
||||
return !isNaN(parseFloat(String(val)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
*/
|
||||
function formatNumber(num: unknown): string {
|
||||
// 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
|
||||
let n = Number(num);
|
||||
n = Number(n.toFixed(Number(props.decimals)));
|
||||
let str = n + '';
|
||||
const x = str.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? String(props.decimal) + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (props.separator && !isNumber(props.separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + props.separator + '$2');
|
||||
}
|
||||
}
|
||||
return x1 + x2;
|
||||
}
|
||||
|
||||
// 销毁时清理动画帧
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rAF.value);
|
||||
});
|
||||
|
||||
// 暴露给父组件的函数
|
||||
defineExpose({
|
||||
start,
|
||||
stop,
|
||||
reStart,
|
||||
resume,
|
||||
reset
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-count-num {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ThemeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-divider 分割线 Props
|
||||
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
|
||||
*/
|
||||
export const DividerProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 单一边divider横线的宽度(数值),单位rpx。或者百分比 */
|
||||
halfWidth: { type: [Number, String] as PropType<number | string>, default: 150 },
|
||||
/** divider横线的颜色,如设置 */
|
||||
borderColor: { type: String, default: 'var(--u-border-color)' },
|
||||
/** 主题色,可以是primary|info|success|warning|error之一值 */
|
||||
type: { type: String as PropType<ThemeType>, default: 'primary' },
|
||||
/** 文字颜色 */
|
||||
color: { type: String, default: 'var(--u-tips-color)' },
|
||||
/** 文字大小,单位rpx */
|
||||
fontSize: { type: [Number, String] as PropType<number | string>, default: 26 },
|
||||
/** 整个divider的背景颜色 */
|
||||
bgColor: { type: String, default: 'var(--u-bg-white)' },
|
||||
/** 整个divider的高度单位rpx */
|
||||
height: { type: [Number, String] as PropType<number | string>, default: 'auto' },
|
||||
/** 上边距 */
|
||||
marginTop: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 下边距 */
|
||||
marginBottom: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙 */
|
||||
useSlot: { type: Boolean, default: true }
|
||||
};
|
||||
|
||||
export type DividerProps = ExtractPropTypes<typeof DividerProps>;
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-divider"
|
||||
:class="customClass"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
height: height === 'auto' ? 'auto' : height + 'rpx',
|
||||
backgroundColor: bgColor,
|
||||
marginBottom: marginBottom + 'rpx',
|
||||
marginTop: marginTop + 'rpx'
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
@tap="onClick"
|
||||
>
|
||||
<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 lang="ts">
|
||||
export default {
|
||||
name: 'u-divider',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DividerProps } from './types';
|
||||
import { $u } from '../../';
|
||||
|
||||
/**
|
||||
* divider 分割线
|
||||
* @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
|
||||
* @tutorial https://uviewpro.cn/zh/components/divider.html
|
||||
* @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
|
||||
* @property {String} border-color 线条颜色,优先级高于type(默认var(--u-border-color))
|
||||
* @property {String} color 文字颜色(默认var(--u-tips-color))
|
||||
* @property {String Number} fontSize 字体大小,单位rpx(默认26)
|
||||
* @property {String} bg-color 整个divider的背景颜色(默认呢var(--u-bg-white))
|
||||
* @property {String Number} height 整个divider的高度,单位rpx(默认40)
|
||||
* @property {String} type 将线条设置主题色(默认primary)
|
||||
* @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
|
||||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
|
||||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
|
||||
* @event {Function} click divider组件被点击时触发
|
||||
* @example <u-divider color="var(--u-type-error)">长河落日圆</u-divider>
|
||||
*/
|
||||
const props = defineProps(DividerProps);
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
/**
|
||||
* divider横线样式
|
||||
*/
|
||||
const lineStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
if (String(props.halfWidth).indexOf('%') !== -1) style.width = String(props.halfWidth);
|
||||
else style.width = props.halfWidth + 'rpx';
|
||||
// borderColor优先级高于type值
|
||||
if (props.borderColor) style.borderColor = props.borderColor;
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* divider组件被点击时触发
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
.u-divider {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.u-divider-line {
|
||||
border-bottom: 1px solid $u-border-color;
|
||||
transform: scale(1, 0.5);
|
||||
transform-origin: center;
|
||||
|
||||
&--bordercolor--primary {
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--bordercolor--success {
|
||||
border-color: $u-type-success;
|
||||
}
|
||||
|
||||
&--bordercolor--error {
|
||||
border-color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--bordercolor--info {
|
||||
border-color: $u-type-info;
|
||||
}
|
||||
|
||||
&--bordercolor--warning {
|
||||
border-color: $u-type-warning;
|
||||
}
|
||||
}
|
||||
|
||||
.u-divider-text {
|
||||
white-space: nowrap;
|
||||
padding: 0 16rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { OptionType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-dropdown-item 下拉菜单项 Props
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
|
||||
* @property {String|Number|Array} modelValue 双向绑定选项卡选择值
|
||||
* @property {String|Number} title 菜单项标题
|
||||
* @property {OptionType[]} options 选项数据,如果传入了默认slot,此参数无效
|
||||
* @property {Boolean} disabled 是否禁用此选项卡(默认false)
|
||||
* @property {String|Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
|
||||
* @property {Boolean} show 控制是否显示菜单项(默认true)
|
||||
*/
|
||||
|
||||
export const DropdownItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 当前选中项的value值 */
|
||||
modelValue: { type: [Number, String, Array] as PropType<number | string | any[]>, default: '' },
|
||||
/** 菜单项标题 */
|
||||
title: { type: [String, Number] as PropType<string | number>, default: '' },
|
||||
/** 选项数据,如果传入了默认slot,此参数无效 */
|
||||
options: { type: Array as PropType<OptionType[]>, default: () => [] },
|
||||
/** 是否禁用此菜单项 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 下拉弹窗的高度 */
|
||||
height: { type: [Number, String] as PropType<number | string>, default: 'auto' },
|
||||
/** 控制是否显示菜单项 */
|
||||
show: { type: Boolean, default: true }
|
||||
};
|
||||
|
||||
export type DropdownItemProps = ExtractPropTypes<typeof DropdownItemProps>;
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="show && active"
|
||||
class="u-dropdown-item"
|
||||
:class="customClass"
|
||||
:style="$u.toStyle(customStyle)"
|
||||
@touchmove.stop.prevent
|
||||
@tap.stop.prevent
|
||||
>
|
||||
<block v-if="!slots.default && !slots.$default">
|
||||
<scroll-view :scroll-y="true" :style="{ height: $u.addUnit(String(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: modelValue == item.value ? activeColor : inactiveColor }"
|
||||
>
|
||||
<u-icon
|
||||
v-if="modelValue == 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 lang="ts">
|
||||
export default {
|
||||
name: 'u-dropdown-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots, watch } from 'vue';
|
||||
import { $u, useChildren } from '../..';
|
||||
import { DropdownItemProps } from './types';
|
||||
import uCellGroup from '../u-cell-group/u-cell-group.vue';
|
||||
import uCellItem from '../u-cell-item/u-cell-item.vue';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* dropdown-item 下拉菜单
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
|
||||
* @tutorial https://uviewpro.cn/zh/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)
|
||||
* @property {Boolean} show 控制是否显示菜单项(默认true)
|
||||
* @example <u-dropdown-item title="标题"></u-dropdown-item>
|
||||
*/
|
||||
|
||||
// props 定义
|
||||
const props = defineProps(DropdownItemProps);
|
||||
|
||||
// emits 定义
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
// 插槽
|
||||
const slots = useSlots();
|
||||
|
||||
const { parentExposed, emitToParent } = useChildren('u-dropdown-item', 'u-dropdown');
|
||||
|
||||
// 当前项是否处于展开状态
|
||||
const active = ref(false);
|
||||
// 激活时左边文字和右边对勾图标的颜色
|
||||
const activeColor = computed(() => parentExposed.value?.props?.activeColor || $u.color.primary);
|
||||
// 未激活时左边文字和右边对勾图标的颜色
|
||||
const inactiveColor = computed(() => parentExposed.value?.props?.inactiveColor || $u.color.contentColor);
|
||||
|
||||
// 监听props变化,通知父组件重新初始化
|
||||
const propsChange = computed(() => `${props.title}-${props.disabled}-${props.show}`);
|
||||
|
||||
// 监听propsChange变化,通知父组件重新init
|
||||
watch(propsChange, () => {
|
||||
emitToParent('init');
|
||||
});
|
||||
|
||||
/**
|
||||
* cell被点击
|
||||
* @param value 选中值
|
||||
*/
|
||||
function cellClick(value: string | number | any) {
|
||||
// 修改通过v-model绑定的值
|
||||
emit('update:modelValue', value);
|
||||
// 通知父组件(u-dropdown)收起菜单
|
||||
emitToParent('close');
|
||||
// 发出事件,抛出当前勾选项的value
|
||||
emit('change', value);
|
||||
}
|
||||
|
||||
function setActive(value: boolean) {
|
||||
active.value = value;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
cellClick,
|
||||
setActive,
|
||||
props
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { getColor } from '../../';
|
||||
import zIndex from '../../libs/config/zIndex';
|
||||
|
||||
/**
|
||||
* u-dropdown 下拉菜单 Props
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
|
||||
*/
|
||||
export const DropdownProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 菜单标题和选项的激活态颜色 */
|
||||
activeColor: { type: String, default: () => getColor('primary') },
|
||||
/** 菜单标题和选项的未激活态颜色 */
|
||||
inactiveColor: { type: String, default: () => getColor('contentColor') },
|
||||
/** 点击遮罩是否关闭菜单 */
|
||||
closeOnClickMask: { type: Boolean, default: true },
|
||||
/** 点击当前激活项标题是否关闭菜单 */
|
||||
closeOnClickSelf: { type: Boolean, default: true },
|
||||
/** 过渡时间 */
|
||||
duration: { type: [Number, String] as PropType<number | string>, default: 300 },
|
||||
/** 标题菜单的高度,单位任意,数值默认为rpx单位 */
|
||||
height: { type: [Number, String] as PropType<number | string>, default: 80 },
|
||||
/** 是否显示下边框 */
|
||||
borderBottom: { type: Boolean, default: false },
|
||||
/** 标题的字体大小 */
|
||||
titleSize: { type: [Number, String] as PropType<number | string>, default: 28 },
|
||||
/** 下拉出来的内容部分的圆角值 */
|
||||
borderRadius: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/** 菜单右侧的icon图标 */
|
||||
menuIcon: { type: String, default: 'arrow-down' },
|
||||
/** 菜单右侧图标的大小 */
|
||||
menuIconSize: { type: [Number, String] as PropType<number | string>, default: 26 },
|
||||
/**
|
||||
* 是否使用fixed定位,开启后下拉菜单固定在顶部,
|
||||
* 自动适配状态栏和导航栏高度,并生成占位区域防止页面塌陷
|
||||
*/
|
||||
fixed: { type: Boolean, default: false },
|
||||
/**
|
||||
* fixed定位时,在自动计算的偏移量基础上的额外顶部偏移量(px),用于用户微调位置。
|
||||
* 实际top = statusBarHeight + navbarHeight + offsetTop
|
||||
*/
|
||||
offsetTop: { type: [Number, String] as PropType<number | string>, default: 0 },
|
||||
/**
|
||||
* fixed定位时,状态栏和导航栏所占的总高度(px)。
|
||||
* 默认不设置(空字符串),组件会自动获取系统状态栏高度 + 默认导航栏高度(44px)。
|
||||
* 设置任意数值(包括0)则使用用户指定值,不再自动计算。
|
||||
*/
|
||||
navbarHeight: { type: [Number, String] as PropType<number | string>, default: '' },
|
||||
/**
|
||||
* 沉浸式模式,仅在fixed下生效。
|
||||
* 开启后不生成占位区域,允许页面内容延伸到dropdown下方(类似沉浸式导航栏)
|
||||
*/
|
||||
immersive: { type: Boolean, default: false },
|
||||
/** fixed定位时的z-index值 */
|
||||
zIndex: { type: [Number, String] as PropType<number | string>, default: zIndex.dropdown }
|
||||
};
|
||||
|
||||
export type DropdownProps = ExtractPropTypes<typeof DropdownProps>;
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<view class="u-dropdown-wrapper">
|
||||
<view class="u-dropdown" :style="dropdownRootStyle" :class="[{ 'u-dropdown__fixed': fixed }, customClass]">
|
||||
<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
|
||||
? 'var(--u-light-color)'
|
||||
: 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 : 'var(--u-light-color)'
|
||||
"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-dropdown__content"
|
||||
:style="[
|
||||
contentStyle,
|
||||
{
|
||||
transition: `opacity ${Number(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>
|
||||
<!-- fixed定位时的占位区域,防止页面内容塌陷 -->
|
||||
<view
|
||||
v-if="fixed && !immersive"
|
||||
class="u-dropdown__placeholder"
|
||||
:style="{ height: dropdownPlaceholderHeight + 'px' }"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-dropdown',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import { $u, useParent } from '../..';
|
||||
import { DropdownProps } from './types';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* dropdown 下拉菜单
|
||||
* @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景。
|
||||
* 支持fixed定位,自动适配状态栏和导航栏高度,并提供占位区域防止页面塌陷。
|
||||
* @tutorial https://uviewpro.cn/zh/components/dropdown.html
|
||||
* @property {String} active-color 标题和选项卡选中的颜色(默认主题色primary)
|
||||
* @property {String} inactive-color 标题和选项卡未选中的颜色(默认var(--u-content-color))
|
||||
* @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)
|
||||
* @property {Boolean} fixed 是否使用fixed定位固定在顶部,自动适配状态栏和导航栏高度(默认false)
|
||||
* @property {String | Number} offset-top fixed定位时,在自动计算的偏移量基础上的额外顶部偏移量,单位px(默认0)
|
||||
* @property {String | Number} navbar-height fixed定位时,状态栏+导航栏的总高度(px)。不设置则自动获取,设置任意数值(包括0)则使用指定值
|
||||
* @property {Boolean} immersive 沉浸式模式,仅在fixed下生效,开启后不生成占位区域(默认false)
|
||||
* @property {String | Number} z-index fixed定位时的z-index值(默认985)
|
||||
* @event {Function} open 下拉菜单被打开时触发
|
||||
* @event {Function} close 下拉菜单被关闭时触发
|
||||
* @example <u-dropdown :fixed="true"></u-dropdown>
|
||||
*/
|
||||
|
||||
const props = defineProps(DropdownProps);
|
||||
const emit = defineEmits(['open', 'close']);
|
||||
|
||||
const { children } = useParent('u-dropdown');
|
||||
// 菜单列表
|
||||
const menuList = ref<any[]>([]);
|
||||
// 下拉菜单的状态
|
||||
const active = ref(false);
|
||||
// 当前激活菜单索引
|
||||
// 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
|
||||
// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
|
||||
const current = ref<number>(99999);
|
||||
// 外层内容样式
|
||||
const contentStyle = ref<any>({ zIndex: -1, opacity: 0 });
|
||||
// 高亮菜单索引
|
||||
const highlightIndex = ref<number>(99999);
|
||||
// 下拉内容高度
|
||||
const contentHeight = ref<number>(0);
|
||||
|
||||
// ==================== fixed定位相关 ====================
|
||||
// 获取系统状态栏高度
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const statusBarHeight = ref(0);
|
||||
// #ifdef APP-HARMONY || MP-WEIXIN
|
||||
const windowInfo = uni.getWindowInfo();
|
||||
statusBarHeight.value = windowInfo.statusBarHeight || 0;
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY || MP-WEIXIN
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0;
|
||||
// #endif
|
||||
|
||||
// 默认导航栏高度(与 u-navbar 保持一致)
|
||||
const defaultNavbarHeight = computed(() => {
|
||||
// #ifdef APP || H5
|
||||
return 44;
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
return systemInfo.platform === 'ios' ? 44 : 48;
|
||||
// #endif
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算 fixed 定位时的总偏移高度(px)
|
||||
* - 若用户通过 navbarHeight 指定了任意数值(包括0),则使用用户指定值
|
||||
* - 否则(空字符串)自动计算:状态栏高度 + 默认导航栏高度
|
||||
* - 最后加上 offsetTop 作为额外偏移
|
||||
*/
|
||||
const dropdownOffsetHeight = computed(() => {
|
||||
let baseHeight: number;
|
||||
if (props.navbarHeight !== '' && props.navbarHeight !== undefined && props.navbarHeight !== null) {
|
||||
// 用户手动指定了 navbar 高度(包括0),直接使用
|
||||
baseHeight = Number(props.navbarHeight);
|
||||
} else {
|
||||
// 自动计算:状态栏高度 + 默认导航栏高度
|
||||
baseHeight = statusBarHeight.value + defaultNavbarHeight.value;
|
||||
}
|
||||
// 加上用户设置的额外 offsetTop 偏移
|
||||
return baseHeight + Number(props.offsetTop);
|
||||
});
|
||||
|
||||
/**
|
||||
* 占位区域高度 = dropdown菜单栏自身高度(px)
|
||||
* 不包含状态栏+导航栏偏移,因为页面上已有 navbar 等元素占据该空间
|
||||
*/
|
||||
const dropdownPlaceholderHeight = computed(() => {
|
||||
return uni.upx2px(Number(props.height));
|
||||
});
|
||||
|
||||
/**
|
||||
* dropdown 根节点样式
|
||||
* fixed 时设置 top 和 z-index
|
||||
*/
|
||||
const dropdownRootStyle = computed(() => {
|
||||
const style: Record<string, any> = {};
|
||||
|
||||
// 兼容头条小程序
|
||||
// #ifdef MP-TOUTIAO
|
||||
style.width = '100vw';
|
||||
// #endif
|
||||
|
||||
// fixed 定位时,计算顶部偏移和 z-index
|
||||
if (props.fixed) {
|
||||
style.top = dropdownOffsetHeight.value + 'px';
|
||||
style.zIndex = props.zIndex ? Number(props.zIndex) : $u.zIndex.dropdown;
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
// 下拉出来部分的样式
|
||||
const popupStyle = computed<any>(() => {
|
||||
const style: any = {};
|
||||
// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
|
||||
style.transform = `translateY(${active.value ? 0 : '-100%'})`;
|
||||
style['transition-duration'] = Number(props.duration) / 1000 + 's';
|
||||
style.borderRadius = `0 0 ${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)}`;
|
||||
return style;
|
||||
});
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getContentHeight();
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化所有子组件
|
||||
* 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
|
||||
* 以保证数据的正确性
|
||||
*/
|
||||
function init() {
|
||||
menuList.value = [];
|
||||
children.forEach(child => {
|
||||
// 过滤不显示的项
|
||||
const show = child?.getExposed()?.props.show !== false;
|
||||
if (!show) return;
|
||||
|
||||
menuList.value.push({
|
||||
title: child?.getExposed()?.props.title ?? '',
|
||||
disabled: child?.getExposed()?.props.disabled ?? false,
|
||||
childIndex: children.indexOf(child)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击菜单
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function menuClick(index: number) {
|
||||
// 判断是否被禁用
|
||||
if (menuList.value[index]?.disabled) return;
|
||||
// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
|
||||
if (index === current.value && props.closeOnClickSelf) {
|
||||
close();
|
||||
// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
|
||||
const childIndex = menuList.value[index]?.childIndex; //避免访问到show为false的项
|
||||
setTimeout(() => {
|
||||
if (childIndex !== undefined && children[childIndex]) {
|
||||
children[childIndex]?.getExposed()?.setActive(false);
|
||||
}
|
||||
}, Number(props.duration));
|
||||
return;
|
||||
}
|
||||
open(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开下拉菜单
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function open(index: number) {
|
||||
// 嵌套popup使用时可能获取不到正确的高度,重新计算
|
||||
if (contentHeight.value < 1) getContentHeight();
|
||||
// 重置高亮索引,否则会造成多个菜单同时高亮
|
||||
// highlightIndex.value = 9999;
|
||||
// 展开时,设置下拉内容的样式
|
||||
contentStyle.value = { zIndex: 11, display: 'block' };
|
||||
// 标记展开状态以及当前展开项的索引
|
||||
active.value = true;
|
||||
current.value = index;
|
||||
// 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
|
||||
// 之所以不是因display: none,是因为nvue没有display这个属性
|
||||
const childIndex = menuList.value[index]?.childIndex; //避免访问到show为false的项
|
||||
children.forEach((child, idx) => {
|
||||
child?.getExposed()?.setActive(childIndex === idx ? true : false);
|
||||
});
|
||||
emit('open', current.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下拉菜单处于收起状态
|
||||
*/
|
||||
function close() {
|
||||
emit('close', current.value);
|
||||
// 设置为收起状态,同时current归位,设置为空字符串
|
||||
active.value = false;
|
||||
current.value = 99999;
|
||||
// 下拉内容的样式进行调整,不透明度设置为0,fixed模式下隐藏display避免遮挡页面元素
|
||||
if (props.fixed) {
|
||||
contentStyle.value = { zIndex: -1, opacity: 0, display: 'none' };
|
||||
} else {
|
||||
contentStyle.value = { zIndex: -1, opacity: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击遮罩
|
||||
*/
|
||||
function maskClick() {
|
||||
// 如果不允许点击遮罩,直接返回
|
||||
if (!props.closeOnClickMask) return;
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部手动设置某个菜单高亮
|
||||
* @param index 菜单索引
|
||||
*/
|
||||
function highlight(index?: number) {
|
||||
highlightIndex.value = index !== undefined ? index : 99999;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下拉菜单内容的高度
|
||||
* 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
|
||||
* 才能让遮罩占满菜单一下,直到屏幕底部的高度
|
||||
* this.$u.sys()为uView封装的获取设备信息的方法
|
||||
*/
|
||||
function getContentHeight() {
|
||||
const windowHeight = $u.sys().windowHeight;
|
||||
contentHeight.value = windowHeight;
|
||||
|
||||
// $u.getRect('.u-dropdown__menu', instance).then((res: any) => {
|
||||
// // 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
|
||||
// // H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
|
||||
// // 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
|
||||
// // 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
|
||||
// contentHeight.value = windowHeight - res.bottom;
|
||||
// });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
init();
|
||||
});
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
props,
|
||||
init,
|
||||
close,
|
||||
open,
|
||||
highlight,
|
||||
getContentHeight,
|
||||
children,
|
||||
menuList
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-dropdown-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.u-dropdown {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&__fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__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 0.3s;
|
||||
align-items: center;
|
||||
@include vue-flex;
|
||||
|
||||
&--rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__popup {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
transition: all 0.3s;
|
||||
transform: translate3D(0, -100%, 0);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-empty 组件 Props 类型定义
|
||||
*/
|
||||
export const EmptyProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 图标路径 */
|
||||
src: { type: String, default: '' },
|
||||
/** 提示文字 */
|
||||
text: { type: String, default: '' },
|
||||
/** 文字颜色 */
|
||||
color: { type: String, default: 'var(--u-light-color)' },
|
||||
/** 图标的颜色 */
|
||||
iconColor: { type: String, default: 'var(--u-light-color)' },
|
||||
/** 图标的大小 */
|
||||
iconSize: { type: [String, Number] as PropType<string | number>, default: 120 },
|
||||
/** 文字大小,单位rpx */
|
||||
fontSize: { type: [String, Number] as PropType<string | number>, default: 26 },
|
||||
/** 选择预置的图标类型 */
|
||||
mode: { type: String, default: 'data' },
|
||||
/** 图标宽度,单位rpx */
|
||||
imgWidth: { type: [String, Number] as PropType<string | number>, default: 120 },
|
||||
/** 图标高度,单位rpx */
|
||||
imgHeight: { type: [String, Number] as PropType<string | number>, default: 'auto' },
|
||||
/** 是否显示组件 */
|
||||
show: { type: Boolean, default: true },
|
||||
/** 组件距离上一个元素之间的距离 */
|
||||
marginTop: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 图标自定义样式 */
|
||||
iconStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) }
|
||||
};
|
||||
|
||||
/**
|
||||
* u-empty 组件 Props 类型
|
||||
*/
|
||||
export type EmptyProps = ExtractPropTypes<typeof EmptyProps>;
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
class="u-empty"
|
||||
:class="customClass"
|
||||
:style="$u.toStyle({ marginTop: marginTop + 'rpx' }, customStyle)"
|
||||
>
|
||||
<u-icon
|
||||
:name="src ? src : 'empty-' + mode"
|
||||
:custom-style="iconStyle"
|
||||
:label="text ? text : (icons as any)[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 lang="ts">
|
||||
export default {
|
||||
name: 'u-empty',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EmptyProps } from './types';
|
||||
import { $u, useLocale } from '../../';
|
||||
import { computed } from 'vue';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* empty 内容为空
|
||||
* @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
|
||||
* @tutorial https://uviewpro.cn/zh/components/empty.html
|
||||
* @property {String} color 文字颜色(默认var(--u-light-color))
|
||||
* @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>
|
||||
*/
|
||||
const props = defineProps(EmptyProps);
|
||||
|
||||
/**
|
||||
* 预置图标对应的提示文字
|
||||
*/
|
||||
const icons = computed(() => {
|
||||
return {
|
||||
car: t('uEmpty.car'),
|
||||
page: t('uEmpty.page'),
|
||||
search: t('uEmpty.search'),
|
||||
address: t('uEmpty.address'),
|
||||
wifi: t('uEmpty.wifi'),
|
||||
order: t('uEmpty.order'),
|
||||
coupon: t('uEmpty.coupon'),
|
||||
favor: t('uEmpty.favor'),
|
||||
permission: t('uEmpty.permission'),
|
||||
history: t('uEmpty.history'),
|
||||
news: t('uEmpty.news'),
|
||||
message: t('uEmpty.message'),
|
||||
list: t('uEmpty.list'),
|
||||
data: t('uEmpty.data')
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-empty {
|
||||
@include vue-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.u-image {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.u-slot-wrap {
|
||||
@include vue-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
|
||||
import type { FabDirection, FabGap, FabPosition, ThemeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* fab 悬浮按钮类型定义
|
||||
* @description 供 u-fab 组件 props 使用
|
||||
*/
|
||||
|
||||
export const FabProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 按钮的预置样式,primary,info,error,warning,success */
|
||||
type: { type: String as PropType<ThemeType>, default: 'primary' },
|
||||
/** 是否禁止状态 */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 按钮能否可以拖动 */
|
||||
draggable: { type: Boolean, default: false },
|
||||
/** 按钮与边缘的间距,单位 px。支持 number 或对象 {top,left,right,bottom},优先级按键存在顺序 */
|
||||
gap: {
|
||||
type: Object as PropType<FabGap>,
|
||||
default: () => ({
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16
|
||||
})
|
||||
},
|
||||
/** 拖动结束时是否自动吸边(仅当 draggable 为 true 时生效) */
|
||||
autoStick: { type: Boolean, default: true },
|
||||
/** 预设定位:控制组件默认停靠位置 */
|
||||
position: {
|
||||
type: String as PropType<FabPosition>,
|
||||
default: 'right-bottom'
|
||||
},
|
||||
/** 菜单出现的方向 */
|
||||
direction: { type: String as PropType<FabDirection>, default: 'top' },
|
||||
/** 按钮自定义层级 */
|
||||
zIndex: { type: Number, default: 99 }
|
||||
};
|
||||
|
||||
export type FabProps = ExtractPropTypes<typeof FabProps>;
|
||||
|
||||
interface DirectionConfig {
|
||||
opposite: keyof CSSProperties;
|
||||
sizeKey: 'width' | 'height';
|
||||
positionKey: 'left' | 'top';
|
||||
flexBase: 'row' | 'row-reverse' | 'column' | 'column-reverse';
|
||||
}
|
||||
|
||||
export const directionConfig: Record<FabDirection, DirectionConfig> = {
|
||||
top: {
|
||||
opposite: 'bottom',
|
||||
sizeKey: 'width',
|
||||
positionKey: 'left',
|
||||
flexBase: 'column'
|
||||
},
|
||||
bottom: {
|
||||
opposite: 'top',
|
||||
sizeKey: 'width',
|
||||
positionKey: 'left',
|
||||
flexBase: 'column-reverse'
|
||||
},
|
||||
left: {
|
||||
opposite: 'right',
|
||||
sizeKey: 'height',
|
||||
positionKey: 'top',
|
||||
flexBase: 'row'
|
||||
},
|
||||
right: {
|
||||
opposite: 'left',
|
||||
sizeKey: 'height',
|
||||
positionKey: 'top',
|
||||
flexBase: 'row-reverse'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-fab"
|
||||
:style="$u.toStyle(fabStyle, customStyle)"
|
||||
:class="[customClass]"
|
||||
@touchstart="handleTouchstart"
|
||||
@touchmove.stop.prevent="handleTouchmove"
|
||||
@touchend="handleTouchend"
|
||||
>
|
||||
<view class="u-fab-trigger" id="trigger">
|
||||
<slot name="trigger">
|
||||
<u-button
|
||||
custom-class="u-fab-trigger-btn"
|
||||
custom-style="width:112rpx;height:112rpx;border-radius:112rpx;"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:throttle-time="0"
|
||||
@click="handleBtnClick"
|
||||
>
|
||||
<u-icon :name="expansion ? 'close' : 'plus'" size="36rpx"></u-icon>
|
||||
</u-button>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="u-fab-actions" id="actions" :class="{ 'u-fab-actions__show': expansion }" :style="actionsStyle">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-fab',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { directionConfig, FabProps } from './types';
|
||||
import { computed, getCurrentInstance, onMounted, reactive, ref, useSlots, watch } from 'vue';
|
||||
import { $u } from '../../';
|
||||
import uButton from '../u-button/u-button.vue';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
const props = defineProps(FabProps);
|
||||
const emit = defineEmits(['trigger']);
|
||||
|
||||
const slots = useSlots();
|
||||
const instance = getCurrentInstance();
|
||||
const sysInfo = $u.sys();
|
||||
const dragging = ref(true);
|
||||
const minLeft = ref(0);
|
||||
const maxLeft = ref(0);
|
||||
const minTop = ref(0);
|
||||
const maxTop = ref(0);
|
||||
const expansion = ref(false);
|
||||
const direction = ref(props.direction);
|
||||
const effectiveWindowHeight = ref(sysInfo.windowHeight);
|
||||
// #ifdef H5
|
||||
effectiveWindowHeight.value = sysInfo.windowTop + sysInfo.windowHeight;
|
||||
// #endif
|
||||
const position = reactive({
|
||||
top: 0,
|
||||
left: 0
|
||||
});
|
||||
const actions = ref();
|
||||
const btnInfo = ref({
|
||||
width: 56,
|
||||
height: 56
|
||||
});
|
||||
const start = reactive({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
// 计算悬浮按钮样式
|
||||
const fabStyle = computed(() => {
|
||||
const style: Record<string, any> = {
|
||||
transition: dragging.value ? 'none' : 'all 0.3s ease',
|
||||
zIndex: props.zIndex,
|
||||
left: `${position.left}px`,
|
||||
top: `${position.top}px`
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
// 动画样式
|
||||
const actionsStyle = computed(() => {
|
||||
const config = directionConfig[direction.value];
|
||||
const base: Record<string, any> = {
|
||||
[config.opposite]: '100%',
|
||||
[config.sizeKey]: '100%',
|
||||
[config.positionKey]: 0,
|
||||
flexDirection: config.flexBase,
|
||||
transition: 'all 0.28s cubic-bezier(0.3, 0, 0.2, 1)'
|
||||
};
|
||||
|
||||
// 展开/收缩动画:根据方向设置初始偏移
|
||||
const offset = 12; // rpx/px 相对轻微偏移,视觉更顺滑
|
||||
if (expansion.value) {
|
||||
base.transform = 'translate3d(0,0,0) scale(1)';
|
||||
base.opacity = 1;
|
||||
} else {
|
||||
// 隐藏时做一个方向上的微位移并缩放以优化动画感
|
||||
if (direction.value === 'top') base.transform = `translate3d(0, ${offset}px, 0) scale(0.96)`;
|
||||
else if (direction.value === 'bottom') base.transform = `translate3d(0, -${offset}px, 0) scale(0.96)`;
|
||||
else if (direction.value === 'left') base.transform = `translate3d(${offset}px, 0, 0) scale(0.96)`;
|
||||
else base.transform = `translate3d(-${offset}px, 0, 0) scale(0.96)`;
|
||||
base.opacity = 0;
|
||||
}
|
||||
|
||||
return base;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.position, props.gap],
|
||||
() => {
|
||||
initPosition();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.direction,
|
||||
() => {
|
||||
if (expansion.value) direction.value = calcDirection();
|
||||
}
|
||||
);
|
||||
|
||||
// helper:支持 gap 为 number 或对象形式
|
||||
function getGap(side: 'top' | 'left' | 'right' | 'bottom') {
|
||||
const g = props.gap as any;
|
||||
if (typeof g === 'number') return g;
|
||||
if (g && typeof g === 'object' && g[side] !== undefined) return Number(g[side]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 拖动开始事件
|
||||
function handleTouchstart(e: TouchEvent) {
|
||||
if (props.disabled || !props.draggable) return;
|
||||
|
||||
const touches = e.touches[0];
|
||||
start.x = touches.clientX;
|
||||
start.y = touches.clientY;
|
||||
dragging.value = true;
|
||||
}
|
||||
|
||||
// 拖动移动事件
|
||||
function handleTouchmove(e: TouchEvent) {
|
||||
if (props.disabled || !props.draggable) return;
|
||||
|
||||
const touches = e.touches[0];
|
||||
const deltaX = touches.clientX - start.x;
|
||||
const deltaY = touches.clientY - start.y;
|
||||
|
||||
position.left += deltaX;
|
||||
position.top += deltaY;
|
||||
|
||||
start.x = touches.clientX;
|
||||
start.y = touches.clientY;
|
||||
|
||||
// 设置边界,防止拖出边界
|
||||
position.left = Math.max(minLeft.value, Math.min(maxLeft.value, position.left));
|
||||
position.top = Math.max(minTop.value, Math.min(maxTop.value, position.top));
|
||||
}
|
||||
|
||||
// 拖动结束事件
|
||||
function handleTouchend() {
|
||||
if (props.disabled || !props.draggable) return;
|
||||
|
||||
dragging.value = false;
|
||||
|
||||
// 如果 autoStick 为 false,则释放后不进行自动吸边,仅做边界限制
|
||||
if (props.autoStick === false) {
|
||||
position.left = Math.max(minLeft.value, Math.min(maxLeft.value, position.left));
|
||||
position.top = Math.max(minTop.value, Math.min(maxTop.value, position.top));
|
||||
return;
|
||||
}
|
||||
|
||||
const middle = sysInfo.windowWidth / 2;
|
||||
const buttonCenter = position.left + btnInfo.value.width / 2;
|
||||
|
||||
// 自动吸边,按钮中心位置大于视口的一半时,自动依附在右边,不然就是左边
|
||||
position.left =
|
||||
buttonCenter > middle ? sysInfo.windowWidth - btnInfo.value.width - getGap('right') : getGap('left');
|
||||
|
||||
if (expansion.value) direction.value = calcDirection();
|
||||
}
|
||||
|
||||
// 按钮点击事件
|
||||
function handleBtnClick() {
|
||||
if (slots?.default) {
|
||||
expansion.value = !expansion.value;
|
||||
if (expansion.value) direction.value = calcDirection();
|
||||
} else {
|
||||
emit('trigger');
|
||||
}
|
||||
}
|
||||
|
||||
// 计算方向
|
||||
function calcDirection() {
|
||||
if (!actions.value) return props.direction;
|
||||
|
||||
let dir = props.direction;
|
||||
|
||||
const actionsHeight = actions.value?.height || 0;
|
||||
const actionsWidth = actions.value?.width || 0;
|
||||
|
||||
// 菜单展开时,如果位置不够,则反转方向
|
||||
if (dir === 'top') {
|
||||
// 按钮上方剩余空间: 按钮顶部 - 顶部边距
|
||||
if (position.top - minTop.value < actionsHeight) dir = 'bottom';
|
||||
} else if (dir === 'bottom') {
|
||||
// 按钮下方剩余空间: 有效窗口高度 - (按钮顶部 + 按钮高 + 边距)
|
||||
const bottom = effectiveWindowHeight.value - (position.top + btnInfo.value.height + getGap('bottom'));
|
||||
if (bottom < actionsHeight) dir = 'top';
|
||||
} else if (dir === 'left') {
|
||||
// 按钮左侧剩余空间: 有效窗口宽度 - 边距
|
||||
if (position.left - getGap('left') < actionsWidth) dir = 'right';
|
||||
} else if (dir === 'right') {
|
||||
// 按钮右侧剩余空间: 有效窗口宽度 - (按钮左侧 + 按钮宽 + 边距)
|
||||
const right = sysInfo.windowWidth - (position.left + btnInfo.value.width + getGap('right'));
|
||||
if (right < actionsWidth) dir = 'left';
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
// 初始化位置
|
||||
function initPosition() {
|
||||
// 根据 props.position 计算初始 left/top
|
||||
const pos = props.position || 'right-bottom';
|
||||
const winW = sysInfo.windowWidth;
|
||||
const winH = effectiveWindowHeight.value;
|
||||
|
||||
switch (pos) {
|
||||
case 'left-top':
|
||||
position.left = getGap('left');
|
||||
position.top = getGap('top') + sysInfo.windowTop;
|
||||
break;
|
||||
case 'right-top':
|
||||
position.left = winW - btnInfo.value.width - getGap('right');
|
||||
position.top = getGap('top') + sysInfo.windowTop;
|
||||
break;
|
||||
case 'left-bottom':
|
||||
position.left = getGap('left');
|
||||
position.top = winH - btnInfo.value.height - getGap('bottom');
|
||||
break;
|
||||
case 'right-bottom':
|
||||
position.left = winW - btnInfo.value.width - getGap('right');
|
||||
position.top = winH - btnInfo.value.height - getGap('bottom');
|
||||
break;
|
||||
case 'left-center':
|
||||
position.left = getGap('left');
|
||||
position.top = Math.round((winH - btnInfo.value.height) / 2);
|
||||
break;
|
||||
case 'right-center':
|
||||
position.left = winW - btnInfo.value.width - getGap('right');
|
||||
position.top = Math.round((winH - btnInfo.value.height) / 2);
|
||||
break;
|
||||
case 'top-center':
|
||||
position.left = Math.round((winW - btnInfo.value.width) / 2);
|
||||
position.top = getGap('top') + sysInfo.windowTop;
|
||||
break;
|
||||
case 'bottom-center':
|
||||
position.left = Math.round((winW - btnInfo.value.width) / 2);
|
||||
position.top = winH - btnInfo.value.height - getGap('bottom');
|
||||
break;
|
||||
default:
|
||||
position.left = winW - btnInfo.value.width - getGap('right');
|
||||
position.top = winH - btnInfo.value.height - getGap('bottom');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
btnInfo.value = await $u.getRect('#trigger', instance);
|
||||
actions.value = await $u.getRect('#actions', instance);
|
||||
|
||||
initPosition();
|
||||
|
||||
minLeft.value = getGap('left');
|
||||
minTop.value = getGap('top') + sysInfo.windowTop;
|
||||
maxLeft.value = sysInfo.windowWidth - btnInfo.value.width - getGap('right');
|
||||
maxTop.value = effectiveWindowHeight.value - btnInfo.value.height - getGap('bottom');
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
toggle: handleBtnClick
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.u-fab {
|
||||
position: fixed;
|
||||
|
||||
.u-fab-trigger {
|
||||
:deep(.u-fab-trigger-btn) {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 112rpx;
|
||||
|
||||
&::after {
|
||||
border-radius: 112rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.u-fab-actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform-origin: center;
|
||||
will-change: transform, opacity;
|
||||
transition: all 0.28s cubic-bezier(0.3, 0, 0.2, 1);
|
||||
|
||||
&.u-fab-actions__show {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { InputAlign, InputConfirmType, InputLabelPosition, InputType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-field 组件 Props 类型定义
|
||||
* @description 表单输入框属性
|
||||
*/
|
||||
export const FieldProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** label左边的图标,限uView的图标名称 */
|
||||
icon: { type: String, default: '' },
|
||||
/** 输入框右边的图标名称,限uView的图标名称(默认false) */
|
||||
rightIcon: { type: String, default: '' },
|
||||
/** 方向属性 */
|
||||
arrowDirection: { type: String, default: 'right' },
|
||||
/** 是否必填,左边显示红色"*"号(默认false) */
|
||||
required: { type: Boolean, default: false },
|
||||
/** 输入框左边的文字提示 */
|
||||
label: { type: String, default: '' },
|
||||
/** 是否密码输入方式(用点替换文字),type为text时有效(默认false) */
|
||||
password: { type: Boolean, default: false },
|
||||
/** 是否显示右侧清空内容的图标控件(默认true) */
|
||||
clearable: { type: Boolean, default: true },
|
||||
/** label的宽度,单位rpx(默认130) */
|
||||
labelWidth: { type: [Number, String] as PropType<string | number>, default: 130 },
|
||||
/** label的文字对齐方式(默认left) */
|
||||
labelAlign: { type: String as PropType<InputAlign>, default: 'left' },
|
||||
/** 输入框内容对齐方式(默认left) */
|
||||
inputAlign: { type: String as PropType<InputAlign>, default: 'left' },
|
||||
/** 左边通过icon配置的图标的颜色(默认var(--u-content-color)) */
|
||||
iconColor: { type: String, default: 'var(--u-content-color)' },
|
||||
/** 是否自动增高输入区域,type为textarea时有效(默认true) */
|
||||
autoHeight: { type: Boolean, default: true },
|
||||
/** 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息 */
|
||||
errorMessage: { type: [String, Boolean] as PropType<string | boolean>, default: '' },
|
||||
/** 输入框的提示文字 */
|
||||
placeholder: { type: String, default: '' },
|
||||
/** placeholder的样式(内联样式,字符串),如"color: var(--u-divider-color)" */
|
||||
placeholderStyle: { type: String, default: '' },
|
||||
/** 是否自动获得焦点(默认false) */
|
||||
focus: { type: Boolean, default: false },
|
||||
/** 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false) */
|
||||
fixed: { type: Boolean, default: false },
|
||||
/** 输入框绑定值 */
|
||||
modelValue: [Number, String],
|
||||
/** 输入框类型(text/textarea/password等,默认text) */
|
||||
type: { type: String as PropType<InputType>, default: 'text' },
|
||||
/** 是否不可输入(默认false) */
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 是否只读,禁止输入但可点击,样式不变,可触发click事件(默认false) */
|
||||
readonly: { type: Boolean, default: false },
|
||||
/** 最大输入长度,设置为 -1 的时候不限制最大长度(默认140) */
|
||||
maxlength: { type: [Number, String] as PropType<string | number>, default: 140 },
|
||||
/** 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done) */
|
||||
confirmType: { type: String as PropType<InputConfirmType>, default: 'done' },
|
||||
/** label位置(默认left)left-左边,top-上边 */
|
||||
labelPosition: { type: String as PropType<InputLabelPosition>, default: 'left' },
|
||||
/** 自定义输入框的样式,对象形式 */
|
||||
fieldStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 清除图标的大小,单位rpx(默认30) */
|
||||
clearSize: { type: [Number, String] as PropType<string | number>, default: 30 },
|
||||
/** 左侧图标样式 */
|
||||
iconStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 是否显示field的上边框(默认false) */
|
||||
borderTop: { type: Boolean, default: false },
|
||||
/** 是否显示field的下边框(默认true) */
|
||||
borderBottom: { type: Boolean, default: true },
|
||||
/** 是否自动去除输入内容首尾空格(默认true) */
|
||||
trim: { type: Boolean, default: true }
|
||||
};
|
||||
|
||||
export type FieldProps = ExtractPropTypes<typeof FieldProps>;
|
||||
@@ -0,0 +1,398 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-field"
|
||||
:class="[
|
||||
{
|
||||
'u-border-top': props.borderTop,
|
||||
'u-border-bottom': props.borderBottom,
|
||||
'u-field--disabled': props.disabled
|
||||
},
|
||||
customClass
|
||||
]"
|
||||
:style="$u.toStyle(customStyle)"
|
||||
>
|
||||
<view
|
||||
class="u-field-inner"
|
||||
:class="[props.type === 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + props.labelPosition]"
|
||||
>
|
||||
<view
|
||||
class="u-label"
|
||||
:class="[props.required ? 'u-required' : '']"
|
||||
:style="{
|
||||
justifyContent: justifyContent,
|
||||
flex: props.labelPosition === 'left' ? `0 0 ${props.labelWidth}rpx` : '1'
|
||||
}"
|
||||
>
|
||||
<view class="u-icon-wrap" v-if="props.icon">
|
||||
<u-icon
|
||||
size="32"
|
||||
:custom-style="props.iconStyle"
|
||||
:name="props.icon"
|
||||
:color="props.iconColor"
|
||||
class="u-icon"
|
||||
></u-icon>
|
||||
</view>
|
||||
<slot name="icon"></slot>
|
||||
<text class="u-label-text" :class="[$slots.icon || props.icon ? 'u-label-left-gap' : '']">
|
||||
{{ props.label }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="fild-body">
|
||||
<view class="u-flex-1 u-flex u-field-input-wrap" :style="[inputWrapStyle]">
|
||||
<textarea
|
||||
v-if="props.type === 'textarea'"
|
||||
class="u-flex-1 u-textarea-class"
|
||||
:style="$u.toStyle(props.fieldStyle)"
|
||||
:value="inputValue"
|
||||
:placeholder="String(props.placeholder)"
|
||||
:placeholderStyle="props.placeholderStyle"
|
||||
:disabled="props.disabled"
|
||||
:readonly="props.readonly"
|
||||
:maxlength="inputMaxlength"
|
||||
:focus="props.focus"
|
||||
:autoHeight="props.autoHeight"
|
||||
:fixed="props.fixed"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@confirm="onConfirm"
|
||||
/>
|
||||
<!-- prettier-ignore -->
|
||||
<input
|
||||
v-else
|
||||
class="u-flex-1 u-field__input-wrap"
|
||||
:style="$u.toStyle(props.fieldStyle)"
|
||||
:type="(props.type as any)"
|
||||
:value="inputValue"
|
||||
:password="props.password || props.type === 'password'"
|
||||
:placeholder="String(props.placeholder)"
|
||||
:placeholderStyle="props.placeholderStyle"
|
||||
:disabled="props.disabled"
|
||||
:readonly="props.readonly"
|
||||
:maxlength="inputMaxlength"
|
||||
:focus="props.focus"
|
||||
:confirmType="props.confirmType"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
@confirm="onConfirm"
|
||||
/>
|
||||
<!-- 透明遮罩,在disabled或readonly时显示,用于捕获点击事件(原生input设置disabled会阻止点击冒泡) -->
|
||||
<view v-if="props.readonly" class="u-field__readonly-overlay" @tap.stop="fieldClick"></view>
|
||||
</view>
|
||||
<u-icon
|
||||
v-if="props.clearable && inputValue !== '' && focused && !props.disabled"
|
||||
:size="props.clearSize"
|
||||
name="close-circle-fill"
|
||||
color="var(--u-light-color)"
|
||||
class="u-clear-icon"
|
||||
@click="onClear"
|
||||
/>
|
||||
<view class="u-button-wrap"><slot name="right" /></view>
|
||||
<u-icon
|
||||
v-if="props.rightIcon"
|
||||
@click="rightIconClick"
|
||||
:name="props.rightIcon"
|
||||
color="var(--u-light-color)"
|
||||
:style="[rightIconStyle]"
|
||||
size="26"
|
||||
class="u-arror-right"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="props.errorMessage !== false && props.errorMessage != ''"
|
||||
class="u-error-message"
|
||||
:style="{
|
||||
paddingLeft: props.labelWidth + 'rpx'
|
||||
}"
|
||||
>{{ props.errorMessage }}</view
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-field',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FieldProps } from './types';
|
||||
import { ref, computed } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* field 输入框
|
||||
* @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
|
||||
* @tutorial https://uviewpro.cn/zh/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配置的图标的颜色(默认var(--u-content-color))
|
||||
* @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
|
||||
* @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
|
||||
* @property {String} placeholder 输入框的提示文字
|
||||
* @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: var(--u-divider-color)"
|
||||
* @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>
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'focus', 'blur', 'confirm', 'right-icon-click', 'click']);
|
||||
|
||||
const props = defineProps(FieldProps);
|
||||
|
||||
const focused = ref(false);
|
||||
|
||||
const inputValue = computed<string>(() => {
|
||||
if (props.modelValue === undefined || props.modelValue === null) return '';
|
||||
return String(props.modelValue);
|
||||
});
|
||||
|
||||
const inputWrapStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
style.textAlign = props.inputAlign;
|
||||
// 判断label的位置,如果是left的话,让input左边两边有间隙
|
||||
if (props.labelPosition === 'left') {
|
||||
style.margin = '0 8rpx';
|
||||
} else {
|
||||
// 如果label是top的,input的左边就没必要有间隙了
|
||||
style.marginRight = '8rpx';
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
const rightIconStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
// 方向属性已注释,保留原有逻辑
|
||||
if (props.arrowDirection === 'top') style.transform = 'rotate(-90deg)';
|
||||
if (props.arrowDirection === 'bottom') style.transform = 'rotate(90deg)';
|
||||
else style.transform = 'rotate(0deg)';
|
||||
return style;
|
||||
});
|
||||
|
||||
const labelStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
if (props.labelAlign === 'left') style.justifyContent = 'flex-start';
|
||||
if (props.labelAlign === 'center') style.justifyContent = 'center';
|
||||
if (props.labelAlign === 'right') style.justifyContent = 'flex-end';
|
||||
return style;
|
||||
});
|
||||
|
||||
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
|
||||
const justifyContent = computed(() => {
|
||||
if (props.labelAlign === 'left') return 'flex-start';
|
||||
if (props.labelAlign === 'center') return 'center';
|
||||
if (props.labelAlign === 'right') return 'flex-end';
|
||||
return 'flex-start';
|
||||
});
|
||||
|
||||
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
|
||||
const inputMaxlength = computed(() => Number(props.maxlength));
|
||||
|
||||
// label的位置
|
||||
const fieldInnerStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
style.flexDirection = props.labelPosition === 'left' ? 'row' : 'column';
|
||||
return style;
|
||||
});
|
||||
|
||||
function onInput(event: any) {
|
||||
// 兼容 uni-app input/textarea 事件参数
|
||||
let value = event?.detail?.value ?? '';
|
||||
// 判断是否去除空格
|
||||
if (props.trim) value = $u.trim(value);
|
||||
emit('update:modelValue', value);
|
||||
emit('input', value);
|
||||
}
|
||||
|
||||
function onFocus(event: any) {
|
||||
focused.value = true;
|
||||
emit('focus', event);
|
||||
}
|
||||
|
||||
function onBlur(event: any) {
|
||||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
|
||||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
|
||||
setTimeout(() => {
|
||||
focused.value = false;
|
||||
}, 100);
|
||||
emit('blur', event);
|
||||
}
|
||||
|
||||
function onConfirm(e: any) {
|
||||
emit('confirm', e?.detail?.value ?? '');
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
|
||||
function rightIconClick() {
|
||||
emit('right-icon-click');
|
||||
emit('click');
|
||||
}
|
||||
|
||||
function fieldClick() {
|
||||
if (props.disabled) return;
|
||||
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;
|
||||
}
|
||||
|
||||
.u-field-input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u-field__readonly-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.u-field--disabled {
|
||||
background-color: $u-bg-gray-light;
|
||||
}
|
||||
|
||||
.u-field--disabled .u-textarea-class,
|
||||
.u-field--disabled .u-field__input-wrap {
|
||||
background-color: transparent;
|
||||
color: $u-light-color;
|
||||
-webkit-text-fill-color: $u-light-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { PropType, ExtractPropTypes } from 'vue';
|
||||
import type { InputAlign, InputLabelPosition, SizeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* form-item 表单item Props
|
||||
*/
|
||||
export const FormItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** input的label提示语 */
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 绑定的值 */
|
||||
prop: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 是否显示表单域的下划线边框 */
|
||||
borderBottom: {
|
||||
type: [String, Boolean] as PropType<string | boolean>,
|
||||
default: ''
|
||||
},
|
||||
/** label的位置,left-左边,top-上边 */
|
||||
labelPosition: {
|
||||
type: String as PropType<InputLabelPosition>,
|
||||
default: ''
|
||||
},
|
||||
/** label的宽度,单位rpx */
|
||||
labelWidth: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: ''
|
||||
},
|
||||
/** label的样式,对象形式 */
|
||||
labelStyle: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** label字体的对齐方式 */
|
||||
labelAlign: {
|
||||
type: String as PropType<InputAlign>,
|
||||
default: ''
|
||||
},
|
||||
/** 右侧图标 */
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 左侧图标 */
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 左侧图标的自定义前缀 */
|
||||
leftIconPrefix: {
|
||||
type: String,
|
||||
default: 'uicon'
|
||||
},
|
||||
/** 右侧图标的自定义前缀 */
|
||||
rightIconPrefix: {
|
||||
type: String,
|
||||
default: 'uicon'
|
||||
},
|
||||
/** 左侧图标的样式 */
|
||||
leftIconStyle: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 右侧图标的样式 */
|
||||
rightIconStyle: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置 */
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 表单内组件的大小,仅支持预设值 default/small/large */
|
||||
size: {
|
||||
type: String as PropType<SizeType | string>,
|
||||
default: ''
|
||||
}
|
||||
};
|
||||
|
||||
export type FormItemProps = ExtractPropTypes<typeof FormItemProps>;
|
||||
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-form-item"
|
||||
:class="[
|
||||
{
|
||||
'u-border-bottom': elBorderBottom,
|
||||
'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom'),
|
||||
'u-form-item__border--error': validateState === 'error' && showError('border')
|
||||
},
|
||||
customClass
|
||||
]"
|
||||
:style="$u.toStyle(customStyle)"
|
||||
>
|
||||
<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
|
||||
v-if="required || leftIcon || label || $slots.label || $slots.leftIcon"
|
||||
class="u-form-item--left__content"
|
||||
>
|
||||
<!-- 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 || $slots.leftIcon">
|
||||
<slot name="leftIcon">
|
||||
<u-icon
|
||||
:name="leftIcon"
|
||||
:size="currentIconSize"
|
||||
:custom-prefix="leftIconPrefix"
|
||||
:custom-style="leftIconStyle"
|
||||
></u-icon>
|
||||
</slot>
|
||||
</view>
|
||||
<view
|
||||
class="u-form-item--left__content__label"
|
||||
:style="[
|
||||
elLabelStyle,
|
||||
{ 'font-size': actualFontSize },
|
||||
{
|
||||
'justify-content':
|
||||
elLabelAlign == 'left'
|
||||
? 'flex-start'
|
||||
: elLabelAlign == 'center'
|
||||
? 'center'
|
||||
: 'flex-end'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-form-item--right u-flex">
|
||||
<view class="u-form-item--right__content">
|
||||
<view class="u-form-item--right__content__slot">
|
||||
<slot />
|
||||
</view>
|
||||
<view
|
||||
class="u-form-item--right__content__icon u-flex"
|
||||
v-if="$slots.right || $slots.rightIcon || rightIcon"
|
||||
>
|
||||
<slot name="rightIcon">
|
||||
<u-icon
|
||||
v-if="rightIcon"
|
||||
:name="rightIcon"
|
||||
:size="currentIconSize"
|
||||
:custom-prefix="rightIconPrefix"
|
||||
:custom-style="rightIconStyle"
|
||||
></u-icon>
|
||||
</slot>
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="u-form-item__message"
|
||||
v-if="validateState === 'error' && showError('message')"
|
||||
:style="{
|
||||
paddingLeft: elLabelPosition == 'left' && label ? $u.addUnit(elLabelWidth) : '0',
|
||||
fontSize: $u.addUnit(currentSizeConfig.messageSize),
|
||||
lineHeight: $u.addUnit(currentSizeConfig.messageSize)
|
||||
}"
|
||||
>
|
||||
{{ validateMessage }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-form-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick, useSlots } from 'vue';
|
||||
import { $u, useChildren, useParent } from '../..';
|
||||
// @ts-ignore
|
||||
import schema from '../../libs/util/async-validator';
|
||||
import { FormItemProps } from './types';
|
||||
import type { SizeType } from '../../types/global';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
// 去除警告信息
|
||||
schema.warning = function () {};
|
||||
|
||||
/**
|
||||
* form-item 表单item
|
||||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
|
||||
* @tutorial https://uviewpro.cn/zh/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 label的样式,对象形式
|
||||
* @property {String} label-align label的对齐方式
|
||||
* @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>
|
||||
*/
|
||||
|
||||
const props = defineProps(FormItemProps);
|
||||
|
||||
// 插槽
|
||||
const $slots = useSlots();
|
||||
|
||||
const { broadcast } = useParent('u-form-item');
|
||||
const { parentExposed } = useChildren('u-form-item', 'u-form');
|
||||
|
||||
// 组件状态
|
||||
const initialValue = ref(''); // 存储初始值,用于重置
|
||||
const validateState = ref(''); // 校验状态 success/error/validating
|
||||
const validateMessage = ref(''); // 校验失败的提示语
|
||||
const errorType = ref<string[]>(['message']); // 错误提示方式,默认 message
|
||||
const fieldValue = ref(''); // 当前表单项的值
|
||||
const parentData = ref({
|
||||
borderBottom: true, // 父表单下划线边框
|
||||
labelWidth: 90, // 父表单 label 宽度
|
||||
labelPosition: 'left', // 父表单 label 位置
|
||||
labelStyle: {}, // 父表单 label 样式
|
||||
labelAlign: 'left' // 父表单 label 对齐
|
||||
});
|
||||
|
||||
// 显示错误提示
|
||||
// errorType: ['message', 'toast', 'border-bottom', 'none']
|
||||
const showError = computed(() => (type: string) => {
|
||||
// 如果errorType数组中含有none,或者toast提示类型
|
||||
if (errorType.value.indexOf('none') >= 0) return false;
|
||||
else if (errorType.value.indexOf(type) >= 0) return true;
|
||||
else return false;
|
||||
});
|
||||
|
||||
// 根据 size 定义不同的配置
|
||||
const sizeConfig = {
|
||||
small: {
|
||||
fontSize: 24,
|
||||
iconSize: 28,
|
||||
messageSize: 20
|
||||
},
|
||||
default: {
|
||||
fontSize: 28,
|
||||
iconSize: 32,
|
||||
messageSize: 24
|
||||
},
|
||||
large: {
|
||||
fontSize: 32,
|
||||
iconSize: 36,
|
||||
messageSize: 28
|
||||
}
|
||||
};
|
||||
|
||||
// 获取实际使用的 size 值(优先级:u-textarea.size > u-form.size)
|
||||
const actualSize = computed(() => {
|
||||
// 优先使用 props 的 size 属性
|
||||
if (props.size !== '') {
|
||||
return String(props.size);
|
||||
}
|
||||
// 次优先:使用 u-form 的 size 属性(u-form 的 size 只支持预设值)
|
||||
if (parentExposed.value?.props?.size) {
|
||||
return String(parentExposed.value.props.size);
|
||||
}
|
||||
// 默认值
|
||||
return 'default';
|
||||
});
|
||||
|
||||
// 判断实际使用的 size 是否在预设配置中
|
||||
const isInSizeConfig = computed(() => actualSize.value in sizeConfig);
|
||||
|
||||
// 获取预设 size(用于查找 sizeConfig 配置,如图标大小、高度等)
|
||||
const presetSize = computed(() => {
|
||||
return (isInSizeConfig.value ? actualSize.value : 'default') as SizeType;
|
||||
});
|
||||
|
||||
// 获取当前尺寸配置
|
||||
const currentSizeConfig = computed(() => sizeConfig[presetSize.value]);
|
||||
|
||||
// 获取实际要使用的 font-size(如果是预设值使用配置值,否则作为自定义值处理)
|
||||
const actualFontSize = computed(() => {
|
||||
if (isInSizeConfig.value) {
|
||||
return $u.addUnit(currentSizeConfig.value.fontSize);
|
||||
}
|
||||
// 自定义size值,直接作为fontSize处理
|
||||
return $u.addUnit(actualSize.value);
|
||||
});
|
||||
|
||||
// 计算当前图标大小
|
||||
const currentIconSize = computed(() => currentSizeConfig.value.iconSize);
|
||||
|
||||
// 监听校验状态和父表单 errorType 变化
|
||||
watch(validateState, () => {
|
||||
broadcastInputError();
|
||||
});
|
||||
|
||||
// 监听u-form组件的errorType的变化
|
||||
watch(
|
||||
() => parentExposed?.value?.props?.errorType,
|
||||
val => {
|
||||
if (val) errorType.value = val;
|
||||
broadcastInputError();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 计算属性
|
||||
const uLabelWidth = computed(() => {
|
||||
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
|
||||
return elLabelPosition.value == 'left'
|
||||
? (props.label === 'true' || props.label === '') && !$slots.label
|
||||
? 'auto'
|
||||
: $u.addUnit(elLabelWidth.value)
|
||||
: '100%';
|
||||
});
|
||||
|
||||
// label的宽度
|
||||
const elLabelWidth = computed(() => {
|
||||
// label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
|
||||
return props.labelWidth != 0 && props.labelWidth !== ''
|
||||
? props.labelWidth
|
||||
: parentData.value.labelWidth
|
||||
? parentData.value.labelWidth
|
||||
: 90;
|
||||
});
|
||||
|
||||
// label的样式
|
||||
const elLabelStyle = computed(() => {
|
||||
return Object.keys(props.labelStyle).length
|
||||
? props.labelStyle
|
||||
: parentData.value.labelStyle
|
||||
? parentData.value.labelStyle
|
||||
: {};
|
||||
});
|
||||
|
||||
// label的位置,左侧或者上方
|
||||
const elLabelPosition = computed(() => {
|
||||
return props.labelPosition
|
||||
? props.labelPosition
|
||||
: parentData.value.labelPosition
|
||||
? parentData.value.labelPosition
|
||||
: 'left';
|
||||
});
|
||||
|
||||
// label的对齐方式
|
||||
const elLabelAlign = computed(() => {
|
||||
return props.labelAlign ? props.labelAlign : parentData.value.labelAlign ? parentData.value.labelAlign : 'left';
|
||||
});
|
||||
|
||||
// label的下划线
|
||||
const elBorderBottom = computed(() => {
|
||||
// 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
|
||||
return props.borderBottom !== ''
|
||||
? props.borderBottom
|
||||
: parentData.value.borderBottom
|
||||
? parentData.value.borderBottom
|
||||
: false;
|
||||
});
|
||||
|
||||
// 事件派发/广播工具
|
||||
function broadcastInputError() {
|
||||
// 子组件发出事件,参数为true或者false,true代表有错误
|
||||
broadcast('onFormItemError', validateState.value === 'error' && showError.value('border'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取对象的值(支持嵌套路径,如 'a.b.c')
|
||||
* @param obj 对象
|
||||
* @param path 路径字符串
|
||||
*/
|
||||
function getPropByPath(obj: any, path: string) {
|
||||
if (!obj || !path) return { o: obj, k: '', v: undefined };
|
||||
|
||||
const paths = path.split('.');
|
||||
let current = obj;
|
||||
let key = '';
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
key = paths[i];
|
||||
if (!current) break;
|
||||
|
||||
if (i === paths.length - 1) {
|
||||
return { o: current, k: key, v: current[key] };
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return { o: current, k: key, v: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前u-form-item的校验规则
|
||||
*/
|
||||
function getRules() {
|
||||
// 父组件的所有规则
|
||||
let rules = parentExposed?.value?.rules?.value || {};
|
||||
rules = rules
|
||||
? rules[props.prop] || getPropByPath(rules, props.prop.replace(/(\.|^)(\d+)\./, '.defaultField.fields.')).v
|
||||
: [];
|
||||
// 保证返回的是一个数组形式
|
||||
return [].concat(rules || []);
|
||||
}
|
||||
|
||||
// blur事件时进行表单校验
|
||||
function onFieldBlur() {
|
||||
validation('blur');
|
||||
}
|
||||
|
||||
// change事件进行表单校验
|
||||
function onFieldChange() {
|
||||
validation('change');
|
||||
}
|
||||
|
||||
function onFormBlur() {
|
||||
onFieldBlur();
|
||||
}
|
||||
|
||||
function onFormChange() {
|
||||
onFieldChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤出符合要求的rule规则
|
||||
* @param triggerType 触发类型
|
||||
*/
|
||||
function getFilteredRule(triggerType = '') {
|
||||
// 获取所有规则
|
||||
const rules = getRules();
|
||||
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
|
||||
if (!triggerType) return rules;
|
||||
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
|
||||
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
|
||||
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
|
||||
return rules.filter((res: any) => res.trigger && res.trigger.indexOf(triggerType) !== -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验数据
|
||||
* @param trigger 触发类型
|
||||
* @param callback 校验回调
|
||||
*/
|
||||
function validation(trigger: string, callback: (msg: string) => void = () => {}) {
|
||||
// 检验之前,先获取需要校验的值(支持嵌套路径)
|
||||
const propPath = getPropByPath(parentExposed?.value?.model?.value, props.prop);
|
||||
fieldValue.value = propPath.v;
|
||||
|
||||
// blur和change是否有当前方式的校验规则
|
||||
let rules = getFilteredRule(trigger);
|
||||
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
|
||||
// 对count变量的统计错误而无法进入上一层的回调
|
||||
if (!rules || rules.length === 0) {
|
||||
callback('');
|
||||
return;
|
||||
}
|
||||
// 设置当前的状态,标识为校验中
|
||||
validateState.value = 'validating';
|
||||
// 调用async-validator的方法
|
||||
let validator = new schema({ [props.prop]: rules });
|
||||
validator.validate({ [props.prop]: fieldValue.value }, { firstFields: true }, (errors: any) => {
|
||||
// 记录状态和报错信息
|
||||
validateState.value = !errors ? 'success' : 'error';
|
||||
validateMessage.value = errors ? errors[0].message : '';
|
||||
// 调用回调方法
|
||||
callback(validateMessage.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前的u-form-item
|
||||
*/
|
||||
function resetField() {
|
||||
if (parentExposed?.value?.model?.value && props.prop) {
|
||||
// 支持嵌套路径的重置
|
||||
const propPath = getPropByPath(parentExposed.value.model.value, props.prop);
|
||||
if (propPath.o && propPath.k) {
|
||||
propPath.o[propPath.k] = initialValue.value;
|
||||
}
|
||||
}
|
||||
// 设置为`success`状态,只是为了清空错误标记
|
||||
// 延时50毫秒,如果立即清空状态,则无法清空错误标记
|
||||
setTimeout(() => {
|
||||
validateState.value = 'success';
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// 组件挂载时注册到父表单
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (parentExposed.value) {
|
||||
// 继承父表单配置
|
||||
// 历遍parentData中的属性,将parent中的同名属性赋值给parentData
|
||||
Object.keys(parentData.value).forEach(key => {
|
||||
(parentData.value as any)[key] = parentExposed?.value?.props[key];
|
||||
});
|
||||
// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
|
||||
if (props.prop) {
|
||||
// 将本实例添加到父组件中
|
||||
parentExposed?.value?.addField &&
|
||||
parentExposed?.value?.addField({
|
||||
validation,
|
||||
resetField,
|
||||
prop: props.prop
|
||||
});
|
||||
errorType.value = parentExposed?.value?.errorType || errorType.value;
|
||||
// 设置初始值(支持嵌套路径)
|
||||
const propPath = getPropByPath(parentExposed?.value?.model?.value, props.prop);
|
||||
fieldValue.value = propPath.v;
|
||||
// 设置初始值
|
||||
initialValue.value = fieldValue.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// 组件销毁前,将实例从u-form的缓存中移除
|
||||
onBeforeUnmount(() => {
|
||||
// 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
|
||||
if (parentExposed?.value && props.prop) {
|
||||
parentExposed?.value?.removeField && parentExposed?.value?.removeField({ prop: props.prop });
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
validation,
|
||||
resetField,
|
||||
onFormBlur,
|
||||
onFormChange
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-form-item {
|
||||
@include vue-flex;
|
||||
// align-items: flex-start;
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
box-sizing: border-box;
|
||||
// line-height: 70rpx;
|
||||
flex-direction: column;
|
||||
|
||||
&__border-bottom--error:after {
|
||||
border-color: $u-type-error;
|
||||
}
|
||||
|
||||
&__border--error {
|
||||
:deep(.u-input--border) {
|
||||
border-color: $u-type-error !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__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;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--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;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: 10rpx;
|
||||
color: $u-light-color;
|
||||
font-size: 30rpx;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
color: $u-type-error;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { FormRules, InputAlign, FormErrorType, InputLabelPosition, SizeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-form 组件 Props 类型定义
|
||||
* @description 表单组件属性
|
||||
*/
|
||||
export const FormProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 当前form的需要验证字段的集合 */
|
||||
model: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 表单验证规则 */
|
||||
rules: { type: Object as PropType<FormRules>, default: () => ({}) },
|
||||
/** 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,border-bottom-下边框呈现红色,none-无提示 */
|
||||
errorType: { type: Array as PropType<FormErrorType[]>, default: () => ['message', 'toast'] },
|
||||
/** 是否显示表单域的下划线边框 */
|
||||
borderBottom: { type: Boolean, default: true },
|
||||
/** label的位置,left-左边,top-上边 */
|
||||
labelPosition: { type: String as PropType<InputLabelPosition>, default: 'left' },
|
||||
/** label的宽度,单位rpx */
|
||||
labelWidth: { type: [String, Number] as PropType<string | number>, default: 90 },
|
||||
/** label字体的对齐方式 */
|
||||
labelAlign: { type: String as PropType<InputAlign>, default: 'left' },
|
||||
/** label的样式,对象形式 */
|
||||
labelStyle: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
|
||||
/** 表单内组件的大小,仅支持预设值 default/small/large(默认 default) */
|
||||
size: { type: String as PropType<SizeType>, default: 'default' }
|
||||
};
|
||||
|
||||
export type FormProps = ExtractPropTypes<typeof FormProps>;
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="u-form" :class="customClass" :style="$u.toStyle(customStyle)">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-form',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormProps } from './types';
|
||||
import { computed, ref } from 'vue';
|
||||
import { $u, useParent } from '../..';
|
||||
|
||||
/**
|
||||
* form 表单
|
||||
* @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
|
||||
* @tutorial https://uviewpro.cn/zh/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 label的样式,对象形式
|
||||
* @property {String} label-align label的对齐方式
|
||||
* @property {Object} rules 通过ref设置,见官网说明
|
||||
* @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
|
||||
* @example <u-form :model="form" ref="uForm"></u-form>
|
||||
*/
|
||||
|
||||
const props = defineProps(FormProps);
|
||||
|
||||
useParent('u-form');
|
||||
|
||||
interface ErrorItem {
|
||||
prop: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 存储当前form下的所有u-form-item的实例
|
||||
const fields = ref<any[]>([]);
|
||||
|
||||
// 校验规则
|
||||
const rules = ref<Record<string, any>>(props.rules);
|
||||
|
||||
/**
|
||||
* 设置校验规则
|
||||
* @param newRules 校验规则对象
|
||||
*/
|
||||
function setRules(newRules: Record<string, any>) {
|
||||
rules.value = newRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
|
||||
*/
|
||||
function resetFields() {
|
||||
fields.value.forEach((field: any) => {
|
||||
field.resetField && field.resetField();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验全部数据
|
||||
* @param callback 校验回调
|
||||
* @returns Promise<boolean>
|
||||
*/
|
||||
function validate(callback?: (valid: boolean, errorArr: ErrorItem[]) => void): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
// 对所有的u-form-item进行校验
|
||||
let valid = true; // 默认通过
|
||||
let count = 0; // 用于标记是否检查完毕
|
||||
let errorArr: ErrorItem[] = []; // 存放错误信息
|
||||
if (fields.value.length === 0) {
|
||||
resolve(true);
|
||||
if (typeof callback === 'function') callback(true, []);
|
||||
return;
|
||||
}
|
||||
// 调用每一个u-form-item实例的validation的校验方法
|
||||
fields.value.forEach((field: any) => {
|
||||
// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
|
||||
field.validation &&
|
||||
field.validation('', (error: any) => {
|
||||
if (error) {
|
||||
valid = false;
|
||||
errorArr.push({ prop: field.prop, message: error });
|
||||
}
|
||||
// 当历遍了所有的u-form-item时,调用promise的then方法
|
||||
if (++count === fields.value.length) {
|
||||
resolve(valid); // 进入promise的then方法
|
||||
// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
|
||||
if (
|
||||
props.errorType.indexOf('none') === -1 &&
|
||||
props.errorType.indexOf('toast') >= 0 &&
|
||||
errorArr.length > 0
|
||||
) {
|
||||
errorArr[0].message && $u.toast(errorArr[0].message);
|
||||
}
|
||||
// 调用回调方法
|
||||
if (typeof callback === 'function') callback(valid, errorArr);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setRules,
|
||||
resetFields,
|
||||
validate,
|
||||
addField(field: any) {
|
||||
if (!fields.value.includes(field)) fields.value.push(field);
|
||||
},
|
||||
removeField(field: any) {
|
||||
fields.value = fields.value.filter(f => f.prop !== field.prop);
|
||||
},
|
||||
fields,
|
||||
rules,
|
||||
props,
|
||||
model: computed(() => props.model)
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-full-screen 组件 Props 类型定义
|
||||
* @description 用于APP弹窗遮盖导航栏和底部tabbar,提示新版本升级内容
|
||||
*/
|
||||
export const FullScreenProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否显示弹窗 */
|
||||
show: { type: Boolean, default: false },
|
||||
/** 弹窗标题 */
|
||||
title: { type: String, default: '' },
|
||||
/** 升级内容,支持富文本 */
|
||||
content: { type: String, default: '' }
|
||||
};
|
||||
|
||||
export type FullScreenProps = ExtractPropTypes<typeof FullScreenProps>;
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<u-modal
|
||||
v-model="show"
|
||||
:class="customClass"
|
||||
:style="$u.toStyle(customStyle)"
|
||||
:show-cancel-button="true"
|
||||
:confirm-text="t('uFullScreen.upgrade')"
|
||||
:title="title || t('uFullScreen.title')"
|
||||
@cancel="cancel"
|
||||
@confirm="confirm"
|
||||
>
|
||||
<view class="u-update-content">
|
||||
<rich-text :nodes="content"></rich-text>
|
||||
</view>
|
||||
</u-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-full-screen',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FullScreenProps } from './types';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { $u, useLocale } from '../../';
|
||||
import uModal from '../u-modal/u-modal.vue';
|
||||
|
||||
/**
|
||||
* 压窗屏升级弹窗组件
|
||||
* @description 用于APP弹窗遮盖导航栏和底部tabbar,提示新版本升级内容
|
||||
* @property {boolean} show 是否显示弹窗
|
||||
* @property {string} content 升级内容,支持富文本
|
||||
*/
|
||||
defineProps(FullScreenProps);
|
||||
|
||||
/**
|
||||
* 是否显示弹窗
|
||||
*/
|
||||
const show = ref(false);
|
||||
/**
|
||||
* 升级内容,支持富文本
|
||||
*/
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* 页面加载完成后自动显示弹窗
|
||||
*/
|
||||
onMounted(() => {
|
||||
show.value = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* 取消按钮点击事件
|
||||
* @description 关闭弹窗并返回上一页
|
||||
*/
|
||||
function cancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级按钮点击事件
|
||||
* @description 关闭弹窗并返回上一页
|
||||
*/
|
||||
function confirm() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗方法
|
||||
* @description 返回上一页
|
||||
*/
|
||||
function closeModal() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-full-content {
|
||||
background-color: var(--u-type-success);
|
||||
}
|
||||
|
||||
.u-update-content {
|
||||
font-size: 26rpx;
|
||||
color: $u-content-color;
|
||||
line-height: 1.7;
|
||||
padding: 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-gap 组件 Props 类型定义
|
||||
* @description 间隔槽组件属性
|
||||
*/
|
||||
export const GapProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 背景颜色 */
|
||||
bgColor: { type: String, default: 'transparent' },
|
||||
/** 高度 */
|
||||
height: { type: [String, Number] as PropType<string | number>, default: 30 },
|
||||
/** 与上一个组件的距离 */
|
||||
marginTop: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 与下一个组件的距离 */
|
||||
marginBottom: { type: [String, Number] as PropType<string | number>, default: 0 }
|
||||
};
|
||||
|
||||
export type GapProps = ExtractPropTypes<typeof GapProps>;
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<view class="u-gap" :style="$u.toStyle(gapStyle, customStyle)"></view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-gap',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GapProps } from './types';
|
||||
import { computed } from 'vue';
|
||||
import { $u } from '../../';
|
||||
|
||||
/**
|
||||
* gap 间隔槽
|
||||
* @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
|
||||
* @tutorial https://uviewpro.cn/zh/components/gap.html
|
||||
* @property {String} bg-color 背景颜色(默认var(--u-bg-color))
|
||||
* @property {String Number} height 分割槽高度,单位rpx(默认30)
|
||||
* @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
|
||||
* @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
|
||||
* @example <u-gap height="80" bg-color="var(--u-light-color)"></u-gap>
|
||||
*/
|
||||
const props = defineProps(GapProps);
|
||||
|
||||
/**
|
||||
* 间隔槽样式
|
||||
*/
|
||||
const gapStyle = computed(() => {
|
||||
return {
|
||||
backgroundColor: props.bgColor,
|
||||
height: $u.addUnit(props.height),
|
||||
marginTop: $u.addUnit(props.marginTop),
|
||||
marginBottom: $u.addUnit(props.marginBottom)
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-grid-item 组件 Props 类型定义
|
||||
* @description 宫格项组件属性
|
||||
*/
|
||||
export const GridItemProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 背景颜色 */
|
||||
bgColor: { type: String, default: 'var(--u-bg-white)' },
|
||||
/** 点击时返回的index */
|
||||
index: { type: [Number, String] as PropType<string | number>, default: '' }
|
||||
};
|
||||
|
||||
export type GridItemProps = ExtractPropTypes<typeof GridItemProps>;
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-grid-item"
|
||||
:class="customClass"
|
||||
:hover-class="hoverClass"
|
||||
:hover-stay-time="200"
|
||||
@tap="click"
|
||||
:style="
|
||||
$u.toStyle(
|
||||
{
|
||||
background: bgColor,
|
||||
width: width
|
||||
},
|
||||
customStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<view class="u-grid-item-box" :style="[customStyle]" :class="[border ? 'u-border-right u-border-bottom' : '']">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-grid-item',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GridItemProps } from './types';
|
||||
import { computed } from 'vue';
|
||||
import { $u, useChildren } from '../..';
|
||||
|
||||
/**
|
||||
* gridItem 宫格项
|
||||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
|
||||
* @tutorial https://uviewpro.cn/zh/components/grid.html
|
||||
* @property {String} bg-color 宫格的背景颜色(默认var(--u-bg-white))
|
||||
* @property {String|Number} index 点击宫格时,返回的值
|
||||
* @property {Object} custom-style 自定义样式,对象形式
|
||||
* @event {Function} click 点击宫格触发
|
||||
* @example <u-grid-item></u-grid-item>
|
||||
*/
|
||||
|
||||
const props = defineProps(GridItemProps);
|
||||
const { parentExposed } = useChildren('u-grid-item', 'u-grid');
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// 宫格按压时的样式类,"none"为无效果
|
||||
const hoverClass = computed(() => {
|
||||
return parentExposed?.value?.props?.hoverClass ?? '';
|
||||
});
|
||||
|
||||
// 是否显示边框
|
||||
const border = computed(() => {
|
||||
return parentExposed?.value?.props?.border ?? true;
|
||||
});
|
||||
|
||||
// 分成几列
|
||||
const col = computed(() => {
|
||||
return parentExposed?.value?.props?.col ?? 3;
|
||||
});
|
||||
|
||||
// 计算每个grid-item的宽度
|
||||
const width = computed(() => 100 / Number(col.value) + '%');
|
||||
|
||||
/**
|
||||
* 点击宫格
|
||||
*/
|
||||
function click() {
|
||||
emit('click', props.index);
|
||||
parentExposed?.value?.click(props.index);
|
||||
}
|
||||
|
||||
defineExpose({ click });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-grid-item {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--u-bg-white);
|
||||
@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: var(--u-bg-gray-light) !important;
|
||||
}
|
||||
|
||||
.u-grid-marker-box {
|
||||
position: absolute;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.u-grid-marker-wrap {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.u-grid-item-box {
|
||||
padding: 30rpx 0;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { InputAlign } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-grid 组件 Props 类型定义
|
||||
* @description 宫格组件属性
|
||||
*/
|
||||
export const GridProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 分成几列 */
|
||||
col: { type: [Number, String] as PropType<string | number>, default: 3 },
|
||||
/** 是否显示边框 */
|
||||
border: { type: Boolean, default: true },
|
||||
/** 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右 */
|
||||
align: { type: String as PropType<InputAlign>, default: 'left' },
|
||||
/** 宫格按压时的样式类,"none"为无效果 */
|
||||
hoverClass: { type: String, default: 'u-hover-class' }
|
||||
};
|
||||
|
||||
export type GridProps = ExtractPropTypes<typeof GridProps>;
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-grid"
|
||||
:class="{ 'u-border-top u-border-left': border, customClass }"
|
||||
:style="$u.toStyle(gridStyle, customStyle)"
|
||||
>
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-grid',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useParent, $u } from '../../';
|
||||
import { GridProps } from './types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
* grid 宫格布局
|
||||
* @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
|
||||
* @tutorial https://uviewpro.cn/zh/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>
|
||||
*/
|
||||
|
||||
const props = defineProps(GridProps);
|
||||
const emit = defineEmits(['click']);
|
||||
useParent('u-grid');
|
||||
|
||||
// 宫格对齐方式
|
||||
const gridStyle = computed(() => {
|
||||
const style: Record<string, string> = {};
|
||||
switch (props.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;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击宫格
|
||||
* @param index 子项索引
|
||||
*/
|
||||
function click(index: number) {
|
||||
emit('click', index);
|
||||
}
|
||||
|
||||
defineExpose({ click, props });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-grid {
|
||||
width: 100%;
|
||||
/* #ifdef MP */
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef MP */
|
||||
@include vue-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { IconLabelPosition, ImgMode } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-icon 组件 Props 类型定义
|
||||
*/
|
||||
export const IconProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 图标名称,见示例图标集 */
|
||||
name: { type: String, default: '' },
|
||||
/** 图标颜色,可接受主题色 */
|
||||
color: { type: String, default: '' },
|
||||
/** 字体大小,单位rpx(默认32) */
|
||||
size: { type: [Number, String] as PropType<string | number>, default: 'inherit' },
|
||||
/** 是否显示粗体 */
|
||||
bold: { type: Boolean, default: false },
|
||||
/** 点击图标的时候传递事件出去的index(用于区分点击了哪一个) */
|
||||
index: { type: [Number, String] as PropType<string | number>, default: '' },
|
||||
/** 触摸图标时的类名 */
|
||||
hoverClass: { type: String, default: '' },
|
||||
/** 自定义扩展前缀,方便用户扩展自己的图标库 */
|
||||
customPrefix: { type: String, default: 'uicon' },
|
||||
/** 图标右边或者下面的文字 */
|
||||
label: { type: [String, Number] as PropType<string | number>, default: '' },
|
||||
/** label的位置,只能右边或者下边 */
|
||||
labelPos: { type: String as PropType<IconLabelPosition>, default: 'right' },
|
||||
/** label的大小,单位rpx(默认28) */
|
||||
labelSize: { type: [String, Number] as PropType<string | number>, default: '28' },
|
||||
/** label的颜色 */
|
||||
labelColor: { type: String, default: 'var(--u-content-color)' },
|
||||
/** label与图标的距离(横向排列),单位rpx(默认6) */
|
||||
marginLeft: { type: [String, Number] as PropType<string | number>, default: '6' },
|
||||
/** label与图标的距离(竖向排列),单位rpx(默认6) */
|
||||
marginTop: { type: [String, Number] as PropType<string | number>, default: '6' },
|
||||
/** label与图标的距离(竖向排列),单位rpx(默认6) */
|
||||
marginRight: { type: [String, Number] as PropType<string | number>, default: '6' },
|
||||
/** label与图标的距离(竖向排列),单位rpx(默认6) */
|
||||
marginBottom: { type: [String, Number] as PropType<string | number>, default: '6' },
|
||||
/** label与图标的距离,单位rpx,权重高于 margin */
|
||||
space: { type: [String, Number] as PropType<string | number>, default: '' },
|
||||
/** 图片的mode,参考uni-app image组件 */
|
||||
imgMode: { type: String as PropType<ImgMode>, default: 'widthFix' },
|
||||
/** 用于显示图片小图标时,图片的宽度,单位rpx */
|
||||
width: { type: [String, Number] as PropType<string | number>, default: '' },
|
||||
/** 用于显示图片小图标时,图片的高度,单位rpx */
|
||||
height: { type: [String, Number] as PropType<string | number>, default: '' },
|
||||
/** 用于解决某些情况下,让图标垂直居中的用途,单位rpx */
|
||||
top: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 是否为DecimalIcon */
|
||||
showDecimalIcon: { type: Boolean, default: false },
|
||||
/** 背景颜色,可接受主题色,仅Decimal时有效 */
|
||||
inactiveColor: { type: String, default: 'var(--u-divider-color)' },
|
||||
/** 显示的百分比,仅Decimal时有效 */
|
||||
percent: { type: [Number, String] as PropType<string | number>, default: '50' }
|
||||
};
|
||||
|
||||
/**
|
||||
* u-icon 组件 Props 类型
|
||||
*/
|
||||
export type IconProps = ExtractPropTypes<typeof IconProps>;
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<view
|
||||
:style="$u.toStyle(customStyle)"
|
||||
class="u-icon"
|
||||
@click="onClick"
|
||||
:class="['u-icon--' + labelPos, customClass]"
|
||||
>
|
||||
<image class="u-icon__img" v-if="isImg" :src="props.name" :mode="imgMode" :style="[imgStyle]" />
|
||||
<text
|
||||
v-else
|
||||
class="u-icon__icon"
|
||||
:class="iconClass"
|
||||
:style="$u.toStyle(iconStyle)"
|
||||
:hover-class="hoverClass"
|
||||
@touchstart="onTouchstart"
|
||||
>
|
||||
<text
|
||||
v-if="showDecimalIcon"
|
||||
:style="$u.toStyle(decimalIconStyle)"
|
||||
:class="decimalIconClass"
|
||||
:hover-class="hoverClass"
|
||||
class="u-icon__decimal"
|
||||
></text>
|
||||
</text>
|
||||
<text v-if="label !== ''" class="u-icon__label" :style="labelStyle">
|
||||
{{ label }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-icon',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import { IconProps } from './types';
|
||||
|
||||
/**
|
||||
* icon 图标
|
||||
* @description 基于字体的图标集,包含了大多数常见场景的图标。
|
||||
* @tutorial https://uviewpro.cn/zh/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字体颜色(默认var(--u-content-color))
|
||||
* @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 | Number} space label与图标的距离,单位rpx,权重高于 margin
|
||||
* @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
|
||||
* @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
|
||||
* @property {String} width 显示图片小图标时的宽度
|
||||
* @property {String} height 显示图片小图标时的高度
|
||||
* @property {String | Number} top 图标在垂直方向上的定位
|
||||
* @property {Boolean} show-decimal-icon 是否为DecimalIcon
|
||||
* @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
|
||||
* @property {String | Number} percent 显示的百分比,仅Decimal时有效
|
||||
* @event {Function} click 点击图标时触发
|
||||
* @event {Function} touchstart 图标触摸时触发
|
||||
* @example <u-icon name="photo" color="var(--u-type-primary)" size="28"></u-icon>
|
||||
*/
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', index: string | number): void;
|
||||
(e: 'touchstart', index: string | number): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps(IconProps);
|
||||
|
||||
/**
|
||||
* 计算图标的类名集合
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const iconClass = computed(() => {
|
||||
let classes: string[] | string = [];
|
||||
classes.push(props.customPrefix + '-' + props.name);
|
||||
// uView的自定义图标类名为u-iconfont
|
||||
if (props.customPrefix === 'uicon') {
|
||||
classes.push('u-iconfont');
|
||||
} else {
|
||||
classes.push(props.customPrefix);
|
||||
}
|
||||
// 主题色,通过类配置
|
||||
if (props.showDecimalIcon && props.inactiveColor && $u.config.type.includes(props.inactiveColor)) {
|
||||
classes.push('u-icon__icon--' + props.inactiveColor);
|
||||
} else if (props.color && $u.config.type.includes(props.color)) {
|
||||
classes.push('u-icon__icon--' + props.color);
|
||||
}
|
||||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
|
||||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
|
||||
// #ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
|
||||
classes = (classes as string[]).join(' ');
|
||||
// #endif
|
||||
return classes;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算图标的样式
|
||||
* @returns {CSSProperties}
|
||||
*/
|
||||
const iconStyle = computed(() => {
|
||||
const style: Record<string, any> = {
|
||||
fontSize: props.size === 'inherit' ? 'inherit' : $u.addUnit(props.size),
|
||||
fontWeight: props.bold ? 'bold' : 'normal',
|
||||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
|
||||
top: $u.addUnit(props.top)
|
||||
};
|
||||
// 非主题色值时,才当作颜色值
|
||||
if (props.showDecimalIcon && props.inactiveColor && !$u.config.type.includes(props.inactiveColor)) {
|
||||
style.color = props.inactiveColor;
|
||||
} else if (props.color && !$u.config.type.includes(props.color)) {
|
||||
style.color = props.color;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 判断传入的name属性是否为图片路径
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isImg = computed(() => {
|
||||
return props.name.indexOf('/') !== -1;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算图片图标的样式
|
||||
* @returns {any}
|
||||
*/
|
||||
const imgStyle = computed(() => {
|
||||
// 如果设置width和height属性,则优先使用,否则使用size属性
|
||||
const style: any = {
|
||||
width: props.width ? $u.addUnit(props.width) : $u.addUnit(props.size),
|
||||
height: props.height ? $u.addUnit(props.height) : $u.addUnit(props.size)
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算小数图标的样式,仅DecimalIcon时有效
|
||||
* @returns {CSSProperties}
|
||||
*/
|
||||
const decimalIconStyle = computed(() => {
|
||||
const style: any = {
|
||||
fontSize: props.size === 'inherit' ? 'inherit' : $u.addUnit(props.size),
|
||||
fontWeight: props.bold ? 'bold' : 'normal',
|
||||
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
|
||||
top: $u.addUnit(props.top),
|
||||
width: props.percent + '%'
|
||||
};
|
||||
// 非主题色值时,才当作颜色值
|
||||
if (props.color && !$u.config.type.includes(props.color)) {
|
||||
style.color = props.color;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算小数图标的类名,仅DecimalIcon时有效
|
||||
* @returns {string | string[]}
|
||||
*/
|
||||
const decimalIconClass = computed(() => {
|
||||
let classes: string[] | string = [];
|
||||
classes.push(props.customPrefix + '-' + props.name);
|
||||
// uView的自定义图标类名为u-iconfont
|
||||
if (props.customPrefix === 'uicon') {
|
||||
classes.push('u-iconfont');
|
||||
} else {
|
||||
classes.push(props.customPrefix);
|
||||
}
|
||||
if (props.color && $u.config.type.includes(props.color)) {
|
||||
classes.push('u-icon__icon--' + props.color);
|
||||
} else {
|
||||
classes.push('u-icon__icon--primary');
|
||||
}
|
||||
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
|
||||
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
|
||||
// #ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
|
||||
classes = (classes as string[]).join(' ');
|
||||
// #endif
|
||||
return classes;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算label的样式
|
||||
* @returns {any}
|
||||
*/
|
||||
const labelStyle = computed(() => {
|
||||
return {
|
||||
color: props.labelColor,
|
||||
fontSize: $u.addUnit(props.labelSize),
|
||||
marginLeft: props.labelPos === 'right' ? $u.addUnit(props.space || props.marginLeft) : 0,
|
||||
marginTop: props.labelPos === 'bottom' ? $u.addUnit(props.space || props.marginTop) : 0,
|
||||
marginRight: props.labelPos === 'left' ? $u.addUnit(props.space || props.marginRight) : 0,
|
||||
marginBottom: props.labelPos === 'top' ? $u.addUnit(props.space || props.marginBottom) : 0
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击图标时触发
|
||||
* @emits click(index)
|
||||
*/
|
||||
function onClick(event: any) {
|
||||
emit('click', props.index || event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标触摸时触发
|
||||
* @emits touchstart(index)
|
||||
*/
|
||||
function onTouchstart(event: any) {
|
||||
emit('touchstart', props.index || event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
@import '../../iconfont.css';
|
||||
|
||||
.u-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&--left {
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--right {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--top {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: relative;
|
||||
|
||||
&--primary {
|
||||
color: $u-type-primary;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: $u-type-success;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: $u-type-error;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: $u-type-warning;
|
||||
}
|
||||
|
||||
&--info {
|
||||
color: $u-type-info;
|
||||
}
|
||||
}
|
||||
|
||||
&__decimal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__img {
|
||||
height: auto;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
&__label {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue';
|
||||
import type { ImgMode, Shape } from '../../types/global';
|
||||
|
||||
// 定义 ImageProps 的类型
|
||||
export const ImageProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 图片地址 */
|
||||
src: { type: String, default: '' },
|
||||
/** 裁剪模式 */
|
||||
mode: { type: String as PropType<ImgMode>, default: 'aspectFill' },
|
||||
/** 宽度,单位任意,如果为数值,则为rpx单位(默认100%) */
|
||||
width: { type: [String, Number], default: '100%' },
|
||||
/** 高度,单位任意,如果为数值,则为rpx单位(默认 auto) */
|
||||
height: { type: [String, Number], default: 'auto' },
|
||||
/** 图片形状,circle-圆形,square-方形(默认square) */
|
||||
shape: { type: String as PropType<Shape>, default: 'square' },
|
||||
/** 圆角值,单位任意,如果为数值,则为rpx单位(默认 0) */
|
||||
borderRadius: { type: [String, Number], default: 0 },
|
||||
/** 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true) */
|
||||
lazyLoad: { type: Boolean, default: true },
|
||||
/** 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 true) */
|
||||
showMenuByLongpress: { type: Boolean, default: true },
|
||||
/** 加载中的图标,或者小图片(默认 photo) */
|
||||
loadingIcon: { type: String, default: 'photo' },
|
||||
/** 加载失败的图标,或者小图片(默认 error-circle) */
|
||||
errorIcon: { type: String, default: 'error-circle' },
|
||||
/** 是否显示加载中的图标或者自定义的slot(默认 true) */
|
||||
showLoading: { type: Boolean, default: true },
|
||||
/** 是否显示加载错误的图标或者自定义的slot(默认 true) */
|
||||
showError: { type: Boolean, default: true },
|
||||
/** 是否需要淡入效果(默认 true) */
|
||||
fade: { type: Boolean, default: true },
|
||||
/** 只支持网络资源,只对微信小程序有效(默认 false) */
|
||||
webp: { type: Boolean, default: false },
|
||||
/** 搭配fade参数的过渡时间,单位ms(默认 500) */
|
||||
duration: { type: [String, Number], default: 500 },
|
||||
/** 背景颜色,用于深色页面加载图片时,为了和背景色融合(默认 var(--u-bg-color)) */
|
||||
bgColor: { type: String, default: 'var(--u-bg-color)' },
|
||||
/** 使用插槽名称对象,用于自定义插槽,默认 undefined,当动态切换slot隐藏时,需要使用useSlots使用,兼容头条小程序 */
|
||||
useSlots: { type: Object as PropType<Record<string, boolean>>, default: undefined }
|
||||
};
|
||||
|
||||
// 将 ImageProps 转换为类型
|
||||
export type ImageProps = ExtractPropTypes<typeof ImageProps>;
|
||||
|
||||
// 暴露的组件实例方法
|
||||
export type ImageExpose = {
|
||||
changeStatus: (status: 'loading' | 'error' | 'normal') => void;
|
||||
};
|
||||
|
||||
// 组件实例类型
|
||||
export type ImageInstance = ComponentPublicInstance<ImageProps, ImageExpose>;
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-image"
|
||||
@tap="onClick"
|
||||
:style="$u.toStyle(wrapStyle, backgroundStyle, customStyle)"
|
||||
:class="customClass"
|
||||
>
|
||||
<image
|
||||
v-if="!isError"
|
||||
:src="src"
|
||||
:mode="mode"
|
||||
@error="onErrorHandler"
|
||||
@load="onLoadHandler"
|
||||
:lazy-load="lazyLoad"
|
||||
class="u-image__image"
|
||||
:show-menu-by-longpress="showMenuByLongpress"
|
||||
:style="{ borderRadius: shape === 'circle' ? '50%' : $u.addUnit(borderRadius) }"
|
||||
></image>
|
||||
<view
|
||||
v-if="showLoading && loading"
|
||||
class="u-image__loading"
|
||||
:style="{ borderRadius: shape === 'circle' ? '50%' : $u.addUnit(borderRadius), backgroundColor: bgColor }"
|
||||
>
|
||||
<slot v-if="hasSlot('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="hasSlot('error')" name="error" />
|
||||
<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-image',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, useSlots } from 'vue';
|
||||
import { type ImageExpose, ImageProps } from './types';
|
||||
import { $u } from '../..';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
/**
|
||||
* Image 图片
|
||||
* @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
|
||||
* @tutorial https://uviewpro.cn/zh/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 {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
|
||||
* @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
|
||||
* @property {String} bg-color 背景颜色,用于深色页面加载图片时,为了和背景色融合(默认 var(--u-bg-color))
|
||||
* @event {Function} click 点击图片时触发
|
||||
* @event {Function} error 图片加载失败时触发
|
||||
* @event {Function} load 图片加载成功时触发
|
||||
* @example <u-image width="100%" height="300rpx" :src="src"></u-image>
|
||||
*/
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void;
|
||||
(e: 'error', err: any): void;
|
||||
(e: 'load'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps(ImageProps);
|
||||
|
||||
// 图片是否加载错误,如果是,则显示错误占位图
|
||||
const isError = ref(false);
|
||||
// 初始化组件时,默认为加载中状态
|
||||
const loading = ref(true);
|
||||
// 不透明度,为了实现淡入淡出的效果
|
||||
const opacity = ref(1);
|
||||
// 过渡时间,因为props的值无法修改,故需要一个中间值
|
||||
const durationTime = ref(props.duration);
|
||||
// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
|
||||
const backgroundStyle = ref<Record<string, any>>({});
|
||||
|
||||
// 监听src变化,处理加载状态
|
||||
watch(
|
||||
() => props.src,
|
||||
n => {
|
||||
if (!n) {
|
||||
// 如果传入null或者'',或者false,或者undefined,标记为错误状态
|
||||
isError.value = true;
|
||||
loading.value = false;
|
||||
} else {
|
||||
isError.value = false;
|
||||
loading.value = true;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 计算图片外层包裹样式
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
const wrapStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
|
||||
style.width = $u.addUnit(props.width);
|
||||
style.height = $u.addUnit(props.height);
|
||||
// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
|
||||
style.borderRadius = props.shape === 'circle' ? '50%' : $u.addUnit(props.borderRadius);
|
||||
// 如果设置圆角,必须要有hidden,否则可能圆角无效
|
||||
style.overflow = Number(props.borderRadius) > 0 ? 'hidden' : 'visible';
|
||||
if (props.fade) {
|
||||
style.opacity = opacity.value;
|
||||
style.transition = `opacity ${Number(durationTime.value) / 1000}s ease-in-out`;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* 点击图片
|
||||
* @emits click
|
||||
*/
|
||||
function onClick() {
|
||||
emit('click');
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片加载失败
|
||||
* @param err 失败事件对象
|
||||
* @emits error
|
||||
*/
|
||||
function onErrorHandler(err: any) {
|
||||
loading.value = false;
|
||||
isError.value = true;
|
||||
emit('error', err);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片加载完成,标记loading结束
|
||||
* @emits load
|
||||
*/
|
||||
function onLoadHandler() {
|
||||
loading.value = false;
|
||||
isError.value = false;
|
||||
emit('load');
|
||||
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
|
||||
// 否则无需fade效果时,png图片依然能看到下方的背景色
|
||||
if (!props.fade) return removeBgColor();
|
||||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
|
||||
opacity.value = 0;
|
||||
// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
|
||||
// 到图片展示的过程中的淡入效果
|
||||
durationTime.value = 0;
|
||||
// 延时50ms,否则在浏览器H5,过渡效果无效
|
||||
setTimeout(() => {
|
||||
durationTime.value = props.duration;
|
||||
opacity.value = 1;
|
||||
setTimeout(() => {
|
||||
removeBgColor();
|
||||
}, Number(durationTime.value));
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除图片的背景色
|
||||
* 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
|
||||
*/
|
||||
function removeBgColor() {
|
||||
backgroundStyle.value = {
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
}
|
||||
|
||||
function changeStatus(status: 'loading' | 'error' | 'normal') {
|
||||
if (status === 'loading') {
|
||||
loading.value = true;
|
||||
isError.value = false;
|
||||
} else if (status === 'error') {
|
||||
loading.value = false;
|
||||
isError.value = true;
|
||||
} else {
|
||||
loading.value = false;
|
||||
isError.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给模板
|
||||
const $slots = useSlots();
|
||||
|
||||
function hasSlot(name: string) {
|
||||
return props.useSlots ? !!$slots[name] && props.useSlots[name] : !!$slots[name];
|
||||
}
|
||||
|
||||
defineExpose<ImageExpose>({
|
||||
changeStatus
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-image {
|
||||
position: relative;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__loading,
|
||||
&__error {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $u-bg-color;
|
||||
color: $u-tips-color;
|
||||
font-size: 46rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-index-anchor 组件 Props 类型定义
|
||||
* @description 索引锚点属性
|
||||
*/
|
||||
export const IndexAnchorProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否使用自定义内容的插槽 */
|
||||
useSlot: { type: Boolean, default: false },
|
||||
/** 索引字符,如果定义了use-slot,此参数自动失效 */
|
||||
index: { type: String, default: '' }
|
||||
};
|
||||
|
||||
export type IndexAnchorProps = ExtractPropTypes<typeof IndexAnchorProps>;
|
||||
@@ -0,0 +1,120 @@
|
||||
<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' : '', customClass]"
|
||||
:style="$u.toStyle(anchorStyle, customStyle)"
|
||||
>
|
||||
<slot v-if="useSlot" />
|
||||
<template v-else>
|
||||
<text>{{ index }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-index-anchor',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IndexAnchorProps } from './types';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { $u, useChildren } from '../..';
|
||||
|
||||
/**
|
||||
* indexAnchor 索引列表锚点
|
||||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
|
||||
* @tutorial https://uviewpro.cn/zh/components/indexList.html#indexanchor-props
|
||||
* @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
|
||||
* @property {String|Number} index 索引字符,如果定义了use-slot,此参数自动失效
|
||||
* @property {Object} customStyle 自定义样式,对象形式,如"{color: 'red'}"
|
||||
* @event {Function} default 锚点位置显示内容,默认为索引字符
|
||||
* @example <u-index-anchor :index="item" />
|
||||
*/
|
||||
|
||||
const props = defineProps(IndexAnchorProps);
|
||||
const { parentExposed } = useChildren('u-index-anchor', 'u-index-list');
|
||||
|
||||
// 响应式变量
|
||||
const active = ref(false);
|
||||
const wrapperStyle = ref<Record<string, any>>({});
|
||||
const anchorStyle = ref<Record<string, any>>({});
|
||||
const top = ref(0);
|
||||
const height = ref(0);
|
||||
|
||||
// 挂载时查找父组件并注册
|
||||
onMounted(() => {
|
||||
if (parentExposed) {
|
||||
parentExposed?.value?.updateData();
|
||||
}
|
||||
});
|
||||
|
||||
function setTop(val: any) {
|
||||
top.value = val;
|
||||
}
|
||||
|
||||
function setHeight(val: any) {
|
||||
height.value = val;
|
||||
}
|
||||
|
||||
function setActive(val: any) {
|
||||
active.value = val;
|
||||
}
|
||||
|
||||
function setAnchorStyle(val: any) {
|
||||
anchorStyle.value = val;
|
||||
}
|
||||
|
||||
function setWrapperStyle(val: any) {
|
||||
wrapperStyle.value = val;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
props,
|
||||
top,
|
||||
height,
|
||||
active,
|
||||
wrapperStyle,
|
||||
anchorStyle,
|
||||
setTop,
|
||||
setHeight,
|
||||
setActive,
|
||||
setAnchorStyle,
|
||||
setWrapperStyle
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-index-anchor {
|
||||
box-sizing: border-box;
|
||||
padding: 14rpx 24rpx;
|
||||
color: var(--u-content-color);
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.2;
|
||||
background-color: $u-bg-color;
|
||||
}
|
||||
|
||||
.u-index-anchor--active {
|
||||
right: 0;
|
||||
left: 0;
|
||||
color: $u-type-primary;
|
||||
background-color: var(--u-bg-white);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* u-index-list 组件 props 类型定义
|
||||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
|
||||
*/
|
||||
export const IndexListProps = {
|
||||
/** 是否开启锚点自动吸顶 */
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 锚点吸顶时的层级 */
|
||||
zIndex: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: ''
|
||||
},
|
||||
/** 当前滚动高度 */
|
||||
scrollTop: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0
|
||||
},
|
||||
/** 锚点自动吸顶时与顶部的距离 */
|
||||
offsetTop: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0
|
||||
},
|
||||
/** 索引字符列表 */
|
||||
indexList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: undefined // 组件内用 getIndexList() 兜底
|
||||
},
|
||||
/** 锚点和右边索引字符高亮颜色 */
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: () => getColor('primary')
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-index-list 组件 props 类型
|
||||
*/
|
||||
export type IndexListProps = ExtractPropTypes<typeof IndexListProps>;
|
||||
@@ -0,0 +1,346 @@
|
||||
<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: Number(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 lang="ts">
|
||||
export default {
|
||||
name: 'u-index-list',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue';
|
||||
import { $u, useParent } from '../..';
|
||||
import { IndexListProps } from './types';
|
||||
|
||||
/**
|
||||
* indexList 索引列表
|
||||
* @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
|
||||
* @tutorial https://uviewpro.cn/zh/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 锚点和右边索引字符高亮颜色(默认主题色primary)
|
||||
* @event {Function} select 选中右边索引字符时触发
|
||||
* @example <u-index-list :scrollTop="scrollTop"></u-index-list>
|
||||
*/
|
||||
const props = defineProps(IndexListProps);
|
||||
const emit = defineEmits(['select']);
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// 索引列表生成函数
|
||||
function getIndexList() {
|
||||
const indexList: string[] = [];
|
||||
const charCodeOfA = 'A'.charCodeAt(0);
|
||||
for (let i = 0; i < 26; i++) {
|
||||
indexList.push(String.fromCharCode(charCodeOfA + i));
|
||||
}
|
||||
return indexList;
|
||||
}
|
||||
|
||||
// 变量定义
|
||||
const activeAnchorIndex = ref(0);
|
||||
const showSidebar = ref(true);
|
||||
const touchmove = ref(false);
|
||||
const touchmoveIndex = ref(0);
|
||||
const sidebar = reactive<{ height: number; top: number }>({ height: 0, top: 0 });
|
||||
const scrollToAnchorIndex = ref<number | null>(null);
|
||||
const timer = ref<any>(null);
|
||||
const top = ref(0);
|
||||
const height = ref(0);
|
||||
const stickyOffsetTop = ref(0);
|
||||
|
||||
// 计算属性
|
||||
// 弹出toast的z-index值
|
||||
const alertZIndex = computed(() => $u.zIndex.toast).value;
|
||||
// indexList 响应式
|
||||
const indexList = computed(() => props?.indexList ?? getIndexList());
|
||||
const zIndex = computed(() => props.zIndex).value;
|
||||
const activeColor = computed(() => props.activeColor).value;
|
||||
|
||||
const { children, broadcast } = useParent('u-index-anchor');
|
||||
|
||||
// 兼容 H5/非H5 stickyOffsetTop
|
||||
onMounted(() => {
|
||||
const offsetTopNum = typeof props.offsetTop === 'string' ? Number(props.offsetTop) : props.offsetTop;
|
||||
// #ifdef H5
|
||||
stickyOffsetTop.value = offsetTopNum ? uni.upx2px(offsetTopNum) : 44;
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
stickyOffsetTop.value = offsetTopNum ? uni.upx2px(offsetTopNum) : 0;
|
||||
// #endif
|
||||
});
|
||||
|
||||
// 监听 scrollTop
|
||||
watch(
|
||||
() => props.scrollTop,
|
||||
() => {
|
||||
updateData();
|
||||
}
|
||||
);
|
||||
|
||||
function updateData() {
|
||||
if (timer.value) clearTimeout(timer.value);
|
||||
timer.value = setTimeout(() => {
|
||||
showSidebar.value = !!children.length;
|
||||
setRect().then(() => {
|
||||
onScroll();
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取各区域尺寸
|
||||
*/
|
||||
function setRect() {
|
||||
return Promise.all([setAnchorsRect(), setListRect(), setSiderbarRect()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锚点尺寸
|
||||
*/
|
||||
function setAnchorsRect() {
|
||||
return Promise.all(
|
||||
children.map((anchor, index) => {
|
||||
$u.getRect('.u-index-anchor-wrapper', anchor.getInstance()).then((rect: any) => {
|
||||
broadcast('setTop', rect.top, anchor.id);
|
||||
broadcast('setHeight', rect.height, anchor.id);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表尺寸
|
||||
*/
|
||||
function setListRect() {
|
||||
return $u.getRect('.u-index-bar', instance).then((rect: any) => {
|
||||
height.value = rect.height;
|
||||
top.value = rect.top + Number(props.scrollTop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取侧边栏尺寸
|
||||
*/
|
||||
function setSiderbarRect() {
|
||||
return $u.getRect('.u-index-bar__sidebar', instance).then((rect: any) => {
|
||||
sidebar.height = rect.height;
|
||||
sidebar.top = rect.top;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活锚点索引
|
||||
*/
|
||||
function getActiveAnchorIndex() {
|
||||
const sticky = props.sticky;
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const preAnchorHeight = i > 0 ? children[i - 1].getExposed().height.value : 0;
|
||||
const reachTop = sticky ? preAnchorHeight : 0;
|
||||
if (reachTop >= children[i].getExposed().top.value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动时处理锚点吸顶和样式
|
||||
*/
|
||||
function onScroll() {
|
||||
if (!children.length) return;
|
||||
const sticky = props.sticky;
|
||||
const scrollTopNum = Number(props.scrollTop);
|
||||
const active = getActiveAnchorIndex();
|
||||
activeAnchorIndex.value = active;
|
||||
if (sticky) {
|
||||
let isActiveAnchorSticky = false;
|
||||
if (active !== -1) {
|
||||
isActiveAnchorSticky = children[active].getExposed().top.value <= 0;
|
||||
}
|
||||
children.forEach((item, index) => {
|
||||
if (index === active) {
|
||||
let wrapperStyle: any = '';
|
||||
let anchorStyle: any = {
|
||||
color: `${activeColor}`
|
||||
};
|
||||
if (isActiveAnchorSticky) {
|
||||
wrapperStyle = { height: `${children[index].getExposed().height.value}px` };
|
||||
anchorStyle = {
|
||||
position: 'fixed',
|
||||
top: `${stickyOffsetTop.value}px`,
|
||||
zIndex: `${zIndex ? zIndex : $u.zIndex.indexListSticky}`,
|
||||
color: `${activeColor}`
|
||||
};
|
||||
}
|
||||
broadcast('setActive', active, item.id);
|
||||
broadcast('setAnchorStyle', anchorStyle, item.id);
|
||||
broadcast('setWrapperStyle', wrapperStyle, item.id);
|
||||
} else if (index === active - 1) {
|
||||
const currentAnchor = children[index];
|
||||
const currentOffsetTop = currentAnchor.getExposed().top.value;
|
||||
const targetOffsetTop =
|
||||
index === children.length - 1 ? top.value : children[index + 1].getExposed().top.value;
|
||||
const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
|
||||
const translateY = parentOffsetHeight - currentAnchor.getExposed().height.value;
|
||||
const anchorStyle = {
|
||||
position: 'relative',
|
||||
transform: `translate3d(0, ${translateY}px, 0)`,
|
||||
zIndex: `${zIndex ? zIndex : $u.zIndex.indexListSticky}`,
|
||||
color: `${activeColor}`
|
||||
};
|
||||
broadcast('setActive', active, currentAnchor.id);
|
||||
broadcast('setAnchorStyle', anchorStyle, currentAnchor.id);
|
||||
broadcast('setWrapperStyle', '', item.id);
|
||||
} else {
|
||||
broadcast('setActive', false, item.id);
|
||||
broadcast('setAnchorStyle', '', item.id);
|
||||
broadcast('setWrapperStyle', '', item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧边栏触摸移动
|
||||
*/
|
||||
function onTouchMove(event: TouchEvent) {
|
||||
touchmove.value = true;
|
||||
const sidebarLength = indexList.value.length;
|
||||
const touch = event.touches[0];
|
||||
const itemHeight = sidebar.height / sidebarLength;
|
||||
let clientY = touch.clientY;
|
||||
let index = Math.floor((clientY - sidebar.top) / itemHeight);
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
} else if (index > sidebarLength - 1) {
|
||||
index = sidebarLength - 1;
|
||||
}
|
||||
touchmoveIndex.value = index;
|
||||
scrollToAnchor(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧边栏触摸结束
|
||||
*/
|
||||
function onTouchStop() {
|
||||
touchmove.value = false;
|
||||
scrollToAnchorIndex.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到指定锚点
|
||||
*/
|
||||
function scrollToAnchor(index: number) {
|
||||
if (scrollToAnchorIndex.value === index) {
|
||||
return;
|
||||
}
|
||||
scrollToAnchorIndex.value = index;
|
||||
const anchor = children.find(item => item.getExposed().props.index === indexList.value[index]);
|
||||
if (anchor) {
|
||||
emit('select', anchor.getExposed().props.index);
|
||||
uni.pageScrollTo({
|
||||
duration: 0,
|
||||
scrollTop: anchor.getExposed().top.value + Number(props.scrollTop)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
updateData,
|
||||
setRect,
|
||||
onScroll,
|
||||
children
|
||||
});
|
||||
</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: var(--u-white-color);
|
||||
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>
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { InputAlign, InputConfirmType, InputType, SizeType } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-input 组件 props 类型定义
|
||||
* @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
|
||||
*/
|
||||
|
||||
export const InputProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 用于双向绑定输入框的值 */
|
||||
modelValue: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: ''
|
||||
},
|
||||
/** 输入框的类型,textarea,text,number */
|
||||
type: {
|
||||
type: String as PropType<InputType>,
|
||||
default: 'text'
|
||||
},
|
||||
/** 输入框文字的对齐方式(默认left) */
|
||||
inputAlign: {
|
||||
type: String as PropType<InputAlign>,
|
||||
default: 'left'
|
||||
},
|
||||
/** 输入框文字的大小(默认default),支持 small/default/large 预设值,也支持 16/16px/16rpx 等自定义值 */
|
||||
size: {
|
||||
type: [String, Number] as PropType<SizeType | string | number>,
|
||||
default: ''
|
||||
},
|
||||
/** placeholder显示值(默认 '请输入内容') */
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 是否禁用输入框(默认false) */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否只读,禁止输入但可点击,样式不变,可触发click事件(默认false) */
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 原生输入模式,PDA 模式可设置为 none 以隐藏软键盘 */
|
||||
inputMode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 输入框的最大可输入长度(默认140) */
|
||||
maxlength: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 140
|
||||
},
|
||||
// 是否显示统计字数,仅textarea有效
|
||||
count: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** placeholder的样式,字符串形式,如"color: red;"(默认 "color: var(--u-light-color);") */
|
||||
placeholderStyle: {
|
||||
type: String,
|
||||
default: 'color: var(--u-light-color);'
|
||||
},
|
||||
/** 设置键盘右下角按钮的文字,仅在type为text时生效(默认done) */
|
||||
confirmType: {
|
||||
type: String as PropType<InputConfirmType>,
|
||||
default: 'done'
|
||||
},
|
||||
/** 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true */
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否自动获得焦点(默认false) */
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 密码类型时,是否显示右侧的密码图标(默认true) */
|
||||
passwordIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** input|textarea是否显示边框(默认false) */
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 输入框的边框颜色(默认var(--u-border-color)) */
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: 'var(--u-border-color)'
|
||||
},
|
||||
/** 是否自动增高输入区域,type为textarea时有效(默认true) */
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态 */
|
||||
selectOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 高度,单位rpx */
|
||||
height: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: ''
|
||||
},
|
||||
/** 是否可清空(默认true) */
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 指定光标与键盘的距离,单位 px(默认0) */
|
||||
cursorSpacing: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0
|
||||
},
|
||||
/** 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1) */
|
||||
selectionStart: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: -1
|
||||
},
|
||||
/** 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1) */
|
||||
selectionEnd: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: -1
|
||||
},
|
||||
/** 是否自动去除两端的空格(默认true) */
|
||||
trim: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否显示键盘上方带有”完成“按钮那一栏(默认true) */
|
||||
showConfirmbar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 弹出键盘时是否自动调节高度,uni-app默认值是true */
|
||||
adjustPosition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 点击键盘右下角按钮时是否保持键盘不收起(默认false) */
|
||||
confirmHold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 输入框的验证状态,用于错误时,边框是否改为红色 */
|
||||
validateState: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-input 组件 props 类型
|
||||
*/
|
||||
export type InputProps = ExtractPropTypes<typeof InputProps>;
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-input"
|
||||
:class="[
|
||||
{
|
||||
'u-input--border': border,
|
||||
'u-input--error': validateState,
|
||||
'u-input--disabled': disabled
|
||||
},
|
||||
customClass
|
||||
]"
|
||||
:style="{
|
||||
padding: type === 'textarea' ? (border ? '20rpx' : '0') : `0 ${border ? 20 : 0}rpx`,
|
||||
borderColor: borderColor,
|
||||
textAlign: inputAlign
|
||||
}"
|
||||
@tap.stop="inputClick"
|
||||
>
|
||||
<textarea
|
||||
v-if="type == 'textarea'"
|
||||
class="u-input__input u-input__textarea"
|
||||
:style="getStyle"
|
||||
:value="inputValue"
|
||||
:placeholder="getPlaceholder"
|
||||
:placeholderStyle="getPlaceholderStyle"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:inputmode="props.inputMode"
|
||||
:maxlength="inputMaxlength"
|
||||
:fixed="fixed"
|
||||
:focus="focus"
|
||||
:autoHeight="autoHeight"
|
||||
:selection-end="Number(uSelectionEnd)"
|
||||
:selection-start="Number(uSelectionStart)"
|
||||
:cursor-spacing="getCursorSpacing"
|
||||
:show-confirm-bar="showConfirmbar"
|
||||
:adjust-position="adjustPosition"
|
||||
:confirm-hold="confirmHold"
|
||||
@input="handleInput"
|
||||
@blur="handleBlur"
|
||||
@focus="onFocus"
|
||||
@confirm="onConfirm"
|
||||
/>
|
||||
<!-- prettier-ignore -->
|
||||
<input
|
||||
v-else
|
||||
class="u-input__input"
|
||||
:type="((type == 'password' ? 'text' : type) as any)"
|
||||
:style="getStyle"
|
||||
:value="inputValue"
|
||||
:password="type == 'password' && !showPassword"
|
||||
:placeholder="getPlaceholder"
|
||||
:placeholderStyle="getPlaceholderStyle"
|
||||
:disabled="disabled || type === 'select'"
|
||||
:readonly="readonly"
|
||||
:inputmode="props.inputMode"
|
||||
:maxlength="inputMaxlength"
|
||||
:focus="focus"
|
||||
:confirmType="confirmType"
|
||||
:cursor-spacing="getCursorSpacing"
|
||||
:selection-end="Number(uSelectionEnd)"
|
||||
:selection-start="Number(uSelectionStart)"
|
||||
:show-confirm-bar="showConfirmbar"
|
||||
:adjust-position="adjustPosition"
|
||||
:confirm-hold="confirmHold"
|
||||
@focus="onFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@confirm="onConfirm"
|
||||
/>
|
||||
<!-- 透明遮罩,在readonly时显示,用于捕获点击事件(原生input设置disabled会阻止点击冒泡) -->
|
||||
<view v-if="readonly || type === 'select'" class="u-input__readonly-overlay" @tap.stop="inputClick"></view>
|
||||
<view class="u-input__right-icon u-flex">
|
||||
<view
|
||||
class="u-input__right-icon__clear u-input__right-icon__item"
|
||||
v-if="clearable && inputValue !== '' && !disabled"
|
||||
:style="{ marginLeft: $u.addUnit(iconSpacing) }"
|
||||
:class="{ 'u-hidden': !focused && type !== 'select' }"
|
||||
@click.stop="onClear"
|
||||
>
|
||||
<u-icon :size="currentIconSize" name="close-circle-fill" color="var(--u-light-color)" />
|
||||
</view>
|
||||
<view
|
||||
class="u-input__right-icon__clear u-input__right-icon__item"
|
||||
v-if="passwordIcon && type == 'password'"
|
||||
:style="{ marginLeft: $u.addUnit(iconSpacing) }"
|
||||
>
|
||||
<u-icon
|
||||
:size="currentIconSize"
|
||||
:name="!showPassword ? 'eye' : 'eye-fill'"
|
||||
color="var(--u-light-color)"
|
||||
@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
|
||||
}"
|
||||
:style="{ marginLeft: $u.addUnit(iconSpacing) }"
|
||||
>
|
||||
<u-icon name="arrow-down-fill" :size="selectIconSize" color="var(--u-light-color)"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<text
|
||||
class="u-input__count"
|
||||
:style="{
|
||||
'background-color': props.disabled ? 'transparent' : 'var(--u-bg-white)'
|
||||
}"
|
||||
v-if="props.type === 'textarea' && props.count"
|
||||
>
|
||||
{{ inputValue.length }}/{{ props.maxlength }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-input',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { $u, useChildren, useLocale } from '../..';
|
||||
import { InputProps } from './types';
|
||||
import type { SizeType } from '../../types/global';
|
||||
import uIcon from '../u-icon/u-icon.vue';
|
||||
|
||||
const props = defineProps(InputProps);
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'blur', 'focus', 'confirm', 'click', 'clear']);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const { emitToParent } = useChildren('u-input', 'u-form-item');
|
||||
const { parentExposed } = useChildren('u-input', 'u-form');
|
||||
|
||||
const validateState = ref(props.validateState); // 当前input的验证状态,用于错误时,边框是否改为红色
|
||||
const focused = ref(false); // 当前是否处于获得焦点的状态
|
||||
const showPassword = ref(false); // 是否预览密码
|
||||
const lastValue = ref(''); // 用于头条小程序,判断@input中,前后的值是否发生了变化
|
||||
|
||||
// 根据 size 定义不同的配置
|
||||
const sizeConfig = {
|
||||
small: {
|
||||
inputHeight: 56,
|
||||
textareaHeight: 70,
|
||||
fontSize: 24,
|
||||
iconSize: 28,
|
||||
iconSpacing: 6
|
||||
},
|
||||
default: {
|
||||
inputHeight: 70,
|
||||
textareaHeight: 100,
|
||||
fontSize: 28,
|
||||
iconSize: 32,
|
||||
iconSpacing: 10
|
||||
},
|
||||
large: {
|
||||
inputHeight: 84,
|
||||
textareaHeight: 130,
|
||||
fontSize: 32,
|
||||
iconSize: 36,
|
||||
iconSpacing: 14
|
||||
}
|
||||
};
|
||||
|
||||
// 获取实际使用的 size 值(优先级:props.size > u-form.size)
|
||||
const actualSize = computed(() => {
|
||||
// 优先使用 props 的 size 属性
|
||||
if (props.size !== '') {
|
||||
return String(props.size);
|
||||
}
|
||||
// 次优先:使用 u-form 的 size 属性(u-form 的 size 只支持预设值)
|
||||
if (parentExposed.value?.props?.size) {
|
||||
return String(parentExposed.value.props.size);
|
||||
}
|
||||
// 默认值
|
||||
return 'default';
|
||||
});
|
||||
|
||||
// 判断实际使用的 size 是否在预设配置中
|
||||
const isInSizeConfig = computed(() => actualSize.value in sizeConfig);
|
||||
|
||||
// 获取预设 size(用于查找 sizeConfig 配置,如图标大小、高度等)
|
||||
const presetSize = computed(() => {
|
||||
return (isInSizeConfig.value ? actualSize.value : 'default') as SizeType;
|
||||
});
|
||||
|
||||
// 获取当前尺寸配置
|
||||
const currentSizeConfig = computed(() => sizeConfig[presetSize.value]);
|
||||
|
||||
// 获取实际要使用的 font-size(如果是预设值使用配置值,否则作为自定义值处理)
|
||||
const actualFontSize = computed(() => {
|
||||
if (isInSizeConfig.value) {
|
||||
return $u.addUnit(currentSizeConfig.value.fontSize);
|
||||
}
|
||||
// 自定义size值,直接作为fontSize处理
|
||||
return $u.addUnit(actualSize.value);
|
||||
});
|
||||
|
||||
// 计算当前图标大小
|
||||
const currentIconSize = computed(() => currentSizeConfig.value.iconSize);
|
||||
|
||||
// 计算 select 图标大小(比常规图标稍小)
|
||||
const selectIconSize = computed(() => currentSizeConfig.value.iconSize - 4);
|
||||
|
||||
// 计算图标间距
|
||||
const iconSpacing = computed(() => currentSizeConfig.value.iconSpacing);
|
||||
|
||||
const inputValue = computed<string>(() => {
|
||||
if (props.modelValue === undefined || props.modelValue === null) return '';
|
||||
return String(props.modelValue);
|
||||
});
|
||||
|
||||
// 监听 value 变化
|
||||
watch(
|
||||
() => inputValue.value,
|
||||
(nVal, oVal) => {
|
||||
// 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
|
||||
if (nVal != oVal && props.type == 'select') handleInput({ detail: { value: nVal } });
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 validateState 变化
|
||||
watch(
|
||||
() => props.validateState,
|
||||
val => {
|
||||
validateState.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
// 计算属性
|
||||
const inputMaxlength = computed(() => Number(props.maxlength));
|
||||
|
||||
const getStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
// 如果没有自定义高度,就根据type为input还是textarea来分配一个默认的高度
|
||||
style.minHeight = props.height
|
||||
? $u.addUnit(props.height)
|
||||
: props.type === 'textarea'
|
||||
? $u.addUnit(currentSizeConfig.value.textareaHeight)
|
||||
: $u.addUnit(currentSizeConfig.value.inputHeight);
|
||||
// 根据 size 属性设置字体大小
|
||||
style.fontSize = actualFontSize.value;
|
||||
return $u.toStyle(style, props.customStyle);
|
||||
});
|
||||
|
||||
const getPlaceholderStyle = computed(() => {
|
||||
return $u.toStyle(
|
||||
{
|
||||
fontSize: actualFontSize.value
|
||||
},
|
||||
props.placeholderStyle
|
||||
);
|
||||
});
|
||||
|
||||
// 计算光标与键盘的距离
|
||||
const getCursorSpacing = computed(() => Number(props.cursorSpacing));
|
||||
// 光标起始位置
|
||||
const uSelectionStart = computed(() => String(props.selectionStart));
|
||||
// 光标结束位置
|
||||
const uSelectionEnd = computed(() => String(props.selectionEnd));
|
||||
// placeholder 国际化
|
||||
const getPlaceholder = computed(() => props.placeholder || t('uInput.placeholder'));
|
||||
|
||||
/**
|
||||
* change 事件
|
||||
* @param event
|
||||
*/
|
||||
function handleInput(event: any) {
|
||||
let value = event.detail.value;
|
||||
// 判断是否去除空格
|
||||
if (props.trim) value = $u.trim(value);
|
||||
emit('update:modelValue', value);
|
||||
emit('input', value);
|
||||
// 过一个生命周期再发送事件给u-form-item,否则this.$emit('update:modelValue')更新了父组件的值,但是微信小程序上
|
||||
// 尚未更新到u-form-item,导致获取的值为空,从而校验混论
|
||||
// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
|
||||
setTimeout(() => {
|
||||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
|
||||
// #ifdef MP-TOUTIAO
|
||||
if ($u.trim(value) == lastValue.value) return;
|
||||
lastValue.value = value;
|
||||
// #endif
|
||||
// 通过 emitter 派发事件
|
||||
emitToParent('onFormChange', value);
|
||||
}, 40);
|
||||
}
|
||||
|
||||
/**
|
||||
* blur 事件
|
||||
* @param event
|
||||
*/
|
||||
function handleBlur(event: any) {
|
||||
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
|
||||
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
|
||||
setTimeout(() => {
|
||||
focused.value = false;
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
let value = inputValue.value;
|
||||
emit('blur', value);
|
||||
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
|
||||
// #ifdef MP-TOUTIAO
|
||||
if ($u.trim(value) == lastValue.value) return;
|
||||
lastValue.value = value;
|
||||
// #endif
|
||||
emitToParent('onFormBlur', value);
|
||||
}, 40);
|
||||
}
|
||||
|
||||
function onFormItemError(status: boolean) {
|
||||
validateState.value = status;
|
||||
}
|
||||
|
||||
function onFocus(e: any) {
|
||||
focused.value = true;
|
||||
emit('focus', e.detail.value);
|
||||
}
|
||||
|
||||
function onConfirm(e: any) {
|
||||
emit('confirm', e.detail.value);
|
||||
}
|
||||
|
||||
function onClear(event: any) {
|
||||
try {
|
||||
event.stopPropagation();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
emit('clear');
|
||||
handleInput({ detail: { value: '' } });
|
||||
}
|
||||
|
||||
function inputClick() {
|
||||
if (props.disabled) return;
|
||||
emit('click');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onFormItemError
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-input {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@include vue-flex;
|
||||
align-items: center;
|
||||
|
||||
&__readonly-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__input {
|
||||
//height: 70rpx;
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
width: auto;
|
||||
font-size: 28rpx;
|
||||
color: $u-main-color;
|
||||
// padding: 10rpx 0;
|
||||
line-height: normal;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__count {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
color: $u-tips-color;
|
||||
background-color: var(--u-bg-white);
|
||||
padding: 1px 4px;
|
||||
border-radius: 10px;
|
||||
line-height: 16px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&--border {
|
||||
border-radius: 4px;
|
||||
border: 1px solid $u-border-color;
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: $u-type-error !important;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: $u-bg-gray-light;
|
||||
}
|
||||
|
||||
&--disabled &__input {
|
||||
background-color: transparent;
|
||||
color: $u-light-color;
|
||||
-webkit-text-fill-color: $u-light-color;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
&--disabled &__textarea {
|
||||
background-color: transparent;
|
||||
color: $u-light-color;
|
||||
-webkit-text-fill-color: $u-light-color;
|
||||
padding-left: 10rpx;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
&__right-icon {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
&--select {
|
||||
transition: transform 0.4s;
|
||||
|
||||
&--reverse {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-keyboard 组件 Props 类型定义
|
||||
* @description 自定义键盘面板属性
|
||||
*/
|
||||
export const KeyboardProps = {
|
||||
/** 键盘的类型,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 },
|
||||
/** 是否开启底部安全区适配 */
|
||||
safeAreaInsetBottom: { type: Boolean, default: false },
|
||||
/** 是否允许通过点击遮罩关闭键盘 */
|
||||
maskCloseAble: { type: Boolean, default: true },
|
||||
/** 通过双向绑定控制键盘的弹出与收起 */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 是否显示遮罩 */
|
||||
mask: { type: Boolean, default: true },
|
||||
/** z-index值 */
|
||||
zIndex: { type: [Number, String] as PropType<string | number>, default: '' },
|
||||
/** 取消按钮的文字 */
|
||||
cancelText: { type: String, default: '' },
|
||||
/** 确认按钮的文字 */
|
||||
confirmText: { type: String, default: '' }
|
||||
};
|
||||
|
||||
export type KeyboardProps = ExtractPropTypes<typeof KeyboardProps>;
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<u-popup
|
||||
: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 ? getCancelText : '' }}
|
||||
</view>
|
||||
<view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
|
||||
{{
|
||||
tips
|
||||
? tips
|
||||
: mode == 'number'
|
||||
? t('uKeyboard.number')
|
||||
: mode == 'card'
|
||||
? t('uKeyboard.idCard')
|
||||
: t('uKeyboard.plate')
|
||||
}}
|
||||
</view>
|
||||
<view
|
||||
v-if="confirmBtn"
|
||||
@tap="onConfirm"
|
||||
class="u-tooltip-item u-tooltips-submit"
|
||||
hover-class="u-tooltips-submit-hover"
|
||||
>
|
||||
{{ confirmBtn ? getConfirmText : '' }}
|
||||
</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 :random="random" @backspace="backspace" @change="change"></u-car-keyboard>
|
||||
</block>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-keyboard',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { KeyboardProps } from './types';
|
||||
import { computed } from 'vue';
|
||||
import { $u, useLocale } from '../..';
|
||||
import uCarKeyboard from '../u-car-keyboard/u-car-keyboard.vue';
|
||||
import uNumberKeyboard from '../u-number-keyboard/u-number-keyboard.vue';
|
||||
import uPopup from '../u-popup/u-popup.vue';
|
||||
|
||||
/**
|
||||
* keyboard 键盘
|
||||
* @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。
|
||||
* @tutorial https://uviewpro.cn/zh/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>
|
||||
*/
|
||||
|
||||
const props = defineProps(KeyboardProps);
|
||||
const emit = defineEmits(['change', 'update:modelValue', 'confirm', 'cancel', 'backspace']);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const uZIndex = computed(() => (props.zIndex ? props.zIndex : $u.zIndex.popup));
|
||||
|
||||
// 国际化计算属性
|
||||
const getCancelText = computed(() => props.cancelText || t('uKeyboard.cancelText'));
|
||||
const getConfirmText = computed(() => props.confirmText || t('uKeyboard.confirmText'));
|
||||
|
||||
const popupValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
/**
|
||||
* 按键被点击(不包含退格键被点击)
|
||||
*/
|
||||
function change(e: any) {
|
||||
emit('change', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 键盘关闭
|
||||
*/
|
||||
function popupClose() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入完成
|
||||
*/
|
||||
function onConfirm() {
|
||||
popupClose();
|
||||
emit('confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消输入
|
||||
*/
|
||||
function onCancel() {
|
||||
popupClose();
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
/**
|
||||
* 退格键
|
||||
*/
|
||||
function backspace() {
|
||||
emit('backspace');
|
||||
}
|
||||
</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: var(--u-main-color);
|
||||
flex: 0 0 30%;
|
||||
text-align: center;
|
||||
padding: 20rpx 10rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.u-tooltip-tips {
|
||||
flex-basis: 40%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.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: var(--u-tips-color);
|
||||
}
|
||||
|
||||
.u-tooltips-submit-hover {
|
||||
color: $u-type-success;
|
||||
}
|
||||
|
||||
.u-tooltip-cancel-hover {
|
||||
color: var(--u-main-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { Effect, ImgMode } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-lazy-load 组件 Props 类型定义
|
||||
* @description 懒加载图片组件属性
|
||||
*/
|
||||
export const LazyLoadProps = {
|
||||
/** 用户自定义值,在事件触发时回调,用以区分是哪个图片 */
|
||||
index: [Number, String],
|
||||
/** 要显示的图片 */
|
||||
image: { type: String, default: '' },
|
||||
/** 图片裁剪模式 */
|
||||
imgMode: { type: String as PropType<ImgMode>, default: 'widthFix' },
|
||||
/** 占位图片路径 */
|
||||
loadingImg: {
|
||||
type: String,
|
||||
default:
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1zbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1zbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+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 },
|
||||
/** 图片进入可见区域前多少像素时,单位rpx,开始加载图片 */
|
||||
threshold: { type: [String, Number] as PropType<string | number>, default: 100 },
|
||||
/** 淡入淡出动画的过渡时间 */
|
||||
duration: { type: [String, Number] as PropType<string | number>, default: 500 },
|
||||
/** 渡效果的速度曲线 */
|
||||
effect: { type: String as PropType<Effect>, default: 'ease-in-out' },
|
||||
/** 是否使用过渡效果 */
|
||||
isEffect: { type: Boolean, default: true },
|
||||
/** 圆角值 */
|
||||
borderRadius: { type: [String, Number] as PropType<string | number>, default: 0 },
|
||||
/** 图片高度,单位rpx */
|
||||
height: { type: [String, Number] as PropType<string | number>, default: 450 }
|
||||
};
|
||||
|
||||
export type LazyLoadProps = ExtractPropTypes<typeof LazyLoadProps>;
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<view
|
||||
class="u-wrap"
|
||||
:style="{
|
||||
opacity: Number(opacity),
|
||||
borderRadius: props.borderRadius + 'rpx',
|
||||
// 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
|
||||
transition: `opacity ${Number(time) / 1000}s ${props.effect}`
|
||||
}"
|
||||
:class="'u-lazy-item-' + elIndex"
|
||||
>
|
||||
<view :class="'u-lazy-item-' + elIndex">
|
||||
<image
|
||||
:style="{ borderRadius: props.borderRadius + 'rpx', height: imgHeight }"
|
||||
v-if="!isError"
|
||||
class="u-lazy-item"
|
||||
:src="isShow ? props.image : props.loadingImg"
|
||||
:mode="props.imgMode"
|
||||
@load="imgLoaded"
|
||||
@error="loadError"
|
||||
@tap="clickImg"
|
||||
></image>
|
||||
<image
|
||||
:style="{ borderRadius: props.borderRadius + 'rpx', height: imgHeight }"
|
||||
class="u-lazy-item error"
|
||||
v-else
|
||||
:src="props.errorImg"
|
||||
:mode="props.imgMode"
|
||||
@load="errorImgLoaded"
|
||||
@tap="clickImg"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-lazy-load',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LazyLoadProps } from './types';
|
||||
import { ref, computed, watch, onMounted, nextTick, getCurrentInstance } from 'vue';
|
||||
import { $u } from '../..';
|
||||
|
||||
/**
|
||||
* lazyLoad 懒加载
|
||||
* @description 懒加载使用的场景为:页面有很多图片时,APP会同时加载所有的图片,导致页面卡顿,各个位置的图片出现前后不一致等.
|
||||
* @tutorial https://uviewpro.cn/zh/components/lazyLoad.html
|
||||
* @property {String | Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
|
||||
* @property {String} image 图片路径
|
||||
* @property {String} loading-img 预加载时的占位图
|
||||
* @property {String} error-img 图片加载出错时的占位图
|
||||
* @property {String | Number} 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} img-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>
|
||||
*/
|
||||
|
||||
const emit = defineEmits(['click', 'load', 'error']);
|
||||
|
||||
const props = defineProps(LazyLoadProps);
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
// 懒加载相关响应式变量
|
||||
const isShow = ref(false);
|
||||
const opacity = ref(1);
|
||||
const time = ref(Number(props.duration));
|
||||
const loadStatus = ref(''); // 默认是懒加载中的状态
|
||||
const isError = ref(false); // 图片加载失败
|
||||
const elIndex = ref('');
|
||||
|
||||
// IntersectionObserver 实例
|
||||
let contentObserver: any = null;
|
||||
|
||||
/**
|
||||
* 将 threshold 从 rpx 转为 px
|
||||
*/
|
||||
const getThreshold = computed(() => {
|
||||
// 先取绝对值,因为 threshold 可能是负数,最后根据 props.threshold 是正数或者负数,重新还原
|
||||
const thresholdPx = uni.upx2px(Math.abs(Number(props.threshold)));
|
||||
return Number(props.threshold) < 0 ? -thresholdPx : thresholdPx;
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算图片的高度,可能为 auto,带 %,或者直接数值
|
||||
*/
|
||||
const imgHeight = computed(() => {
|
||||
return $u.addUnit(props.height);
|
||||
});
|
||||
|
||||
// 监听 isShow 变化,处理淡入淡出动画
|
||||
watch(isShow, nVal => {
|
||||
// 如果是不开启过渡效果,直接返回
|
||||
if (!props.isEffect) return;
|
||||
time.value = 0;
|
||||
// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果
|
||||
opacity.value = 0;
|
||||
// 延时30ms,否则在浏览器H5,过渡效果无效
|
||||
setTimeout(() => {
|
||||
time.value = Number(props.duration);
|
||||
opacity.value = 1;
|
||||
}, 30);
|
||||
});
|
||||
|
||||
// 监听图片路径变化,重置状态,图片路径发生变化时,需要重新标记一些变量,否则会一直卡在某一个状态,比如isError
|
||||
watch(
|
||||
() => props.image,
|
||||
n => {
|
||||
if (!n) {
|
||||
// 如果传入null或者'',或者undefined,标记为错误状态
|
||||
isError.value = true;
|
||||
} else {
|
||||
init();
|
||||
isError.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 用于重新初始化
|
||||
*/
|
||||
function init() {
|
||||
isError.value = false;
|
||||
loadStatus.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击图片触发的事件
|
||||
* loadlazy-还是懒加载中状态,loading-图片正在加载,loaded-图片加加载完成
|
||||
*/
|
||||
function clickImg() {
|
||||
let whichImg = '';
|
||||
// 如果isShow为false,意味着图片还没开始加载,点击的只能是最开始的占位图
|
||||
if (!isShow.value) whichImg = 'lazyImg';
|
||||
// 如果isError为true,意味着图片加载失败,这是只剩下错误的占位图,所以点击的只能是错误占位图
|
||||
// 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
|
||||
else if (isError.value) whichImg = 'errorImg';
|
||||
// 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
|
||||
else whichImg = 'realImg';
|
||||
// 只通知当前图片的index
|
||||
emit('click', props.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片加载完成事件
|
||||
* 可能是加载占位图时触发,也可能是加载真正的图片完成时触发,通过 isShow 区分
|
||||
*/
|
||||
function imgLoaded() {
|
||||
// 占位图加载完成
|
||||
if (loadStatus.value === '') {
|
||||
loadStatus.value = 'lazyed';
|
||||
} else if (loadStatus.value === 'lazyed') {
|
||||
// 真正的图片加载完成
|
||||
loadStatus.value = 'loaded';
|
||||
emit('load', props.index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误的图片加载完成
|
||||
*/
|
||||
function errorImgLoaded() {
|
||||
emit('error', props.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片加载失败
|
||||
*/
|
||||
function loadError() {
|
||||
isError.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开 IntersectionObserver
|
||||
*/
|
||||
function disconnectObserver(observerName: string) {
|
||||
if (observerName === 'contentObserver' && contentObserver) {
|
||||
contentObserver.disconnect();
|
||||
contentObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 生成唯一 id
|
||||
elIndex.value = $u.guid();
|
||||
nextTick(() => {
|
||||
uni.$once('uOnReachBottom', () => {
|
||||
if (!isShow.value) isShow.value = true;
|
||||
});
|
||||
// mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果
|
||||
setTimeout(() => {
|
||||
// 这里是组件内获取布局状态,不能用uni.createIntersectionObserver,而必须用this.createIntersectionObserver
|
||||
disconnectObserver('contentObserver');
|
||||
contentObserver = uni.createIntersectionObserver(instance?.proxy);
|
||||
// 要理解这里怎么计算的,请看这个:
|
||||
// https://blog.csdn.net/qq_25324335/article/details/83687695
|
||||
contentObserver
|
||||
.relativeToViewport({ bottom: getThreshold.value })
|
||||
.observe('.u-lazy-item-' + elIndex.value, (res: any) => {
|
||||
if (res.intersectionRatio > 0) {
|
||||
// 懒加载状态改变
|
||||
isShow.value = true;
|
||||
// 如果图片已经加载,去掉监听,减少性能的消耗
|
||||
disconnectObserver('contentObserver');
|
||||
}
|
||||
});
|
||||
}, 30);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-wrap {
|
||||
background-color: var(--u-divider-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.u-lazy-item {
|
||||
width: 100%;
|
||||
// 骗系统开启硬件加速
|
||||
transform: transition3d(0, 0, 0);
|
||||
// 防止图片加载“闪一下”
|
||||
will-change: transform;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
/**
|
||||
* u-line-progress 组件 props 类型定义
|
||||
* @description 线型进度条,支持多种样式
|
||||
*/
|
||||
export const LineProgressProps = {
|
||||
/** 两端是否显示半圆形 */
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 主题颜色 */
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 激活部分的颜色 */
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: 'var(--u-type-success)'
|
||||
},
|
||||
/** 进度条的底色 */
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: 'var(--u-divider-color)'
|
||||
},
|
||||
/** 进度百分比,数值 */
|
||||
percent: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 0
|
||||
},
|
||||
/** 是否在进度条内部显示百分比的值 */
|
||||
showPercent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 进度条的高度,单位rpx */
|
||||
height: {
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
default: 28
|
||||
},
|
||||
/** 是否显示条纹 */
|
||||
striped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 条纹是否显示活动状态 */
|
||||
stripedActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-line-progress 组件 props 类型
|
||||
*/
|
||||
export type LineProgressProps = ExtractPropTypes<typeof LineProgressProps>;
|
||||
@@ -0,0 +1,131 @@
|
||||
<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" />
|
||||
<text class="u-progress-text" v-else-if="showPercent">
|
||||
{{ percent + '%' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-line-progress',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue';
|
||||
import { LineProgressProps } from './types';
|
||||
|
||||
/**
|
||||
* lineProgress 线型进度条
|
||||
* @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
|
||||
* @tutorial https://uviewpro.cn/zh/components/lineProgress.html
|
||||
* @property {String Number} percent 进度条百分比值,为数值类型,0-100
|
||||
* @property {Boolean} round 进度条两端是否为半圆(默认true)
|
||||
* @property {String} type 如设置,active-color值将会失效
|
||||
* @property {String} active-color 进度条激活部分的颜色(默认var(--u-type-success))
|
||||
* @property {String} inactive-color 进度条的底色(默认var(--u-divider-color))
|
||||
* @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>
|
||||
*/
|
||||
const props = defineProps(LineProgressProps);
|
||||
const slots = useSlots();
|
||||
|
||||
/**
|
||||
* 进度条激活部分的样式
|
||||
*/
|
||||
const progressStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
style.width = props.percent + '%';
|
||||
if (!props.type && props.activeColor) style.backgroundColor = props.activeColor;
|
||||
return style;
|
||||
});
|
||||
</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: var(--u-white-color);
|
||||
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;
|
||||
}
|
||||
|
||||
.u-progress-text {
|
||||
color: var(--u-light-color);
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
@keyframes progress-stripes {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 39px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import type { LineBorderStyle, LineDirection } from '../../types/global';
|
||||
|
||||
/**
|
||||
* u-line 组件 props 类型定义
|
||||
* @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
|
||||
*/
|
||||
export const LineProps = {
|
||||
/** 线条的颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--u-border-color)'
|
||||
},
|
||||
/** 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等 */
|
||||
length: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
/** 线条方向,col-竖向,row-横向 */
|
||||
direction: {
|
||||
type: String as PropType<LineDirection>,
|
||||
default: 'row'
|
||||
},
|
||||
/** 是否显示细边框 */
|
||||
hairLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx" */
|
||||
margin: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
/** 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线 */
|
||||
borderStyle: {
|
||||
type: String as PropType<LineBorderStyle>,
|
||||
default: 'solid'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-line 组件 props 类型
|
||||
*/
|
||||
export type LineProps = ExtractPropTypes<typeof LineProps>;
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view class="u-line" :style="lineStyle"></view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-line',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $u } from '../..';
|
||||
import { LineProps } from './types';
|
||||
|
||||
/**
|
||||
* line 线条
|
||||
* @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
|
||||
* @tutorial https://uviewpro.cn/zh/components/line.html
|
||||
* @property {String} color 线条的颜色(默认var(--u-border-color))
|
||||
* @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>
|
||||
*/
|
||||
const props = defineProps(LineProps);
|
||||
|
||||
/**
|
||||
* 线条样式
|
||||
*/
|
||||
const lineStyle = computed(() => {
|
||||
let style: Record<string, string> = {};
|
||||
style.margin = props.margin;
|
||||
// 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了
|
||||
if (props.direction === 'row') {
|
||||
// 此处采用兼容分开写,兼容nvue的写法
|
||||
style.borderBottomWidth = '1px';
|
||||
style.borderBottomStyle = props.borderStyle;
|
||||
style.width = $u.addUnit(props.length);
|
||||
if (props.hairLine) style.transform = 'scaleY(0.5)';
|
||||
} else {
|
||||
// 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了
|
||||
style.borderLeftWidth = '1px';
|
||||
style.borderLeftStyle = props.borderStyle;
|
||||
style.height = $u.addUnit(props.length);
|
||||
if (props.hairLine) style.transform = 'scaleX(0.5)';
|
||||
}
|
||||
style.borderColor = props.color;
|
||||
return style;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-line {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { getColor } from '../../';
|
||||
|
||||
/**
|
||||
* u-link 组件 props 类型定义
|
||||
* @description 超链接组件,支持多端跳转/复制/提示
|
||||
*/
|
||||
export const LinkProps = {
|
||||
/** 文字颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: () => getColor('primary')
|
||||
},
|
||||
/** 字体大小,单位rpx */
|
||||
fontSize: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: 28
|
||||
},
|
||||
/** 是否显示下划线 */
|
||||
underLine: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 要跳转的链接 */
|
||||
href: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 小程序中复制到粘贴板的提示语 */
|
||||
mpTips: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 下划线颜色 */
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 是否默认点击跳转 */
|
||||
defaultClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-link 组件 props 类型
|
||||
*/
|
||||
export type LinkProps = ExtractPropTypes<typeof LinkProps>;
|
||||
@@ -0,0 +1,95 @@
|
||||
<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 lang="ts">
|
||||
export default {
|
||||
name: 'u-link',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { $u, useLocale } from '../..';
|
||||
import { LinkProps } from './types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
/**
|
||||
* link 超链接
|
||||
* @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。
|
||||
* @tutorial https://uviewpro.cn/zh/components/link.html
|
||||
* @property {String} color 文字颜色(默认var(--u-content-color))
|
||||
* @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="https://uviewpro.cn">蜀道难,难于上青天</u-link>
|
||||
*/
|
||||
const props = defineProps(LinkProps);
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// 国际化计算属性
|
||||
const getMpTips = computed(() => props.mpTips || t('uLink.mpTips'));
|
||||
|
||||
/**
|
||||
* 打开链接方法
|
||||
* 不同平台有不同表现形式
|
||||
*/
|
||||
function openLink() {
|
||||
if (!props.defaultClick) {
|
||||
emit('click', props.href);
|
||||
return;
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus !== 'undefined' && plus.runtime) {
|
||||
plus.runtime.openURL(props.href);
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open(props.href);
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
if (typeof uni !== 'undefined' && uni.setClipboardData) {
|
||||
uni.setClipboardData({
|
||||
data: props.href,
|
||||
success: () => {
|
||||
uni.hideToast();
|
||||
if (typeof $u !== 'undefined' && $u.toast) {
|
||||
$u.toast(getMpTips.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/style.components.scss';
|
||||
|
||||
.u-link {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
type LoadingMode = 'circle' | 'flower';
|
||||
|
||||
/**
|
||||
* u-loading-popup 组件 props 类型定义
|
||||
*/
|
||||
export const LoadingPopupProps = {
|
||||
/** 是否显示加载弹窗 */
|
||||
modelValue: { type: Boolean, default: false },
|
||||
/** 加载提示文本 */
|
||||
text: { type: String, default: '' },
|
||||
/** 方向,可选 'vertical' | 'horizontal' */
|
||||
direction: { type: String as PropType<'vertical' | 'horizontal'>, default: 'vertical' },
|
||||
/** 自动关闭的持续时间(ms),0为不自动关闭 */
|
||||
duration: { type: Number, default: 0 },
|
||||
/** 允许点击遮罩关闭的最短时间(ms) */
|
||||
cancelTime: { type: Number, default: 10000 },
|
||||
/** 动画的类型 */
|
||||
mode: { type: String as PropType<LoadingMode>, default: 'circle' },
|
||||
/** 动画的颜色 */
|
||||
color: { type: String, default: 'var(--u-light-color)' },
|
||||
/** 加载图标的大小,单位rpx */
|
||||
size: { type: [String, Number] as PropType<string | number>, default: '48' }
|
||||
};
|
||||
|
||||
export type LoadingPopupProps = ExtractPropTypes<typeof LoadingPopupProps>;
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<view @touchmove.stop.prevent>
|
||||
<view v-if="popupValue" class="u-loading-init" :class="[direction]">
|
||||
<u-loading :mode="mode" :color="color" :size="size" />
|
||||
<view v-if="currentText" class="u-loading-tips">
|
||||
{{ currentText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="u-loading-mask" :class="[popupValue ? 'u-mask-show' : '']" @click="onMaskClick" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-loading-popup',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { LoadingPopupProps } from './types';
|
||||
import uLoading from '../u-loading/u-loading.vue';
|
||||
|
||||
// 组件props类型
|
||||
const props = defineProps(LoadingPopupProps);
|
||||
const emit = defineEmits(['update:modelValue', 'cancel']);
|
||||
|
||||
// 自动关闭的持续时间定时器
|
||||
let durationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// 关闭按钮倒计时定时器
|
||||
let cancelTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// 记录弹窗显示的时间戳
|
||||
const now = ref(0);
|
||||
// 点击遮罩层是否可关闭(超时后)
|
||||
const canClose = ref(false);
|
||||
// 当前显示的text,优先级:loading参数 > props.text
|
||||
const currentText = ref(props.text);
|
||||
|
||||
const popupValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
watch(
|
||||
() => popupValue.value,
|
||||
val => {
|
||||
if (val) {
|
||||
doOpen(currentText.value);
|
||||
} else {
|
||||
doClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 响应 props 变更,自动刷新当前 text
|
||||
watch(
|
||||
() => props.text,
|
||||
val => {
|
||||
currentText.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
// 响应 cancelTime/duration 变化,重启定时器
|
||||
watch(
|
||||
() => [props.cancelTime, props.duration],
|
||||
() => {
|
||||
if (popupValue.value) {
|
||||
clearDurationTimer();
|
||||
startCancelTime();
|
||||
startDurationTime();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 启动超时关闭按钮计时
|
||||
*/
|
||||
function startCancelTime() {
|
||||
clearCancelTimer();
|
||||
canClose.value = false;
|
||||
if (props.cancelTime > 0) {
|
||||
cancelTimer = setTimeout(() => {
|
||||
canClose.value = true;
|
||||
}, props.cancelTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动持续时间计时
|
||||
*/
|
||||
function startDurationTime() {
|
||||
clearDurationTimer();
|
||||
if (props.duration) {
|
||||
durationTimer = setTimeout(() => {
|
||||
close();
|
||||
}, props.duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部显示逻辑,初始化所有状态
|
||||
*/
|
||||
function doOpen(text?: string) {
|
||||
canClose.value = false;
|
||||
clearDurationTimer();
|
||||
clearCancelTimer();
|
||||
now.value = Date.now();
|
||||
if (typeof text === 'string' && text !== '') {
|
||||
currentText.value = text;
|
||||
} else {
|
||||
currentText.value = props.text;
|
||||
}
|
||||
startCancelTime();
|
||||
startDurationTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部关闭逻辑,重置所有状态
|
||||
*/
|
||||
function doClose() {
|
||||
canClose.value = false;
|
||||
currentText.value = props.text;
|
||||
clearDurationTimer();
|
||||
clearCancelTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示弹窗
|
||||
* @param text 可选,优先显示的文案
|
||||
*/
|
||||
function open(text?: string) {
|
||||
currentText.value = text || props.text;
|
||||
popupValue.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏弹窗
|
||||
*/
|
||||
function close() {
|
||||
popupValue.value = false;
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
function clearDurationTimer() {
|
||||
if (durationTimer) {
|
||||
clearTimeout(durationTimer);
|
||||
durationTimer = null;
|
||||
}
|
||||
}
|
||||
function clearCancelTimer() {
|
||||
if (cancelTimer) {
|
||||
clearTimeout(cancelTimer);
|
||||
cancelTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 遮罩点击事件
|
||||
function onMaskClick() {
|
||||
// 只有显示关闭按钮时才允许关闭
|
||||
if (canClose.value) {
|
||||
emit('cancel');
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearDurationTimer();
|
||||
clearCancelTimer();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.u-loading-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 9999996;
|
||||
transition: all 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.u-mask-show {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.u-loading-init {
|
||||
position: relative;
|
||||
min-width: 200rpx;
|
||||
min-height: 200rpx;
|
||||
max-width: 500rpx;
|
||||
padding: 15rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
font-size: 30rpx;
|
||||
color: var(--u-white-color);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 7px;
|
||||
|
||||
.u-icon-close {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 2rpx;
|
||||
color: var(--u-white-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
width: 600rpx;
|
||||
max-width: 600rpx;
|
||||
min-height: 150rpx;
|
||||
padding-left: 40rpx;
|
||||
|
||||
.u-loading-tips {
|
||||
margin-top: 0;
|
||||
margin-left: 20rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.u-loading-tips {
|
||||
text-align: center;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
type LoadingMode = 'circle' | 'flower';
|
||||
|
||||
/**
|
||||
* u-loading 组件 props 类型定义
|
||||
* @description 加载动画,支持多种模式
|
||||
*/
|
||||
export const LoadingProps = {
|
||||
/** 自定义根节点样式 */
|
||||
customStyle: {
|
||||
type: [String, Object] as PropType<string | Record<string, any>>,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 自定义根节点样式类 */
|
||||
customClass: {
|
||||
type: String as unknown as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
/** 动画的类型 */
|
||||
mode: {
|
||||
type: String as PropType<LoadingMode>,
|
||||
default: 'circle'
|
||||
},
|
||||
/** 动画的颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--u-light-color)'
|
||||
},
|
||||
/** 加载图标的大小,单位rpx */
|
||||
size: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
default: '34'
|
||||
},
|
||||
/** 是否显示动画 */
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* u-loading 组件 props 类型
|
||||
*/
|
||||
export type LoadingProps = ExtractPropTypes<typeof LoadingProps>;
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
class="u-loading"
|
||||
:class="[mode === 'circle' ? 'u-loading-circle' : 'u-loading-flower', customClass]"
|
||||
:style="$u.toStyle(cricleStyle, customStyle)"
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'u-loading',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
// #ifndef MP-TOUTIAO
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { LoadingProps } from './types';
|
||||
import { $u } from '../../';
|
||||
|
||||
/**
|
||||
* loading 加载动画
|
||||
* @description 此组件为一个小动画,目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。
|
||||
* @tutorial https://uviewpro.cn/zh/components/loading.html
|
||||
* @property {String} mode 模式选择,见官网说明(默认circle)
|
||||
* @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认var(--u-light-color))
|
||||
* @property {String|Number} size 加载图标的大小,单位rpx(默认34)
|
||||
* @property {Boolean} show 是否显示动画(默认true)
|
||||
* @example <u-loading mode="circle"></u-loading>
|
||||
*/
|
||||
const props = defineProps(LoadingProps);
|
||||
|
||||
/**
|
||||
* 加载中圆圈动画的样式
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
const cricleStyle = computed(() => {
|
||||
let style: Record<string, any> = {};
|
||||
style.width = props.size + 'rpx';
|
||||
style.height = props.size + 'rpx';
|
||||
// 只对圆圈模式生效
|
||||
if (props.mode === 'circle') {
|
||||
style.borderColor = `var(--u-divider-color) var(--u-divider-color) var(--u-divider-color) ${props.color ? props.color : 'var(--u-light-color)'}`;
|
||||
}
|
||||
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: var(--u-divider-color) var(--u-divider-color) var(--u-divider-color) var(--u-tips-color);
|
||||
animation: u-circle 1s linear infinite;
|
||||
}
|
||||
|
||||
.u-loading-flower {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
-webkit-animation: u-flower 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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes u-circle {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user