1
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick, onUpdated } from "vue"
|
||||
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, setTextColorLightByDark } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { toFillMarginAr, checkIsCssUnit, getUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
import { XACTION_MENU_ITEM_INFO } from "../../interface.uts"
|
||||
|
||||
/**
|
||||
* 内部使用:动作菜单项目类型
|
||||
*/
|
||||
type XACTION_MENU_ITEM_INFO_PRIVATE = {
|
||||
iconSize: string,
|
||||
fontSize: string,
|
||||
iconColor: string,
|
||||
fontColor: string,
|
||||
icon: string,
|
||||
disabled: boolean,
|
||||
id: string,
|
||||
text: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 动作菜单面板 xActionMenu
|
||||
* @page /pages/index/action-menu
|
||||
* @category 反馈组件
|
||||
* @description 从底部弹出来的操作菜单。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xActionMenu" })
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
const proxy = getCurrentInstance()?.proxy ?? null;
|
||||
defineSlots<{
|
||||
trigger(props: { show: boolean }): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show',
|
||||
/**
|
||||
* 项目被点击时触发
|
||||
* @param {Number} index - 项目索引
|
||||
*/
|
||||
'item-click'
|
||||
])
|
||||
|
||||
export type xActionMenuPropsType = {
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: string,
|
||||
/**
|
||||
* 标题,请选择
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 是否显示标题
|
||||
*/
|
||||
showTitle: boolean,
|
||||
/**
|
||||
* 是否显示关闭
|
||||
*/
|
||||
showClose: boolean,
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: boolean,
|
||||
/**
|
||||
* 选项点击时,是否允许关闭弹层。
|
||||
*/
|
||||
cellClickClose: boolean,
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: boolean,
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showCancel: boolean,
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: number,
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。注意是取drawer的圆角,统一弹层的圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: string,
|
||||
/**
|
||||
* 菜单条目
|
||||
*/
|
||||
list: XACTION_MENU_ITEM_INFO[],
|
||||
/**
|
||||
* 弹层的层,两边是否留空白间隙,包括底部。
|
||||
*/
|
||||
space: boolean,
|
||||
/**
|
||||
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
|
||||
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
|
||||
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
|
||||
* 此值仅在ios下生效。
|
||||
*/
|
||||
watiDuration: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xActionMenuPropsType>(), {
|
||||
customStyle: "",
|
||||
title: "",
|
||||
showTitle: true,
|
||||
showClose: false,
|
||||
overlayClick: true,
|
||||
cellClickClose: true,
|
||||
show: false,
|
||||
showCancel: true,
|
||||
duration: 350,
|
||||
round: "",
|
||||
maxHeight: "",
|
||||
list: [] as XACTION_MENU_ITEM_INFO[],
|
||||
space: true,
|
||||
watiDuration: 120
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const showOverflayRef = ref<UniElement | null>(null)
|
||||
const xActionMenuWrapContentRef = ref<UniElement | null>(null)
|
||||
const actioning = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("xActionMenu" + getUid())
|
||||
const wrapId = ref("xActionMenuWrap" + getUid())
|
||||
const first = ref(true)
|
||||
const tid = ref(0)
|
||||
const windtop = ref(0)
|
||||
const nowClickIndex = ref(-1)
|
||||
const clienEventType = ref('')
|
||||
// #ifdef H5
|
||||
const teleportElH5 = ref("uni-app")
|
||||
const teleportTarget = ref<string | null>(null)
|
||||
const getTeleportTarget = () => {
|
||||
try {
|
||||
if(status.value == ''||status.value=='close') return 'uni-app'
|
||||
// 优先尝试 uni-page
|
||||
if (document.querySelector('uni-page')) {
|
||||
return 'uni-page'
|
||||
}
|
||||
// 优先尝试 uni-app
|
||||
if (document.querySelector('uni-app')) {
|
||||
return 'uni-app'
|
||||
}
|
||||
// 备用方案:尝试 app
|
||||
if (document.querySelector('#app')) {
|
||||
return '#app'
|
||||
}
|
||||
// 最后备用:body
|
||||
return 'body'
|
||||
} catch (error) {
|
||||
console.warn('Failed to get teleport target:', error)
|
||||
return 'body'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查teleport目标是否可用
|
||||
const isTeleportTargetValid = (target: string) => {
|
||||
try {
|
||||
return !!document.querySelector(target)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 计算属性
|
||||
|
||||
function getFontSize(size: string): string {
|
||||
let fontSize = checkIsCssUnit(size, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
}
|
||||
|
||||
const _customStyle = computed((): string => props.customStyle)
|
||||
const _show = computed((): boolean => props.show)
|
||||
const _showClose = computed((): boolean => props.showClose)
|
||||
const _duration = computed((): number => props.duration)
|
||||
const _cellClickClose = computed((): boolean => props.cellClickClose)
|
||||
const _showTitle = computed((): boolean => props.showTitle)
|
||||
const _space = computed((): boolean => props.space)
|
||||
const _showCancel = computed((): boolean => props.showCancel)
|
||||
|
||||
const _title = computed((): string => {
|
||||
if (props.title == '') return i18n.t("tmui4x.actionMenu.title")
|
||||
return props.title
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
let round = props.round;
|
||||
if (round == "") {
|
||||
round = xConfig.drawerRadius
|
||||
}
|
||||
let radius = checkIsCssUnit(round, xConfig.unit);
|
||||
if (!_space.value) {
|
||||
return `${radius} ${radius} 0px 0px`
|
||||
}
|
||||
return `${radius}`
|
||||
})
|
||||
|
||||
const _maxHeight = computed((): string => {
|
||||
if (props.maxHeight == "") return "80%"
|
||||
return checkIsCssUnit(props.maxHeight, xConfig.unit);
|
||||
})
|
||||
|
||||
const _animationFun = computed((): string => xConfig.animationFun)
|
||||
|
||||
const __height = computed((): string => {
|
||||
let h = '100%';
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtop.value}px)`
|
||||
// #endif
|
||||
return h;
|
||||
})
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
return xConfig.dark == 'dark' ? xConfig.sheetDarkColor : "#f5f5f5"
|
||||
})
|
||||
|
||||
const _cellBgColor = computed((): string => {
|
||||
return xConfig.dark == 'dark' ? xConfig.inputDarkColor : "#ffffff"
|
||||
})
|
||||
|
||||
const _list = computed((): XACTION_MENU_ITEM_INFO_PRIVATE[] => {
|
||||
return props.list.map((el: XACTION_MENU_ITEM_INFO): XACTION_MENU_ITEM_INFO_PRIVATE => {
|
||||
let psd = el.disabled == null ? false : (el.disabled!)
|
||||
let defaultSize = getFontSize('16');
|
||||
let temiconcolor = el.iconColor != null ? getDefaultColor(el.iconColor as string) : '#333333';
|
||||
let temfontcolor = el.iconColor != null ? getDefaultColor(el.iconColor as string) : '#333333';
|
||||
let iconColor = xConfig.dark == 'dark' ? setTextColorLightByDark(temiconcolor) : temiconcolor;
|
||||
let fontColor = xConfig.dark == 'dark' ? setTextColorLightByDark(temfontcolor) : temfontcolor;
|
||||
return {
|
||||
iconSize: el.iconSize != null ? (getFontSize(el.iconSize!)) : defaultSize,
|
||||
icon: el.icon != null ? (el.icon!) : '',
|
||||
fontSize: el.fontSize != null ? (getFontSize(el.fontSize!)) : defaultSize,
|
||||
iconColor: iconColor,
|
||||
fontColor: fontColor,
|
||||
disabled: psd,
|
||||
id: el.id,
|
||||
text: el.text,
|
||||
} as XACTION_MENU_ITEM_INFO_PRIVATE
|
||||
})
|
||||
})
|
||||
|
||||
// 方法
|
||||
|
||||
function setStyleAni() {
|
||||
let watiDuration = 60;
|
||||
// #ifdef APP-IOS
|
||||
watiDuration = props.watiDuration
|
||||
// #endif
|
||||
try {
|
||||
if (status.value == 'open') {
|
||||
showOverflay.value = true;
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function () {
|
||||
if (showOverflayRef.value == null || xActionMenuWrapContentRef.value == null) return;
|
||||
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
xActionMenuWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
showOverflayRef.value!.style.setProperty('opacity', '1')
|
||||
xActionMenuWrapContentRef.value!.style.setProperty('transform', `translate(0%,${_space.value ? -24 : 0}rpx)`)
|
||||
}, watiDuration);
|
||||
} else if (status.value == 'close') {
|
||||
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
xActionMenuWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
|
||||
showOverflayRef.value!.style.setProperty('opacity', '0')
|
||||
xActionMenuWrapContentRef.value!.style.setProperty('transform', `translate(0%,100%)`)
|
||||
}
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
console.error("xActionMenu Error:",e)
|
||||
}
|
||||
}
|
||||
|
||||
function closeAlert() {
|
||||
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
|
||||
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
|
||||
// #ifdef APP-IOS
|
||||
actioning.value = false;
|
||||
// #endif
|
||||
|
||||
if (actioning.value || status.value == 'close') return;
|
||||
actioning.value = true;
|
||||
status.value = 'close'
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emits('beforeClose')
|
||||
setStyleAni();
|
||||
}
|
||||
|
||||
function showAlert() {
|
||||
if (actioning.value) return;
|
||||
if (status.value == 'open') return;
|
||||
|
||||
showOverflay.value = true;
|
||||
actioning.value = true;
|
||||
status.value = 'open'
|
||||
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emits('beforeOpen')
|
||||
setStyleAni();
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
actioning.value = false;
|
||||
if (status.value == 'close') {
|
||||
showOverflay.value = false;
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
emits('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
emits('update:show', false)
|
||||
if (clienEventType.value == 'click') {
|
||||
/**
|
||||
* 项目被点击。由于安卓端动画关闭前触发,会触发view渲染异常.导致动画失败.从而造成,下个页面返回时,上页无法执行结束.
|
||||
* @param index {number} 当前项目索引。
|
||||
*/
|
||||
emits('item-click', nowClickIndex.value)
|
||||
clienEventType.value = ''
|
||||
}
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
emits('open')
|
||||
nowClickIndex.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
function overTouch(evt: UniTouchEvent) {
|
||||
// #ifdef WEB
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
evt.stopPropagation()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
closeAlert();
|
||||
emits('cancel')
|
||||
}
|
||||
|
||||
function itemClick(index: number, disabled: boolean) {
|
||||
if (actioning.value || disabled || status.value == 'close') return;
|
||||
nowClickIndex.value = index;
|
||||
if (!_cellClickClose.value) return;
|
||||
clienEventType.value = 'click'
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
function overflayMoveTouch(evt: TouchEvent) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
function onClickOverflowy(evt: Event) {
|
||||
evt.stopPropagation()
|
||||
emits("click")
|
||||
if (!props.overlayClick) return;
|
||||
onCancel();
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
showAlert();
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch((): boolean => props.show, (newval: boolean) => {
|
||||
if (newval) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifndef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight;
|
||||
windtop.value = sys.windowTop;
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight + 44;
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
})
|
||||
// #endif
|
||||
|
||||
if (_show.value) {
|
||||
tid.value = setTimeout(() => {
|
||||
showAlert();
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听页面变化,重新获取teleport目标
|
||||
onUpdated(() => {
|
||||
// #ifdef H5
|
||||
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** 打开 **/
|
||||
open: () => showAlert(),
|
||||
/** 关闭 **/
|
||||
close: () => closeAlert()
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer" >
|
||||
<!--
|
||||
@slot 标签触发显示遮罩,免于使用变量控制
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="_show"></slot>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
|
||||
<view
|
||||
@click="onClickOverflowy"
|
||||
@touchmove="overTouch"
|
||||
v-if="showOverflay"
|
||||
:id="id"
|
||||
ref="showOverflayRef"
|
||||
class="xActionMenuWrap xActionMenuWrap_bottom"
|
||||
:style="[{width:'100%',top:windtop+'px',height:__height,'transition-timing-function':_animationFun},_customStyle]">
|
||||
<!-- @touchmove="overflayMoveTouch" -->
|
||||
<view
|
||||
@transitionend="onEnd"
|
||||
@click.stop=""
|
||||
:class="[_space?'onOpenSpace':'']"
|
||||
class="xActionMenuWrapContent xActionMenuWrapContent_bottom"
|
||||
ref="xActionMenuWrapContentRef"
|
||||
:id="wrapId" :style="{
|
||||
borderRadius:_round,
|
||||
'transition-timing-function':_animationFun,
|
||||
backgroundColor:_bgColor
|
||||
}">
|
||||
<x-icon v-if="_showClose" class="xActionMenuXclose" @click="closeAlert" color="#dcdcdc" font-size="21"
|
||||
name="close-circle-fill"></x-icon>
|
||||
<view>
|
||||
<view v-if="_showTitle" class="xActionMenuTitleBox" :style="{backgroundColor:_cellBgColor}">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="_show">
|
||||
<text class="xActionMenutitleBox">{{_title}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xActionMenuWrapContentBox" :style="{maxHeight:_maxHeight!=''?_maxHeight:'100%'}">
|
||||
|
||||
<scroll-view :style="
|
||||
{
|
||||
flex:1,backgroundColor:_bgColor
|
||||
}
|
||||
" :scroll-y="true" :rebound="false">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="default" >
|
||||
<view v-for="(item,index) in _list" :key="index" @click="itemClick(index,item.disabled)"
|
||||
:style="{backgroundColor:_cellBgColor,opacity:item.disabled?'0.5':'1'}"
|
||||
class="xActionMenuItem" :hover-class="item.disabled?'':'xActionMenuHover'"
|
||||
hover-stay-time="100" hover-start-time="10">
|
||||
<x-icon v-if="item.icon!=''" :color="item.iconColor" :name="item.icon"
|
||||
:style="{'margin-right': '10rpx'}"></x-icon>
|
||||
<text class="xActionText" :style="{fontSize:item.fontSize,color:item.fontColor}">{{item.text}}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view @click="onCancel" hover-class="xActionMenuHover" :style="{backgroundColor:_cellBgColor}" hover-stay-time="100" hover-start-time="10"
|
||||
v-if="_showCancel" class="xActionMenuFooter">
|
||||
<x-text font-size='16' class="xActionMenuFooterText">
|
||||
<!-- 取消 -->
|
||||
{{i18n.t("tmui4x.cancel")}}
|
||||
</x-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xActionText{
|
||||
lines: 1;
|
||||
|
||||
}
|
||||
.xActionMenuWrapContentBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.xActionMenuItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.xActionMenuFooter {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
/* background-color: #ffffff; */
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xActionMenuHover {
|
||||
/* background-color: #f7f7f7; */
|
||||
}
|
||||
|
||||
.xActionMenuFooterText {
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xActionMenuXclose {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 6px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.xActionMenuTitleBox {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.xActionMenutitleBox {
|
||||
max-width: 175px;
|
||||
overflow: hidden;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.xActionMenuWrap_bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.xActionMenuWrapContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform;
|
||||
/* background-color: #f5f5f5; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 500px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.onOpenSpace {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.xActionMenuWrapContent_bottom {
|
||||
transform: translate(0%, 100%);
|
||||
/* #ifndef APP */
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
|
||||
.xActionMenuWrap {
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
z-index: 1100;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
/* #ifndef APP-HARMONY */
|
||||
transition-duration: 350ms;
|
||||
/* #endif */
|
||||
/* #ifdef APP-HARMONY */
|
||||
transition-duration: 0ms;
|
||||
/* #endif */
|
||||
transition-property: opacity;
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,589 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick, onUpdated } from "vue"
|
||||
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 底部对话框 xActionModal
|
||||
* @page /pages/index/action-modal
|
||||
* @category 反馈组件
|
||||
* @description 样式与darawer不一样,风格更为圆润精致,适于提醒框,阅读对话框等场景。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xActionModal" })
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
const proxy = getCurrentInstance()?.proxy ?? null;
|
||||
defineSlots<{
|
||||
trigger(props: { show: boolean }): any
|
||||
title(props: { show: boolean }): any
|
||||
default(props: { show: boolean }): any
|
||||
footer(props: { show: boolean }): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 底部按钮被点击时触发
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show'
|
||||
])
|
||||
|
||||
export type xActionModalPropsType = {
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: string,
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showTitle: boolean,
|
||||
/**
|
||||
* 是否显示关闭
|
||||
*/
|
||||
showClose: boolean,
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: boolean,
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: boolean,
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showConfirm: boolean,
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: number,
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。注意是取drawer的圆角,统一弹层的圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: string,
|
||||
/**
|
||||
* 弹层的背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 弹层的暗黑背景,如果为空取sheetDarkColor
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 空值取全局主题值。
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
*/
|
||||
btnText: string,
|
||||
/**
|
||||
* 空值最自动计算文本色。
|
||||
*/
|
||||
btnFontColor: string,
|
||||
/**
|
||||
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
|
||||
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
|
||||
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
|
||||
* 此值仅在ios下生效。
|
||||
*/
|
||||
watiDuration: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xActionModalPropsType>(), {
|
||||
customStyle: "",
|
||||
title: "",
|
||||
showTitle: true,
|
||||
showClose: false,
|
||||
overlayClick: true,
|
||||
show: false,
|
||||
showConfirm: true,
|
||||
duration: 350,
|
||||
round: "",
|
||||
maxHeight: "",
|
||||
bgColor: "white",
|
||||
darkBgColor: "",
|
||||
btnColor: "",
|
||||
btnText: "",
|
||||
btnFontColor: "",
|
||||
watiDuration: 120
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const showOverflayRef = ref<UniElement | null>(null)
|
||||
const xActionModalWrapContentRef = ref<UniElement | null>(null)
|
||||
const actioning = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("xActionModal" + getUid())
|
||||
const wrapId = ref("xActionModalWrap" + getUid())
|
||||
const tid = ref(0)
|
||||
const windtop = ref(0)
|
||||
const pageOninit = ref(false)
|
||||
const isOpenedDefault = ref(false)
|
||||
// #ifdef H5
|
||||
const teleportElH5 = ref("uni-app")
|
||||
const teleportTarget = ref<string | null>(null)
|
||||
const getTeleportTarget = () => {
|
||||
try {
|
||||
if(status.value == ''||status.value=='close') return 'uni-app'
|
||||
// 优先尝试 uni-page
|
||||
if (document.querySelector('uni-page')) {
|
||||
return 'uni-page'
|
||||
}
|
||||
// 优先尝试 uni-app
|
||||
if (document.querySelector('uni-app')) {
|
||||
return 'uni-app'
|
||||
}
|
||||
// 备用方案:尝试 app
|
||||
if (document.querySelector('#app')) {
|
||||
return '#app'
|
||||
}
|
||||
// 最后备用:body
|
||||
return 'body'
|
||||
} catch (error) {
|
||||
console.warn('Failed to get teleport target:', error)
|
||||
return 'body'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查teleport目标是否可用
|
||||
const isTeleportTargetValid = (target: string) => {
|
||||
try {
|
||||
return !!document.querySelector(target)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 计算属性
|
||||
const _customStyle = computed((): string => props.customStyle)
|
||||
const _show = computed((): boolean => props.show)
|
||||
const _showClose = computed((): boolean => props.showClose)
|
||||
const _duration = computed((): number => props.duration)
|
||||
const _showTitle = computed((): boolean => props.showTitle)
|
||||
const _showConfirm = computed((): boolean => props.showConfirm)
|
||||
|
||||
const _btnFontColor = computed((): string => {
|
||||
return getDefaultColor(props.btnFontColor)
|
||||
})
|
||||
|
||||
const _btnColor = computed((): string => {
|
||||
if (props.btnColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
|
||||
const _btnText = computed((): string => {
|
||||
if (props.btnText == '') return i18n.t("tmui4x.actionModal.btnText")
|
||||
return props.btnText
|
||||
})
|
||||
|
||||
const _title = computed((): string => {
|
||||
if (props.title == '') return i18n.t("tmui4x.actionModal.title")
|
||||
return props.title
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
let round = props.round;
|
||||
if (round == "") {
|
||||
round = xConfig.drawerRadius
|
||||
}
|
||||
let radius = checkIsCssUnit(round, xConfig.unit);
|
||||
return `${radius}`
|
||||
})
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
|
||||
const _maxHeight = computed((): string => {
|
||||
if (props.maxHeight == "") return "80%"
|
||||
return checkIsCssUnit(props.maxHeight, xConfig.unit);
|
||||
})
|
||||
|
||||
const _animationFun = computed((): string => xConfig.animationFun)
|
||||
|
||||
const __height = computed((): string => {
|
||||
let h = '100%';
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtop.value}px)`
|
||||
// #endif
|
||||
return h;
|
||||
})
|
||||
|
||||
// 方法 - 按依赖顺序定义,被调用的函数在前面
|
||||
function setStyleAni() {
|
||||
try {
|
||||
let watiDuration = 60;
|
||||
// #ifdef APP-IOS
|
||||
watiDuration = props.watiDuration
|
||||
// #endif
|
||||
if (status.value == 'open') {
|
||||
showOverflay.value = true;
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function () {
|
||||
if (showOverflayRef.value == null || xActionModalWrapContentRef.value == null) return;
|
||||
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
xActionModalWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
showOverflayRef.value!.style.setProperty('opacity', '1')
|
||||
xActionModalWrapContentRef.value!.style.setProperty('transform', `translate(0%,-24rpx)`)
|
||||
}, watiDuration);
|
||||
} else if (status.value == 'close') {
|
||||
showOverflayRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
xActionModalWrapContentRef.value!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
showOverflayRef.value!.style.setProperty('opacity', '0')
|
||||
xActionModalWrapContentRef.value!.style.setProperty('transform', `translate(0%,100%)`)
|
||||
}
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
console.error("xActionModal Error:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
actioning.value = false;
|
||||
if (status.value == 'close') {
|
||||
showOverflay.value = false;
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
emits('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
emits('update:show', false)
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
emits('open')
|
||||
}
|
||||
}
|
||||
|
||||
function closeAlert() {
|
||||
if (actioning.value || status.value == 'close') return;
|
||||
actioning.value = true;
|
||||
status.value = 'close'
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emits('beforeClose')
|
||||
setStyleAni();
|
||||
}
|
||||
|
||||
function showAlert() {
|
||||
if (actioning.value) return;
|
||||
if (status.value == 'open') return;
|
||||
|
||||
showOverflay.value = true;
|
||||
actioning.value = true;
|
||||
status.value = 'open'
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emits('beforeOpen')
|
||||
setStyleAni();
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
showAlert();
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
closeAlert()
|
||||
emits('confirm')
|
||||
}
|
||||
|
||||
function onClickOverflowy(evt: Event) {
|
||||
evt.stopPropagation()
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
emits("click")
|
||||
if (!props.overlayClick) return;
|
||||
closeAlert();
|
||||
}
|
||||
|
||||
function overflayMoveTouch(evt: TouchEvent) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
function overTouch(evt: UniTouchEvent) {
|
||||
// #ifdef WEB
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
evt.stopPropagation()
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch((): boolean => props.show, (newval: boolean) => {
|
||||
if (newval) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
function oninitready() {
|
||||
let sys = uni.getWindowInfo()
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight;
|
||||
windtop.value = sys.windowTop;
|
||||
if (_show.value) {
|
||||
showAlert();
|
||||
}
|
||||
}
|
||||
oninitready()
|
||||
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
|
||||
// 监听页面变化,重新获取teleport目标
|
||||
onUpdated(() => {
|
||||
// #ifdef H5
|
||||
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** 打开 **/
|
||||
open: () => showAlert(),
|
||||
/** 关闭 **/
|
||||
close: () => closeAlert()
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer">
|
||||
<!--
|
||||
@slot 标签触发显示遮罩,免于使用变量控制
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="_show"></slot>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
|
||||
<view
|
||||
@click="onClickOverflowy"
|
||||
@touchmove="overTouch"
|
||||
v-if="showOverflay"
|
||||
@transitionend="onEnd"
|
||||
:id="id"
|
||||
ref="showOverflayRef"
|
||||
class="xActionModalWrap xActionModalWrap_bottom"
|
||||
:style="[{width:'100%',height:__height,top:windtop+'px','transition-timing-function':_animationFun},_customStyle]">
|
||||
<!-- @touchmove="overflayMoveTouch" -->
|
||||
<view
|
||||
@click.stop=""
|
||||
class="xActionModalWrapContent xActionModalWrapContent_bottom"
|
||||
ref="xActionModalWrapContentRef"
|
||||
:id="wrapId"
|
||||
:style="{
|
||||
borderRadius:_round,
|
||||
maxHeight:_maxHeight!=''?_maxHeight:'100%',
|
||||
backgroundColor:_bgColor,
|
||||
'transition-timing-function':_animationFun
|
||||
}">
|
||||
<x-icon v-if="_showClose" class="xActionModalXclose" @click="closeAlert" color="#dcdcdc" font-size="24"
|
||||
name="close-circle-fill"></x-icon>
|
||||
<view>
|
||||
<view v-if="_showTitle" class="xActionModalTitleBox">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="_show">
|
||||
<text class="xActionModaltitleBox">{{_title}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view style="flex:1;" :scroll-y="true" :rebound="false">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="default">
|
||||
</slot>
|
||||
</scroll-view>
|
||||
<view v-if="_showConfirm" class="xActionModalFooter">
|
||||
<!--
|
||||
@slot 页脚按钮插槽
|
||||
-->
|
||||
<slot name="footer">
|
||||
<x-button @click="onConfirm" :block="true" :color="_btnColor" :round="_round"
|
||||
:font-color="_btnFontColor">{{_btnText}}</x-button>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xActionModalItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
}
|
||||
|
||||
.xActionModalFooter {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.xActionModalFooterText {
|
||||
font-size: 15px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xActionModalXclose {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 11px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.xActionModalTitleBox {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.xActionModaltitleBox {
|
||||
max-width: 350rpx;
|
||||
overflow: hidden;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.xActionModalWrap_bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
/* #ifndef MP-WEIXIN */
|
||||
align-items: center;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
|
||||
.xActionModalWrapContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 16px;
|
||||
max-width: 500px;
|
||||
/* width:100%; */
|
||||
}
|
||||
|
||||
.xActionModalWrapContent_bottom {
|
||||
transform: translate(0%, 100%);
|
||||
}
|
||||
|
||||
|
||||
.xActionModalWrap {
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
/* #ifndef APP-HARMONY */
|
||||
transition-duration: 350ms;
|
||||
/* #endif */
|
||||
/* #ifdef APP-HARMONY */
|
||||
transition-duration: 0ms;
|
||||
/* #endif */
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
z-index: 400;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
transition-property: opacity;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<view v-if="showAlert" @click="onclickbar" class="xAlert" :style="_styleMap.bgStyle">
|
||||
<!--
|
||||
@slot 左边图标插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<view class="xAlertLeft">
|
||||
<x-icon :font-size="props.iconSize" :color="(_styleMap.textStyle.get('color')! as string)"
|
||||
:dark-color="(_styleMap.textStyle.get('color')! as string)" :name="_iconName"></x-icon>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="xAlertContent">
|
||||
<text :style="_styleMap.textStyle">
|
||||
<!--
|
||||
@slot 默认插槽内容
|
||||
-->
|
||||
<slot></slot>
|
||||
</text>
|
||||
</view>
|
||||
<view @click="closeAlert" v-if="_showClose" class="xAlertRight">
|
||||
<x-icon :font-size="props.iconSize" :color="(_styleMap.textStyle.get('color')! as string)"
|
||||
:dark-color="(_styleMap.textStyle.get('color')! as string)" :name="props.closeIcon"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from "vue"
|
||||
import { xDate } from "../../core/util/xDate.uts"
|
||||
import { checkIsCssUnit, getUnit, fillArrayCssValue, fillArrayCssValueByround, fillArrayCssValueBycolor } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, getDefaultColorObj, getThinColorObj } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type styleMapType = {
|
||||
textStyle: Map<string, any>,
|
||||
bgStyle: Map<string, any>
|
||||
}
|
||||
|
||||
type xAlertStatusType = "primary"|"warn"|"success"|"error"|"info"
|
||||
|
||||
/**
|
||||
* @name 警告 xAlert
|
||||
* @page /pages/index/alert
|
||||
* @category 展示组件
|
||||
* @description 样式丰富常用警告提醒
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xAlert" })
|
||||
|
||||
defineSlots<{
|
||||
left(): any
|
||||
default(): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 关闭时触发
|
||||
*/
|
||||
"close",
|
||||
/**
|
||||
* 组件被点击时触发
|
||||
*/
|
||||
"click"
|
||||
])
|
||||
|
||||
export type xAlertPropsType = {
|
||||
/**
|
||||
* 类型
|
||||
* warn:警告
|
||||
* success:成功
|
||||
* error:错误
|
||||
* info:信息
|
||||
* primary:正常主题
|
||||
*/
|
||||
status: xAlertStatusType,
|
||||
/**
|
||||
* 警告图标,不填写取status默认图标
|
||||
* 填写以填写为准
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* 警告图标大小
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 关闭图标
|
||||
*/
|
||||
closeIcon: string,
|
||||
/**
|
||||
* 显示还是隐藏关闭按钮
|
||||
*/
|
||||
showClose: boolean,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 主题色,如果不填写以status为准
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 文字颜色,如果不填写以status为准
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 暗黑主题颜色,如果不填写自动计算
|
||||
*/
|
||||
darkColor: string,
|
||||
/**
|
||||
* 暗黑文字颜色,如果不填写自动计算
|
||||
*/
|
||||
fontDarkColor: string,
|
||||
/**
|
||||
* 它是建立在你没有提供color时才有效。
|
||||
* 如果提供了color是以你color为背景最终色。
|
||||
* thin浅色模式,
|
||||
* normal标准背景色
|
||||
*/
|
||||
skin: string,
|
||||
/**
|
||||
* 圆角
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [顶左,顶右,底右,底左]
|
||||
* [顶左,底右]
|
||||
* [顶左,顶右,底右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
round: string[],
|
||||
/**
|
||||
* 边线
|
||||
* 数组数字时
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [左,上,右,下]
|
||||
* [左右,上下]
|
||||
* [左,上,右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
border: string[],
|
||||
/**
|
||||
* 边框颜色
|
||||
* 格式同border边线。
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
borderColor: string[],
|
||||
/**
|
||||
* 如果不填写,自动计算
|
||||
*/
|
||||
darkBorderColor: string[],
|
||||
/**
|
||||
* 边线类型,默认solid,可以为none
|
||||
*/
|
||||
borderStyle: string,
|
||||
/**
|
||||
* 间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
margin: string[],
|
||||
/**
|
||||
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
padding: string[],
|
||||
/**
|
||||
* 自定义高度,可以是数字,单位或者百分比,auto
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 宽,单位合法即可数字,字符串带单位,百分比,auto
|
||||
*/
|
||||
width: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xAlertPropsType>(), {
|
||||
status: "primary",
|
||||
icon: "",
|
||||
iconSize: "20",
|
||||
closeIcon: "close-line",
|
||||
showClose: true,
|
||||
fontSize: "15",
|
||||
color: "",
|
||||
fontColor: "",
|
||||
darkColor: "",
|
||||
fontDarkColor: "",
|
||||
skin: "thin",
|
||||
round:():string[] => [] as string[],
|
||||
border: ():string[] => [] as string[],
|
||||
borderColor: ():string[] => [] as string[],
|
||||
darkBorderColor: ():string[] => [] as string[],
|
||||
borderStyle: 'solid',
|
||||
margin: ():string[] => ['16', '0', '16', '16'] as string[],
|
||||
padding: ():string[] => ['16', '12'] as string[],
|
||||
height: "auto",
|
||||
width: "auto"
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const showAlert = ref(true)
|
||||
|
||||
// 计算属性
|
||||
const _color = computed((): string => {
|
||||
let tcolor = props.color;
|
||||
if (tcolor == '') {
|
||||
tcolor = props.status;
|
||||
}
|
||||
if (xConfig.dark == 'dark' && props.darkColor != '') {
|
||||
tcolor = props.darkColor
|
||||
}
|
||||
let color = getDefaultColor(tcolor)
|
||||
return color;
|
||||
})
|
||||
|
||||
const _fontColor = computed((): string => {
|
||||
let tcolor = props.fontColor;
|
||||
if (xConfig.dark == 'dark' && props.fontDarkColor != '') {
|
||||
tcolor = props.fontDarkColor
|
||||
}
|
||||
return tcolor;
|
||||
})
|
||||
|
||||
const _isDark = computed((): boolean => xConfig.dark == 'dark')
|
||||
const _showClose = computed((): boolean => props.showClose)
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _margin = computed((): string => {
|
||||
if (props.margin.length == 0) {
|
||||
let par = fillArrayCssValue(xConfig.sheetMargin)
|
||||
if (par.length == 0) return "0px 0px 0px 0px";
|
||||
return par.join(" ")
|
||||
}
|
||||
let ar: string[] = fillArrayCssValue(props.margin as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _padding = computed((): string => {
|
||||
if (props.padding.length == 0) {
|
||||
let par = fillArrayCssValue(xConfig.sheetMargin)
|
||||
if (par.length == 0) return "0px 0px 0px 0px";
|
||||
return par.join(" ")
|
||||
}
|
||||
let ar: string[] = fillArrayCssValue(props.padding as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
if (props.round.length == 0) {
|
||||
let par = fillArrayCssValueByround(xConfig.sheetRadius)
|
||||
if (par.length == 0) return "0px 0px 0px 0px";
|
||||
return par.join(" ")
|
||||
}
|
||||
let ar: string[] = fillArrayCssValueByround(props.round as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _border = computed((): string => {
|
||||
let ar: string[] = fillArrayCssValue(props.border as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _borderColor = computed((): string => {
|
||||
let bordercolor = props.borderColor as string[];
|
||||
if (xConfig.dark == 'dark') {
|
||||
bordercolor = props.darkBorderColor.length == 0 ? xConfig.sheetDarkBorderColor : props.darkBorderColor
|
||||
}
|
||||
let ar: string[] = fillArrayCssValueBycolor(bordercolor as string[])
|
||||
if (ar.length == 0) return "transparent transparent transparent transparent";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _iconName = computed((): string => {
|
||||
const iconsmap = new Map<string, string>([
|
||||
['warn', 'alert-line'],
|
||||
['success', 'check-double-line'],
|
||||
['error', 'close-circle-line'],
|
||||
['info', 'information-2-line'],
|
||||
['primary', 'notification-line'],
|
||||
])
|
||||
let dicon = iconsmap.get(props.status)
|
||||
return props.icon == '' ? (dicon == null ? '' : dicon!) : props.icon
|
||||
})
|
||||
|
||||
const _styleMap = computed((): styleMapType => {
|
||||
let bgStylemap = new Map<string, any>()
|
||||
let textStylemap = new Map<string, any>()
|
||||
let colorObj = getDefaultColorObj(_color.value, _color.value)
|
||||
|
||||
if (props.skin == 'thin') {
|
||||
colorObj = getThinColorObj(_color.value, _color.value, _isDark.value)
|
||||
}
|
||||
let defaultObj: UTSJSONObject = colorObj.getJSON("default")!
|
||||
bgStylemap.set("backgroundColor", defaultObj.getString("background")!)
|
||||
bgStylemap.set("margin", _margin.value)
|
||||
bgStylemap.set("padding", _padding.value)
|
||||
bgStylemap.set("border-radius", _round.value)
|
||||
bgStylemap.set("border-width", _border.value)
|
||||
bgStylemap.set("height", checkIsCssUnit(props.height, xConfig.unit))
|
||||
bgStylemap.set("width", checkIsCssUnit(props.width, xConfig.unit))
|
||||
|
||||
bgStylemap.set("border-style", props.borderStyle)
|
||||
if (props.borderColor.length > 0) {
|
||||
bgStylemap.set("border-color", _borderColor.value)
|
||||
} else {
|
||||
bgStylemap.set("border-color", getDefaultColor(_color.value))
|
||||
}
|
||||
|
||||
if (_fontColor.value == '') {
|
||||
textStylemap.set("color", defaultObj.getString("fontColor")!)
|
||||
} else {
|
||||
textStylemap.set("color", _fontColor.value)
|
||||
}
|
||||
textStylemap.set("font-size", _fontSize.value)
|
||||
return {
|
||||
bgStyle: bgStylemap,
|
||||
textStyle: textStylemap
|
||||
} as styleMapType;
|
||||
})
|
||||
|
||||
// 方法
|
||||
function closeAlert(): void {
|
||||
emits('close')
|
||||
showAlert.value = false
|
||||
}
|
||||
|
||||
function onclickbar(): void {
|
||||
emits('click')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.xAlert {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
}
|
||||
|
||||
.xAlertContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xAlertLeft {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.xAlertLeft {
|
||||
flex-shrink: 0;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.xAlertRight {
|
||||
flex-shrink: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<script lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
import { xTween } from "@/uni_modules/tmx-ui/index.uts"
|
||||
import { xTweenAnimate, xTweenEventCallFunType} from "@/uni_modules/tmx-ui/interface.uts"
|
||||
|
||||
/**
|
||||
* @name 动画 xAnimation
|
||||
* @page /pages/index/animation
|
||||
* @description 动画组件
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
element: null as Element | null,
|
||||
id: "xAnimation" + getUid() as String,
|
||||
playStatus: 'complete' as 'reset' | 'playing' | 'complete',
|
||||
playFag: '',
|
||||
tid: 0
|
||||
}
|
||||
},
|
||||
|
||||
emits: [
|
||||
/**
|
||||
* 播放前执行
|
||||
*/
|
||||
'beforePlay',
|
||||
/**
|
||||
* 播放完成执行
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 播放时触发
|
||||
*/
|
||||
'play',
|
||||
/**
|
||||
* 同步控制播放参数
|
||||
* 等同v-model:control
|
||||
*/
|
||||
'update:control',
|
||||
/**
|
||||
* 当前播放的状态
|
||||
* 等同vmodel:status
|
||||
* 它是单向输出的
|
||||
*/
|
||||
'update:status'],
|
||||
props: {
|
||||
/**
|
||||
* 是否自动播放
|
||||
*/
|
||||
autoPlay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 动画播放的时间+50+5
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 350
|
||||
},
|
||||
/**
|
||||
* 动画名称
|
||||
*/
|
||||
name: {
|
||||
type: String as PropType<'fadeIn' | 'fadeOut' | 'zoomIn' | 'zoomOut' | 'left' | 'right' | 'top' | 'bottom'>,
|
||||
default: 'fadeIn'
|
||||
},
|
||||
/**
|
||||
* 是否允许反转,如果允许反转
|
||||
* 播放当前动画后。再点播放,会反方向播放动画。接着再播放又是正常,这样反复。
|
||||
*/
|
||||
revert: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 自动监测是否播放。如果标志为play就开始播放动画,但
|
||||
* 如果动画在播放中这个值不会有任何响应,而且需要v-model:playing用法来双向绑定读取此属性
|
||||
* 你可以通过播放事件来确定修改此值。
|
||||
*
|
||||
*/
|
||||
control: {
|
||||
type: String as PropType<'play' | 'default'>,
|
||||
default: 'default'
|
||||
},
|
||||
/**
|
||||
* 播放状态,只能用来读取此值,不可更改,使用时请v-model:status来读取动态的状态值
|
||||
*
|
||||
*/
|
||||
status: {
|
||||
type: String as PropType<'reset' | 'playing' | 'complete'>,
|
||||
default: 'complete'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
control() {
|
||||
if (this.playStatus == 'playing' || this.control != 'play') {
|
||||
|
||||
this.$emit('update:control', 'default')
|
||||
return;
|
||||
}
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_animationFun() : string {
|
||||
return xConfig.animationFun
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let t = this;
|
||||
this.resetStatusInitType();
|
||||
this.tid = setTimeout(function () {
|
||||
t.autoPlaySetFlex()
|
||||
if (t.autoPlay) {
|
||||
|
||||
t.play();
|
||||
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
methods: {
|
||||
|
||||
resetStatusInitType() {
|
||||
// ||this.playStatus == 'reset'
|
||||
this.element = this.$refs["xAnimation"] as UniElement
|
||||
if (this.element == null) return;
|
||||
this.playStatus = 'reset'
|
||||
this.$emit('update:status', 'reset')
|
||||
// if(this.name=='fadeIn'||this.name=='fadeOut'){
|
||||
// this.element!.style.setProperty("transition-property",'opacity')
|
||||
// }else{
|
||||
// this.element!.style.setProperty("transition-property",'transform')
|
||||
// }
|
||||
if (this.playFag == '') {
|
||||
this.element!.style.setProperty("transition-duration", '0ms')
|
||||
if (this.name == 'fadeIn') {
|
||||
this.element!.style.setProperty("opacity", 0)
|
||||
}
|
||||
if (this.name == 'fadeOut') {
|
||||
this.element!.style.setProperty("opacity", 1)
|
||||
}
|
||||
if (this.name == 'zoomIn') {
|
||||
this.element!.style.setProperty("opacity", 0)
|
||||
this.element!.style.setProperty("transform", 'scale(0.65)')
|
||||
}
|
||||
if (this.name == 'zoomOut') {
|
||||
this.element!.style.setProperty("opacity", 1)
|
||||
this.element!.style.setProperty("transform", 'scale(1)')
|
||||
}
|
||||
if (this.name == 'left') {
|
||||
this.element!.style.setProperty("transform", 'translateX(1000%)')
|
||||
}
|
||||
if (this.name == 'right') {
|
||||
this.element!.style.setProperty("transform", 'translateX(-100%)')
|
||||
}
|
||||
if (this.name == 'top') {
|
||||
this.element!.style.setProperty("transform", 'translateY(-100%)')
|
||||
}
|
||||
if (this.name == 'bottom') {
|
||||
this.element!.style.setProperty("transform", 'translateY(100%)')
|
||||
}
|
||||
}
|
||||
},
|
||||
autoPlaySetFlex() {
|
||||
this.element = this.$refs["xAnimation"] as UniElement
|
||||
if (this.element == null) return;
|
||||
this.element!.style.setProperty("display", 'flex')
|
||||
},
|
||||
/**
|
||||
* 播放动画
|
||||
* @public
|
||||
*/
|
||||
play() {
|
||||
this.element = this.$refs["xAnimation"] as UniElement
|
||||
if (this.element == null || this.playStatus == 'playing') return;
|
||||
if (this.playStatus != 'reset') {
|
||||
this.resetStatusInitType()
|
||||
}
|
||||
/**
|
||||
* 播放前触发
|
||||
*/
|
||||
this.$emit('beforePlay')
|
||||
this.playStatus = 'playing'
|
||||
this.$emit('update:status', 'playing')
|
||||
let t = this;
|
||||
clearTimeout(this.tid)
|
||||
//需要一个反应时间。
|
||||
this.tid = setTimeout(function () {
|
||||
/**
|
||||
* 播放时触发
|
||||
*/
|
||||
t.$emit('play')
|
||||
if (t.playFag == '') {
|
||||
t.element!.style.setProperty("transition-duration", t.duration.toString() + 'ms')
|
||||
if (t.name == 'fadeIn') {
|
||||
t.element!.style.setProperty("opacity", 1)
|
||||
}
|
||||
if (t.name == 'fadeOut') {
|
||||
t.element!.style.setProperty("opacity", 0)
|
||||
}
|
||||
if (t.name == 'zoomIn') {
|
||||
t.element!.style.setProperty("transform", 'scale(1)')
|
||||
t.element!.style.setProperty("opacity", 1)
|
||||
}
|
||||
if (t.name == 'zoomOut') {
|
||||
t.element!.style.setProperty("transform", 'scale(0.65)')
|
||||
t.element!.style.setProperty("opacity", 0)
|
||||
}
|
||||
if (t.name == 'left' || t.name == 'right') {
|
||||
t.element!.style.setProperty("transform", 'translateX(0%)')
|
||||
}
|
||||
|
||||
if (t.name == 'top' || t.name == 'bottom') {
|
||||
t.element!.style.setProperty("transform", 'translateY(0%)')
|
||||
}
|
||||
} else {
|
||||
t.element!.style.setProperty("display", 'flex')
|
||||
t.element!.style.setProperty("transition-duration", t.duration.toString() + 'ms')
|
||||
if (t.name == 'fadeIn') {
|
||||
t.element!.style.setProperty("opacity", 0)
|
||||
}
|
||||
if (t.name == 'fadeOut') {
|
||||
t.element!.style.setProperty("opacity", 1)
|
||||
}
|
||||
if (t.name == 'zoomIn') {
|
||||
t.element!.style.setProperty("transform", 'scale(0.65)')
|
||||
t.element!.style.setProperty("opacity", 0)
|
||||
}
|
||||
if (t.name == 'zoomOut') {
|
||||
t.element!.style.setProperty("transform", 'scale(1)')
|
||||
t.element!.style.setProperty("opacity", 1)
|
||||
}
|
||||
if (t.name == 'left') {
|
||||
t.element!.style.setProperty("transform", 'translateX(100%)')
|
||||
}
|
||||
if (t.name == 'right') {
|
||||
t.element!.style.setProperty("transform", 'translateX(-100%)')
|
||||
}
|
||||
|
||||
if (t.name == 'top') {
|
||||
t.element!.style.setProperty("transform", 'translateY(-100%)')
|
||||
}
|
||||
if (t.name == 'bottom') {
|
||||
t.element!.style.setProperty("transform", 'translateY(100%)')
|
||||
}
|
||||
}
|
||||
|
||||
}, 5);
|
||||
},
|
||||
playEnd() {
|
||||
|
||||
if (this.playStatus != 'reset') {
|
||||
|
||||
this.playStatus = 'complete'
|
||||
/**
|
||||
* 播放完成触发
|
||||
*/
|
||||
this.$emit('complete')
|
||||
this.$emit('update:status', 'complete')
|
||||
this.$emit('update:control', 'default');
|
||||
if (this.revert) {
|
||||
if (this.playFag == '') {
|
||||
this.playFag = 'revert'
|
||||
} else {
|
||||
this.playFag = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view ref="xAnimation" @transitionend="playEnd" class="xAnimation" :id="id"
|
||||
:style="{'transition-timing-function':_animationFun}">
|
||||
<!--
|
||||
默认插槽
|
||||
@prop {object} status - 状态 {state:'reset'|'playing'|'complete',flag:'play'|'default'}
|
||||
-->
|
||||
<slot :status="{state:playStatus,flag:playFag}"></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xAnimation {
|
||||
display: none;
|
||||
transition-property: opacity, transform;
|
||||
transition-duration: 350ms;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<script lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
import { xTween } from "@/uni_modules/tmx-ui/index.uts"
|
||||
import { XANIMATE_OPIONS, xTweenAnimate, xTweenCallbackFunType, xTweenEventCallFunType} from "@/uni_modules/tmx-ui/interface.uts"
|
||||
|
||||
/**
|
||||
* @name 动画 xAnimation
|
||||
* @page /pages/index/animation
|
||||
* @description 动画组件
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
element: null as Element | null,
|
||||
id: "xAnimation" + getUid() as String,
|
||||
playStatus: 'complete' as 'reset' | 'playing' | 'complete',
|
||||
playFag: '',
|
||||
tid: 0,
|
||||
tw:new xTween()
|
||||
}
|
||||
},
|
||||
|
||||
emits: [
|
||||
/**
|
||||
* 播放前执行
|
||||
*/
|
||||
'beforePlay',
|
||||
/**
|
||||
* 播放完成执行
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 播放时触发
|
||||
*/
|
||||
'play',
|
||||
/**
|
||||
* 同步控制播放参数
|
||||
* 等同v-model:control
|
||||
*/
|
||||
'update:control',
|
||||
/**
|
||||
* 当前播放的状态
|
||||
* 等同vmodel:status
|
||||
* 它是单向输出的
|
||||
*/
|
||||
'update:status'],
|
||||
props: {
|
||||
/**
|
||||
* 是否自动播放
|
||||
*/
|
||||
autoPlay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 动画播放的时间+50+5
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 350
|
||||
},
|
||||
/**
|
||||
* 动画名称
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: 'fadeIn'
|
||||
},
|
||||
/**
|
||||
* 缓动动画名称
|
||||
*/
|
||||
ease: {
|
||||
type: String,
|
||||
default: 'tmxEase'
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动监测是否播放。如果标志为play就开始播放动画,但
|
||||
* 如果动画在播放中这个值不会有任何响应,而且需要v-model:playing用法来双向绑定读取此属性
|
||||
* 你可以通过播放事件来确定修改此值。
|
||||
*
|
||||
*/
|
||||
control: {
|
||||
type: String as PropType<'play' | 'default'>,
|
||||
default: 'default'
|
||||
},
|
||||
/**
|
||||
* 播放状态,只能用来读取此值,不可更改,使用时请v-model:status来读取动态的状态值
|
||||
*
|
||||
*/
|
||||
status: {
|
||||
type: String as PropType<'reset' | 'playing' | 'complete'>,
|
||||
default: 'complete'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
control() {
|
||||
if (this.playStatus == 'playing' || this.control != 'play') {
|
||||
this.$emit('update:control', 'default')
|
||||
return;
|
||||
}
|
||||
this.play();
|
||||
|
||||
},
|
||||
name(){
|
||||
this.resetStatusInitType();
|
||||
this.autoPlaySetFlex()
|
||||
if (this.autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_animationFun() : string {
|
||||
return xConfig.animationFun
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
this.tw.startRender()
|
||||
this.resetStatusInitType();
|
||||
this.autoPlaySetFlex()
|
||||
if (this.autoPlay) {
|
||||
this.play();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.tw.destroy()
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
methods: {
|
||||
|
||||
resetStatusInitType() {
|
||||
// ||this.playStatus == 'reset'
|
||||
let element = this.$refs["xAnimation"] as UniElement
|
||||
if (element == null) return;
|
||||
this.playStatus = 'reset'
|
||||
this.$emit('update:status', 'reset')
|
||||
|
||||
if (this.playFag == '') {
|
||||
|
||||
if (this.name == 'fadeIn') {
|
||||
element.style.setProperty("opacity", 0)
|
||||
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'fadeOut') {
|
||||
element.style.setProperty("opacity", 1)
|
||||
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'zoomIn') {
|
||||
element.style.setProperty("opacity", 0)
|
||||
element.style.setProperty("transform", 'scale(0) translateX(0%) translateY(0%)')
|
||||
|
||||
}
|
||||
if (this.name == 'zoomOut') {
|
||||
element.style.setProperty("opacity", 1)
|
||||
element.style.setProperty("transform", 'scale(1) translateX(0%) translateY(0%)')
|
||||
|
||||
}
|
||||
if (this.name == 'leftIn') {
|
||||
element.style.setProperty("transform", 'translateX(-100%) scale(1) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'leftOut') {
|
||||
element.style.setProperty("transform", 'translateX(0%) scale(1) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'rightIn') {
|
||||
element.style.setProperty("opacity", 0)
|
||||
element.style.setProperty("transform", 'translateX(100%) scale(1) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'rightOut') {
|
||||
element.style.setProperty("opacity", 1)
|
||||
element.style.setProperty("transform", 'translateX(0%) scale(1) translateY(0%)')
|
||||
}
|
||||
if (this.name == 'topIn') {
|
||||
element.style.setProperty("opacity", 0)
|
||||
element.style.setProperty("transform", 'translateY(-100%) scale(1) translateX(0%)')
|
||||
}
|
||||
if (this.name == 'topOut') {
|
||||
element.style.setProperty("opacity", 1)
|
||||
element.style.setProperty("transform", 'translateY(0%) scale(1) translateX(0%)')
|
||||
}
|
||||
if (this.name == 'bottomIn') {
|
||||
element.style.setProperty("opacity", 0)
|
||||
element.style.setProperty("transform", 'translateY(-100%) scale(1) translateX(0%)')
|
||||
}
|
||||
if (this.name == 'bottomOut') {
|
||||
element.style.setProperty("opacity", 1)
|
||||
element.style.setProperty("transform", 'translateY(0%) scale(1) translateX(0%)')
|
||||
}
|
||||
}
|
||||
},
|
||||
autoPlaySetFlex() {
|
||||
let element = this.$refs["xAnimation"] as UniElement
|
||||
if (element == null) return;
|
||||
element.style.setProperty("display", 'flex')
|
||||
},
|
||||
/**
|
||||
* 播放动画
|
||||
* @public
|
||||
*/
|
||||
play(){
|
||||
let _this = this;
|
||||
let element = this.$refs["xAnimation"] as UniElement
|
||||
if (element == null || this.playStatus == 'playing') return;
|
||||
if (this.playStatus != 'reset') {
|
||||
this.resetStatusInitType()
|
||||
}
|
||||
/**
|
||||
* 播放前触发
|
||||
*/
|
||||
this.$emit('beforePlay')
|
||||
this.playStatus = 'playing'
|
||||
this.$emit('update:status', 'playing')
|
||||
let atr = this.name
|
||||
this.tw.stop()
|
||||
this.tw.removeAnimate()
|
||||
this.tw
|
||||
.addAnimate({
|
||||
duration:this.duration,
|
||||
ease:this.ease,
|
||||
enter:(item : xTweenEventCallFunType) =>{
|
||||
if(atr == 'fadeIn'){
|
||||
element.style.setProperty('opacity',(item.progress).toString())
|
||||
}else if(atr == 'fadeOut'){
|
||||
element.style.setProperty('opacity',(1-item.progress).toString())
|
||||
}else if(atr == 'zoomIn'){
|
||||
|
||||
element.style.setProperty("opacity", item.progress)
|
||||
element.style.setProperty("transform", `scale(${item.progress})`)
|
||||
}else if(atr == 'zoomOut'){
|
||||
element.style.setProperty("opacity", (1-item.progress))
|
||||
element.style.setProperty("transform", `scale(${1-item.progress})`)
|
||||
}else if(atr == 'leftIn'){
|
||||
let left = (1-item.progress)*100*-1
|
||||
element.style.setProperty('opacity',(item.progress).toString())
|
||||
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
|
||||
|
||||
}else if(atr == 'leftOut'){
|
||||
let left = (item.progress)*100*-1
|
||||
element.style.setProperty('opacity',(1-item.progress).toString())
|
||||
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
|
||||
}else if(atr == 'rightIn'){
|
||||
let left = (1-item.progress)*100
|
||||
element.style.setProperty('opacity',(item.progress).toString())
|
||||
|
||||
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
|
||||
|
||||
}else if(atr == 'rightOut'){
|
||||
let left = (item.progress)*100
|
||||
element.style.setProperty('opacity',(1-item.progress).toString())
|
||||
element.style.setProperty("transform", `translateX(${left.toString()}%)`)
|
||||
}else if(atr == 'topIn'){
|
||||
let left = (1-item.progress)*100*-1
|
||||
element.style.setProperty('opacity',(item.progress).toString())
|
||||
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
|
||||
|
||||
}else if(atr == 'topOut'){
|
||||
let left = (item.progress)*100*-1
|
||||
element.style.setProperty('opacity',(1-item.progress).toString())
|
||||
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
|
||||
}else if(atr == 'bottomIn'){
|
||||
let left = (1-item.progress)*100
|
||||
element.style.setProperty('opacity',(item.progress).toString())
|
||||
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
|
||||
|
||||
}else if(atr == 'bottomOut'){
|
||||
let left = (item.progress)*100
|
||||
element.style.setProperty('opacity',(1-item.progress).toString())
|
||||
element.style.setProperty("transform", `translateY(${left.toString()}%)`)
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
complete: (_ : xTweenEventCallFunType) => {
|
||||
_this.playStatus = 'complete'
|
||||
/**
|
||||
* 播放完成触发
|
||||
*/
|
||||
_this.$emit('complete')
|
||||
_this.$emit('update:status', 'complete')
|
||||
_this.$emit('update:control', 'default');
|
||||
}
|
||||
} as xTweenAnimate)
|
||||
this.tw.play()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view ref="xAnimation" class="xAnimation" :id="id">
|
||||
<!-- #ifdef APP||WEB -->
|
||||
<!--
|
||||
@slot 默认插槽,下面的数据微信没有
|
||||
@prop {object} status - 状态 {state:'reset'|'playing'|'complete',flag:'play'|'default'}
|
||||
-->
|
||||
<slot :status="{state:playStatus,flag:playFag}"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<slot ></slot>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xAnimation {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
/* transition-property: opacity, transform;
|
||||
transition-duration: 350ms; */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,430 @@
|
||||
<template>
|
||||
|
||||
<view class="xAvatarGroup" ref="xAvatarGroup" :class="[_flat?'xAvatarGroupWrap':'xAvatarGroupNoWrap']">
|
||||
<template v-for="(item,index) in _list" :key="index">
|
||||
<image @click="itemClick(item,index)" v-if="item.isImg" :style="{
|
||||
width:_size,
|
||||
height:_size,
|
||||
borderRadius:_round,
|
||||
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
|
||||
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter
|
||||
|
||||
}" class="xAvatarGroupImage" :src="item.name" :mode="props.model"></image>
|
||||
<view @click="itemClick(item,index)" v-else :style="{
|
||||
width:_size,
|
||||
height:_size,
|
||||
borderRadius:_round,
|
||||
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
|
||||
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter,
|
||||
backgroundColor:item.bgColor
|
||||
}" class="xAvatarGroupImage xAvatarGroupImageBytext">
|
||||
<x-text v-if="item.name!=''" :font-size="props.fontSize"
|
||||
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor">
|
||||
{{item.name}}
|
||||
</x-text>
|
||||
<x-icon v-if="item.name==''" :font-size="props.fontSize"
|
||||
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor"
|
||||
:name="props.placeIcon"></x-icon>
|
||||
</view>
|
||||
</template>
|
||||
<!--
|
||||
@slot more更多插槽,如果使用了这个插槽moreClick事件会丢失,请自己写在自己的布局上。
|
||||
-->
|
||||
<slot name="more">
|
||||
<view @click="moreClick" class="xAvatarGroupImageMore" :style="{
|
||||
width:_size,
|
||||
height:_size,
|
||||
borderRadius:_round,
|
||||
backgroundColor:_BgColor
|
||||
}" v-if="_showCount">
|
||||
<x-text :font-size="props.fontSize" :color="props.fontColor"
|
||||
:dark-color="props.darkFontColor">{{_count==0?(_list.length>99?99:_list.length)+'+':(_count>99?99:_count)+'+'}}</x-text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed, ref } from "vue"
|
||||
import { type PropType } from "vue"
|
||||
import { getDefaultColor, hslaToCss } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xProvitae } from "../../config/xConfig.uts"
|
||||
type XAVATARGROUPITEMTYPE = {
|
||||
name : string,
|
||||
isImg : boolean,
|
||||
bgColor : string
|
||||
}
|
||||
/**
|
||||
* @name 头像组 xAvatarGroup
|
||||
* @page /pages/index/avatar-group
|
||||
* @category 展示组件
|
||||
* @description 平铺和堆叠方式。如果想要单头像建议使用:xSheet+xImage配合,+xBadge达到效果,因此我不再提供单头像组件,没有意义。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xAvatarGroup" })
|
||||
|
||||
let resizeObserver = null as UniResizeObserver | null
|
||||
const xAvatarGroup = ref<UniElement | null>(null)
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 头像被点击时
|
||||
* @param {number} index 当前索引
|
||||
* @param {string} src 当前图片地址
|
||||
*/
|
||||
"click",
|
||||
/**
|
||||
* 最后一个数字头像more被点击时
|
||||
*/
|
||||
"moreClick"
|
||||
])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 头像列表,也可以是文本数组,也可以是空字符串数组
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
default: '32'
|
||||
},
|
||||
/**
|
||||
* 最多显示几个头像。
|
||||
*/
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: '16'
|
||||
},
|
||||
/**
|
||||
* 平铺或者堆叠时的间隙或者前推差值。
|
||||
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
|
||||
*/
|
||||
gutter: {
|
||||
type: String,
|
||||
default: '16'
|
||||
},
|
||||
/**
|
||||
* 显示类型见:
|
||||
* https://doc.dcloud.net.cn/uni-app-x/component/image.html#%E5%B1%9E%E6%80%A7
|
||||
*/
|
||||
model: {
|
||||
type: String,
|
||||
default: "scaleToFill"
|
||||
},
|
||||
/**
|
||||
* 显示在最后一个时,显示的数字。如果为0取list的长度
|
||||
*/
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 是否显示最后一个数字头像
|
||||
*/
|
||||
showCount: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否平铺,如果否就是堆叠。是就是正常排列。
|
||||
*/
|
||||
flat: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 如果为文本头像时的背景
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "#f5f5f5"
|
||||
},
|
||||
/**
|
||||
* 如果为文本头像时的暗黑背景
|
||||
* 空时默认取inputDarkBgcolor
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 如果为文本头像时的文字颜色
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "#a6a6a6"
|
||||
},
|
||||
/**
|
||||
* 如果为文本头像时的暗黑背景
|
||||
* 空时默认取inputDarkBgcolor
|
||||
*/
|
||||
darkFontColor: {
|
||||
type: String,
|
||||
default: "#ffffff"
|
||||
},
|
||||
/**
|
||||
* 字号
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "14"
|
||||
},
|
||||
/**
|
||||
* 文本头像时,是否随机背景色
|
||||
*/
|
||||
randomBgColor: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 如果当图片或者文本为空时的图片占位符
|
||||
* 可以是图片地址或者图标名称
|
||||
*/
|
||||
placeIcon: {
|
||||
type: String,
|
||||
default: 'user-3-fill'
|
||||
}
|
||||
})
|
||||
const _isFileImg = (name : string) : boolean => {
|
||||
if (name.lastIndexOf(".") > -1 ||
|
||||
name.indexOf("ftp:") > -1 ||
|
||||
name.indexOf("https:") > -1 ||
|
||||
name.indexOf("http:") > -1 ||
|
||||
name.indexOf("data:image") > -1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const _randomBgColor = computed(() : boolean => {
|
||||
return props.randomBgColor
|
||||
})
|
||||
const _BgColor = computed(() : string => {
|
||||
let bgcolor = props.bgColor;
|
||||
if (xConfig.dark == 'dark') {
|
||||
bgcolor = props.darkBgColor;
|
||||
if (bgcolor == '') {
|
||||
bgcolor = xConfig.inputDarkColor
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
})
|
||||
|
||||
const UpperEn = new Map<string, string>([
|
||||
['a', 'A'],
|
||||
['b', 'B'],
|
||||
['c', 'C'],
|
||||
['d', 'D'],
|
||||
['e', 'E'],
|
||||
['f', 'F'],
|
||||
['g', 'G'],
|
||||
['h', 'H'],
|
||||
['i', 'I'],
|
||||
['j', 'J'],
|
||||
['k', 'K'],
|
||||
['l', 'L'],
|
||||
['m', 'M'],
|
||||
['n', 'N'],
|
||||
['o', 'O'],
|
||||
['p', 'P'],
|
||||
['q', 'Q'],
|
||||
['r', 'R'],
|
||||
['s', 'S'],
|
||||
['t', 'T'],
|
||||
['u', 'U'],
|
||||
['v', 'V'],
|
||||
['w', 'W'],
|
||||
['x', 'X'],
|
||||
['y', 'Y'],
|
||||
['z', 'Z']
|
||||
]);
|
||||
const _list = computed(() : XAVATARGROUPITEMTYPE[] => {
|
||||
let nowlist = props.list.slice(0)
|
||||
if (props.maxCount > 0) {
|
||||
nowlist = nowlist.slice(0, props.maxCount)
|
||||
}
|
||||
let compeltedList = [] as XAVATARGROUPITEMTYPE[];
|
||||
for (let i = 0; i < nowlist.length; i++) {
|
||||
let name = nowlist[i]
|
||||
let isImg = _isFileImg(name);
|
||||
let bgColor = ''
|
||||
if (!isImg) {
|
||||
name = name.slice(0, 1)
|
||||
if (UpperEn.get(name) != null) {
|
||||
name = UpperEn.get(name)!
|
||||
}
|
||||
let h = Math.random() * 360
|
||||
let s = 78
|
||||
let l = 62
|
||||
|
||||
bgColor = hslaToCss({ h: parseInt(h.toFixed(0)), s, l, a: 1 } as UTSJSONObject)
|
||||
}
|
||||
|
||||
compeltedList.push({
|
||||
name,
|
||||
isImg,
|
||||
bgColor: props.randomBgColor ? bgColor : _BgColor.value
|
||||
} as XAVATARGROUPITEMTYPE)
|
||||
}
|
||||
return compeltedList;
|
||||
})
|
||||
|
||||
|
||||
const _size = computed(() : string => checkIsCssUnit(props.size, xConfig.unit))
|
||||
const _sizeBypx = computed(() : number => {
|
||||
let rpx = checkIsCssUnit(props.size, xConfig.unit);
|
||||
let unit = getUnit(rpx)
|
||||
let zhi = parseFloat(rpx)
|
||||
if (unit == 'rpx') {
|
||||
zhi = uni.rpx2px(parseFloat(rpx))
|
||||
}
|
||||
return zhi
|
||||
})
|
||||
|
||||
const _showCount = computed(() : boolean => props.showCount)
|
||||
const _round = computed(() : string => checkIsCssUnit(props.round, xConfig.unit))
|
||||
const _flat = computed(() : boolean => props.flat)
|
||||
const _gutter = computed(() : string => {
|
||||
if (props.flat) {
|
||||
return checkIsCssUnit(props.gutter, xConfig.unit)
|
||||
}
|
||||
return '-' + checkIsCssUnit(props.gutter, xConfig.unit)
|
||||
})
|
||||
const _gutterBypx = computed(() : number => {
|
||||
let rpx = checkIsCssUnit(props.gutter, xConfig.unit);
|
||||
let unit = getUnit(rpx)
|
||||
let zhi = parseFloat(rpx)
|
||||
if (unit == 'rpx') {
|
||||
zhi = uni.rpx2px(parseFloat(rpx))
|
||||
}
|
||||
return zhi
|
||||
})
|
||||
const _count = computed(() : number => props.count)
|
||||
const totalWidth = ref(0)
|
||||
const column = ref(1)
|
||||
const row = ref(0)
|
||||
let tid = 0
|
||||
|
||||
const calcColumn = (width : number) => {
|
||||
totalWidth.value = width;
|
||||
if (_sizeBypx.value > 0) {
|
||||
column.value = Math.floor((totalWidth.value + _gutterBypx.value) / (_sizeBypx.value + _gutterBypx.value * (props.flat ? 1 : -1)));
|
||||
}
|
||||
if (column.value > 0 && _list.value.length > 0) {
|
||||
row.value = Math.ceil(props.list.length / column.value)
|
||||
}
|
||||
}
|
||||
const getNodeinfo = () => {
|
||||
uni.createSelectorQuery()
|
||||
.in(getCurrentInstance()?.proxy)
|
||||
.select(".xAvatarGroup")
|
||||
.boundingClientRect((res : any) => {
|
||||
let node = res as NodeInfo
|
||||
calcColumn(node.width!)
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
|
||||
const createrObr = () => {
|
||||
// #ifndef MP
|
||||
let ele = xAvatarGroup.value as UniElement | null;
|
||||
|
||||
if (ele == null) return;
|
||||
if (resizeObserver == null) {
|
||||
resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target == ele) {
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function () {
|
||||
let bund = ele!.getBoundingClientRect()
|
||||
calcColumn(bund.width)
|
||||
}, 150);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resizeObserver!.observe(ele!)
|
||||
// #endif
|
||||
}
|
||||
const removeObr = () => {
|
||||
// #ifndef MP
|
||||
resizeObserver?.disconnect()
|
||||
// #endif
|
||||
}
|
||||
const itemClick = (item : XAVATARGROUPITEMTYPE, index : number) => {
|
||||
emits('click', item.name, index)
|
||||
}
|
||||
const moreClick = () => {
|
||||
emits('moreClick')
|
||||
}
|
||||
onMounted(() => {
|
||||
getNodeinfo()
|
||||
// #ifndef MP
|
||||
createrObr();
|
||||
// #endif
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearTimeout(tid)
|
||||
// #ifndef MP
|
||||
removeObr()
|
||||
// #endif
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.xAvatarGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
}
|
||||
|
||||
.xAvatarGroupImage {
|
||||
position: relative;
|
||||
/* transition-duration: 200ms;
|
||||
transition-property: margin-right, margin-bottom;
|
||||
transition-timing-function: linear; */
|
||||
}
|
||||
|
||||
.xAvatarGroupImageMore {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xAvatarGroupImageBytext {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xAvatarGroupWrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.xAvatarGroupNoWrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xProvitae } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 返回顶部 xBacktop
|
||||
* @page /pages/index/backtop
|
||||
* @category 导航组件
|
||||
* @description 在uvue页面中,根节点一定是scroll-view并且设置为flex:1才可滚动到顶部。
|
||||
* 如果你想局部放到scroll-view组件中,你需要scroll事件中的top传递到属性scrollTop上并启用局部滚动置顶
|
||||
* 如果你想改变或者自定位置,你可以直接在组件上写style来覆盖定位。详见:https://doc.dcloud.net.cn/uni-app-x/api/page-scroll-to.html
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xBacktop" })
|
||||
|
||||
type xBacktopPropsType = {
|
||||
/**
|
||||
* 圆角,空值时取全局的drawr圆角。
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 向下滚动页面时,如果超过此值显示返回顶部按钮。
|
||||
*/
|
||||
offset: number,
|
||||
/**
|
||||
* 背景,支持渐变值如:linear-gradient(to left, #FFED46, #FF7EC7)
|
||||
* 默认空值,取全局主题值。
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 图标颜色。
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* 图标大小
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 如果你想让本组件放置到局部的scroll中时,你外部scrollview通过scroll事件取得距离顶部的位置传递到此。
|
||||
*/
|
||||
scrollTop: number,
|
||||
/**
|
||||
* 禁用页面级根节点滚动后,可以通过scrollTop来实现局部置顶功能。
|
||||
*/
|
||||
disabledPageScroll:boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xBacktopPropsType>(), {
|
||||
round: "50px",
|
||||
offset: 100,
|
||||
bgColor: "",
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
color: "white",
|
||||
icon: "skip-up-fill",
|
||||
iconSize: "30",
|
||||
disabledPageScroll:false,
|
||||
scrollTop:0,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 点击时触发
|
||||
*/
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const opened = ref<boolean>(false)
|
||||
|
||||
// 计算属性scrollTop
|
||||
const _stop = computed((): number => {
|
||||
if(props.disabledPageScroll){
|
||||
return props.scrollTop
|
||||
}
|
||||
return xProvitae.scrollTop;
|
||||
})
|
||||
const _icon = computed((): string => props.icon)
|
||||
const _iconSize = computed((): string => props.iconSize)
|
||||
const _round = computed((): string => checkIsCssUnit(props.round, xConfig.unit))
|
||||
|
||||
const _width = computed((): number => {
|
||||
let p = parseInt(props.width)
|
||||
if (props.width.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p)
|
||||
}
|
||||
return Math.floor(p)
|
||||
})
|
||||
|
||||
const _height = computed((): number => {
|
||||
let p = parseInt(props.height)
|
||||
if (props.height.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p)
|
||||
}
|
||||
return Math.floor(p)
|
||||
})
|
||||
|
||||
const _styleMap = computed((): Map<string, any> => {
|
||||
const styleMap = new Map<string, any>()
|
||||
styleMap.set('width', _width.value.toString() + 'px')
|
||||
styleMap.set('height', _height.value.toString() + 'px')
|
||||
styleMap.set('borderRadius', _round.value)
|
||||
if (props.bgColor.indexOf('linear-gradient') > -1) {
|
||||
styleMap.set('backgroundImage', props.bgColor)
|
||||
} else {
|
||||
const color = props.bgColor == "" ? getDefaultColor(xConfig.color) : getDefaultColor(props.bgColor)
|
||||
styleMap.set('backgroundColor', color)
|
||||
}
|
||||
if(props.disabledPageScroll){
|
||||
styleMap.set('position', 'absolute')
|
||||
}else{
|
||||
styleMap.set('position', 'fixed')
|
||||
}
|
||||
|
||||
styleMap.set('transform', opened.value ? 'scale(1)' : 'scale(0)')
|
||||
styleMap.set('opacity', opened.value ? '1' : '0')
|
||||
return styleMap
|
||||
})
|
||||
|
||||
const _color = computed((): string => getDefaultColor(props.color))
|
||||
|
||||
// 监听器
|
||||
watch(_stop, (newValue: number) => {
|
||||
opened.value = newValue >= props.offset
|
||||
})
|
||||
|
||||
// 方法
|
||||
function onclick(): void {
|
||||
/**
|
||||
* 点击组件时触发。
|
||||
*/
|
||||
emits('click')
|
||||
if(props.disabledPageScroll) return;
|
||||
|
||||
/**
|
||||
* h5端的uni.pageScrollTo有bug,延迟厉害,性能差,不如原生快。
|
||||
*/
|
||||
// #ifdef WEB
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: 650
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="onclick" class="xBackTop" :style="_styleMap">
|
||||
<!--
|
||||
@slot 默认图标插槽
|
||||
-->
|
||||
<slot>
|
||||
<x-icon :font-size="_iconSize" :name="_icon" :color="_color"></x-icon>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xBackTop {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
right: 16px;
|
||||
bottom: 24px;
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
/* box-shadow: 0 5px 24px rgba(0, 0, 0, 0.06); */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, nextTick, getCurrentInstance } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type positionType = "right" | "left" | "bottomLeft" | "bottomRight" | 'top' | 'bottom'
|
||||
|
||||
/**
|
||||
* @name 角标 xBadge
|
||||
* @page /pages/index/badge
|
||||
* @category 展示组件
|
||||
* @description 角标
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xBadge"})
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
|
||||
type xBadgePropsType = {
|
||||
/**
|
||||
* 文字大小,可数字或者带单位
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 背景颜色,合法和颜色值及主题名称
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 文字颜色,合法和颜色值及主题名称
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 是否显示为点,优先级小于count,label
|
||||
*/
|
||||
dot: boolean,
|
||||
/**
|
||||
* 是否显示为文本数字,优先级小于label
|
||||
*/
|
||||
count: number,
|
||||
/**
|
||||
* 为数字时大于此值显示+号
|
||||
*/
|
||||
maxCount: number,
|
||||
/**
|
||||
* 是否显示为文本,优先级最大
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
position: positionType,
|
||||
/**
|
||||
* 偏移
|
||||
*/
|
||||
offset: number[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xBadgePropsType>(), {
|
||||
fontSize: "9",
|
||||
bgColor: "error",
|
||||
fontColor: "white",
|
||||
dot: true,
|
||||
count: 0,
|
||||
maxCount: 99,
|
||||
label: "",
|
||||
position: "right",
|
||||
offset: ():number[] => [0, 0] as number[]
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const padding = ref<string>('4px 4px')
|
||||
const test = ref<Map<string, string>>(new Map<string, string>([['border', '2px solid red'], ['background-color', 'green']]))
|
||||
|
||||
// 方法
|
||||
function getNodeInfo(): void {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".xBadge-countAndLabel")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
if(ret.length==0) return;
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
if(nodeinfo==null) return;
|
||||
let width = nodeinfo.width as number
|
||||
let height = nodeinfo.height as number
|
||||
let max = Math.max(width, height)
|
||||
let px = Math.ceil(max / 2);
|
||||
// let py = max / 2;
|
||||
padding.value = `${px}px ${px}px`;
|
||||
})
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const _offset = computed((): number[] => {
|
||||
return props.offset;
|
||||
})
|
||||
|
||||
const _isDot = computed((): boolean => {
|
||||
if (props.label != "" || props.count > 0 || !props.dot) return false;
|
||||
return true;
|
||||
})
|
||||
|
||||
const _fontColor = computed((): string => {
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
return checkIsCssUnit(props.fontSize, xConfig.unit)
|
||||
})
|
||||
|
||||
const _label = computed((): string => {
|
||||
if (props.label != "") return props.label;
|
||||
if (props.count > 0 && props.count <= props.maxCount) return props.count.toString();
|
||||
if (props.count <= 0) return ""
|
||||
return props.maxCount.toString() + "+"
|
||||
})
|
||||
|
||||
const _cStyles = computed((): Map<string, string>[] => {
|
||||
let trs = ''
|
||||
if (props.position == 'right') {
|
||||
trs = 'translate(50%, -50%)'
|
||||
} else if (props.position == 'left') {
|
||||
trs = 'translate(-50%, -50%)'
|
||||
} else if (props.position == 'bottomLeft') {
|
||||
trs = 'translate(-50%, 50%)'
|
||||
} else if (props.position == 'bottomRight') {
|
||||
trs = 'translate(50%, 50%)'
|
||||
} else if (props.position == 'top') {
|
||||
trs = 'translate(0%, -50%)'
|
||||
} else if (props.position == 'bottom') {
|
||||
trs = 'translate(0%, 50%)'
|
||||
}
|
||||
let top = ''
|
||||
let bottom = ''
|
||||
let left = ''
|
||||
let right = ''
|
||||
if (props.position == 'top') {
|
||||
top = '0px'
|
||||
left = 'auto'
|
||||
right = 'auto'
|
||||
} else if (props.position == 'bottom') {
|
||||
bottom = '0px'
|
||||
left = 'auto'
|
||||
right = 'auto'
|
||||
} else if (props.position == 'right') {
|
||||
top = _offset.value[1].toString() + 'px'
|
||||
right = _offset.value[0].toString() + 'px'
|
||||
} else if (props.position == 'left') {
|
||||
top = '0px'
|
||||
left = '0px'
|
||||
} else if (props.position == 'bottomLeft') {
|
||||
bottom = '0px'
|
||||
left = '0px'
|
||||
} else if (props.position == 'bottomRight') {
|
||||
bottom = '0px'
|
||||
right = '0px'
|
||||
}
|
||||
|
||||
let dotMapCs = new Map<string, string>()
|
||||
dotMapCs.set("background", getDefaultColor(props.bgColor))
|
||||
dotMapCs.set("left", left)
|
||||
dotMapCs.set("right", right)
|
||||
dotMapCs.set("top", top)
|
||||
dotMapCs.set("bottom", bottom)
|
||||
dotMapCs.set("transform", trs)
|
||||
|
||||
let labelMapCs = new Map<string, string>()
|
||||
|
||||
labelMapCs.set("background", getDefaultColor(props.bgColor))
|
||||
labelMapCs.set("left", left)
|
||||
labelMapCs.set("right", right)
|
||||
labelMapCs.set("top", top)
|
||||
labelMapCs.set("bottom", bottom)
|
||||
labelMapCs.set("transform", trs)
|
||||
labelMapCs.set("visibility", _label.value == "" ? "hidden" : "visible")
|
||||
|
||||
nextTick(() => {
|
||||
getNodeInfo();
|
||||
})
|
||||
|
||||
return [dotMapCs, labelMapCs] as Map<string, string>[]
|
||||
})
|
||||
|
||||
// 监听器
|
||||
watch([
|
||||
(): string => props.label,
|
||||
(): string => props.position,
|
||||
(): number => props.count,
|
||||
(): number[] => props.offset,
|
||||
], (): void => {
|
||||
nextTick(() => {
|
||||
getNodeInfo();
|
||||
})
|
||||
})
|
||||
// 生命周期
|
||||
onMounted((): void => {
|
||||
getNodeInfo();
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xBadge" :style='{padding:padding}'>
|
||||
<view class="xBadgeWrap">
|
||||
<text :style='_cStyles[0]!' class="xBadge-dot" :class="[_isDot?'noneShow':'nonex']"></text>
|
||||
<view id="xBadge-countAndLabel" class="xBadge-countAndLabel" :class="[_isDot?'nonex':'noneShow']"
|
||||
:style='_cStyles[1]!'>
|
||||
<text class="xBadge-countAndLabelText"
|
||||
:style='{color:_fontColor,fontSize:_fontSize}'>{{_label}}</text>
|
||||
</view>
|
||||
|
||||
<!--
|
||||
@slot 默认内容区域,你的正常内容放置在标签内
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xBadge {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.xBadge-countAndLabel {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
border-radius: 100px;
|
||||
/* min-width: 16px; */
|
||||
padding: 0rpx 4px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xBadge-countAndLabelText {
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xBadge-dot {
|
||||
position: absolute;
|
||||
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 18px;
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.noneShow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nonex {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.xBadgeWrap {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
class code128b {
|
||||
// 输入数据
|
||||
data = ""
|
||||
|
||||
// Code 128 模式表:索引0..106 -> 二进制模块串(1=黑,0=白),来自业界通用表
|
||||
// 参考:StartB=104 -> 11010010000, Stop=106 -> 1100011101011
|
||||
private static PATTERNS = [
|
||||
"11011001100","11001101100","11001100110","10010011000","10010001100","10001001100",
|
||||
"10011001000","10011000100","10001100100","11001001000","11001000100","11000100100",
|
||||
"10110011100","10011011100","10011001110","10111001100","10011101100","10011100110",
|
||||
"11001110010","11001011100","11001001110","11011100100","11001110100","11101101110",
|
||||
"11101001100","11100101100","11100100110","11101100100","11100110100","11100110010",
|
||||
"11011011000","11011000110","11000110110","10100011000","10001011000","10001000110",
|
||||
"10110001000","10001101000","10001100010","11010001000","11000101000","11000100010",
|
||||
"10110111000","10110001110","10001101110","10111011000","10111000110","10001110110",
|
||||
"11101110110","11010001110","11000101110","11011101000","11011100010","11011101110",
|
||||
"11101011000","11101000110","11100010110","11101101000","11101100010","11100011010",
|
||||
"11101111010","11001000010","11110001010","10100110000","10100001100","10010110000",
|
||||
"10010000110","10000101100","10000100110","10110010000","10110000100","10011010000",
|
||||
"10011000010","10000110100","10000110010","11000010010","11001010000","11110111010",
|
||||
"11000010100","10001111010","10100111100","10010111100","10010011110","10111100100",
|
||||
"10011110100","10011110010","11110100100","11110010100","11110010010","11011011110",
|
||||
"11011110110","11110110110","10101111000","10100011110","10001011110","10111101000",
|
||||
"10111100010","11110101000","11110100010","10111011110","10111101110","11101011110",
|
||||
"11110101110","11010000100","11010010000","11010011100","1100011101011"
|
||||
]
|
||||
|
||||
// Code Set B: 字符到值(0..95 => ASCII 32..127)
|
||||
private mapCharToValueB(ch:string):number {
|
||||
const code = ch.charCodeAt(0)!;
|
||||
|
||||
if (code < 32 || code > 127) {
|
||||
return -1
|
||||
}
|
||||
return code - 32
|
||||
}
|
||||
|
||||
constructor(text:string) {
|
||||
this.data = text
|
||||
}
|
||||
|
||||
encode():string {
|
||||
if (this.data.length == 0) return ""
|
||||
const values = [] as number[]
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
values.push(this.mapCharToValueB(this.data.charAt(i)))
|
||||
}
|
||||
// 起始码 CodeB=104
|
||||
const startVal = 104
|
||||
let checksum = startVal
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
checksum += values[i] * (i + 1)
|
||||
}
|
||||
checksum = checksum % 103
|
||||
// 组合:StartB + 数据 + 校验 + Stop
|
||||
const bits = [] as string[]
|
||||
bits.push(code128b.PATTERNS[startVal])
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
bits.push(code128b.PATTERNS[values[i]])
|
||||
}
|
||||
bits.push(code128b.PATTERNS[checksum])
|
||||
// Stop=106
|
||||
bits.push(code128b.PATTERNS[106])
|
||||
return bits.join("")
|
||||
}
|
||||
}
|
||||
|
||||
export { code128b };
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 商品条码严格按照国际通用码
|
||||
* @author tmzdy
|
||||
* @description 资料参考:https://zhuanlan.zhihu.com/p/120676811
|
||||
*/
|
||||
class ean13 {
|
||||
//编码数据
|
||||
data=""
|
||||
//护码
|
||||
safeCode = "101"
|
||||
middleCode = "01010"
|
||||
//编码方式,我们中国是6:LGGGLL
|
||||
EAN13_STRUCTURE = [
|
||||
'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG',
|
||||
'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL'
|
||||
];
|
||||
|
||||
|
||||
constructor(text:string) {
|
||||
let data = text;
|
||||
if (data.search(/^[0-9]{12}$/)!== -1 as number ) {
|
||||
data += (this.checksum(data)).toString();
|
||||
}
|
||||
this.data = data.toUpperCase();
|
||||
}
|
||||
//校验码。
|
||||
checksum(codestr:string):number{
|
||||
const res = codestr.substring(0, 12).split('')
|
||||
const resNumber = res.map((n:string):number => parseInt(n))
|
||||
|
||||
const jg=resNumber.reduce((sum:number, a:number, idx:number):number => (idx % 2) > 0 ? sum + a * 3 : sum + a, 0);
|
||||
|
||||
return (10 - (jg % 10)) % 10;
|
||||
}
|
||||
//按钮编码规则提取逻辑值,
|
||||
getEan13Str(key:string):string[]{
|
||||
if(key=="L") return [
|
||||
'0001101', '0011001', '0010011', '0111101', '0100011',
|
||||
'0110001', '0101111', '0111011', '0110111', '0001011'
|
||||
]
|
||||
if(key=="G") return [
|
||||
'0100111', '0110011', '0011011', '0100001', '0011101',
|
||||
'0111001', '0000101', '0010001', '0001001', '0010111'
|
||||
]
|
||||
if(key=="R") return [
|
||||
'1110010', '1100110', '1101100', '1000010', '1011100',
|
||||
'1001110', '1010000', '1000100', '1001000', '1110100'
|
||||
]
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
encodeByGuzhe(dataStr:string, structure:string):string{
|
||||
let encoded = dataStr.split('')
|
||||
let atrartys = [] as string[][]
|
||||
for(let i=0;i<encoded.length;i++){
|
||||
let chart = structure.substring(i,i+1)
|
||||
atrartys.push(this.getEan13Str(chart))
|
||||
}
|
||||
|
||||
let encoded_str = atrartys.map((val:string[], idx:number):string => {
|
||||
let stur = dataStr.substring(idx,idx+1)
|
||||
return val[parseInt(stur)]
|
||||
});
|
||||
|
||||
return encoded_str.join("")
|
||||
};
|
||||
encode():string[] {
|
||||
// 取编码方式。
|
||||
let codeFang = this.EAN13_STRUCTURE[parseInt(this.data.substring(0,1))]
|
||||
//取后面的12位数字
|
||||
let data = this.data.substring(1,13)
|
||||
let leftCode = data.substring(0,6)
|
||||
let rightCode = data.substring(6,13)
|
||||
|
||||
let left_code_str = this.encodeByGuzhe(leftCode,codeFang);
|
||||
//右资料码按规则是物料码+校验码
|
||||
let right_code_str = this.encodeByGuzhe(rightCode,"RRRRRR");
|
||||
// 组合码制
|
||||
//护码+左资料码+中间码+右资料码+护码
|
||||
// let totalcode = this.safeCode+left_code_str+this.middleCode+right_code_str+this.safeCode
|
||||
return [this.safeCode,left_code_str,this.middleCode,right_code_str,this.safeCode]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export { ean13 };
|
||||
@@ -0,0 +1,245 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { code128b } from "./utsbarcode/code128.uts"
|
||||
import { ean13 } from "./utsbarcode/ean13.uts"
|
||||
|
||||
/**
|
||||
* @name 条码 xBarcode
|
||||
* @page /pages/index/barcode
|
||||
* @category 其它组件
|
||||
* @description 本条码暂只开发了cdoebar,ean13两种。ean13是国际通用码,也是国内商品的码,
|
||||
* 我严格按照编码规则进行开发,所以提供商品码时,一定要准确。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: ('xCircleProgeress-' + getUid()) as string,
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 窗口宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 宽器高,这将影响条码的高度
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: '140px'
|
||||
},
|
||||
/**
|
||||
* 上下间隙,单位是px
|
||||
*/
|
||||
pading: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
/**
|
||||
* 条码颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "black"
|
||||
},
|
||||
/**
|
||||
* 目前我仅开发两种常见的国内格式
|
||||
* codebar正常的数字字符条码
|
||||
* ean13国际通用物品编码,也是国内的商品码以69开头。
|
||||
*/
|
||||
encode: {
|
||||
type: String as PropType<"codebar" | "ean13" | "code128">,
|
||||
default: "ean13"
|
||||
},
|
||||
/**
|
||||
* 条码内容
|
||||
*/
|
||||
text: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
_width() : string {
|
||||
return checkIsCssUnit(this.width, 'rpx')
|
||||
},
|
||||
_height() : string {
|
||||
|
||||
return checkIsCssUnit(this.height, 'rpx')
|
||||
},
|
||||
_color() : string {
|
||||
return getDefaultColor(this.color);
|
||||
},
|
||||
_text() : string {
|
||||
return this.text;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
text(newValue : string) {
|
||||
if (newValue == "") return;
|
||||
this.getNodeInfo();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.text == "") return;
|
||||
// #ifndef APP-HARMONY
|
||||
this.getNodeInfo();
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
let t = this;
|
||||
setTimeout(function() {
|
||||
t.getNodeInfo();
|
||||
}, 120);
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
getNodeInfo() {
|
||||
let _this = this;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xBarcode")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
this.boxWidth = nodeinfo.width!
|
||||
this.boxHeight = nodeinfo.height!
|
||||
// #ifdef APP-IOS || WEB
|
||||
_this.dreawer()
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID||APP-HARMONY
|
||||
setTimeout(function() {
|
||||
_this.dreawer()
|
||||
}, 50);
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
dreawer() {
|
||||
this.clear()
|
||||
|
||||
let canvas = this.$refs['xBarcode'] as UniCanvasElement
|
||||
let ctx = canvas.getContext('2d')!
|
||||
// 处理高清屏逻辑
|
||||
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
||||
canvas.width = canvas.offsetWidth * dpr;
|
||||
canvas.height = canvas.offsetHeight * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
let ratio = 1;
|
||||
let code = ""
|
||||
let eanCode = [] as string[]
|
||||
if (this.encode == "ean13") {
|
||||
eanCode = new ean13(this._text).encode()
|
||||
code = eanCode.join("")
|
||||
} else if (this.encode == "codebar" || this.encode == "code128") {
|
||||
code = new code128b(this._text).encode()
|
||||
}
|
||||
let strCode = code.split("");
|
||||
let linewidth = 2;
|
||||
let totalWidth = strCode.length * (linewidth);
|
||||
let barheight = this.boxHeight - this.pading * 2;
|
||||
let start_x = (this.boxWidth - totalWidth) / 2
|
||||
let start_y = this.pading
|
||||
ctx!.beginPath()
|
||||
ctx!.fillStyle = this._color
|
||||
|
||||
if (this.encode == "codebar" || this.encode == "code128") {
|
||||
ctx.font = `${16 * ratio}px Arial`;
|
||||
let texts = this._text.split("")
|
||||
let textwidth = ctx.measureText(this._text).width;
|
||||
let space = (totalWidth - textwidth)/(texts.length-1)
|
||||
// let textwidth = ctx.measureText(this._text).width + (texts.length-1) * 6
|
||||
// 绘制数字。
|
||||
let sx = (this.boxWidth-totalWidth)/2;
|
||||
for (let a0 = 0; a0 < texts.length; a0++) {
|
||||
let sxx = (ctx.measureText(texts[a0]).width+space) * a0 + sx
|
||||
ctx!.fillText(texts[a0], sxx * ratio, (start_y + barheight) * ratio)
|
||||
}
|
||||
|
||||
for (let i = 0; i < strCode.length; i++) {
|
||||
if (strCode[i] == "1") {
|
||||
ctx!.fillRect((i * linewidth + start_x + linewidth) * ratio, start_y * ratio, linewidth * ratio, (barheight - 24) * ratio)
|
||||
}
|
||||
}
|
||||
} else if (this.encode == "ean13") {
|
||||
let k = 0;
|
||||
ctx.font = `${13 * ratio}px Arial`;
|
||||
ctx!.fillText(this._text.substring(0, 1), (start_x - 10) * ratio, (start_y + barheight) * ratio)
|
||||
for (let j = 0; j < eanCode.length; j++) {
|
||||
let item = eanCode[j];
|
||||
let itemcodeas = item.split("")
|
||||
let offsetHeigt = (j == 0 || j == 2 || j == 4) ? 0 : 12
|
||||
// 绘制数字。
|
||||
if (j == 0) {
|
||||
let ncsr = this._text.substring(1, 7).split("")
|
||||
for (let a0 = 0; a0 < ncsr.length; a0++) {
|
||||
ctx!.fillText(ncsr[a0], (start_x + 10 + (a0 * 13) + linewidth) * ratio, (start_y + barheight) * ratio)
|
||||
}
|
||||
}
|
||||
if (j == 3) {
|
||||
let ncsr = this._text.substring(7).split("")
|
||||
for (let a0 = 0; a0 < ncsr.length; a0++) {
|
||||
ctx!.fillText(ncsr[a0], ((this.boxWidth / 2) + 10 + (a0 * 13) + linewidth) * ratio, (start_y + barheight) * ratio)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < itemcodeas.length; i++) {
|
||||
if (itemcodeas[i] == "1") {
|
||||
ctx!.fillRect(
|
||||
(k * linewidth + start_x + linewidth) * ratio,
|
||||
start_y * ratio,
|
||||
linewidth * ratio,
|
||||
(barheight - offsetHeigt) * ratio)
|
||||
}
|
||||
++k;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
ctx.fill()
|
||||
|
||||
},
|
||||
clear() {
|
||||
let canvas = this.$refs['xBarcode'] as UniCanvasElement
|
||||
let ctx = canvas.getContext('2d')!
|
||||
// 处理高清屏逻辑
|
||||
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
||||
canvas.width = canvas.offsetWidth * dpr;
|
||||
canvas.height = canvas.offsetHeight * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
let ratio = 1;
|
||||
|
||||
try{
|
||||
ctx?.reset()
|
||||
}catch(e){
|
||||
//TODO handle the exception
|
||||
}
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)'
|
||||
ctx.fillRect(0, 0, this.boxWidth, this.boxHeight)
|
||||
ctx.fill()
|
||||
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<canvas class="xBarcode" ref="xBarcode" :id="id" :style="{width:_width,height:_height}"></canvas>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import { checkIsCssUnit, getUid } from '../../core/util/xCoreUtil.uts'
|
||||
import { BARRAGE_ITEM_AR } from "../x-barrage/interface.uts"
|
||||
import { PropType } from "vue"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 弹幕子节点 BarrageItem
|
||||
* @page /pages/index/barrage
|
||||
* @category 反馈组件
|
||||
* @description 弹幕内部私有组件,不要引用。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: 'xBarrage-' + getUid(),
|
||||
real_w: 0,
|
||||
real_h: 0,
|
||||
totalWidth: 10000,
|
||||
dur: 0,
|
||||
endX: 0,
|
||||
rightX: 0,
|
||||
status: 'none',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 速度
|
||||
*/
|
||||
speed: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 速度
|
||||
*/
|
||||
boxWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
data: {
|
||||
type: Array as PropType<BARRAGE_ITEM_AR[]>,
|
||||
default: () : BARRAGE_ITEM_AR[] => [] as BARRAGE_ITEM_AR[]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getNodes();
|
||||
},
|
||||
methods: {
|
||||
getNodes() {
|
||||
if (this.status == 'play') return;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xBarrageItemTextContentBox")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
this.real_w = nodeinfo.width!;
|
||||
this.real_h = nodeinfo.height!;
|
||||
let syw = this.boxWidth + this.real_w;
|
||||
this.totalWidth = syw
|
||||
this.dur = Math.ceil(syw / this.speed) * 1000;
|
||||
this.endX = this.boxWidth + this.real_w
|
||||
this.status = 'play'
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view class="xBarrageItemTextContent" :style="{width:totalWidth+'px'}">
|
||||
<view class="xBarrageItemTextContentBox" :style="{
|
||||
'transition-duration':dur+'ms',
|
||||
'transform':`translateX(-${endX}px)`,
|
||||
'transition-delay':index*800,
|
||||
|
||||
}">
|
||||
<text v-for="(item) in data" :key="item.id" class="xBarrageItemText">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xBarrageItemTextContent {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.xBarrageItemTextContentBox {
|
||||
transition-property: transform;
|
||||
transition-timing-function: linear;
|
||||
/* transform: translateX(100%); */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xBarrageItemText {
|
||||
padding: 0 10px;
|
||||
height: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.64);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 110px;
|
||||
border-radius: 20px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
margin-right: 20px;
|
||||
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
export type BARRAGE_ITEM_AR = {
|
||||
id:string,
|
||||
top:string,
|
||||
parentIndex:number,
|
||||
chirenIndex:number,
|
||||
label:string,
|
||||
delay:number
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { checkIsCssUnit, getUid, splitArrayByGroup } from '../../core/util/xCoreUtil.uts'
|
||||
|
||||
// import barrageText from "./barrage-text.uvue"
|
||||
import { BARRAGE_ITEM_AR } from "./interface.uts"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 弹幕 Barrage
|
||||
* @page /pages/index/barrage
|
||||
* @category 反馈组件
|
||||
* @description 弹幕,当前版本比较初级。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
// components: {
|
||||
// 'barrage-text': barrageText
|
||||
// },
|
||||
data() {
|
||||
return {
|
||||
id: 'xBarrage-' + getUid(),
|
||||
real_w: 0,
|
||||
real_h: 0,
|
||||
datas: [] as BARRAGE_ITEM_AR[][]
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 弹幕的总高度。如果你的容器高小于此高会被裁切。
|
||||
*/
|
||||
layerHeight: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 字符数组
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => [] as string[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_list() : string[] {
|
||||
return this.list
|
||||
},
|
||||
_height() : string {
|
||||
if (this.layerHeight == "") return '100%'
|
||||
return checkIsCssUnit(this.layerHeight, 'rpx')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.chuliDatas();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getNodes();
|
||||
},
|
||||
methods: {
|
||||
chuliDatas() {
|
||||
let maxLines = Math.floor(this.real_h / 34);
|
||||
let ps = splitArrayByGroup(this.list, maxLines)
|
||||
let mbl = ps.map((el : string[], index : number) : BARRAGE_ITEM_AR[] => {
|
||||
return el.map((item : string, chirenIndex : number) : BARRAGE_ITEM_AR => {
|
||||
let tels = {
|
||||
id: 'xBarrage_item_id' + getUid(),
|
||||
top: (index * 24 + 10).toString() + 'px',
|
||||
label: item,
|
||||
parentIndex: index,
|
||||
chirenIndex,
|
||||
delay: index * 500 + (chirenIndex) * 1500
|
||||
} as BARRAGE_ITEM_AR
|
||||
return tels
|
||||
})
|
||||
})
|
||||
this.datas = mbl
|
||||
},
|
||||
getIds(index : number) : string {
|
||||
return 'xBarrageBOx-' + getUid() + '-' + (index).toString()
|
||||
},
|
||||
getNodes() {
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select("#" + this.id)
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
this.real_w = nodeinfo.width!;
|
||||
this.real_h = nodeinfo.height!;
|
||||
this.chuliDatas();
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<!--
|
||||
@slot 默认内容插槽,内容的高度至少要大于弹幕整体的layerHeight高度。
|
||||
-->
|
||||
<slot></slot>
|
||||
<view class="xBarrageWrap" :id="id" :style="{height:_height}">
|
||||
<x-barrage-item v-for="(item2,index2) in datas" :key='getIds(index2)' :index="index2" :boxWidth="real_w"
|
||||
:data="item2" :style="{
|
||||
top:(index2*34+10).toString()+'px',
|
||||
}"></x-barrage-item>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xBarrageWrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,736 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, setPagePullRefresh, getPagePullRefresh ,checkIsCssUnit} from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xDate, xDateTypeTime, createDate } from "../../core/util/xDate.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { PICKER_ITEM_INFO } from "../../interface.uts"
|
||||
import { DateTimeFormatOrNull} from "@/uni_modules/x-vuei18n-s/interface.uts"
|
||||
|
||||
type coverValueType = {
|
||||
value : string[],
|
||||
str : string
|
||||
}
|
||||
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
/**
|
||||
* @name 时间区间选择 xBetweenTime
|
||||
* @description 快速的时间区间选择器,方便时间选择自动判断前后时间大小并校正。
|
||||
* @page /pages/index/between-time
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xBetweenTime"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
|
||||
defineSlots<{
|
||||
default(props: { show: boolean, startVal: string, endVal: string }): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认触发
|
||||
* @param {string} date 当前选中时间id值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 快速日期被选中时触发
|
||||
* @param {UTSJSONObject<{text:string,value:string[]}>} item - 当前选中时间组
|
||||
*/
|
||||
'dateClick',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
type xBetweenTimePropsType = {
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
modelValue: string[],
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: string,
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: boolean,
|
||||
/**
|
||||
* 顶部标题
|
||||
* 空默认为:请选择时间(根据语言不同也不同)
|
||||
* 如果你提供了值,组件内的多语言失效以你设定的为准
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 开始时间,请提供正确的时间格式
|
||||
*/
|
||||
start: string,
|
||||
/**
|
||||
* 结束时间,请提供正确的时间格式
|
||||
*/
|
||||
end: string,
|
||||
/**
|
||||
* 精确到的级别,这里只是展示,具体的返回值还是完整的值。
|
||||
* year:年
|
||||
* month:年月
|
||||
* day:年月日
|
||||
* hour:年月日小时
|
||||
* minute:年月日小时分钟
|
||||
* second:年月日小时分钟秒
|
||||
*/
|
||||
type: ModelType,
|
||||
/**
|
||||
* 输出时间格式,只对v-model:modelStr及输入框展示有效YYYY-MM-DD,为空时由当前语文决定
|
||||
* 因此它可能不是一个标准时间,比如YY SS ,所以不能作为modelValue使用
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: string,
|
||||
/**
|
||||
* 上方的单位名称
|
||||
* 默认为:['年', '月', '日', '时', '分', '秒']
|
||||
*/
|
||||
cellUnits: string[],
|
||||
/**
|
||||
* 快速时间区间选择,如果直接填写数字字符,会以你提供的数字最近多少来天来算。
|
||||
* d:本日
|
||||
* w:本周
|
||||
* m:本月
|
||||
* y:本年
|
||||
* q:本季度
|
||||
* 7:最近7天,后面的依此类推,数字的就是最近xx天。
|
||||
* px:前x年,p+[x]数字依此类推,表示前x年,如:p1,p2...
|
||||
* 如果提供以下json结构,则以你自定的为准。
|
||||
* UTSJSONObject:{title:'本学年',start:'2025-1-1',end:'2025-12-31'} as UTSJSONObject
|
||||
*/
|
||||
quickDate: Array<any>,
|
||||
/**
|
||||
* 是否懒加载内部内容。
|
||||
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
|
||||
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
|
||||
* 要兼容微信,必须设置为true,非微信可以为false
|
||||
*/
|
||||
lazyContent: boolean,
|
||||
/**
|
||||
* 如果你的快捷选择较多可能会让高度不足,需要自行设置下高。
|
||||
*/
|
||||
drawerSize: string,
|
||||
/**
|
||||
* 是否禁用清除按钮,默认不禁用,允许用户清空选择。点确认,以清空选项数据
|
||||
*/
|
||||
disabledClear: boolean,
|
||||
/**
|
||||
* 是否禁用弹出
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 宽屏时是否让内容剧中显示
|
||||
* 并限制其宽为屏幕宽,只展示中间内容以适应宽屏。
|
||||
*/
|
||||
widthCoverCenter: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xBetweenTimePropsType>(), {
|
||||
modelValue: () : string[] => [] as string[],
|
||||
modelStr: "",
|
||||
modelShow: false,
|
||||
title: "",
|
||||
start: "",
|
||||
end: "",
|
||||
type: "day",
|
||||
format: "",
|
||||
cellUnits: () : string[] => [] as string[],
|
||||
quickDate: () : Array<any> => ['d', 'w', 'm', 'y', 'q'] as Array<any>,
|
||||
lazyContent: true,
|
||||
drawerSize: '540px',
|
||||
disabledClear: false,
|
||||
disabled: false,
|
||||
widthCoverCenter: true
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
let startValue = new xDate();
|
||||
let endValue = new xDate();
|
||||
startValue.subtraction(1, 'y')
|
||||
|
||||
const show = ref(false)
|
||||
const nowValue = ref(['', ''] as string[])
|
||||
const nowModelValue = ref(['', ''] as string[])
|
||||
const startDate = ref(startValue)
|
||||
const endDate = ref(endValue)
|
||||
const changeIndex = ref(0)
|
||||
const yanchiDuration = ref(false)
|
||||
const quicklist = ref([] as coverValueType[])
|
||||
const quicklistSelectedStr = ref('')
|
||||
const tid = ref(1)
|
||||
|
||||
// 计算属性
|
||||
const _formatValStr = computed((): string[] => {
|
||||
let estrt = nowValue.value?.[0]??'';
|
||||
let eend = nowValue.value?.[1]??'';
|
||||
let selfformat = props.format==''?'YYYY-MM-DD':props.format
|
||||
let start = estrt == '' ? '' : (new xDate(estrt)).format(selfformat)
|
||||
let ebd = eend == '' ? '' : (new xDate(eend)).format(selfformat)
|
||||
return [start,ebd] as string[]
|
||||
})
|
||||
|
||||
const _lazyContent = computed((): boolean => props.lazyContent)
|
||||
const _disabledClear = computed((): boolean => props.disabledClear)
|
||||
|
||||
const _start_date = computed((): xDate => {
|
||||
if (props.start == "") return startDate.value
|
||||
return new xDate(props.start)
|
||||
})
|
||||
|
||||
const _end_date = computed((): xDate => {
|
||||
if (props.end == "") return endDate.value
|
||||
return new xDate(props.end)
|
||||
})
|
||||
|
||||
const _start_date_str = computed((): string => _start_date.value.format())
|
||||
const _end_date_str = computed((): string => _end_date.value.format())
|
||||
|
||||
const _start_date_str_format = computed((): string => {
|
||||
// '开始时间'
|
||||
if (nowValue.value[0] == '') return xConfig.i18n.t("tmui4x.betweentTime.start")
|
||||
// const date = (new xDate(nowValue.value[0])).format(props.format);
|
||||
if(props.format!=''){
|
||||
return (new xDate(nowValue.value[0])).format(props.format);
|
||||
}
|
||||
return xConfig.i18n.d(nowValue.value[0],null,{year:"numeric",month:"numeric",day:"numeric"} as DateTimeFormatOrNull)
|
||||
})
|
||||
|
||||
const _end_date_str_format = computed((): string => {
|
||||
// '结束时间'
|
||||
if (nowValue.value[1] == '') return xConfig.i18n.t("tmui4x.betweentTime.end")
|
||||
// (new xDate(nowValue.value[1])).format(props.format)
|
||||
if(props.format!=''){
|
||||
return (new xDate(nowValue.value[1])).format(props.format)
|
||||
}
|
||||
return xConfig.i18n.d(nowValue.value[1],null,{year:"numeric",month:"numeric",day:"numeric"} as DateTimeFormatOrNull)
|
||||
})
|
||||
|
||||
const _backgroundColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(xConfig.inputDarkColor)
|
||||
}
|
||||
return getDefaultColor(xConfig.inputBgColor)
|
||||
})
|
||||
|
||||
const _borderColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(xConfig.borderDarkColor)
|
||||
}
|
||||
return getDefaultColor(xConfig.inputBgColor)
|
||||
})
|
||||
|
||||
const _activeBorderColor = computed((): string => getDefaultColor(xConfig.color))
|
||||
|
||||
const _placeStyle = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
return "color:#c7c7c7;"
|
||||
}
|
||||
return "color:#838383;"
|
||||
})
|
||||
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
return "#efefef"
|
||||
}
|
||||
return "#333"
|
||||
})
|
||||
|
||||
const _isDark = computed((): boolean => xConfig.dark == 'dark')
|
||||
|
||||
const _checkPass = computed((): boolean => nowValue.value.some((el:string):boolean=>el==''))
|
||||
|
||||
const _disabled = computed((): boolean => props.disabled)
|
||||
|
||||
const _cellUnits = computed((): string[] => {
|
||||
if(props.cellUnits.length==0){
|
||||
return [
|
||||
xConfig.i18n.t("tmui4x.pickerDate.year"),
|
||||
xConfig.i18n.t("tmui4x.pickerDate.month"),
|
||||
xConfig.i18n.t("tmui4x.pickerDate.day"),
|
||||
xConfig.i18n.t("tmui4x.pickerDate.hour"),
|
||||
xConfig.i18n.t("tmui4x.pickerDate.minute"),
|
||||
xConfig.i18n.t("tmui4x.pickerDate.second"),
|
||||
]
|
||||
}
|
||||
return props.cellUnits;
|
||||
})
|
||||
|
||||
|
||||
function getTypes() : xDateTypeTime {
|
||||
|
||||
if (props.type == 'year') {
|
||||
return 'y' as xDateTypeTime
|
||||
} else if (props.type == 'month') {
|
||||
return 'm' as xDateTypeTime
|
||||
} else if (props.type == 'day') {
|
||||
return 'd' as xDateTypeTime
|
||||
} else if (props.type == 'hour') {
|
||||
return 'h' as xDateTypeTime
|
||||
} else if (props.type == 'minute') {
|
||||
return 'M' as xDateTypeTime
|
||||
} else if (props.type == 'second') {
|
||||
return 's' as xDateTypeTime
|
||||
}
|
||||
return 's' as xDateTypeTime
|
||||
}
|
||||
|
||||
function coverStrVal() : string {
|
||||
let estrt = nowValue.value[0];
|
||||
let eend = nowValue.value[1];
|
||||
let selfformat = props.format==''?'YYYY-MM-DD':props.format
|
||||
let start = estrt == '' ? '' : (new xDate(estrt)).format(selfformat)
|
||||
let ebd = eend == '' ? '' : (new xDate(eend)).format(selfformat)
|
||||
return start + '~' + ebd
|
||||
}
|
||||
|
||||
function sorDateVaild(str : string[]) : string[] {
|
||||
let types = getTypes()
|
||||
str.sort((a : string, b : string) : number => {
|
||||
return new xDate(a).getTime(types) - new xDate(b).getTime(types)
|
||||
})
|
||||
return str;
|
||||
}
|
||||
|
||||
function validTimeDate(val : string[]) : string[] {
|
||||
// let defaulttime = new xDate().format()
|
||||
let str = ['', '']
|
||||
if (val.length >= 1) {
|
||||
str[0] = val[0]!
|
||||
}
|
||||
if (val.length >= 2) {
|
||||
str[1] = val[1]!
|
||||
}
|
||||
|
||||
return sorDateVaild(str)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function tongbuModelStr(){
|
||||
let str = coverStrVal()
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emits('update:modelStr', str == '~' ? '' : str);
|
||||
}
|
||||
|
||||
function getQuickDateType() : coverValueType[] {
|
||||
let typelist = [] as coverValueType[];
|
||||
let list:Array<any> = props.quickDate;
|
||||
if (list.length == 0) return typelist;
|
||||
let _date = new xDate()
|
||||
let startFDate = "YYYY/MM/DD 00:00:00"
|
||||
let endFDate = "YYYY/MM/DD 23:59:59"
|
||||
let _start = new xDate(new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
|
||||
let _end = new xDate(new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
|
||||
_date = new xDate(_start)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let item : any = list[i];
|
||||
|
||||
if(typeof item == 'string'){
|
||||
if(item.substring(0,1)=='p'){
|
||||
let dshi = parseInt(item.substring(1))! as number
|
||||
let date = _date.getClone()
|
||||
let start = _start
|
||||
date.subtraction(dshi, 'y')
|
||||
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
|
||||
let end = date.format()
|
||||
// '前' + dshi + '年'
|
||||
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle3",dshi) } as coverValueType)
|
||||
}else if (!isNaN(parseInt(item))) {
|
||||
let dshi = parseInt(item)! as number;
|
||||
let date = _date.getClone()
|
||||
let start = _start
|
||||
date.subtraction(dshi, 'd')
|
||||
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
|
||||
let end = date.format()
|
||||
// '最近' + item + '天'
|
||||
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle2",dshi) } as coverValueType)
|
||||
} else {
|
||||
let dshi = 0;
|
||||
let date = new xDate(_end)
|
||||
let startFDate = "YYYY/MM/DD 00:00:00"
|
||||
let endFDate = "YYYY/MM/DD 23:59:59"
|
||||
let start = date.format(startFDate)
|
||||
let end = date.format(endFDate)
|
||||
|
||||
// "本日"
|
||||
let desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",0)
|
||||
if (item == 'w') {
|
||||
start = date.getDateStartOf('w').format(startFDate)
|
||||
end = date.getDateEndOf('w').format(endFDate)
|
||||
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
|
||||
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
|
||||
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",1);//"本周"
|
||||
} else if (item == 'm') {
|
||||
start = date.getDateStartOf('m').format(startFDate)
|
||||
end = date.getDateEndOf('m').format(endFDate)
|
||||
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
|
||||
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
|
||||
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",2);//"本月"
|
||||
} else if (item == 'y') {
|
||||
start = date.getDateStartOf('y').format(startFDate)
|
||||
end = date.getDateEndOf('y').format(endFDate)
|
||||
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
|
||||
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
|
||||
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",3);//"本年"
|
||||
} else if (item == 'q') {
|
||||
let nowq = date.getQuarter('')
|
||||
let itemqatar = nowq[0]
|
||||
start = itemqatar.start
|
||||
end = itemqatar.end
|
||||
start = new xDate(new xDate(start).getBetweenDate(_start_date_str.value,_end_date_str.value,'min')).format(startFDate)
|
||||
end = new xDate(new xDate(end).getBetweenDate(_start_date_str.value,_end_date_str.value,'max')).format(endFDate)
|
||||
desc = xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle",4);//"本季度"
|
||||
}
|
||||
|
||||
typelist.push({ value: [start, end], str: desc } as coverValueType)
|
||||
}
|
||||
}else if(typeof item == 'number'){
|
||||
let dshi = item as number;
|
||||
let date = _date.getClone()
|
||||
let start = _start
|
||||
date.subtraction(dshi, 'd')
|
||||
date = new xDate(date.getBetweenDate(_start_date_str.value,_end_date_str.value,'min'))
|
||||
let end = date.format()
|
||||
// '最近' + item + '天'
|
||||
typelist.push({ value: [end, start], str: xConfig.i18n.t("tmui4x.betweentTime.quiakListTitle2",dshi) } as coverValueType)
|
||||
}
|
||||
|
||||
else if(item instanceof UTSJSONObject){
|
||||
let title = item.getString('title')
|
||||
let v_start = item.getString('start')
|
||||
let v_end = item.getString('end')
|
||||
if(title!=null&&v_start!=null&&v_end!=null){
|
||||
typelist.push({ value: [v_start, v_end], str: title } as coverValueType)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return typelist;
|
||||
}
|
||||
function cancelResetDataCol() {
|
||||
nowValue.value = nowModelValue.value.slice(0)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function clearDate(){
|
||||
let dst = ['',''] as string[]
|
||||
nowValue.value = dst
|
||||
}
|
||||
|
||||
function dateChangeView(datestr : string) {
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function() {
|
||||
quicklistSelectedStr.value = ''
|
||||
let nowvalu = nowValue.value.slice(0)
|
||||
nowvalu[changeIndex.value] = datestr;
|
||||
let strStart = nowvalu[0]
|
||||
let strEnd = nowvalu[1]
|
||||
|
||||
let types = getTypes()
|
||||
if (strEnd != '' && strStart != '') {
|
||||
if (changeIndex.value == 0) {
|
||||
if (new xDate(strStart).isBetweenOf(new xDate(strEnd), '>', types)) {
|
||||
strEnd = strStart
|
||||
}
|
||||
} else if (changeIndex.value == 1) {
|
||||
if (new xDate(strEnd).isBetweenOf(new xDate(strStart), '<', types)) {
|
||||
strStart = strEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nowValue.value = [strStart, strEnd]
|
||||
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function tagsClick(item : coverValueType) {
|
||||
nowValue.value = item.value
|
||||
quicklistSelectedStr.value = item.str
|
||||
|
||||
emits('dateClick',{text:item.str,value:item.value.slice(0)} as UTSJSONObject)
|
||||
}
|
||||
|
||||
function inputClick(index : number) {
|
||||
changeIndex.value = index;
|
||||
|
||||
let nowvalu = nowValue.value.slice(0)
|
||||
let strStart = nowvalu[0]
|
||||
let strEnd = nowvalu[1]
|
||||
|
||||
if (changeIndex.value == 0) {
|
||||
if (strStart == '') {
|
||||
// strStart = strEnd != '' ? strEnd : (endDate.value.format())
|
||||
let tempStart = new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')
|
||||
strStart = new xDate(tempStart).format()
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
} else if (changeIndex.value == 1) {
|
||||
if (strEnd == '') {
|
||||
// strEnd = strStart != '' ? strStart : (endDate.value.format())
|
||||
let tempEnd = new xDate().getBetweenDate(_start_date_str.value,_end_date_str.value,'max')
|
||||
strEnd = new xDate(tempEnd).format()
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openShow() {
|
||||
if(_disabled.value) return;
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', false)
|
||||
cancelResetDataCol()
|
||||
if (_lazyContent.value) {
|
||||
yanchiDuration.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
yanchiDuration.value = true
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emits('cancel')
|
||||
cancelResetDataCol()
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onConfirm() {
|
||||
let nowval = nowModelValue.value.slice(0)
|
||||
|
||||
let str = coverStrVal()
|
||||
let tmdate = [] as string[];
|
||||
if(nowValue.value[0]!=''&&nowValue.value[1]!=''){
|
||||
tmdate = nowValue.value.slice(0)
|
||||
nowModelValue.value = tmdate
|
||||
}
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emits('update:modelValue', tmdate);
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emits('update:modelStr', str == '~' ? '' : str);
|
||||
|
||||
emits('confirm', tmdate);
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch((): string[] => props.modelValue, (newvalue : string[]) => {
|
||||
let sortvalue = sorDateVaild(validTimeDate(newvalue))
|
||||
let newvaluestr = sortvalue.join('');
|
||||
if (newvaluestr == nowModelValue.value.join('')) return;
|
||||
nowValue.value = sortvalue;
|
||||
nowModelValue.value = sortvalue;
|
||||
tongbuModelStr()
|
||||
quicklistSelectedStr.value = ''
|
||||
})
|
||||
|
||||
watch((): boolean => props.modelShow, (newValue : boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
watch((): Array<any> => props.quickDate, (newvalue : Array<any>) => {
|
||||
quicklist.value = getQuickDateType()
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
yanchiDuration.value = _lazyContent.value ? false : true
|
||||
|
||||
let str = validTimeDate(props.modelValue)
|
||||
nowValue.value = str;
|
||||
nowModelValue.value = str;
|
||||
quicklist.value = getQuickDateType()
|
||||
tongbuModelStr()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** 打开选择器 **/
|
||||
open: () => openShow(),
|
||||
/** 关闭选择器 **/
|
||||
close: () => onClose(),
|
||||
/** 清空选择 **/
|
||||
clear: () => clearDate()
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@prop {boolean} show - 控制打开关闭状态
|
||||
@prop {boolean} startVal - 选日期的开始值,可能为空值
|
||||
@prop {boolean} endVal - 选日期的结束值,可能为空值
|
||||
-->
|
||||
<slot :show="show" :startVal="_formatValStr[0]" :endVal="_formatValStr[1]"></slot>
|
||||
</view>
|
||||
<x-drawer :disabledConfirm="_checkPass&&_disabledClear" @open="onOpen" :widthCoverCenter="widthCoverCenter" :disabledScroll="true" @close="onClose"
|
||||
@confirm="onConfirm" @cancel="onCancel" :showFooter="true" v-model:show="show"
|
||||
:size="drawerSize" :showClose="false">
|
||||
<template v-slot:title>
|
||||
<view class="xPickerClear" :style="{height:'50px'}">
|
||||
<x-text>{{title!=''?title:i18n!.t("tmui4x.betweentTime.title")}}</x-text>
|
||||
<!-- 清空 -->
|
||||
<x-text v-if="!_disabledClear" @click="clearDate" style="opacity: 0.5;">{{i18n!.t("tmui4x.clear")}}</x-text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="xPickerDateWrap">
|
||||
<view v-if="quicklist.length>0" class="xPickerDateWrapQuickTags">
|
||||
<x-tag font-size="14" :font-color="(_isDark?'white':'')" @click="tagsClick(item)" :skin='quicklistSelectedStr==item.str?"normal":"thin"' :round="8" size="large"
|
||||
style="margin-right:10px;margin-bottom: 5px;" v-for="(item,index) in quicklist" :key="index">{{item.str}}</x-tag>
|
||||
</view>
|
||||
|
||||
<x-divider></x-divider>
|
||||
<view style="height: 8px;"></view>
|
||||
|
||||
<view class="xPickerDateWrapQuickInput">
|
||||
<view class="xPickerInputMaskerParent" @click="inputClick(0)" :style="{flex:'1',height:'40px'}">
|
||||
<input :placeholder-style="_placeStyle"
|
||||
:style="{color:changeIndex==0?_activeBorderColor:_fontColor,
|
||||
border: `2px solid ${changeIndex==0?_activeBorderColor:_backgroundColor}`,
|
||||
backgroundColor:_backgroundColor,
|
||||
fontSize:'15px'
|
||||
}"
|
||||
class="xPickerInput" :value="_start_date_str_format" />
|
||||
<view class="xPickerInputMasker"></view>
|
||||
</view>
|
||||
<!-- 至 -->
|
||||
<x-text style="width: 64px;text-align: center;">
|
||||
{{i18n!.t("tmui4x.betweentTime.splite")}}
|
||||
</x-text>
|
||||
<view class="xPickerInputMaskerParent" @click="inputClick(1)" :style="{flex:'1',height:'40px'}">
|
||||
<input :placeholder-style="_placeStyle"
|
||||
:style="{color:changeIndex==1?_activeBorderColor:_fontColor,
|
||||
border: `2px solid ${changeIndex==1?_activeBorderColor:_backgroundColor}`,
|
||||
backgroundColor:_backgroundColor,
|
||||
fontSize:'15px'
|
||||
}"
|
||||
class="xPickerInput" :value="_end_date_str_format" />
|
||||
<view class="xPickerInputMasker"></view>
|
||||
</view>
|
||||
</view>
|
||||
<x-divider style="margin-bottom: 8px;"></x-divider>
|
||||
<x-date-view v-if="yanchiDuration" @change="dateChangeView" :format="format" :type="type" :cell-units="_cellUnits"
|
||||
:start="_start_date_str" :end="_end_date_str" :model-value='nowValue[changeIndex]'></x-date-view>
|
||||
</view>
|
||||
<x-loading v-if="!yanchiDuration"></x-loading>
|
||||
</x-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xPickerClear{
|
||||
padding: 0 20px;
|
||||
/* height:50px; */
|
||||
display: flex;flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.xPickerDateText {
|
||||
text-align: left;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: linear;
|
||||
transition-property: transform, opacity;
|
||||
transform: translateY(100%) scale(0);
|
||||
opacity: 0;
|
||||
|
||||
}
|
||||
|
||||
.xPickerDateWrapQuickInputPlackeTips {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: none;
|
||||
}
|
||||
.xPickerInputMaskerParent{
|
||||
position: relative;
|
||||
}
|
||||
.xPickerInputMasker{
|
||||
position: absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
.xPickerInput {
|
||||
border-radius: 40px;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
/* font-size: 16px; */
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xPickerDateWrapQuickInput {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.xPickerDateWrapQuickTags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.xPickerDateWrap {}
|
||||
</style>
|
||||
@@ -0,0 +1,584 @@
|
||||
<script lang="ts" setup>
|
||||
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, getOutlineColorObj } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { toFillMarginAr, checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { PropType, computed, watch, onMounted, getCurrentInstance, ref, type Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* @name 按钮 xButton
|
||||
* @page /pages/index/button
|
||||
* @category 常用组件
|
||||
* @description 圆角,主题可通过配置统一设置或者动态全局设置,使设计风格统一并保持一致性。让你的风格独一无二。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: 'xButton' })
|
||||
|
||||
type SkinType = "default" | "secondary" | "text" | "outline" | "dashed" | "thin"
|
||||
type SizeType = "mini" | "large" | "normal" | "small"
|
||||
|
||||
export type XButtonProps = {
|
||||
/** 主题颜色,空取全局 */
|
||||
color : string,
|
||||
/** 暗黑主题颜色,空取全局 */
|
||||
darkColor : string,
|
||||
/** 自定背景色 */
|
||||
bgColor : string,
|
||||
/** 渐变[方向,颜色1,颜色2] */
|
||||
linearGradient : string[],
|
||||
/** 字号颜色 */
|
||||
fontColor : string,
|
||||
/** 暗黑字号颜色 */
|
||||
fontDarkColor : string,
|
||||
/** 字号大小 */
|
||||
fontSize : string,
|
||||
/** 圆角,空取全局 */
|
||||
round : string,
|
||||
/** 边线大小 */
|
||||
border : number,
|
||||
/** 投影[x,y,大小] */
|
||||
shadow : number[],
|
||||
/** 边线颜色 */
|
||||
borderColor : string,
|
||||
/** 样式主题"default" | "secondary" | "text" | "outline" | "dashed" | "thin" */
|
||||
skin : SkinType,
|
||||
/** 按钮图标 */
|
||||
icon : string,
|
||||
/** 是否是纯按钮图标 */
|
||||
iconBtn : boolean,
|
||||
/** 图标大小 */
|
||||
iconSize : string,
|
||||
/** 按钮大小"mini" | "large" | "normal" | "small" */
|
||||
size : SizeType,
|
||||
/** 跳转链接 */
|
||||
url : string,
|
||||
/** 跳转方式,同官方 */
|
||||
navigateMode : string,
|
||||
/** 是否禁用 */
|
||||
disabled : boolean,
|
||||
/** 加载状态 */
|
||||
loading : boolean,
|
||||
/** 高 */
|
||||
height : string,
|
||||
/** 宽 */
|
||||
width : string,
|
||||
/** 是否占据整行 */
|
||||
block : boolean,
|
||||
/** 是否作为x-form提交表单用于触发提交表单 */
|
||||
formType : 'form' | '',
|
||||
/** 行高 */
|
||||
lineHeight : string,
|
||||
/** 加粗 */
|
||||
fontWeight : string,
|
||||
/** 开放类型,同官方 */
|
||||
openType : string,
|
||||
lang : string,
|
||||
sessionFrom : string,
|
||||
sendMessageTitle : string,
|
||||
sendMessagePath : string,
|
||||
sendMessageImg : string,
|
||||
appParameter : string,
|
||||
showMessageCard : boolean,
|
||||
phoneNumberNoQuotaToast : boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<XButtonProps>(), {
|
||||
color: '',
|
||||
darkColor: '',
|
||||
bgColor: '',
|
||||
linearGradient: ():string[] => [] as string[],
|
||||
fontColor: '',
|
||||
fontDarkColor: '',
|
||||
fontSize: '',
|
||||
round: '',
|
||||
border: 0.5,
|
||||
shadow: ():number[] => [] as number[],
|
||||
borderColor: '',
|
||||
skin: 'default' as SkinType,
|
||||
icon: '',
|
||||
iconBtn: false,
|
||||
iconSize: '',
|
||||
size: 'normal' as SizeType,
|
||||
url: '',
|
||||
navigateMode: 'navigateTo',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
height: '',
|
||||
width: '',
|
||||
block: false,
|
||||
formType: '' as 'form' | '',
|
||||
lineHeight: '1.4',
|
||||
fontWeight: 'normal',
|
||||
openType: '',
|
||||
lang: 'en',
|
||||
sessionFrom: '',
|
||||
sendMessageTitle: '',
|
||||
sendMessagePath: '',
|
||||
sendMessageImg: '',
|
||||
appParameter: '',
|
||||
showMessageCard: false,
|
||||
phoneNumberNoQuotaToast: true
|
||||
})
|
||||
|
||||
const emits = defineEmits([
|
||||
'click',
|
||||
'getuserinfo',
|
||||
'contact',
|
||||
'getphonenumber',
|
||||
'getrealtimephonenumber',
|
||||
'error',
|
||||
'opensetting',
|
||||
'launchapp',
|
||||
'chooseavatar',
|
||||
'chooseaddress',
|
||||
'chooseinvoicetitle',
|
||||
'addgroupapp',
|
||||
'subscribe',
|
||||
'login',
|
||||
'agreeprivacyauthorization'
|
||||
])
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const proxy = instance?.proxy ?? null
|
||||
|
||||
const _set_border_color = ref(`1px solid rgba(0,0,0,0)`) as Ref<string>
|
||||
const _set_background_color = ref(`rgba(0,0,0,0)`) as Ref<string>
|
||||
const _set_background_img = ref(``) as Ref<string>
|
||||
const _set_font_color = ref(`#ffffff`) as Ref<string>
|
||||
const _isHover = ref(false) as Ref<boolean>
|
||||
const boxShadow = ref('0 0px 0px rgba(0,0,0,0)') as Ref<string>
|
||||
|
||||
const _fontWeight = computed(() : string => props.fontWeight)
|
||||
|
||||
const _color = computed(() : string => {
|
||||
let color = props.color
|
||||
if (xConfig.dark == 'dark' && props.darkColor != '') {
|
||||
color = props.darkColor
|
||||
} else {
|
||||
if (color == '') {
|
||||
color = xConfig.color
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = props.fontSize
|
||||
if (fontSize == '') {
|
||||
if (props.size == 'mini') fontSize = '12'
|
||||
if (props.size == 'small') fontSize = '14'
|
||||
if (props.size == 'normal') fontSize = '16'
|
||||
if (props.size == 'large') fontSize = '18'
|
||||
}
|
||||
fontSize = checkIsCssUnit(fontSize, xConfig.unit)
|
||||
if (xConfig.fontScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) sizeNumber = 16
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(props.fontSize)
|
||||
})
|
||||
|
||||
const _iconSize = computed(() : string => {
|
||||
if (props.iconSize != '') {
|
||||
let fontSize = checkIsCssUnit(props.iconSize, xConfig.unit)
|
||||
if (xConfig.fontScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) sizeNumber = 16
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(props.fontSize)
|
||||
}
|
||||
return _fontSize.value
|
||||
})
|
||||
|
||||
const _disabled = computed(() : boolean => props.disabled)
|
||||
const _icon = computed(() : string => props.icon)
|
||||
const _loading = computed(() : boolean => props.loading)
|
||||
|
||||
const _radius = computed(() : string => {
|
||||
let radius = props.round
|
||||
if (radius == '') {
|
||||
radius = xConfig.buttonRadius
|
||||
if (props.size == 'mini') radius = '6'
|
||||
if (props.size == 'small') radius = '8'
|
||||
}
|
||||
return checkIsCssUnit(radius, xConfig.unit)
|
||||
})
|
||||
|
||||
const _border = computed(() : string => checkIsCssUnit(props.border.toString(), xConfig.unit))
|
||||
const _iconBtn = computed(() : boolean => props.iconBtn)
|
||||
|
||||
const _height = computed(() : string => {
|
||||
if (props.height != '') return checkIsCssUnit(props.height, xConfig.unit)
|
||||
if (props.size == 'mini') return checkIsCssUnit('24', xConfig.unit)
|
||||
if (props.size == 'small') return checkIsCssUnit('32', xConfig.unit)
|
||||
if (props.size == 'normal') return checkIsCssUnit('46', xConfig.unit)
|
||||
if (props.size == 'large') return checkIsCssUnit('56', xConfig.unit)
|
||||
return checkIsCssUnit(props.height == '' ? '44' : props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
const _width = computed(() : string => {
|
||||
if (_iconBtn.value) return _height.value
|
||||
if (props.block) return '100%'
|
||||
if (props.width != '') return checkIsCssUnit(props.width, xConfig.unit)
|
||||
if (props.size == 'mini') return checkIsCssUnit('46', xConfig.unit)
|
||||
if (props.size == 'small') return checkIsCssUnit('60', xConfig.unit)
|
||||
if (props.size == 'normal') return checkIsCssUnit('98', xConfig.unit)
|
||||
if (props.size == 'large') return checkIsCssUnit('128', xConfig.unit)
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
|
||||
const _shadow = computed(() : number[] => {
|
||||
if (props.shadow.length == 0) return [0, 0] as number[]
|
||||
if (props.shadow.length == 1) return [props.shadow[0], props.shadow[0]] as number[]
|
||||
return props.shadow
|
||||
})
|
||||
|
||||
const _styleMap = computed(() : Map<string, any> => {
|
||||
const styleMap = new Map<string, any>()
|
||||
styleMap.set('width', _width.value)
|
||||
styleMap.set('height', _height.value)
|
||||
styleMap.set('border', _set_border_color.value)
|
||||
styleMap.set('backgroundColor', _set_background_color.value)
|
||||
if (_set_background_img.value != '') {
|
||||
styleMap.set('backgroundImage', _set_background_img.value)
|
||||
}
|
||||
styleMap.set('borderRadius', _radius.value)
|
||||
let opacity = '1'
|
||||
if (_disabled.value || _loading.value) {
|
||||
opacity = '0.5'
|
||||
}
|
||||
styleMap.set('opacity', opacity)
|
||||
return styleMap
|
||||
})
|
||||
|
||||
function findParent(parent : VueComponent | null) : VueComponent | null {
|
||||
if (parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS||MP-WEIXIN
|
||||
if (parent.$parent?.$options?.name?.indexOf('xForm') > -1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if (parent.$parent?.$options?.name?.indexOf('xForm') > -1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if (parent.$parent instanceof XFormComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
let parents = findParent(parent.$parent)
|
||||
// #ifdef WEB||APP-IOS||MP-WEIXIN
|
||||
if (parents?.$options?.name?.indexOf('xForm') > -1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if (parents?.$options?.name?.indexOf('xForm') > -1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if (parents instanceof XFormComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
function formSubmit() {
|
||||
let pelement = findParent(proxy);
|
||||
if (pelement == null) return;
|
||||
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
|
||||
parent.submit();
|
||||
}
|
||||
|
||||
function customStyles(hover : boolean) {
|
||||
let dePrimarycolor = getDefaultColor(xConfig.color);
|
||||
let color = getDefaultColor(_color.value);
|
||||
let hoverColor = props.color == 'info' ? getDefaultColor(xConfig.color) : color
|
||||
let colorInit : UTSJSONObject = getDefaultColorObj(color, hoverColor);
|
||||
let borderStyle = "solid"
|
||||
if (props.skin == 'text') {
|
||||
colorInit = getTextColorObj(color, hoverColor, xConfig.dark == 'dark')
|
||||
}
|
||||
if (props.skin == 'thin') {
|
||||
colorInit = getThinColorObj(color, hoverColor, xConfig.dark == 'dark')
|
||||
}
|
||||
if (props.skin == 'outline' || props.skin == 'dashed') {
|
||||
colorInit = getOutlineColorObj(color, hoverColor, xConfig.dark == 'dark')
|
||||
}
|
||||
if (props.skin == 'dashed') {
|
||||
borderStyle = 'dashed'
|
||||
}
|
||||
let defaultObj : UTSJSONObject = colorInit.getJSON("default")!
|
||||
let defaultActive : UTSJSONObject = colorInit.getJSON("active")!
|
||||
let borderWidth = checkIsCssUnit(props.border.toString(), 'rpx')
|
||||
let dbordercolor = getDefaultColor(props.borderColor)
|
||||
let background = getDefaultColor(props.bgColor)
|
||||
let fontcolor = getDefaultColor(props.fontColor)
|
||||
let realColor = fontcolor
|
||||
let realBackground = background
|
||||
let realBackImg = ''
|
||||
let shadowX = _shadow.value[0].toString();
|
||||
let shadowY = _shadow.value[1].toString();
|
||||
if (shadowX != shadowX && _shadow.value[0] != 0) {
|
||||
boxShadow.value = `0 ${shadowX}px ${shadowY}px ${_iconBtn.value ? 'rgba(0,0,0,0)' : defaultObj.getString("shadow")!}`
|
||||
}
|
||||
_set_border_color.value = `${borderWidth} ${borderStyle} ${dbordercolor == "" ? defaultObj.getString("borderColor")! : dbordercolor}`
|
||||
realBackground = background == "" ? defaultObj.getString("background")! : background
|
||||
if (props.color == 'info') {
|
||||
realColor = dePrimarycolor;
|
||||
} else {
|
||||
realColor = fontcolor == "" ? defaultObj.getString("fontColor")! : fontcolor
|
||||
}
|
||||
if (hover) {
|
||||
if (shadowX != shadowX && _shadow.value[0] != 0) {
|
||||
boxShadow.value = `0 ${shadowX}px ${shadowY}px ${_iconBtn.value ? 'rgba(0,0,0,0)' : defaultActive.getString("shadow")!}`
|
||||
}
|
||||
_set_border_color.value = `${borderWidth} ${borderStyle} ${dbordercolor == "" ? defaultActive.getString("borderColor")! : dbordercolor}`
|
||||
realBackground = background == "" ? defaultActive.getString("background")! : background
|
||||
realColor = fontcolor == "" ? defaultActive.getString("fontColor")! : fontcolor
|
||||
}
|
||||
if (props.linearGradient.length > 0) {
|
||||
let dirs = props.linearGradient[0]
|
||||
if (props.linearGradient[0] == 'top') {
|
||||
dirs = 'to top'
|
||||
} else if (props.linearGradient[0] == 'bottom') {
|
||||
dirs = 'to bottom'
|
||||
} else if (props.linearGradient[0] == 'left') {
|
||||
dirs = 'to left'
|
||||
} else if (props.linearGradient[0] == 'right') {
|
||||
dirs = 'to right'
|
||||
}
|
||||
realBackground = ``
|
||||
realBackImg = `linear-gradient(${dirs},${props.linearGradient[1]},${props.linearGradient[2]})`
|
||||
}
|
||||
if (props.fontDarkColor != '' && xConfig.dark == 'dark') {
|
||||
realColor = getDefaultColor(props.fontDarkColor)
|
||||
}
|
||||
_set_background_color.value = realBackground
|
||||
_set_background_img.value = realBackImg
|
||||
_set_font_color.value = realColor
|
||||
}
|
||||
|
||||
function touchStart() {
|
||||
if (_disabled.value || _loading.value) return;
|
||||
customStyles(true);
|
||||
_isHover.value = true;
|
||||
}
|
||||
function touchCacel() {
|
||||
if (_disabled.value || _loading.value) return;
|
||||
customStyles(false);
|
||||
_isHover.value = false
|
||||
}
|
||||
function touchEnd() {
|
||||
if (_disabled.value || _loading.value) return;
|
||||
customStyles(false);
|
||||
_isHover.value = false
|
||||
}
|
||||
|
||||
function clickListen(e : UniPointerEvent) {
|
||||
if (!_disabled.value && !_loading.value) {
|
||||
emits('click', e)
|
||||
if (props.formType == 'form') {
|
||||
formSubmit();
|
||||
}
|
||||
}
|
||||
if (_disabled.value == false && props.url != "" && _loading.value == false) {
|
||||
if (props.navigateMode == 'navigateTo') {
|
||||
uni.navigateTo({
|
||||
url: props.url,
|
||||
fail: (error) => {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
} else if (props.navigateMode == 'redirectTo') {
|
||||
uni.redirectTo({
|
||||
url: props.url,
|
||||
fail: (error) => {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
} else if (props.navigateMode == 'switchTab') {
|
||||
uni.switchTab({
|
||||
url: props.url,
|
||||
fail: (error) => {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
} else if (props.navigateMode == 'reLaunch') {
|
||||
uni.reLaunch({
|
||||
url: props.url,
|
||||
fail: (error) => {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
} else if (props.navigateMode == 'navigateBack') {
|
||||
uni.navigateBack({
|
||||
fail: (error) => {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function getuserinfo(event : UniEvent) { emits('getuserinfo', event) }
|
||||
function contact(event : UniEvent) { emits('contact', event) }
|
||||
function getphonenumber(event : UniEvent) { emits('getphonenumber', event) }
|
||||
function getrealtimephonenumber(event : UniEvent) { emits('getrealtimephonenumber', event) }
|
||||
function error(event : UniEvent) { emits('error', event) }
|
||||
function opensetting(event : UniEvent) { emits('opensetting', event) }
|
||||
function launchapp(event : UniEvent) { emits('launchapp', event) }
|
||||
function chooseavatar(event : UniEvent) { emits('chooseavatar', event) }
|
||||
function chooseaddress(event : UniEvent) { emits('chooseaddress', event) }
|
||||
function chooseinvoicetitle(event : UniEvent) { emits('chooseinvoicetitle', event) }
|
||||
function addgroupapp(event : UniEvent) { emits('addgroupapp', event) }
|
||||
function subscribe(event : UniEvent) { emits('subscribe', event) }
|
||||
function login(event : UniEvent) { emits('login', event) }
|
||||
function agreeprivacyauthorization(event : UniEvent) { emits('agreeprivacyauthorization', event) }
|
||||
// #endif
|
||||
|
||||
watch([
|
||||
():any => _color.value,
|
||||
():any => props.bgColor,
|
||||
():any => props.borderColor,
|
||||
():any => props.skin,
|
||||
():any => props.fontColor,
|
||||
],
|
||||
() => {
|
||||
customStyles(false)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
customStyles(false)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view <!-- #ifdef APP||H5 -->
|
||||
@touchcancel="touchCacel"
|
||||
@touchend="touchEnd"
|
||||
@touchstart="touchStart"
|
||||
@click="clickListen"
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="touchStart"
|
||||
@mouseup="touchEnd"
|
||||
@mouseleave="touchEnd"
|
||||
<!-- #endif -->
|
||||
|
||||
|
||||
:style="_styleMap" class="parentButton" :class="[_disabled||_loading?'noDrag':'']">
|
||||
<view :class="['xButton',_loading?'load':'']">
|
||||
<x-icon v-if="_icon!=''&&!_loading" :style="{marginRight:_iconBtn?'0px':' 3px'}" :font-size="_iconSize"
|
||||
:color="_set_font_color" :name="_icon"></x-icon>
|
||||
<x-icon v-if="_loading" :color="_set_font_color" :font-size="_iconSize" :spin="true" name="loader-fill"></x-icon>
|
||||
|
||||
|
||||
<!-- #ifdef MP -->
|
||||
<view v-if="!_iconBtn"
|
||||
:style="{fontWeight:_fontWeight,'color':_set_font_color,fontSize:_fontSize,lineHeight:lineHeight}">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP -->
|
||||
<text v-if="!_iconBtn"
|
||||
:style="{fontWeight:_fontWeight,'color':_set_font_color,fontSize:_fontSize,lineHeight:lineHeight}">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
|
||||
|
||||
</view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button :disabled="_disabled||_loading" :open-type="openType" :lang="lang" :sessionFrom="sessionFrom"
|
||||
:sendMessageTitle="sendMessageTitle" :sendMessagePath="sendMessagePath" :sendMessageImg="sendMessageImg"
|
||||
:appParameter="appParameter" :showMessageCard="showMessageCard"
|
||||
:phoneNumberNoQuotaToast="phoneNumberNoQuotaToast" @touchcancel="touchCacel" @touchend="touchEnd"
|
||||
@touchstart="touchStart" @click="clickListen" @getuserinfo="getuserinfo" @contact="contact"
|
||||
@getphonenumber="getphonenumber" @getrealtimephonenumber="getrealtimephonenumber" @error="error"
|
||||
@opensetting="opensetting" @launchapp="launchapp" @chooseavatar="chooseavatar"
|
||||
@chooseaddress="chooseaddress" @chooseinvoicetitle="chooseinvoicetitle" @addgroupapp="addgroupapp"
|
||||
@subscribe="subscribe" @login="login" @agreeprivacyauthorization="agreeprivacyauthorization"
|
||||
class="xButtonReal">
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.parentButton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* #ifdef WEB*/
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB*/
|
||||
.parentButton.noDrag {
|
||||
cursor: no-drop;
|
||||
}
|
||||
|
||||
.parentButton:hover {}
|
||||
|
||||
.parentButton:active {}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.xButtonReal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
background: transparent !important;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
pointer-events: all;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
|
||||
.xButton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.loadingMask {
|
||||
position: absolute;
|
||||
|
||||
background-color: rgba(200, 200, 200, 0.6);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* #ifndef APP */
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP*/
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
|
||||
import { xCalendar } from "./xCalendar"
|
||||
import { xDateArrayItem,xDateArrayItemType,xCalendarArgs,xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
// #ifdef APP
|
||||
import { calendarDraw } from "./calendarDraw"
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const xCalendarViewItemRef = ref<UniElement|null>(null)
|
||||
let calendarDom:calendarDraw|null = null;
|
||||
// #endif
|
||||
const i18n = xConfig.i18n;
|
||||
type xCalendarMultiplePropsType = {
|
||||
/**
|
||||
* 同步当前时间v-model
|
||||
* 不想受控:model-value
|
||||
*/
|
||||
modelValue : string[],
|
||||
/**
|
||||
* 范围选择模式
|
||||
* day:天数多选,通过multipleMax可以设置允许选择的天数
|
||||
* range:天数的范围选择,起始和终止
|
||||
* week:按周次选择范围
|
||||
* quarter:按季度选择范围
|
||||
* year:按年
|
||||
*/
|
||||
model: xCalendarMode,
|
||||
/**
|
||||
* 多选模式时,允许选择的最大天数。
|
||||
*/
|
||||
multipleMax : number,
|
||||
/**
|
||||
* 禁用的日期字符串如"2023-12-12"
|
||||
* 它与下面的start,end不冲突。
|
||||
*/
|
||||
disabledDays: string[],
|
||||
/**
|
||||
* 允许选择的开始日期
|
||||
*/
|
||||
startDate: string,
|
||||
/**
|
||||
* 允许选择的结束日期
|
||||
*/
|
||||
endDate: string,
|
||||
/**
|
||||
* 设置指定日期的样式
|
||||
* 数据类型见:xCalendarDateStyle_type
|
||||
*/
|
||||
dateStyle: xCalendarDateStyle_type[],
|
||||
/**
|
||||
* 同步vmodel时格式化模板
|
||||
*/
|
||||
format: string,
|
||||
/**
|
||||
* 选中的主题色,默认空值,取全局主题色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 默认的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontColor:string,
|
||||
/**
|
||||
* 默认的暗黑文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontDarkColor:string,
|
||||
/**
|
||||
* 默认选中时的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
activeFontColor:string,
|
||||
/**
|
||||
* 范围选中时,范围中间的选中颜色,
|
||||
* 如果为空,为color的透明度0.5;
|
||||
*/
|
||||
rangColor:string,
|
||||
rangFontColor:string,
|
||||
currentDate:string,
|
||||
/**
|
||||
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
|
||||
*/
|
||||
seekDay:number,
|
||||
/**
|
||||
* 给日期设定状态
|
||||
* 类型为:xCalendarDateStyleStatusType[]
|
||||
*/
|
||||
dateStatus:xCalendarDateStyleStatusType[]
|
||||
}
|
||||
|
||||
|
||||
const emit = defineEmits(['change','click'])
|
||||
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
|
||||
modelValue: [] as string[],
|
||||
currentDate:'',
|
||||
model:'day' as xCalendarMode,
|
||||
multipleMax: -1,
|
||||
disabledDays:[] as string[],
|
||||
startDate:'1900-1-1',
|
||||
endDate:'2025-5-13',
|
||||
dateStyle:[] as xCalendarDateStyle_type[],
|
||||
format:'YYYY-MM-DD',
|
||||
color:'',
|
||||
fontColor:'#333333',
|
||||
fontDarkColor:'#ffffff',
|
||||
activeFontColor:'#ffffff',
|
||||
rangColor:'',
|
||||
rangFontColor:'',
|
||||
seekDay:0,
|
||||
dateStatus:[] as xCalendarDateStyleStatusType[]
|
||||
})
|
||||
const calendar = new xCalendar()
|
||||
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
|
||||
const _rangColor = computed(()=>{
|
||||
let color = props.rangColor == ''?xConfig.color:props.rangColor
|
||||
let rgba = hexToRgb(getDefaultColor(color));
|
||||
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${props.rangColor==''?0.2:1})`
|
||||
})
|
||||
const _modelValue = computed(():string[]=> props.modelValue )
|
||||
const _model = computed(():xCalendarMode=> props.model )
|
||||
function splitArray<T>(ar : Array<T>, len : number) : Array<Array<T>> {
|
||||
const result : Array<Array<T>> = [];
|
||||
for (let i = 0; i < ar.length; i += len) {
|
||||
result.push(ar.slice(i, i + len));
|
||||
}
|
||||
return result
|
||||
}
|
||||
const _fontSize = computed(():string=> checkIsCssUnit('16',''))
|
||||
const dateArrayList = computed(():xDateArrayItemType[][]=>{
|
||||
const primaryColor = getDefaultColor(props.color==''?xConfig.color:props.color);
|
||||
const dates = calendar.getCalendar(
|
||||
props.seekDay,
|
||||
props.model,
|
||||
props.currentDate,
|
||||
props.modelValue,
|
||||
props.startDate!=''?new Date(props.startDate.replace(/-/g,'/')):null,
|
||||
props.endDate!=''?new Date(props.endDate.replace(/-/g,'/')):null,
|
||||
{
|
||||
color:primaryColor,
|
||||
fontColor:getDefaultColor(xConfig.dark=='dark'?props.fontDarkColor:props.fontColor),
|
||||
activeFontColor:getDefaultColor(props.activeFontColor),
|
||||
rangColor:_rangColor.value,
|
||||
rangFontColor:props.rangFontColor==''?primaryColor:getDefaultColor(props.rangFontColor)
|
||||
} as xCalendarArgs,props.dateStyle,props.disabledDays);
|
||||
|
||||
return splitArray<xDateArrayItemType>(dates,7)
|
||||
})
|
||||
function showLabel(item:xDateArrayItemType):string {
|
||||
if(item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',2)
|
||||
if(item.isInstart&&!item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',0)
|
||||
if(!item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',1)
|
||||
return ""
|
||||
}
|
||||
function dateClick(item:xDateArrayItemType){
|
||||
if(item.disabled) return;
|
||||
emit('click',item)
|
||||
}
|
||||
|
||||
function checkDataIsInDateStatus(date:string|null):string{
|
||||
if(date ==''||date == null) return '';
|
||||
for(let k =0 ;k <_dateStatus.value.length;k++){
|
||||
let itemStatus = _dateStatus.value[k]
|
||||
let dates = itemStatus?.date??[];
|
||||
let start = itemStatus?.between?.start??''
|
||||
let end = itemStatus?.between?.end??''
|
||||
let betweenColor = itemStatus?.between?.color??''
|
||||
let notDates = itemStatus?.between?.notDate??[]
|
||||
let nowDate = new xDate(date);
|
||||
let isInBetweenDate = false;
|
||||
if(start!=''&&end!=''){
|
||||
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
|
||||
let isNotDate = false
|
||||
for(let i=0;i<notDates.length;i++){
|
||||
let item = notDates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
|
||||
isNotDate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
isInBetweenDate = isBetween && !isNotDate
|
||||
}
|
||||
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
|
||||
|
||||
let selfColor = ''
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
|
||||
selfColor = item.color==''?'primary':item.color
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(selfColor!=''){
|
||||
return getDefaultColor(selfColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
// #ifdef APP
|
||||
|
||||
function draw():Promise<any>{
|
||||
return new Promise(()=>{
|
||||
calendarDom?.draw(dateArrayList.value,_model.value,_dateStatus.value)
|
||||
})
|
||||
}
|
||||
|
||||
function canvsClick(e:UniPointerEvent){
|
||||
|
||||
xCalendarViewItemRef.value?.getBoundingClientRectAsync()
|
||||
?.then((rect:DOMRect)=>{
|
||||
let cellHeight = 50;
|
||||
let cellWidth = rect.width / 7;
|
||||
let top = rect.top;
|
||||
let left = rect.left;
|
||||
let x = e.clientX - left;
|
||||
let y = e.clientY - top;
|
||||
let col = Math.floor(x/cellWidth);
|
||||
let row = Math.floor(y/cellHeight);
|
||||
let item = dateArrayList.value[row][col];
|
||||
function testThread():Promise<any>{
|
||||
return new Promise(()=>{
|
||||
dateClick(item)
|
||||
})
|
||||
}
|
||||
testThread()
|
||||
})
|
||||
.catch((er)=>{
|
||||
console.error(er)
|
||||
})
|
||||
}
|
||||
|
||||
watch(():any => dateArrayList.value,()=>{
|
||||
draw()
|
||||
})
|
||||
|
||||
// #endif
|
||||
onMounted(()=>{
|
||||
// #ifdef APP
|
||||
|
||||
calendarDom = new calendarDraw(
|
||||
xCalendarViewItemRef.value,
|
||||
proxy,
|
||||
[i18n.t('tmui4x.calendar.rangStatus',0),i18n.t('tmui4x.calendar.rangStatus',1),i18n.t('tmui4x.calendar.rangStatus',2)]
|
||||
)
|
||||
nextTick(()=>{
|
||||
draw()
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarViewItem"
|
||||
ref="xCalendarViewItemRef"
|
||||
<!-- #ifdef APP -->
|
||||
@click="canvsClick"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<!-- #ifndef APP -->
|
||||
<view class="xCalendarViewItemCol" v-for="(children,index) in dateArrayList" :key="index">
|
||||
<view class="xCalendarViewItemColItem"
|
||||
@click="dateClick(item)"
|
||||
v-for="(item,index2) in (children as xDateArrayItemType[])" :key="index2">
|
||||
<view class="xCalendarViewItemColItemBox"
|
||||
:style="{
|
||||
backgroundColor:item.style.dstyle.backgroundColor,
|
||||
opacity:item.style.dstyle.opacity,
|
||||
}"
|
||||
>
|
||||
<view v-if="item.style.dot.dot"
|
||||
class="xCalendarViewItemDot"
|
||||
:class="[item.style.dot.dotLabel==''?'xCalendarViewItemDotNolabel':'']"
|
||||
:style="{
|
||||
color:item.style.dot.dotLabelColor,
|
||||
backgroundColor:item.style.dot.dotColor
|
||||
}"
|
||||
>
|
||||
{{item.style.dot.dotLabel}}
|
||||
</view>
|
||||
<text class="xCalendarViewItemColDate"
|
||||
:style="{
|
||||
color:item.style.dstyle.fontColor,
|
||||
fontSize:_fontSize
|
||||
}"
|
||||
>
|
||||
{{item.date.day}}
|
||||
</text>
|
||||
<text class="xCalendarViewItemColLabel"
|
||||
:style="{
|
||||
color:item.style.dstyle.fontColor
|
||||
}"
|
||||
>{{showLabel(item)||item.style.dstyle.label}}</text>
|
||||
<view class="xCalendarViewStatus" :style="{backgroundColor:checkDataIsInDateStatus(item.date.date)}" v-if="checkDataIsInDateStatus(item.date.date)!=''"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.xCalendarViewStatus{
|
||||
width:4px;
|
||||
height:4px;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
|
||||
}
|
||||
// #ifdef APP
|
||||
.xCalendarViewItem{
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
.xCalendarViewItemDot{
|
||||
padding:2px 4px;
|
||||
min-width:18px;
|
||||
min-height:18px;
|
||||
font-size:10px;
|
||||
border-radius:9px;
|
||||
position: absolute;
|
||||
right:0px;
|
||||
top:0px;
|
||||
&.xCalendarViewItemDotNolabel{
|
||||
padding:0;
|
||||
min-width:8px;
|
||||
min-height:8px;
|
||||
font-size:10px;
|
||||
border-radius:9px;
|
||||
}
|
||||
}
|
||||
.xCalendarViewItem{
|
||||
width:100%;
|
||||
height:100%;
|
||||
.xCalendarViewItemCol{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
height: 50px;
|
||||
.xCalendarViewItemColItem{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 100%;
|
||||
width:14.285%;
|
||||
.xCalendarViewItemColItemBox{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 25px;
|
||||
overflow: visible;
|
||||
|
||||
}
|
||||
.xCalendarViewItemColDate{
|
||||
text-align: center;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
margin-top: -4px;
|
||||
// font-weight: bold;
|
||||
// font-size: 16px;
|
||||
display: block;
|
||||
}
|
||||
.xCalendarViewItemColLabel{
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
margin-top: -4px;
|
||||
display: block;
|
||||
min-height:10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
</style>
|
||||
@@ -0,0 +1,189 @@
|
||||
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import {xCalendarDateStyleStatusType} from "@/uni_modules/tmx-ui/interface.uts"
|
||||
import { xDate } from "../../core/util/xDate.uts"
|
||||
export class calendarDraw {
|
||||
ele : UniElement | null = null
|
||||
proxy : any | null = null
|
||||
cellHeight = 50;
|
||||
sapce = 5
|
||||
model : xCalendarMode = 'day'
|
||||
_fontSize : string = checkIsCssUnit('16', xConfig.unit)
|
||||
_dateStatus : xCalendarDateStyleStatusType[] | null = null;
|
||||
dateCnStrs:string[] = ["开始","结束","本日"]
|
||||
constructor(target : UniElement | null, proxyx : any | null,dateCn:string[]) {
|
||||
this.ele = target
|
||||
this.proxy = proxyx
|
||||
this.dateCnStrs = dateCn
|
||||
}
|
||||
checkDataIsInDateStatus(date:string|null):string{
|
||||
if(date ==''||date == null || this._dateStatus == null) return '';
|
||||
for(let k =0 ;k <this._dateStatus.length;k++){
|
||||
let itemStatus = this._dateStatus[k]
|
||||
let dates = itemStatus?.date??[];
|
||||
let start = itemStatus?.between?.start??''
|
||||
let end = itemStatus?.between?.end??''
|
||||
let betweenColor = itemStatus?.between?.color??''
|
||||
let notDates = itemStatus?.between?.notDate??[]
|
||||
let nowDate = new xDate(date);
|
||||
let isInBetweenDate = false;
|
||||
if(start!=''&&end!=''){
|
||||
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
|
||||
let isNotDate = false
|
||||
for(let i=0;i<notDates.length;i++){
|
||||
let item = notDates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
|
||||
isNotDate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
isInBetweenDate = isBetween && !isNotDate
|
||||
}
|
||||
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
|
||||
|
||||
let selfColor = ''
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
|
||||
selfColor = item.color==''?'primary':item.color
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(selfColor!=''){
|
||||
return getDefaultColor(selfColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
showLabel(item : xDateArrayItemType) : string {
|
||||
if (item.isInstart && item.isInEnd && this.model == 'range') return this.dateCnStrs[2]
|
||||
if (item.isInstart && !item.isInEnd && this.model == 'range') return this.dateCnStrs[0]
|
||||
if (!item.isInstart && item.isInEnd && this.model == 'range') return this.dateCnStrs[1]
|
||||
return ""
|
||||
}
|
||||
getColor(color : string, alpha : number) : string {
|
||||
if (alpha == 1) return color;
|
||||
let rgba = hexToRgb(getDefaultColor(color));
|
||||
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${alpha})`
|
||||
}
|
||||
_draw(element : UniElement, Grect : DOMRect, list : xDateArrayItemType[][]) {
|
||||
const ctx = element.getDrawableContext()!;
|
||||
|
||||
ctx.reset()
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)'
|
||||
ctx.fillRect(0, 0, Grect.width, Grect.height)
|
||||
ctx.textAlign = 'center'
|
||||
|
||||
|
||||
const _realCellWidth = Grect.width / 7
|
||||
const _h = Math.min(_realCellWidth, 50) - this.sapce * 2
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const children = list[i]
|
||||
for (let col = 0; col < children.length; col++) {
|
||||
let item = children[col];
|
||||
let dstyle = item.style
|
||||
|
||||
let xy_x = _realCellWidth * col + _realCellWidth / 2;
|
||||
let xy_y = this.cellHeight * i + this.cellHeight / 2;
|
||||
|
||||
// 绘制选中的背景
|
||||
ctx.fillStyle = this.getColor(dstyle.dstyle.backgroundColor, (item.disabled || !item.inCurrentMonth) && !item.isInstart && !item.isInEnd ? 0.3 : 1);
|
||||
|
||||
if (dstyle.dstyle.backgroundColor != 'transparent') {
|
||||
ctx.beginPath()
|
||||
ctx.arc(xy_x, xy_y, _h / 2, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// 绘制右角标。
|
||||
if (dstyle.dot.dot) {
|
||||
|
||||
if (dstyle.dot.dotLabel == '') {
|
||||
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2-10;
|
||||
let lasty = _realCellWidth * (i) + 10
|
||||
ctx.fillStyle = dstyle.dot.dotColor
|
||||
ctx.beginPath()
|
||||
ctx.arc(lastx, lasty, 4, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
} else {
|
||||
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 5;
|
||||
let lasty = _realCellWidth * (i) + 10
|
||||
ctx.fillStyle = dstyle.dot.dotColor;
|
||||
ctx.beginPath()
|
||||
ctx.arc(lastx, lasty, 10, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.font = "10px"
|
||||
ctx.fillStyle = dstyle.dot.dotLabelColor
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 3);
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 5);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 绘制文字
|
||||
const text = item.date.day.toString();
|
||||
ctx.fillStyle = this.getColor(dstyle.dstyle.fontColor, (item.disabled || !item.inCurrentMonth) ? 0.7 : 1);
|
||||
ctx.font = this._fontSize
|
||||
const textHeight = 34;
|
||||
const _t_x = _realCellWidth * col + _realCellWidth / 2
|
||||
let _t_y = this.cellHeight * i + this.cellHeight / 2 + 2;
|
||||
// #ifdef APP-IOS
|
||||
_t_y = this.cellHeight * i + this.cellHeight / 2 + 6;
|
||||
// #endif
|
||||
ctx.fillText(text, _t_x, _t_y);
|
||||
let label = this.showLabel(item)
|
||||
label = label == '' ? dstyle.dstyle.label : label
|
||||
|
||||
|
||||
// 绘制底部的label
|
||||
if (label != '') {
|
||||
let labely = this.cellHeight * i + this.cellHeight - 9;
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
labely = this.cellHeight * i + this.cellHeight - 12;
|
||||
// #endif
|
||||
ctx.font = "8px"
|
||||
ctx.fillText(label, _t_x, labely);
|
||||
}
|
||||
|
||||
// 绘制状态小点点。
|
||||
let statusColor = this.checkDataIsInDateStatus(item.date.date);
|
||||
if(statusColor!=''){
|
||||
ctx.fillStyle = statusColor
|
||||
ctx.beginPath()
|
||||
let statusY = this.cellHeight * i + this.cellHeight - this.sapce - 3;
|
||||
ctx.arc(_t_x,statusY,2,0,Math.PI*2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
ctx.update()
|
||||
}
|
||||
draw(list : xDateArrayItemType[][], modelv : xCalendarMode = 'day',status:xCalendarDateStyleStatusType[]|null = null) {
|
||||
let _this = this;
|
||||
_this.model = modelv;
|
||||
_this._dateStatus = status;
|
||||
this.ele?.getBoundingClientRectAsync()
|
||||
?.then((rect : DOMRect) => {
|
||||
_this._draw(_this.ele!, rect, list)
|
||||
})
|
||||
.catch((er) => {
|
||||
console.error(er)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,750 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType, getCurrentInstance } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig, getI18n } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type, xCalendarDateStyleStatusType } from "../../interface.uts"
|
||||
import { xCalendar } from "./xCalendar"
|
||||
import calendarMultipleUvue from "./calendar-multiple.uvue"
|
||||
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
const i18n = xConfig.i18n
|
||||
type xCalendarMultiplePropsType = {
|
||||
/**
|
||||
* 同步当前时间v-model
|
||||
* 不想受控:model-value
|
||||
*/
|
||||
modelValue : string[],
|
||||
/**
|
||||
* 范围选择模式
|
||||
* day:天数多选,通过multipleMax可以设置允许选择的天数
|
||||
* range:天数的范围选择,起始和终止
|
||||
* week:按周次选择范围(未开放)
|
||||
* quarter:按季度选择范围(未开放)
|
||||
* year:按年(未开放)
|
||||
*/
|
||||
model : "day" | "range" | "week" | "quarter" | "year",
|
||||
/**
|
||||
* 多选模式时,允许选择的最大天数。
|
||||
*/
|
||||
multipleMax : number,
|
||||
/**
|
||||
* 禁用的日期字符串如"2023-12-12"
|
||||
* 它与下面的start,end不冲突。
|
||||
*/
|
||||
disabledDays : string[],
|
||||
/**
|
||||
* 允许选择的开始日期
|
||||
*/
|
||||
startDate : string,
|
||||
/**
|
||||
* 是否上下切换日历
|
||||
*/
|
||||
vertical : boolean,
|
||||
/**
|
||||
* 允许选择的结束日期
|
||||
*/
|
||||
endDate : string,
|
||||
/**
|
||||
* 当前显示的月份,默认以modalValue中的第一项为初始月
|
||||
* 如果为空,显示本月,可以控制这里切换显示的日期
|
||||
*/
|
||||
currentDate : string,
|
||||
/**
|
||||
* 设置指定日期的样式
|
||||
* 数据类型见:xCalendarDateStyle_type
|
||||
*/
|
||||
dateStyle : xCalendarDateStyle_type[],
|
||||
/**
|
||||
* 同步vmodel时格式化模板
|
||||
*/
|
||||
format : string,
|
||||
/**
|
||||
* 选中的主题色,默认空值,取全局主题色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
color : string,
|
||||
/**
|
||||
* 默认的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontColor : string,
|
||||
/**
|
||||
* 默认的暗黑文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontDarkColor : string,
|
||||
/**
|
||||
* 默认选中时的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
activeFontColor : string,
|
||||
/**
|
||||
* 范围选中时,范围中间的选中颜色,
|
||||
* 如果为空,为color的透明度0.5;
|
||||
*/
|
||||
rangColor : string,
|
||||
rangFontColor : string,
|
||||
/**
|
||||
* 头的背景颜色,默认为透明
|
||||
*/
|
||||
headBgColor : string,
|
||||
/**
|
||||
* 头的文字颜色,提供了后暗黑失效会以这个为准。
|
||||
*/
|
||||
headFontColor : string,
|
||||
/**
|
||||
* 头部自定义样式。
|
||||
*/
|
||||
headStyle : string,
|
||||
/**
|
||||
* 循环渲染时,是否只渲染当前面板(如果你在pad等10年前的低端机上渲染日历有压力请打开此值为true)
|
||||
* 关闭后可以提升滑动体验。
|
||||
*/
|
||||
renderOnly : boolean,
|
||||
/**
|
||||
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
|
||||
*/
|
||||
seekDay : number,
|
||||
/**
|
||||
* 给日期设定状态
|
||||
* 类型为:xCalendarDateStyleStatusType[]
|
||||
*/
|
||||
dateStatus : xCalendarDateStyleStatusType[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 多选日历 xCalendarMultiple
|
||||
* @description 可以单选和多选,无限循环滚动,目前4.67dev以下版本使用可能会有性能风险,请关注后续官方针对性的优化。
|
||||
* @page /pages/index/calendar-multiple
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| - | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xCalendarMultiple" })
|
||||
const calendar = new xCalendar()
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 时间变化时触发
|
||||
* @param {string[]} value - 当前变化的日期
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 当前日历面板的日期被点击时触发
|
||||
* @param {string} value - 当前被点击的日期
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 当前激活面板月份改变时触发(就是当前看到的月份面板)
|
||||
* @param {string} value - 当前激活面板的日期
|
||||
*/
|
||||
'currentChange',
|
||||
/**
|
||||
* 同步当前的选中的日期绑定
|
||||
* @param {string[]} value - 当前选中日期
|
||||
*/
|
||||
'update:modelValue',
|
||||
/**
|
||||
* 同步当前查看的月份日期,请以日期形式提供值
|
||||
* @param {string} value - 当前查看的月分日期
|
||||
*/
|
||||
'update:currentDate'
|
||||
])
|
||||
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
|
||||
modelValue: () : string[] => [] as string[],
|
||||
model: 'day',
|
||||
multipleMax: -1,
|
||||
disabledDays: () : string[] => [] as string[],
|
||||
startDate: '1900-1-1',
|
||||
endDate: '2100-1-1',
|
||||
dateStyle: () : xCalendarDateStyle_type[] => [] as xCalendarDateStyle_type[],
|
||||
format: 'YYYY-MM-DD',
|
||||
color: '',
|
||||
fontColor: '#333333',
|
||||
fontDarkColor: '#ffffff',
|
||||
activeFontColor: '#ffffff',
|
||||
rangColor: '',
|
||||
rangFontColor: '',
|
||||
headBgColor: 'transparent',
|
||||
headFontColor: '',
|
||||
headStyle: '',
|
||||
currentDate: '',
|
||||
vertical: false,
|
||||
renderOnly: true,
|
||||
seekDay: 0,
|
||||
dateStatus: () : xCalendarDateStyleStatusType[] => [] as xCalendarDateStyleStatusType[]
|
||||
})
|
||||
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
|
||||
const weeksCn = computed(() : string[] => {
|
||||
// ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
// 根据 seekDay 属性调整周名称的顺序
|
||||
const weekNames = [
|
||||
i18n!.t("tmui4x.calendar.week", 0),
|
||||
i18n!.t("tmui4x.calendar.week", 1),
|
||||
i18n!.t("tmui4x.calendar.week", 2),
|
||||
i18n!.t("tmui4x.calendar.week", 3),
|
||||
i18n!.t("tmui4x.calendar.week", 4),
|
||||
i18n!.t("tmui4x.calendar.week", 5),
|
||||
i18n!.t("tmui4x.calendar.week", 6),
|
||||
]
|
||||
// 如果 seekDay 为 0(默认周一),直接返回原数组
|
||||
if (props.seekDay == 0) {
|
||||
return weekNames
|
||||
}
|
||||
// 根据 seekDay 重新排列数组
|
||||
const result : string[] = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const index = (i + props.seekDay) % 7
|
||||
result.push(weekNames[index])
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const _headBgColor = computed(() : string => getDefaultColor(props.headBgColor))
|
||||
const _headFontColor = computed(() : string => {
|
||||
if (props.headFontColor == '') return '#333333'
|
||||
return getDefaultColor(props.headFontColor)
|
||||
})
|
||||
|
||||
const _modelValue = ref(props.modelValue)
|
||||
const _currentDate = ref(new xDate(props.currentDate).format("YYYY-MM-DD"))
|
||||
const _currentDateSwipersIndex = ref(0)
|
||||
|
||||
const _currentDateSwipers = ref<string[]>([])
|
||||
const _startDate = computed(() : string => props.startDate)
|
||||
const _endDate = computed(() : string => props.endDate)
|
||||
const _currentDateLabel = computed(() : string => {
|
||||
let ars = _currentDate.value.split('-');
|
||||
// `${ars[0]}年${ars[1]}月`
|
||||
return i18n.t('tmui4x.calendar.titleCurrentMonth', [ars[0], ars[1]])
|
||||
})
|
||||
const _currentYear = ref(new xDate(props.currentDate).getYear())
|
||||
let _modelValueDate = computed(() : Date[] => {
|
||||
return _modelValue.value.map((d : string) : Date => {
|
||||
return new Date(d.replace(/-/g, '/'))
|
||||
})
|
||||
})
|
||||
const _tipsText = computed(() : string => {
|
||||
|
||||
if (props.model == 'day') {
|
||||
// `已选择${_modelValue.value.length}日` : '未选择日期'
|
||||
return _modelValue.value.length > 0 ? i18n.t('tmui4x.calendar.selectedStatus', 0, { count: _modelValue.value.length }) : i18n.t('tmui4x.calendar.selectedStatus', 1)
|
||||
} else if (props.model == 'range') {
|
||||
if (_modelValue.value.length == 0) return i18n.t('tmui4x.calendar.selectedStatus', 1);
|
||||
if (_modelValue.value.length == 1) return i18n.t('tmui4x.calendar.selectedStatus', 3);
|
||||
if (_modelValue.value.length > 1) {
|
||||
let start = _modelValueDate.value[0].getTime()
|
||||
let end = _modelValueDate.value[1].getTime()
|
||||
let diff = Math.abs(start - end);
|
||||
let diffDay = diff / (24 * 60 * 60 * 1000)
|
||||
if (start - end > 0) return i18n.t('tmui4x.calendar.selectedStatus', 1);
|
||||
return i18n.t('tmui4x.calendar.selectedStatus', 0, { count: (diffDay + 1) })
|
||||
}
|
||||
|
||||
}
|
||||
// '未选择日期'
|
||||
return i18n.t('tmui4x.calendar.selectedStatus', 1);
|
||||
})
|
||||
|
||||
const _monthBgColor = computed(() : string => xConfig.dark == 'dark' ? xConfig.sheetDarkColor : '#ffffff')
|
||||
const _color = computed(() : string => props.color == '' ? getDefaultColor(xConfig.color) : getDefaultColor(props.color))
|
||||
const showPanel = ref(false)
|
||||
function dateClick(item : xDateArrayItemType) {
|
||||
const isInIndex = calendar.isInRangeDateByIndex(new Date(item.date.date), _modelValueDate.value, props.model)
|
||||
console.log(item.date.date)
|
||||
emit('click', item.date.date)
|
||||
let dates = _modelValue.value.slice(0)
|
||||
function showToastFun() {
|
||||
// `最大${props.multipleMax}天`
|
||||
uni.showToast({
|
||||
title: i18n.t('tmui4x.calendar.tips', props.multipleMax),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
if (props.model == 'day') {
|
||||
|
||||
if (isInIndex == -1) {
|
||||
if (props.multipleMax <= dates.length && props.multipleMax > -1) {
|
||||
showToastFun()
|
||||
return;
|
||||
}
|
||||
dates.push(item.date.date)
|
||||
} else {
|
||||
dates.splice(isInIndex, 1)
|
||||
}
|
||||
} else if (props.model == 'range') {
|
||||
if (isInIndex == -1) {
|
||||
if (dates.length == 0) {
|
||||
dates = [item.date.date] as string[]
|
||||
} else if (dates.length == 1) {
|
||||
dates.push(item.date.date)
|
||||
} else if (dates.length > 1) {
|
||||
dates = [item.date.date] as string[]
|
||||
}
|
||||
} else if (isInIndex == 0) {
|
||||
if (dates.length == 1) {
|
||||
dates = [] as string[]
|
||||
} else if (dates.length > 1) {
|
||||
dates = [item.date.date] as string[]
|
||||
}
|
||||
} else if (isInIndex == 1) {
|
||||
dates = [item.date.date] as string[]
|
||||
}
|
||||
if (dates.length >= 2) {
|
||||
let adates = dates.map((d : string) : Date => {
|
||||
return new Date(d.replace(/-/g, '/'))
|
||||
})
|
||||
let start = adates[0].getTime()
|
||||
let end = adates[1].getTime()
|
||||
let diff = Math.abs(start - end);
|
||||
let diffDay = diff / (24 * 60 * 60 * 1000)
|
||||
if (props.multipleMax <= diffDay && props.multipleMax > -1) {
|
||||
showToastFun()
|
||||
return;
|
||||
}
|
||||
if (start > end) {
|
||||
dates = [dates[1], dates[0]] as string[]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
_modelValue.value = dates
|
||||
emit('update:modelValue', _modelValue.value)
|
||||
if ((_modelValue.value.length >= 2 || _modelValue.value.length == 0) && props.model == 'range') {
|
||||
emit('change', _modelValue.value)
|
||||
} else if (props.model == 'day') {
|
||||
emit('change', _modelValue.value)
|
||||
}
|
||||
}
|
||||
function getSwiperListCurrentDates(nowCurrentDate : string) : string[] {
|
||||
let index = _currentDateSwipersIndex.value
|
||||
let currentData = new xDate(nowCurrentDate)
|
||||
let xd = currentData.getClone().setDateOf(1, 'd').format('YYYY-MM-DD')
|
||||
let start = currentData.getClone().setDateOf(1, 'd').subtraction(1, 'm').format('YYYY-MM-DD')
|
||||
let end = currentData.getClone().setDateOf(1, 'd').add(1, 'm').format('YYYY-MM-DD')
|
||||
let datas = [xd, end, start]
|
||||
if (_currentDateSwipers.value.length == 0) return datas;
|
||||
if (index == 0) {
|
||||
datas = [xd, end, start]
|
||||
} else if (index == 1) {
|
||||
datas = [start, xd, end]
|
||||
} else if (index == 2) {
|
||||
datas = [end, start, xd]
|
||||
}
|
||||
return datas;
|
||||
}
|
||||
function clear() {
|
||||
if (_modelValue.value.length == 0) return;
|
||||
_modelValue.value = [] as string[]
|
||||
emit('update:modelValue', _modelValue.value)
|
||||
emit('change', _modelValue.value)
|
||||
}
|
||||
|
||||
function nowMonth() {
|
||||
let nowdate = new xDate()
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function stepperChangeYear(eyear : number) {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.setDateOf(eyear, 'y')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function changeMonth(eyear : number) {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.setDateOf(eyear, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
showPanel.value = false;
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
}
|
||||
function nextMonth() {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.add(1, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
function prevMonth() {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.subtraction(1, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function swiperChange(evt : UniSwiperChangeEvent) {
|
||||
_currentDateSwipersIndex.value = evt.detail.current;
|
||||
nextTick(() => {
|
||||
_currentDate.value = _currentDateSwipers.value[evt.detail.current]
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
_currentYear.value = nowdate.getYear()
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : string[] => props.modelValue, (newVal : string[]) => {
|
||||
_modelValue.value = newVal.slice(0);
|
||||
|
||||
})
|
||||
watch(() : string => props.currentDate, (newVal : string) => {
|
||||
let nowdate = new xDate(newVal).setDateOf(1, 'd')
|
||||
if (nowdate.format("YYYY-MM-DD") == _currentDate.value) return;
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipersIndex.value = 0;
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
// console.log(_currentDate.value)
|
||||
})
|
||||
onMounted(() => {
|
||||
if (props.modelValue.length > 0 && props.currentDate == '') {
|
||||
let nowdate = new xDate(props.modelValue[0])
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
}
|
||||
nextTick(() => {
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function getmonth(item : string) : number {
|
||||
let vls = item.split("-");
|
||||
return parseInt(vls[1])
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
/**
|
||||
* 下月
|
||||
*/
|
||||
next() {
|
||||
nextMonth();
|
||||
},
|
||||
/**
|
||||
* 上月
|
||||
*/
|
||||
prev() {
|
||||
prevMonth();
|
||||
},
|
||||
/**
|
||||
* 设置日历返回到本月
|
||||
*/
|
||||
setCurrentMonth() {
|
||||
nowMonth()
|
||||
},
|
||||
/**
|
||||
* 清空选择
|
||||
*/
|
||||
clear() {
|
||||
clear()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
<!--
|
||||
@slot 日历头,隐藏使用空插槽,将隐藏,如果想自定,请通过ref函数来翻页控制日历走向。
|
||||
-->
|
||||
<slot name="header">
|
||||
<view class="xCalendarViewDataHeaderWrap"
|
||||
:class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']"
|
||||
:style="[{backgroundColor:_headBgColor},headStyle]">
|
||||
<view class="xCalendarViewHeader" style="padding: 0 12px">
|
||||
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
|
||||
<!-- 2024年5月 -->
|
||||
<x-text :color="_headFontColor" font-size="21">{{_currentDateLabel}}</x-text>
|
||||
<x-icon :color="_headFontColor" font-size="21" name="arrow-down-s-fill"></x-icon>
|
||||
</view>
|
||||
<view class="xCalendarViewHeaderRight">
|
||||
<!-- <view @click="prevMonth" class="px-10 py-15">
|
||||
<x-icon font-size="21" :color="_headFontColor" name="arrow-up-s-fill">上月</x-icon>
|
||||
</view>
|
||||
<view @click="nextMonth" class="px-10 py-15">
|
||||
<x-icon font-size="21" :color="_headFontColor" name="arrow-down-s-fill">下月</x-icon>
|
||||
</view> -->
|
||||
<!-- 清空 -->
|
||||
<x-text @click="clear" :color="_headFontColor"
|
||||
style="padding: 10px 20px;">{{i18n.t('tmui4x.clear')}}</x-text>
|
||||
<!-- 本月 -->
|
||||
<x-text @click="nowMonth" :color="_headFontColor"
|
||||
style="padding: 10px 0px;">{{i18n.t('tmui4x.calendar.currentMonthTitle')}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xCalendarViewDataHeader">
|
||||
<view class="xCalendarViewDataHeaderItem" v-for="(item,index) in 7" :key="item">
|
||||
<x-text :color="_headFontColor" font-size="12">{{weeksCn[index]}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="xCalendarViewSpace"></view>
|
||||
<view :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']" class="xCalendarViewWrap">
|
||||
<swiper v-if="_currentDateSwipers.length>0" :vertical="props.vertical" @change="swiperChange"
|
||||
:current="_currentDateSwipersIndex" :circular="true" style="width:100%;height:100%">
|
||||
<swiper-item v-for="(item,index) in _currentDateSwipers" :key="index" style="width:100%;height:100%;">
|
||||
<view style="width:100%;height:100%;position: relative;">
|
||||
<view class="xCalendarViewContentBox">
|
||||
<calendar-multiple-uvue v-if="index == _currentDateSwipersIndex||!props.renderOnly"
|
||||
@click="dateClick" :currentDate="item" :dateStatus="_dateStatus"
|
||||
:seekDay="props.seekDay" :modelValue="props.modelValue" :model="props.model"
|
||||
:multipleMax="props.multipleMax" :disabledDays="props.disabledDays"
|
||||
:startDate="_startDate" :endDate="_endDate" :dateStyle="props.dateStyle"
|
||||
:format="props.format" :color="props.color" :fontColor="props.fontColor"
|
||||
:fontDarkColor="props.fontDarkColor" :activeFontColor="props.activeFontColor"
|
||||
:rangColor="props.rangColor" :rangFontColor="props.rangFontColor">
|
||||
</calendar-multiple-uvue>
|
||||
</view>
|
||||
<view class="xCalendarViewNum">
|
||||
<text class="xCalendarViewNumText">{{getmonth(item)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="xCalendarViewSpace"></view>
|
||||
<!--
|
||||
@slot 日历尾部
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view class="xCalendarViewFooter">
|
||||
<x-text color="#707070" font-size="14">{{_tipsText}}</x-text>
|
||||
</view>
|
||||
</slot>
|
||||
|
||||
<!-- 月和年 -->
|
||||
<view :class="[showPanel?'xCalendarViewMonthOn':'xCalendarViewMonthOff']" class="xCalendarViewMonth"
|
||||
:style="{backgroundColor:_monthBgColor}">
|
||||
<view class="xCalendarViewHeader" style="padding: 0 12px">
|
||||
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
|
||||
<!-- 2024年5月 -->
|
||||
<x-text :color="_color" font-size="21">{{_currentDateLabel}}</x-text>
|
||||
<x-icon :color="_color" font-size="21" name="arrow-up-s-fill"></x-icon>
|
||||
</view>
|
||||
<view class="xCalendarViewHeaderRight">
|
||||
<!-- #ifdef APP-ANDROID -->
|
||||
<x-stepper @change="stepperChangeYear" v-model="(_currentYear as number)" :min="1900" :max="5000"
|
||||
width="120"></x-stepper>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-ANDROID -->
|
||||
<x-stepper @change="stepperChangeYear" v-model="_currentYear" :min="1900" :max="5000"
|
||||
width="120"></x-stepper>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="xCalendarViewMonthWrap" style="flex:1">
|
||||
<view @click="changeMonth(index)" class="xCalendarViewMonthItem" v-for="(item,index) in 12"
|
||||
:key="index">
|
||||
<!-- <x-text font-size="18">{{item}}月</x-text> -->
|
||||
<x-text font-size="18">{{i18n.t('tmui4x.calendar.monthCountSelected',item)}}</x-text>
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
$headerHeight: 50px;
|
||||
$weekheaderHeight: 40px;
|
||||
$footerHeight: 40px;
|
||||
$spaceHeight: 5px;
|
||||
$cellHeight: 50px;
|
||||
$minBodyHeight: (
|
||||
50px * 6) + $headerHeight + $weekheaderHeight + $footerHeight + ($spaceHeight * 2
|
||||
);
|
||||
|
||||
.xCalendarViewMonth {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
|
||||
// #ifdef APP-IOS
|
||||
opacity: 0;
|
||||
transform: scale(0, 0);
|
||||
width: 0;
|
||||
height: 0;
|
||||
// #endif
|
||||
// #ifndef APP-IOS
|
||||
opacity: 0;
|
||||
transform: scale(0, 0);
|
||||
// #endif
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
.xCalendarViewMonthWrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.xCalendarViewMonthItem {
|
||||
width: 33.3333%;
|
||||
height: 25%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewWrap {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
// #ifndef APP-IOS
|
||||
transform: scale(2, 2);
|
||||
// #endif
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
// #ifndef APP-IOS
|
||||
transform: scale(1, 1);
|
||||
// #endif
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
.xCalendarViewContentBox {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.xCalendarViewNum {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.xCalendarViewNumText {
|
||||
font-size: 200px;
|
||||
color: rgba(125, 125, 125, 0.1);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.xCalendarViewDataHeaderWrap {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewSpace {
|
||||
height: $spaceHeight;
|
||||
}
|
||||
|
||||
.xCalendarViewHeader {
|
||||
height: $headerHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.xCalendarViewHeaderLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCalendarViewHeaderRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewDataHeader {
|
||||
height: $weekheaderHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.xCalendarViewDataHeaderItem {
|
||||
width: 14.285%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarView {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: $minBodyHeight;
|
||||
}
|
||||
|
||||
.xCalendarViewFooter {
|
||||
height: $footerHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
import { xDate,createDate } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
import { xDateArrayItemType,xCalendarArgs,dateStyleDot,dateStyleBg,dateStyleType,xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
export class xCalendar {
|
||||
date : xDate;
|
||||
calendar:xDateArrayItemType[] = [];
|
||||
constructor(currentDate : string | number | Date | null = null) {
|
||||
this.date = new xDate(currentDate)
|
||||
}
|
||||
isInCurrentMonth(current:Date,target:Date):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
// let d1 = current.getDate()
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
// let d2 = target.getDate()
|
||||
return y1 == y2 && m1 == m2
|
||||
}
|
||||
isInRangeDate(current:Date,targets:Date[],mode:xCalendarMode):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
if(mode == 'day'){
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
return true
|
||||
}
|
||||
}
|
||||
}else if(mode == 'range'){
|
||||
if(targets.length<2) return false;
|
||||
let start = targets[0]
|
||||
let end = targets[targets.length-1]
|
||||
return current.getTime()>start.getTime()&¤t.getTime()<end.getTime()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
isInRangeDateByIndex(current:Date,targets:Date[],mode:xCalendarMode):number{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
if(mode == 'day'){
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
return i
|
||||
}
|
||||
}
|
||||
}else if(mode == 'range'){
|
||||
if(targets.length<2) return -1;
|
||||
let start = targets[0]
|
||||
let end = targets[targets.length-1]
|
||||
if(current.getTime()>start.getTime()&¤t.getTime()<end.getTime()) return -1
|
||||
if(current.getTime()==start.getTime()) return 0
|
||||
if(current.getTime()==end.getTime()) return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
isDisabled(current:Date,start:Date|null,end:Date|null,targets:Date[]):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
let disabled = false
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(start!=null && end!=null){
|
||||
if(start.getTime() <= end.getTime()){
|
||||
// 正常的日期范围:start <= current <= end
|
||||
disabled = disabled || (start.getTime() > current.getTime() || end.getTime() < current.getTime())
|
||||
}
|
||||
// 如果start > end,则忽略范围限制,只应用disabledDays
|
||||
} else {
|
||||
// 只有一个边界时,正常应用
|
||||
if(start!=null){
|
||||
disabled = disabled || start.getTime() > current.getTime()
|
||||
}
|
||||
if(end!=null){
|
||||
disabled = disabled || end.getTime() < current.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
return disabled
|
||||
}
|
||||
isInStart(current:Date,targets:Date[]):boolean{
|
||||
if(targets.length==0) return false;
|
||||
return targets[0].getTime() == current.getTime()
|
||||
}
|
||||
isInEnd(current:Date,targets:Date[]):boolean{
|
||||
if(targets.length<2) return false;
|
||||
return targets[targets.length-1].getTime() == current.getTime()
|
||||
}
|
||||
diffDays(start:Date,end:Date):number{
|
||||
return end.getTime()-start.getTime()
|
||||
}
|
||||
getDateStyle(current:Date,defaultStyle:xCalendarArgs,disabled:boolean,inMonth:boolean,inRange:boolean,isInStart:boolean,isInEnd:boolean,dateStyle:xCalendarDateStyle_type[],mode:xCalendarMode):dateStyleType{
|
||||
let nowdatestyleIndex = dateStyle.findIndex((d:xCalendarDateStyle_type):boolean => {
|
||||
return new Date(d.date.replace(/-/g,'/')).getTime() == current.getTime()
|
||||
})
|
||||
let item:xCalendarDateStyle_type|null = nowdatestyleIndex==-1?null:dateStyle[nowdatestyleIndex]
|
||||
const label = (item?.label??'') as string;
|
||||
let fontColor = (item?.fontColor??defaultStyle.fontColor) as string;
|
||||
fontColor = isInStart||isInEnd||(mode!='range'&&inRange) ? defaultStyle.activeFontColor:(inRange?defaultStyle.rangFontColor:fontColor)
|
||||
let bgColor = (item?.color??'transparent') as string;
|
||||
bgColor = isInStart||isInEnd||(mode!='range'&&inRange) ? defaultStyle.color : (inRange?defaultStyle.rangColor:bgColor)
|
||||
const bgstyle = {
|
||||
/** 底部文本 */
|
||||
label : label,
|
||||
/** 日期文字颜色 */
|
||||
fontColor : fontColor,
|
||||
backgroundColor : bgColor,
|
||||
opacity : disabled||!inMonth?0.5:1
|
||||
} as dateStyleBg
|
||||
|
||||
const dotstyle = {
|
||||
/** 是否显示右角标 */
|
||||
dot : item?.dot??false,
|
||||
/** 右角标背景颜色 */
|
||||
dotColor : item?.dotColor??defaultStyle.color,
|
||||
/** 右角标文字颜色 */
|
||||
dotLabelColor : item?.dotLabelColor??'#ffffff',
|
||||
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
|
||||
dotLabel : item?.dotLabel??'',
|
||||
} as dateStyleDot
|
||||
|
||||
return {
|
||||
dot : dotstyle,
|
||||
dstyle : bgstyle
|
||||
} as dateStyleType
|
||||
}
|
||||
getCalendar(
|
||||
seekDay:number,
|
||||
mode:xCalendarMode,
|
||||
currentDate : string | number | Date | null = null,
|
||||
selectedDate:string[],
|
||||
start:Date|null,
|
||||
end:Date|null,
|
||||
defaultStyle:xCalendarArgs,
|
||||
dateStyle:xCalendarDateStyle_type[] = [],
|
||||
disabledDays:string[] = [],
|
||||
isPadding:boolean = true):xDateArrayItemType[]{
|
||||
|
||||
const nowCutime = Date.now()
|
||||
let nowdate = (currentDate == null?this.date:new xDate(currentDate)) as xDate;
|
||||
const dateAr = nowdate.getDaysOf('m')
|
||||
|
||||
let dates = [] as xDateDayInfoType[]
|
||||
if(isPadding){
|
||||
// 使用 seekDay 参数控制月份第一天的周偏移量
|
||||
// seekDay: 0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
|
||||
// 注意:week 值实际是 0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
|
||||
// 需要转换为 0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日的映射
|
||||
let firstDayOfMonth = dateAr[0]
|
||||
let firstDayWeek = firstDayOfMonth.week
|
||||
|
||||
// 将 week 值转换为 0=周一, 1=周二, ..., 6=周日的映射
|
||||
// 原始:0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
|
||||
// 目标:0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
|
||||
let mappedWeek = (firstDayWeek + 6) % 7 // 将周日(0)映射为6,周一(1)映射为0
|
||||
|
||||
// 计算需要向前填充的天数
|
||||
let beforeNum = 0
|
||||
if (seekDay === 0) {
|
||||
// 周一开始:mappedWeek 为 0 时不需要填充
|
||||
beforeNum = mappedWeek
|
||||
} else {
|
||||
// 其他日期开始:计算到目标起始日的偏移量
|
||||
beforeNum = (mappedWeek - seekDay + 7) % 7
|
||||
}
|
||||
|
||||
// 如果 beforeNum 为 0,说明第一天正好是目标起始日,不需要填充
|
||||
if (beforeNum > 0) {
|
||||
const beforeDates = new xDate(firstDayOfMonth.date).getDaysOfNum(beforeNum,'before')
|
||||
dates = [...beforeDates,...dateAr]
|
||||
} else {
|
||||
dates = [...dateAr]
|
||||
}
|
||||
|
||||
if(dates.length<42){
|
||||
//补齐最后一周的内容
|
||||
const lastDate = new xDate(dates[dates.length-1].date);
|
||||
let lastWeek = lastDate.getDaysOfNum(42-dates.length,'after')
|
||||
dates = [...dates,...lastWeek]
|
||||
}
|
||||
}else{
|
||||
dates = dateAr
|
||||
}
|
||||
|
||||
let selectedTargets = selectedDate.map((d:string):Date =>{
|
||||
|
||||
return new Date(d.replace(/-/g,'/'))
|
||||
})
|
||||
let disabledDaysAs = disabledDays.map((d:string):Date =>{
|
||||
return new Date(d.replace(/-/g,'/'))
|
||||
})
|
||||
|
||||
const current = nowdate.date
|
||||
const list = [] as xDateArrayItemType[]
|
||||
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i]
|
||||
let checkDate = new Date(item.date);
|
||||
const inmonth = this.isInCurrentMonth(checkDate,current);
|
||||
const inRange = this.isInRangeDate(checkDate,selectedTargets,mode);
|
||||
const disabled = this.isDisabled(checkDate,start,end,disabledDaysAs)
|
||||
const isInstart = this.isInStart(checkDate,selectedTargets)
|
||||
const isInEnd = this.isInEnd(checkDate,selectedTargets)
|
||||
const astyle = this.getDateStyle(checkDate,defaultStyle,disabled,inmonth,inRange,isInstart,isInEnd,dateStyle,mode)
|
||||
list.push({
|
||||
date : item,
|
||||
disabled : disabled,
|
||||
inCurrentMonth : inmonth,
|
||||
inRange : inRange,
|
||||
isInstart : isInstart,
|
||||
isInEnd : isInEnd,
|
||||
style : astyle
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
// console.log(`执行组时间:${Date.now()-nowCutime}毫秒,循环数组:${dates.length}`)
|
||||
|
||||
return list
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType,getCurrentInstance } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
|
||||
import { xCalendar } from "./xCalendar"
|
||||
import { xDateArrayItem,xDateArrayItemType,xCalendarArgs,xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
// #ifdef APP
|
||||
import { calendarDraw } from "./calendarDraw"
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const xCalendarViewItemRef = ref<UniElement|null>(null)
|
||||
let calendarDom:calendarDraw|null = null;
|
||||
// #endif
|
||||
type xCalendarMultiplePropsType = {
|
||||
/**
|
||||
* 同步当前时间v-model
|
||||
* 不想受控:model-value
|
||||
*/
|
||||
modelValue : string,
|
||||
/**
|
||||
* 范围选择模式
|
||||
* day:天数多选,通过multipleMax可以设置允许选择的天数
|
||||
* range:天数的范围选择,起始和终止
|
||||
* week:按周次选择范围
|
||||
* quarter:按季度选择范围
|
||||
* year:按年
|
||||
*/
|
||||
model: xCalendarMode,
|
||||
|
||||
/**
|
||||
* 禁用的日期字符串如"2023-12-12"
|
||||
* 它与下面的start,end不冲突。
|
||||
*/
|
||||
disabledDays: string[],
|
||||
/**
|
||||
* 允许选择的开始日期
|
||||
*/
|
||||
startDate: string,
|
||||
/**
|
||||
* 允许选择的结束日期
|
||||
*/
|
||||
endDate: string,
|
||||
/**
|
||||
* 设置指定日期的样式
|
||||
* 数据类型见:xCalendarDateStyle_type
|
||||
*/
|
||||
dateStyle: xCalendarDateStyle_type[],
|
||||
/**
|
||||
* 同步vmodel时格式化模板
|
||||
*/
|
||||
format: string,
|
||||
/**
|
||||
* 选中的主题色,默认空值,取全局主题色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 默认的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontColor:string,
|
||||
/**
|
||||
* 默认的暗黑文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontDarkColor:string,
|
||||
/**
|
||||
* 默认选中时的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
activeFontColor:string,
|
||||
/**
|
||||
* 范围选中时,范围中间的选中颜色,
|
||||
* 如果为空,为color的透明度0.5;
|
||||
*/
|
||||
rangColor:string,
|
||||
rangFontColor:string,
|
||||
currentDate:string,
|
||||
/**
|
||||
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
|
||||
*/
|
||||
seekDay:number,
|
||||
/**
|
||||
* 给日期设定状态类型为:xCalendarDateStyleStatusProps
|
||||
*/
|
||||
dateStatus:xCalendarDateStyleStatusType[],
|
||||
}
|
||||
|
||||
const emit = defineEmits(['change','click'])
|
||||
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
|
||||
modelValue: "",
|
||||
currentDate:'',
|
||||
model:'day' as xCalendarMode,
|
||||
disabledDays:[] as string[],
|
||||
startDate:'1900-1-1',
|
||||
endDate:'2025-5-13',
|
||||
dateStyle:[] as xCalendarDateStyle_type[],
|
||||
format:'YYYY-MM-DD',
|
||||
color:'',
|
||||
fontColor:'#333333',
|
||||
fontDarkColor:'#ffffff',
|
||||
activeFontColor:'#ffffff',
|
||||
rangColor:'',
|
||||
rangFontColor:'',
|
||||
seekDay:0,
|
||||
dateStatus:[] as xCalendarDateStyleStatusType[]
|
||||
})
|
||||
const calendar = new xCalendar()
|
||||
const _rangColor = computed(()=>{
|
||||
let color = props.rangColor == ''?xConfig.color:props.rangColor
|
||||
let rgba = hexToRgb(getDefaultColor(color));
|
||||
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${props.rangColor==''?0.2:1})`
|
||||
})
|
||||
const _modelValue = computed(():string=> props.modelValue )
|
||||
const _model = computed(():xCalendarMode=> props.model )
|
||||
function splitArray<T>(ar : Array<T>, len : number) : Array<Array<T>> {
|
||||
const result : Array<Array<T>> = [];
|
||||
for (let i = 0; i < ar.length; i += len) {
|
||||
result.push(ar.slice(i, i + len));
|
||||
}
|
||||
return result
|
||||
}
|
||||
const _fontSize = computed(():string=> checkIsCssUnit('16',''))
|
||||
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
|
||||
const dateArrayList = computed(():xDateArrayItemType[][]=>{
|
||||
const primaryColor = getDefaultColor(props.color==''?xConfig.color:props.color);
|
||||
const dates = calendar.getCalendar(
|
||||
props.seekDay,
|
||||
props.model,
|
||||
props.currentDate,
|
||||
props.modelValue,
|
||||
props.startDate!=''?new Date(props.startDate.replace(/-/g,'/')):null,
|
||||
props.endDate!=''?new Date(props.endDate.replace(/-/g,'/')):null,
|
||||
{
|
||||
color:primaryColor,
|
||||
fontColor:getDefaultColor(xConfig.dark=='dark'?props.fontDarkColor:props.fontColor),
|
||||
activeFontColor:getDefaultColor(props.activeFontColor),
|
||||
rangColor:_rangColor.value,
|
||||
rangFontColor:props.rangFontColor==''?primaryColor:getDefaultColor(props.rangFontColor)
|
||||
} as xCalendarArgs,
|
||||
props.dateStyle,
|
||||
props.disabledDays
|
||||
|
||||
);
|
||||
|
||||
return splitArray<xDateArrayItemType>(dates,7)
|
||||
})
|
||||
function showLabel(item:xDateArrayItemType):string {
|
||||
if(item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',2);//'本日'
|
||||
if(item.isInstart&&!item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',0);//'开始'
|
||||
if(!item.isInstart&&item.isInEnd&&_model.value=='range') return i18n.t('tmui4x.calendar.rangStatus',1);//'结束'
|
||||
return ""
|
||||
}
|
||||
function dateClick(item:xDateArrayItemType){
|
||||
if(item.disabled) return;
|
||||
if(!calendar.isInCurrentMonth(new Date(item.date.date),new Date(props.currentDate))) return;
|
||||
emit('click',item)
|
||||
}
|
||||
|
||||
function checkDataIsInDateStatus(date:string|null):string{
|
||||
if(date ==''||date == null) return '';
|
||||
for(let k =0 ;k <_dateStatus.value.length;k++){
|
||||
let itemStatus = _dateStatus.value[k]
|
||||
let dates = itemStatus?.date??[];
|
||||
let start = itemStatus?.between?.start??''
|
||||
let end = itemStatus?.between?.end??''
|
||||
let betweenColor = itemStatus?.between?.color??''
|
||||
let notDates = itemStatus?.between?.notDate??[]
|
||||
let nowDate = new xDate(date);
|
||||
let isInBetweenDate = false;
|
||||
if(start!=''&&end!=''){
|
||||
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
|
||||
let isNotDate = false
|
||||
for(let i=0;i<notDates.length;i++){
|
||||
let item = notDates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
|
||||
isNotDate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
isInBetweenDate = isBetween && !isNotDate
|
||||
}
|
||||
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
|
||||
|
||||
let selfColor = ''
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
|
||||
selfColor = item.color==''?'primary':item.color
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(selfColor!=''){
|
||||
return getDefaultColor(selfColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
// #ifdef APP
|
||||
|
||||
function draw():Promise<any>{
|
||||
return new Promise(()=>{
|
||||
calendarDom?.draw(dateArrayList.value,_model.value,_dateStatus.value)
|
||||
})
|
||||
}
|
||||
|
||||
function canvsClick(e:UniPointerEvent){
|
||||
|
||||
xCalendarViewItemRef.value?.getBoundingClientRectAsync()
|
||||
?.then((rect:DOMRect)=>{
|
||||
let cellHeight = 50;
|
||||
let cellWidth = rect.width / 7;
|
||||
let top = rect.top;
|
||||
let left = rect.left;
|
||||
let x = e.clientX - left;
|
||||
let y = e.clientY - top;
|
||||
let col = Math.floor(x/cellWidth);
|
||||
let row = Math.floor(y/cellHeight);
|
||||
let item = dateArrayList.value[row][col];
|
||||
function testThread():Promise<any>{
|
||||
return new Promise(()=>{
|
||||
dateClick(item)
|
||||
})
|
||||
}
|
||||
testThread()
|
||||
})
|
||||
.catch((er)=>{
|
||||
console.error(er)
|
||||
})
|
||||
}
|
||||
|
||||
watch([():any => dateArrayList.value,():any => _dateStatus.value],()=>{
|
||||
draw()
|
||||
})
|
||||
|
||||
// #endif
|
||||
onMounted(()=>{
|
||||
// #ifdef APP
|
||||
calendarDom = new calendarDraw(xCalendarViewItemRef.value,proxy)
|
||||
nextTick(()=>{
|
||||
draw()
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarViewItem"
|
||||
ref="xCalendarViewItemRef"
|
||||
<!-- #ifdef APP -->
|
||||
@click="canvsClick"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<!-- #ifndef APP -->
|
||||
<view class="xCalendarViewItemCol" v-for="(children,index) in dateArrayList" :key="index">
|
||||
<view class="xCalendarViewItemColItem"
|
||||
@click="dateClick(item)"
|
||||
v-for="(item,index2) in (children as xDateArrayItemType[])" :key="index2">
|
||||
<view class="xCalendarViewItemColItemBox"
|
||||
:style="{
|
||||
backgroundColor:item.style.dstyle.backgroundColor,
|
||||
opacity:item.style.dstyle.opacity,
|
||||
}"
|
||||
>
|
||||
<view v-if="item.style.dot.dot"
|
||||
class="xCalendarViewItemDot"
|
||||
:class="[item.style.dot.dotLabel==''?'xCalendarViewItemDotNolabel':'']"
|
||||
:style="{
|
||||
color:item.style.dot.dotLabelColor,
|
||||
backgroundColor:item.style.dot.dotColor
|
||||
}"
|
||||
>
|
||||
{{item.style.dot.dotLabel}}
|
||||
</view>
|
||||
<text class="xCalendarViewItemColDate"
|
||||
:style="{
|
||||
color:item.style.dstyle.fontColor,
|
||||
fontSize:_fontSize
|
||||
}"
|
||||
>
|
||||
{{item.date.day}}
|
||||
</text>
|
||||
<text class="xCalendarViewItemColLabel"
|
||||
:style="{
|
||||
color:item.style.dstyle.fontColor
|
||||
}"
|
||||
>{{showLabel(item)||item.style.dstyle.label}}</text>
|
||||
<view class="xCalendarViewStatus" :style="{backgroundColor:checkDataIsInDateStatus(item.date.date)}" v-if="checkDataIsInDateStatus(item.date.date)!=''"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.xCalendarViewStatus{
|
||||
width:4px;
|
||||
height:4px;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
|
||||
}
|
||||
// #ifdef APP
|
||||
.xCalendarViewItem{
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
.xCalendarViewItemDot{
|
||||
padding:2px 4px;
|
||||
min-width:18px;
|
||||
min-height:18px;
|
||||
font-size:10px;
|
||||
border-radius:9px;
|
||||
position: absolute;
|
||||
right:0px;
|
||||
top:0px;
|
||||
&.xCalendarViewItemDotNolabel{
|
||||
padding:0;
|
||||
min-width:8px;
|
||||
min-height:8px;
|
||||
font-size:10px;
|
||||
border-radius:9px;
|
||||
}
|
||||
}
|
||||
.xCalendarViewItem{
|
||||
width:100%;
|
||||
height:100%;
|
||||
.xCalendarViewItemCol{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
height: 50px;
|
||||
.xCalendarViewItemColItem{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 100%;
|
||||
width:14.285%;
|
||||
.xCalendarViewItemColItemBox{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 25px;
|
||||
overflow: visible;
|
||||
|
||||
}
|
||||
.xCalendarViewItemColDate{
|
||||
text-align: center;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
margin-top: -4px;
|
||||
// font-weight: bold;
|
||||
// font-size: 16px;
|
||||
display: block;
|
||||
}
|
||||
.xCalendarViewItemColLabel{
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
margin-top: -4px;
|
||||
display: block;
|
||||
min-height:10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
import { getDefaultColor, hexToRgb, rgbToHex } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import {xCalendarDateStyleStatusType} from "@/uni_modules/tmx-ui/interface.uts"
|
||||
import { xDate } from "../../core/util/xDate.uts"
|
||||
export class calendarDraw {
|
||||
ele : UniElement | null = null
|
||||
proxy : any | null = null
|
||||
cellHeight = 50;
|
||||
sapce = 5
|
||||
model : xCalendarMode = 'day'
|
||||
_fontSize : string = checkIsCssUnit('16', xConfig.unit)
|
||||
_dateStatus : xCalendarDateStyleStatusType[] | null = null;
|
||||
constructor(target : UniElement | null, proxyx : any | null) {
|
||||
this.ele = target
|
||||
this.proxy = proxyx
|
||||
}
|
||||
checkDataIsInDateStatus(date:string|null):string{
|
||||
if(date ==''||date == null || this._dateStatus == null) return '';
|
||||
for(let k =0 ;k <this._dateStatus.length;k++){
|
||||
let itemStatus = this._dateStatus[k]
|
||||
let dates = itemStatus?.date??[];
|
||||
let start = itemStatus?.between?.start??''
|
||||
let end = itemStatus?.between?.end??''
|
||||
let betweenColor = itemStatus?.between?.color??''
|
||||
let notDates = itemStatus?.between?.notDate??[]
|
||||
let nowDate = new xDate(date);
|
||||
let isInBetweenDate = false;
|
||||
if(start!=''&&end!=''){
|
||||
let isBetween = nowDate.isBetween(new xDate(start),new xDate(end),'d','[]');
|
||||
let isNotDate = false
|
||||
for(let i=0;i<notDates.length;i++){
|
||||
let item = notDates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item),'=','d')){
|
||||
isNotDate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
isInBetweenDate = isBetween && !isNotDate
|
||||
}
|
||||
if(isInBetweenDate) return getDefaultColor(betweenColor==''?'primary':betweenColor)
|
||||
|
||||
let selfColor = ''
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i];
|
||||
if(nowDate.isBetweenOf(new xDate(item.date),'=','d')){
|
||||
selfColor = item.color==''?'primary':item.color
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
if(selfColor!=''){
|
||||
return getDefaultColor(selfColor)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
showLabel(item : xDateArrayItemType) : string {
|
||||
if (item.isInstart && item.isInEnd && this.model == 'range') return '本日'
|
||||
if (item.isInstart && !item.isInEnd && this.model == 'range') return '开始'
|
||||
if (!item.isInstart && item.isInEnd && this.model == 'range') return '结束'
|
||||
return ""
|
||||
}
|
||||
getColor(color : string, alpha : number) : string {
|
||||
if (alpha == 1) return color;
|
||||
let rgba = hexToRgb(getDefaultColor(color));
|
||||
return `rgba(${rgba.getNumber('r')},${rgba.getNumber('g')},${rgba.getNumber('b')},${alpha})`
|
||||
}
|
||||
_draw(element : UniElement, Grect : DOMRect, list : xDateArrayItemType[][]) {
|
||||
const ctx = element.getDrawableContext()!;
|
||||
ctx.reset()
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)'
|
||||
ctx.fillRect(0, 0, Grect.width, Grect.height)
|
||||
ctx.textAlign = 'center'
|
||||
const _realCellWidth = Grect.width / 7
|
||||
const _h = Math.min(_realCellWidth, 50) - this.sapce * 2
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const children = list[i]
|
||||
for (let col = 0; col < children.length; col++) {
|
||||
let item = children[col];
|
||||
let dstyle = item.style
|
||||
|
||||
let xy_x = _realCellWidth * col + _realCellWidth / 2;
|
||||
let xy_y = this.cellHeight * i + this.cellHeight / 2;
|
||||
|
||||
// 绘制选中的背景
|
||||
ctx.fillStyle = this.getColor(dstyle.dstyle.backgroundColor, (item.disabled || !item.inCurrentMonth) && !item.isInstart && !item.isInEnd ? 0.3 : 1);
|
||||
|
||||
if (dstyle.dstyle.backgroundColor != 'transparent') {
|
||||
ctx.beginPath()
|
||||
ctx.arc(xy_x, xy_y, _h / 2, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// 绘制右角标。
|
||||
if (dstyle.dot.dot) {
|
||||
|
||||
if (dstyle.dot.dotLabel == '') {
|
||||
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 5;
|
||||
let lasty = _realCellWidth * (i) + 5
|
||||
ctx.fillStyle = dstyle.dot.dotColor
|
||||
ctx.beginPath()
|
||||
ctx.arc(lastx, lasty, 4, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
} else {
|
||||
let lastx = _realCellWidth * col + _realCellWidth / 2 + this.cellHeight / 2 - 10;
|
||||
let lasty = _realCellWidth * (i) + 10
|
||||
ctx.fillStyle = dstyle.dot.dotColor;
|
||||
ctx.beginPath()
|
||||
ctx.arc(lastx, lasty, 10, 0, Math.PI * 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.font = "10px"
|
||||
ctx.fillStyle = dstyle.dot.dotLabelColor
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 3);
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
ctx.fillText(dstyle.dot.dotLabel, lastx, lasty + 5);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 绘制文字
|
||||
const text = item.date.day.toString();
|
||||
ctx.fillStyle = this.getColor(dstyle.dstyle.fontColor, (item.disabled || !item.inCurrentMonth) ? 0.7 : 1);
|
||||
ctx.font = this._fontSize
|
||||
const textHeight = 34;
|
||||
const _t_x = _realCellWidth * col + _realCellWidth / 2
|
||||
let _t_y = this.cellHeight * i + this.cellHeight / 2 + 2;
|
||||
// #ifdef APP-IOS
|
||||
_t_y = this.cellHeight * i + this.cellHeight / 2 + 6;
|
||||
// #endif
|
||||
ctx.fillText(text, _t_x, _t_y);
|
||||
let label = this.showLabel(item)
|
||||
label = label == '' ? dstyle.dstyle.label : label
|
||||
|
||||
|
||||
// 绘制底部的label
|
||||
if (label != '') {
|
||||
let labely = this.cellHeight * i + this.cellHeight - 9;
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
labely = this.cellHeight * i + this.cellHeight - 12;
|
||||
// #endif
|
||||
ctx.font = "8px"
|
||||
ctx.fillText(label, _t_x, labely);
|
||||
}
|
||||
|
||||
// 绘制状态小点点。
|
||||
let statusColor = this.checkDataIsInDateStatus(item.date.date);
|
||||
if(statusColor!=''){
|
||||
ctx.fillStyle = statusColor
|
||||
ctx.beginPath()
|
||||
let statusY = this.cellHeight * i + this.cellHeight - this.sapce - 3;
|
||||
ctx.arc(_t_x,statusY,2,0,Math.PI*2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
ctx.update()
|
||||
}
|
||||
draw(list : xDateArrayItemType[][], modelv : xCalendarMode = 'day',status:xCalendarDateStyleStatusType[]|null = null) {
|
||||
let _this = this;
|
||||
_this.model = modelv;
|
||||
_this._dateStatus = status;
|
||||
this.ele?.getBoundingClientRectAsync()
|
||||
?.then((rect : DOMRect) => {
|
||||
_this._draw(_this.ele!, rect, list)
|
||||
})
|
||||
.catch((er) => {
|
||||
console.error(er)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
import { xDate } from "../../core/util/xDate.uts"
|
||||
export type xDateArrayItem = {
|
||||
date : xDateDayInfoType,
|
||||
//是否禁用
|
||||
isDisabled : boolean,
|
||||
//是否在当前月份
|
||||
isInCureentMonth : boolean,
|
||||
//是否已经选中
|
||||
isSelected : boolean,
|
||||
//样式
|
||||
style : dateStyleType
|
||||
}
|
||||
|
||||
export type xDateArrayItemType = {
|
||||
date : xDateDayInfoType,
|
||||
//是否禁用
|
||||
disabled : boolean,
|
||||
//是否在当前月份
|
||||
inCurrentMonth : boolean,
|
||||
//是否已经选中
|
||||
inRange : boolean,
|
||||
isInstart : boolean,
|
||||
isInEnd : boolean,
|
||||
//样式
|
||||
style : dateStyleType
|
||||
}
|
||||
|
||||
/**
|
||||
* 日历单个日期的样式对象
|
||||
*/
|
||||
export type xCalendarDateStyle_real_type = {
|
||||
/** 是否显示右角标 */
|
||||
dot : boolean,
|
||||
/** 右角标背景颜色 */
|
||||
dotColor : string,
|
||||
/** 右角标文字颜色 */
|
||||
dotLabelColor : string,
|
||||
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
|
||||
dotLabel : string,
|
||||
/** 底部文本 */
|
||||
label : string,
|
||||
/** 背景颜色 */
|
||||
color : string,
|
||||
/** 日期文字颜色 */
|
||||
fontColor : string,
|
||||
date : string
|
||||
}
|
||||
export type dateStyleDot = {
|
||||
/** 是否显示右角标 */
|
||||
dot : boolean,
|
||||
/** 右角标背景颜色 */
|
||||
dotColor : string,
|
||||
/** 右角标文字颜色 */
|
||||
dotLabelColor : string,
|
||||
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
|
||||
dotLabel : string,
|
||||
}
|
||||
export type dateStyleBg = {
|
||||
/** 底部文本 */
|
||||
label : string,
|
||||
/** 日期文字颜色 */
|
||||
fontColor : string,
|
||||
backgroundColor : string,
|
||||
opacity : number
|
||||
}
|
||||
export type dateStyleType = {
|
||||
dot : dateStyleDot,
|
||||
dstyle : dateStyleBg
|
||||
}
|
||||
export type xCalendarMode = "day" | "range" | "week" | "quarter" | "year"
|
||||
|
||||
export type BODY_SIZE_TYPE = {
|
||||
width : number,
|
||||
height : number
|
||||
}
|
||||
export type xCalendarArgs = {
|
||||
color:string,
|
||||
fontColor:string,
|
||||
activeFontColor:string,
|
||||
rangColor:string
|
||||
rangFontColor:string,
|
||||
}
|
||||
|
||||
export type xCalendarViewUpdateType = {
|
||||
ar : xDateArrayItem[],
|
||||
disabledDays : string[],
|
||||
selectedDate : xDate | null,
|
||||
nowDate : xDate,
|
||||
start : xDate,
|
||||
end : xDate,
|
||||
color : string,
|
||||
dateStyle : xCalendarDateStyle_real_type[]
|
||||
}
|
||||
|
||||
export type GRID_SIZE = {
|
||||
width : 0,
|
||||
height : 0
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType,getCurrentInstance } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xDate, dateCovertXdate, xDateTypeTime } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type,xCalendarDateStyleStatusType } from "../../interface.uts"
|
||||
import { xCalendar } from "./xCalendar"
|
||||
import calendarMultipleUvue from "./calendar-multiple.uvue"
|
||||
import { xDateArrayItem, xDateArrayItemType, xCalendarArgs, xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
|
||||
type xCalendarMultiplePropsType = {
|
||||
/**
|
||||
* 同步当前时间v-model
|
||||
* 不想受控:model-value
|
||||
*/
|
||||
modelValue : string,
|
||||
/**
|
||||
* day:固定此值
|
||||
*/
|
||||
model : "day" | "range" | "week" | "quarter" | "year",
|
||||
|
||||
/**
|
||||
* 禁用的日期字符串如"2023-12-12"
|
||||
* 它与下面的start,end不冲突。
|
||||
*/
|
||||
disabledDays : string[],
|
||||
/**
|
||||
* 是否禁用用户交互,相当于展示日期。
|
||||
*/
|
||||
disabled:boolean,
|
||||
/**
|
||||
* 是否上下切换日历
|
||||
*/
|
||||
vertical:boolean,
|
||||
/**
|
||||
* 允许选择的开始日期
|
||||
*/
|
||||
startDate : string,
|
||||
/**
|
||||
* 允许选择的结束日期
|
||||
*/
|
||||
endDate : string,
|
||||
/**
|
||||
* 设置指定日期的样式
|
||||
* 数据类型见:xCalendarDateStyle_type
|
||||
*/
|
||||
dateStyle : xCalendarDateStyle_type[],
|
||||
/**
|
||||
* 同步vmodel时格式化模板
|
||||
*/
|
||||
format : string,
|
||||
/**
|
||||
* 选中的主题色,默认空值,取全局主题色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
color : string,
|
||||
/**
|
||||
* 默认的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontColor : string,
|
||||
/**
|
||||
* 默认的暗黑文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
fontDarkColor : string,
|
||||
/**
|
||||
* 默认选中时的文字颜色
|
||||
* 如果提供了dateStyle,以dateStyle为准
|
||||
*/
|
||||
activeFontColor : string,
|
||||
/**
|
||||
* 范围选中时,范围中间的选中颜色,
|
||||
* 如果为空,为color的透明度0.5;
|
||||
*/
|
||||
rangColor : string,
|
||||
rangFontColor : string,
|
||||
/**
|
||||
* 头的背景颜色,默认为透明
|
||||
*/
|
||||
headBgColor : string,
|
||||
/**
|
||||
* 头的文字颜色,提供了后暗黑失效会以这个为准。
|
||||
*/
|
||||
headFontColor : string,
|
||||
/**
|
||||
* 头部自定义样式。
|
||||
*/
|
||||
headStyle : string,
|
||||
/**
|
||||
* 循环渲染时,是否只渲染当前面板(如果你在pad等10年前的低端机上渲染日历有压力请打开此值为true)
|
||||
* 关闭后可以提升滑动体验。
|
||||
*/
|
||||
renderOnly:boolean,
|
||||
/**
|
||||
* 你当前的一周的第一天的索引值是几:0: 周一,1: 周二,2: 周三,3: 周四,4: 周五,5: 周六,6: 周日
|
||||
*/
|
||||
seekDay:number,
|
||||
/**
|
||||
* 给日期设定状态
|
||||
* 类型为:xCalendarDateStyleStatusType
|
||||
*/
|
||||
dateStatus:xCalendarDateStyleStatusType[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 日历 xCalendar
|
||||
* @page /pages/index/calendar-view
|
||||
* @category 表单组件
|
||||
* @description 日历面板,支持指定日期新式设置,角标,底部文本设置等暂不同时支持多选,因为不支持联合类型后期需要分开组件使用。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| - | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCalendarMultiple"})
|
||||
const calendar = new xCalendar()
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 时间变化时触发
|
||||
* @param {string[]} value - 当前变化的日期
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 当前日历面板的日期被点击时触发
|
||||
* @param {string} value - 当前被点击的日期
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 当前激活面板月份改变时触发(就是当前看到的月份面板)
|
||||
* @param {string} value - 当前激活面板的日期
|
||||
*/
|
||||
'currentChange',
|
||||
/**
|
||||
* 同步当前的选中的日期绑定
|
||||
* @param {string[]} value - 当前选中日期
|
||||
*/
|
||||
'update:modelValue',
|
||||
/**
|
||||
* 同步当前查看的月份日期,请以日期形式提供值
|
||||
* @param {string} value - 当前查看的月分日期
|
||||
*/
|
||||
'update:currentDate'
|
||||
])
|
||||
const props = withDefaults(defineProps<xCalendarMultiplePropsType>(), {
|
||||
modelValue: "",
|
||||
model: 'day',
|
||||
disabledDays:():string[] => [] as string[],
|
||||
disabled:false,
|
||||
vertical:false,
|
||||
startDate: '1900-1-1',
|
||||
endDate: '2100-1-1',
|
||||
dateStyle: ():xCalendarDateStyle_type[] => [] as xCalendarDateStyle_type[],
|
||||
format: 'YYYY-MM-DD',
|
||||
color: '',
|
||||
fontColor: '#333333',
|
||||
fontDarkColor: '#ffffff',
|
||||
activeFontColor: '#ffffff',
|
||||
rangColor: '',
|
||||
rangFontColor: '',
|
||||
headBgColor: 'transparent',
|
||||
headFontColor: '',
|
||||
headStyle: '',
|
||||
renderOnly:true,
|
||||
seekDay:0,
|
||||
dateStatus:():xCalendarDateStyleStatusType[] => [] as xCalendarDateStyleStatusType[]
|
||||
})
|
||||
const _dateStatus = computed(() : xCalendarDateStyleStatusType[] => props.dateStatus)
|
||||
const weeksCn = computed(() : string[] => {
|
||||
// ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
// 根据 seekDay 属性调整周名称的顺序
|
||||
const weekNames = [
|
||||
i18n!.t("tmui4x.calendar.week",0),
|
||||
i18n!.t("tmui4x.calendar.week",1),
|
||||
i18n!.t("tmui4x.calendar.week",2),
|
||||
i18n!.t("tmui4x.calendar.week",3),
|
||||
i18n!.t("tmui4x.calendar.week",4),
|
||||
i18n!.t("tmui4x.calendar.week",5),
|
||||
i18n!.t("tmui4x.calendar.week",6),
|
||||
]
|
||||
// 如果 seekDay 为 0(默认周一),直接返回原数组
|
||||
if (props.seekDay == 0) {
|
||||
return weekNames
|
||||
}
|
||||
// 根据 seekDay 重新排列数组
|
||||
const result: string[] = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const index = (i + props.seekDay) % 7
|
||||
result.push(weekNames[index])
|
||||
}
|
||||
return result
|
||||
})
|
||||
const _headBgColor = computed(() : string => getDefaultColor(props.headBgColor))
|
||||
const _headFontColor = computed(() : string => {
|
||||
if (props.headFontColor == '') return '#333333'
|
||||
return getDefaultColor(props.headFontColor)
|
||||
})
|
||||
|
||||
const _modelValue = ref(props.modelValue)
|
||||
const _currentDate = ref(new xDate(props.modelValue).format("YYYY-MM-DD"))
|
||||
const _currentDateSwipersIndex = ref(0)
|
||||
|
||||
const _currentDateSwipers = ref<string[]>([])
|
||||
const _currentDateLabel = computed(() : string => {
|
||||
let ars = _currentDate.value.split('-');
|
||||
// `${ars[0]}年${ars[1]}月`
|
||||
return i18n.t('tmui4x.calendar.titleCurrentMonth',[ars[0],ars[1]])
|
||||
})
|
||||
const _currentYear = ref(new xDate(props.modelValue).getYear())
|
||||
let _modelValueDate = computed(() : Date => {
|
||||
if(_modelValue.value=="") return new Date(_currentDate.value.replace(/-/g, '/'));
|
||||
return new Date(_modelValue.value.replace(/-/g, '/'))
|
||||
})
|
||||
const _tipsText = computed(() : string => {
|
||||
// return _modelValue.value!='' ? `已选择` : '未选择日期'
|
||||
return _modelValue.value!='' ? i18n.t('tmui4x.calendar.selectedStatus',2) : i18n.t('tmui4x.calendar.selectedStatus',1)
|
||||
})
|
||||
|
||||
const _monthBgColor = computed(() : string => xConfig.dark == 'dark' ? xConfig.sheetDarkColor : '#ffffff')
|
||||
const _color = computed(() : string => props.color == '' ? getDefaultColor(xConfig.color) : getDefaultColor(props.color))
|
||||
|
||||
const showPanel = ref(false)
|
||||
function dateClick(item : xDateArrayItemType) {
|
||||
|
||||
|
||||
const isInselected = calendar.isInCurrente(new Date(item.date.date), _modelValueDate.value)
|
||||
|
||||
emit('click', item.date.date)
|
||||
if(props.disabled) return;
|
||||
|
||||
let dates = ''
|
||||
if(_modelValue.value==''){
|
||||
dates = item.date.date
|
||||
}else{
|
||||
dates = isInselected?'':item.date.date
|
||||
}
|
||||
const nowFormat = new xDate(dates).format(props.format)
|
||||
_modelValue.value = dates
|
||||
|
||||
emit('update:modelValue',dates==''?'':nowFormat)
|
||||
emit('change', dates==''?'':nowFormat)
|
||||
}
|
||||
function getSwiperListCurrentDates(nowCurrentDate:string):string[]{
|
||||
let index = _currentDateSwipersIndex.value
|
||||
let currentData = new xDate(nowCurrentDate)
|
||||
|
||||
let xd = currentData.getClone().setDateOf(1,'d').format('YYYY-MM-DD')
|
||||
let start = currentData.getClone().setDateOf(1,'d').subtraction(1,'m').format('YYYY-MM-DD')
|
||||
let end = currentData.getClone().setDateOf(1,'d').add(1,'m').format('YYYY-MM-DD')
|
||||
let datas = [xd,end,start]
|
||||
|
||||
if(_currentDateSwipers.value.length == 0) return datas;
|
||||
if(index==0){
|
||||
datas = [xd,end,start]
|
||||
}else if(index==1){
|
||||
datas = [start,xd,end]
|
||||
}else if(index==2){
|
||||
datas = [end,start,xd]
|
||||
}
|
||||
return datas;
|
||||
}
|
||||
function clear() {
|
||||
if (_modelValue.value == "") return;
|
||||
_modelValue.value = ""
|
||||
emit('update:modelValue', _modelValue.value)
|
||||
emit('change', _modelValue.value)
|
||||
}
|
||||
|
||||
function nowMonth() {
|
||||
let nowdate = new xDate()
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function stepperChangeYear(eyear : number) {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.setDateOf(eyear, 'y')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function changeMonth(eyear : number) {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.setDateOf(eyear, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
showPanel.value = false;
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
}
|
||||
function nextMonth() {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.add(1, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
function prevMonth() {
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
nowdate.subtraction(1, 'm')
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
}
|
||||
|
||||
function swiperChange(evt:UniSwiperChangeEvent){
|
||||
_currentDateSwipersIndex.value = evt.detail.current;
|
||||
nextTick(()=>{
|
||||
_currentDate.value = _currentDateSwipers.value[ evt.detail.current]
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
let nowdate = new xDate(_currentDate.value)
|
||||
_currentYear.value = nowdate.getYear()
|
||||
emit('currentChange', _currentDate.value)
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : string => props.modelValue, (newVal:string) => {
|
||||
_modelValue.value = props.modelValue;
|
||||
if(newVal!=""){
|
||||
let nowdate = new xDate(newVal).setDateOf(1,'d')
|
||||
let nowcurrentDate = nowdate
|
||||
if(nowcurrentDate.format("YYYY-MM-DD") == _currentDate.value) return;
|
||||
_currentDate.value = nowcurrentDate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowcurrentDate.getYear()
|
||||
_currentDateSwipersIndex.value = 0;
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
}
|
||||
|
||||
})
|
||||
onMounted(() => {
|
||||
if (props.modelValue !="") {
|
||||
let nowdate = new xDate(props.modelValue)
|
||||
_currentDate.value = nowdate.format("YYYY-MM-DD")
|
||||
_currentYear.value = nowdate.getYear()
|
||||
}
|
||||
nextTick(()=>{
|
||||
_currentDateSwipers.value = getSwiperListCurrentDates(_currentDate.value);
|
||||
emit('update:currentDate', _currentDate.value)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function getmonth(item:string):number{
|
||||
let vls = item.split("-");
|
||||
return parseInt(vls[1])
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
/**
|
||||
* 下月
|
||||
*/
|
||||
next(){
|
||||
nextMonth();
|
||||
},
|
||||
/**
|
||||
* 上月
|
||||
*/
|
||||
prev(){
|
||||
prevMonth();
|
||||
},
|
||||
/**
|
||||
* 设置日历返回到本月
|
||||
*/
|
||||
setCurrentMonth(){
|
||||
nowMonth()
|
||||
},
|
||||
/**
|
||||
* 清空选择
|
||||
*/
|
||||
clear(){
|
||||
clear()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCalendarView">
|
||||
<!--
|
||||
@slot 日历头,隐藏使用空插槽,将隐藏,如果想自定,请通过ref函数来翻页控制日历走向。
|
||||
-->
|
||||
<slot name="header">
|
||||
<view class="xCalendarViewDataHeaderWrap" :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']"
|
||||
:style="[{backgroundColor:_headBgColor},headStyle]">
|
||||
<view class="xCalendarViewHeader" style="padding: 0 12px">
|
||||
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
|
||||
<!-- 2024年5月 -->
|
||||
<x-text :color="_headFontColor" font-size="21">{{_currentDateLabel}}</x-text>
|
||||
<x-icon :color="_headFontColor" font-size="21" name="arrow-down-s-fill"></x-icon>
|
||||
</view>
|
||||
<view class="xCalendarViewHeaderRight">
|
||||
<!-- <view @click="prevMonth" class="px-10 py-15">
|
||||
<x-icon font-size="21" :color="_headFontColor" name="arrow-up-s-fill">上月</x-icon>
|
||||
</view>
|
||||
<view @click="nextMonth" class="px-10 py-15">
|
||||
<x-icon font-size="21" :color="_headFontColor" name="arrow-down-s-fill">下月</x-icon>
|
||||
</view> -->
|
||||
<!-- 清空 -->
|
||||
<x-text @click="clear" :color="_headFontColor" style="padding: 10px 20px;">{{i18n.t('tmui4x.clear')}}</x-text>
|
||||
<!-- 本月 -->
|
||||
<x-text @click="nowMonth" :color="_headFontColor" style="padding: 10px 0px;">{{i18n.t('tmui4x.calendar.currentMonthTitle')}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xCalendarViewDataHeader">
|
||||
<view class="xCalendarViewDataHeaderItem" v-for="(item,index) in 7" :key="item">
|
||||
<!-- {{weeksCn[index]}} -->
|
||||
<x-text :color="_headFontColor" font-size="12">{{weeksCn[index]}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="xCalendarViewSpace"></view>
|
||||
<view :class="[showPanel?'xCalendarViewMonthOff':'xCalendarViewMonthOn']" class="xCalendarViewWrap">
|
||||
<swiper :vertical="props.vertical" @change="swiperChange" :current="_currentDateSwipersIndex" v-if="_currentDateSwipers.length>0" :circular="true" style="width:100%;height:100%">
|
||||
<swiper-item v-for="(item,index) in _currentDateSwipers" :key="index" style="width:100%;height:100%;">
|
||||
<view style="width:100%;height:100%;position: relative;">
|
||||
<view class="xCalendarViewContentBox">
|
||||
<!-- 打开下方,可以在更低端机上提高2/3的性能 -->
|
||||
<calendar-multiple-uvue v-if="index == _currentDateSwipersIndex||!props.renderOnly" @click="dateClick" :currentDate="item"
|
||||
:dateStatus="_dateStatus"
|
||||
:seekDay="props.seekDay"
|
||||
:modelValue="props.modelValue" :model="props.model"
|
||||
:disabledDays="props.disabledDays" :startDate="props.startDate" :endDate="props.endDate"
|
||||
:dateStyle="props.dateStyle" :format="props.format" :color="props.color"
|
||||
:fontColor="props.fontColor" :fontDarkColor="props.fontDarkColor"
|
||||
:activeFontColor="props.activeFontColor" :rangColor="props.rangColor"
|
||||
:rangFontColor="props.rangFontColor">
|
||||
</calendar-multiple-uvue>
|
||||
</view>
|
||||
<view class="xCalendarViewNum">
|
||||
<text class="xCalendarViewNumText">{{getmonth(item)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="xCalendarViewSpace"></view>
|
||||
<!--
|
||||
@slot 日历尾部
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view class="xCalendarViewFooter">
|
||||
<x-text color="#707070" font-size="14">{{_tipsText}}</x-text>
|
||||
</view>
|
||||
</slot>
|
||||
|
||||
<!-- 月和年 -->
|
||||
<view :class="[showPanel?'xCalendarViewMonthOn':'xCalendarViewMonthOff']" class="xCalendarViewMonth"
|
||||
:style="{backgroundColor:_monthBgColor}">
|
||||
<view class="xCalendarViewHeader" style="padding: 0 12px">
|
||||
<view @click="showPanel= !showPanel" class="xCalendarViewHeaderLeft">
|
||||
<!-- 2024年5月 -->
|
||||
<x-text :color="_color" font-size="21">{{_currentDateLabel}}</x-text>
|
||||
<x-icon :color="_color" font-size="21" name="arrow-up-s-fill"></x-icon>
|
||||
</view>
|
||||
<view class="xCalendarViewHeaderRight">
|
||||
<x-stepper @change="stepperChangeYear" v-model="(_currentYear as number)" :min="1900" :max="5000"
|
||||
width="120"></x-stepper>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xCalendarViewMonthWrap" style="flex:1">
|
||||
<view @click="changeMonth(index)" class="xCalendarViewMonthItem" v-for="(item,index) in 12"
|
||||
:key="index">
|
||||
<!-- <x-text font-size="18">{{item}}月</x-text> -->
|
||||
<x-text font-size="18">{{i18n.t('tmui4x.calendar.monthCountSelected',item)}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
$headerHeight: 50px;
|
||||
$weekheaderHeight: 40px;
|
||||
$footerHeight: 40px;
|
||||
$spaceHeight: 5px;
|
||||
$cellHeight: 50px;
|
||||
$minBodyHeight: (
|
||||
50px * 6) + $headerHeight + $weekheaderHeight + $footerHeight + ($spaceHeight * 2
|
||||
);
|
||||
|
||||
.xCalendarViewMonth {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
// #ifdef APP-IOS
|
||||
opacity: 0;
|
||||
transform: scale(0, 0);
|
||||
width:0;
|
||||
height:0;
|
||||
// #endif
|
||||
// #ifndef APP-IOS
|
||||
opacity: 0;
|
||||
transform: scale(0, 0);
|
||||
// #endif
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
.xCalendarViewMonthWrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.xCalendarViewMonthItem {
|
||||
width: 33.3333%;
|
||||
height: 25%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewWrap {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
// #ifndef APP-IOS
|
||||
transform: scale(2, 2);
|
||||
// #endif
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
// #ifndef APP-IOS
|
||||
transform: scale(1, 1);
|
||||
// #endif
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
.xCalendarViewContentBox {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.xCalendarViewNum {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.xCalendarViewNumText {
|
||||
font-size: 200px;
|
||||
color: rgba(125, 125, 125, 0.1);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.xCalendarViewDataHeaderWrap {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(.42, .38, .15, .93);
|
||||
transition-duration: 0.3s;
|
||||
|
||||
&.xCalendarViewMonthOff {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.xCalendarViewMonthOn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewSpace {
|
||||
height: $spaceHeight;
|
||||
}
|
||||
|
||||
.xCalendarViewHeader {
|
||||
height: $headerHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.xCalendarViewHeaderLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCalendarViewHeaderRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarViewDataHeader {
|
||||
height: $weekheaderHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.xCalendarViewDataHeaderItem {
|
||||
width: 14.285%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.xCalendarView {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: $minBodyHeight;
|
||||
}
|
||||
|
||||
.xCalendarViewFooter {
|
||||
height: $footerHeight;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,237 @@
|
||||
import { xDate } from "../../core/util/xDate.uts"
|
||||
import { xDateDayInfoType, xCalendarDateStyle_type } from "../../interface.uts"
|
||||
import { xDateArrayItemType,xCalendarArgs,dateStyleDot,dateStyleBg,dateStyleType,xCalendarMode } from "../x-calendar-view/interface.uts"
|
||||
export class xCalendar {
|
||||
date : xDate;
|
||||
calendar:xDateArrayItemType[] = [];
|
||||
constructor(currentDate : string | number | Date | null = null) {
|
||||
this.date = new xDate(currentDate)
|
||||
}
|
||||
isInCurrentMonth(current:Date,target:Date):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
// let d1 = current.getDate()
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
// let d2 = target.getDate()
|
||||
return y1 == y2 && m1 == m2
|
||||
}
|
||||
isInpanel(current:Date|string):boolean{
|
||||
console.log(this.calendar)
|
||||
if(this.calendar.length==0) return false;
|
||||
let s = new xDate(this.calendar[0].date.date);
|
||||
let e = new xDate(this.calendar[this.calendar.length-1].date.date);
|
||||
let c = new xDate(current);
|
||||
|
||||
return c.getTime('d')<=e.getTime('d')&&c.getTime('d')>=s.getTime('d')
|
||||
}
|
||||
isInRangeDate(current:Date,targets:Date[],mode:xCalendarMode):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
if(mode == 'day'){
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
return true
|
||||
}
|
||||
}
|
||||
}else if(mode == 'range'){
|
||||
if(targets.length<2) return false;
|
||||
let start = targets[0]
|
||||
let end = targets[targets.length-1]
|
||||
return current.getTime()>start.getTime()&¤t.getTime()<end.getTime()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
isInRangeDateByIndex(current:Date,targets:Date[],mode:xCalendarMode):number{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
if(mode == 'day'){
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
return i
|
||||
}
|
||||
}
|
||||
}else if(mode == 'range'){
|
||||
if(targets.length<2) return -1;
|
||||
let start = targets[0]
|
||||
let end = targets[targets.length-1]
|
||||
if(current.getTime()>start.getTime()&¤t.getTime()<end.getTime()) return -1
|
||||
if(current.getTime()==start.getTime()) return 0
|
||||
if(current.getTime()==end.getTime()) return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
isInCurrente(current:Date,targets:Date):boolean{
|
||||
return targets.getTime() == current.getTime()
|
||||
}
|
||||
isDisabled(current:Date,start:Date|null,end:Date|null,targets:Date[]):boolean{
|
||||
let y1 = current.getFullYear()
|
||||
let m1 = current.getMonth()
|
||||
let d1 = current.getDate()
|
||||
let disabled = false
|
||||
for(let i=0;i<targets.length;i++){
|
||||
let target = targets[i]
|
||||
let y2 = target.getFullYear()
|
||||
let m2 = target.getMonth()
|
||||
let d2 = target.getDate()
|
||||
if(y1 == y2 && m1 == m2 && d1 == d2){
|
||||
disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(start!=null && end!=null){
|
||||
if(start.getTime() <= end.getTime()){
|
||||
disabled = disabled || (start.getTime() > current.getTime() || end.getTime() < current.getTime())
|
||||
}
|
||||
} else {
|
||||
if(start!=null){
|
||||
disabled = disabled || start.getTime() > current.getTime()
|
||||
}
|
||||
if(end!=null){
|
||||
disabled = disabled || end.getTime() < current.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return disabled
|
||||
}
|
||||
isInStart(current:Date,targets:Date[]):boolean{
|
||||
if(targets.length==0) return false;
|
||||
return targets[0].getTime() == current.getTime()
|
||||
}
|
||||
isInEnd(current:Date,targets:Date[]):boolean{
|
||||
if(targets.length<2) return false;
|
||||
return targets[targets.length-1].getTime() == current.getTime()
|
||||
}
|
||||
diffDays(start:Date,end:Date):number{
|
||||
return end.getTime()-start.getTime()
|
||||
}
|
||||
getDateStyle(current:Date,defaultStyle:xCalendarArgs,disabled:boolean,inMonth:boolean,inRange:boolean,isInStart:boolean,isInEnd:boolean,dateStyle:xCalendarDateStyle_type[],mode:xCalendarMode):dateStyleType{
|
||||
let nowdatestyleIndex = dateStyle.findIndex((d:xCalendarDateStyle_type):boolean => {
|
||||
return new Date(d.date.replace(/-/g,'/')).getTime() == current.getTime()
|
||||
})
|
||||
let item:xCalendarDateStyle_type|null = nowdatestyleIndex==-1?null:dateStyle[nowdatestyleIndex]
|
||||
const label = (item?.label??'') as string;
|
||||
let fontColor = (item?.fontColor??defaultStyle.fontColor) as string;
|
||||
fontColor = inRange ? defaultStyle.activeFontColor:(inRange?defaultStyle.rangFontColor:fontColor)
|
||||
let bgColor = (item?.color??'transparent') as string;
|
||||
bgColor = inRange ? defaultStyle.color : (inRange?defaultStyle.rangColor:bgColor)
|
||||
const bgstyle = {
|
||||
/** 底部文本 */
|
||||
label : label,
|
||||
/** 日期文字颜色 */
|
||||
fontColor : fontColor,
|
||||
backgroundColor : bgColor,
|
||||
opacity : disabled||!inMonth?0.5:1
|
||||
} as dateStyleBg
|
||||
|
||||
const dotstyle = {
|
||||
/** 是否显示右角标 */
|
||||
dot : item?.dot??false,
|
||||
/** 右角标背景颜色 */
|
||||
dotColor : item?.dotColor??defaultStyle.color,
|
||||
/** 右角标文字颜色 */
|
||||
dotLabelColor : item?.dotLabelColor??'#ffffff',
|
||||
/** 注意如果dot为true,此内容为空就会显示小圆点。如果有内容优先显示本文本 */
|
||||
dotLabel : item?.dotLabel??'',
|
||||
} as dateStyleDot
|
||||
|
||||
return {
|
||||
dot : dotstyle,
|
||||
dstyle : bgstyle
|
||||
} as dateStyleType
|
||||
}
|
||||
getCalendar(
|
||||
seekDay:number,
|
||||
mode:xCalendarMode,
|
||||
currentDate : string | number | Date | null = null,
|
||||
selectedDate:string,
|
||||
start:Date|null,
|
||||
end:Date|null,
|
||||
defaultStyle:xCalendarArgs,
|
||||
dateStyle:xCalendarDateStyle_type[] = [],
|
||||
disabledDays:string[] = [],
|
||||
isPadding:boolean = true
|
||||
):xDateArrayItemType[]{
|
||||
const nowCutime = Date.now()
|
||||
let nowdate = (currentDate == null?this.date:new xDate(currentDate)) as xDate;
|
||||
const dateAr = nowdate.getDaysOf('m')
|
||||
let dates = [] as xDateDayInfoType[]
|
||||
if(isPadding){
|
||||
let firstDayOfMonth = dateAr[0]
|
||||
let firstDayWeek = firstDayOfMonth.week
|
||||
// 将 week 值转换为 0=周一, 1=周二, ..., 6=周日的映射
|
||||
// 原始:0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六
|
||||
// 目标:0=周一, 1=周二, 2=周三, 3=周四, 4=周五, 5=周六, 6=周日
|
||||
// 将周日(0)映射为6,周一(1)映射为0
|
||||
let mappedWeek = (firstDayWeek + 6) % 7
|
||||
// 计算需要向前填充的天数
|
||||
let beforeNum = 0
|
||||
if (seekDay === 0) {
|
||||
// 周一开始:mappedWeek 为 0 时不需要填充
|
||||
beforeNum = mappedWeek
|
||||
} else {
|
||||
// 其他日期开始:计算到目标起始日的偏移量
|
||||
beforeNum = (mappedWeek - seekDay + 7) % 7
|
||||
}
|
||||
// 如果 beforeNum 为 0,说明第一天正好是目标起始日,不需要填充
|
||||
if (beforeNum > 0) {
|
||||
const beforeDates = new xDate(firstDayOfMonth.date).getDaysOfNum(beforeNum,'before')
|
||||
dates = [...beforeDates,...dateAr]
|
||||
} else {
|
||||
dates = [...dateAr]
|
||||
}
|
||||
// 补齐到完整的6周(42天)
|
||||
if(dates.length < 42){
|
||||
//补齐最后一周的内容
|
||||
let lastWeek = new xDate(dates[dates.length-1].date).getDaysOfNum(42-dates.length,'after')
|
||||
dates = [...dates,...lastWeek]
|
||||
}
|
||||
}else{
|
||||
dates = dateAr
|
||||
}
|
||||
|
||||
let selectedTargets = selectedDate==""?new Date():new Date(selectedDate.replace(/-/g,'/'))
|
||||
let disabledDaysAs = disabledDays.map((d:string):Date =>{
|
||||
return new Date(d.replace(/-/g,'/'))
|
||||
})
|
||||
|
||||
const current = nowdate.date
|
||||
const list = [] as xDateArrayItemType[]
|
||||
for(let i=0;i<dates.length;i++){
|
||||
let item = dates[i]
|
||||
let checkDate = new Date(item.date);
|
||||
const inmonth = this.isInCurrentMonth(checkDate,current);
|
||||
const inRange = this.isInCurrente(checkDate,selectedTargets);
|
||||
const disabled = this.isDisabled(checkDate,start,end,disabledDaysAs)
|
||||
const isInstart = false
|
||||
const isInEnd = false
|
||||
const astyle = this.getDateStyle(checkDate,defaultStyle,disabled,inmonth,inRange,isInstart,isInEnd,dateStyle,mode)
|
||||
list.push({
|
||||
date : item,
|
||||
disabled : disabled,
|
||||
inCurrentMonth : inmonth,
|
||||
inRange : inRange,
|
||||
isInstart : isInstart,
|
||||
isInEnd : isInEnd,
|
||||
style : astyle
|
||||
})
|
||||
|
||||
}
|
||||
this.calendar = list;
|
||||
return list
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 卡片 xCard
|
||||
* @page /pages/index/card
|
||||
* @category 展示组件
|
||||
* @description 圆角,主题可统一全局配置风格。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCard"})
|
||||
|
||||
const emits = defineEmits([
|
||||
/** 卡片被点击 */
|
||||
'click',
|
||||
/** 右边状态小图标被点击 */
|
||||
'status',
|
||||
/** 底部按钮被点击 */
|
||||
'action'
|
||||
])
|
||||
|
||||
type BtnSizeType = "mini" | "small" | "normal" | "large"
|
||||
|
||||
type xCardPropsType = {
|
||||
/** 内容的内边距。 */
|
||||
padding: string,
|
||||
/** 标题文字大小 */
|
||||
titleSize: string,
|
||||
/** 按钮颜色 */
|
||||
btnColor: string,
|
||||
/** 标题颜色 */
|
||||
color: string,
|
||||
/** 背景颜色 */
|
||||
bgColor: string,
|
||||
/** 暗黑背景颜色,如果为空,取sheetDarkColor */
|
||||
darkBgColor: string,
|
||||
/** 底部按钮数组。如果不满意风格布局请使用插槽footer来布局 */
|
||||
btns: string[],
|
||||
/** 副标题 */
|
||||
subtitle: string,
|
||||
/** 标题 */
|
||||
title: string,
|
||||
/** 右边的小图标,如果你是想显示状态,日期请使用对应插槽 */
|
||||
statusIcon: string,
|
||||
/** 中间内容。如果有大量内容请直接在默认插槽(标签内)内布局 */
|
||||
content: string,
|
||||
/** 头部图片地址。 */
|
||||
image: string,
|
||||
/** 头图片高度 */
|
||||
imageHeight: string,
|
||||
/** 圆角请不要动态更改此会,默认为空,取全局设置的风格值。 */
|
||||
round: string,
|
||||
/** 请不要动态更改些投影值,截止4.75+鸿蒙无法使用投影 */
|
||||
shadow: string,
|
||||
/** 按钮尺寸 */
|
||||
btnSize: BtnSizeType
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCardPropsType>(), {
|
||||
padding: "16",
|
||||
titleSize: "18",
|
||||
btnColor: "",
|
||||
color: "#333333",
|
||||
bgColor: "#ffffff",
|
||||
darkBgColor: "",
|
||||
btns: () : string[] => [] as string[],
|
||||
subtitle: "",
|
||||
title: "",
|
||||
statusIcon: "more-fill",
|
||||
content: "",
|
||||
image: "",
|
||||
imageHeight: "150",
|
||||
round: "",
|
||||
shadow: "0 3px 10px rgba(0, 0, 0, 0.05)",
|
||||
btnSize: "small" as BtnSizeType
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const _image = computed((): string => props.image)
|
||||
const _round = computed((): string => {
|
||||
if (props.round == "") return checkIsCssUnit(xConfig.cardRound, xConfig.unit)
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
const _imageHeight = computed((): string => checkIsCssUnit(props.imageHeight, xConfig.unit))
|
||||
const _padding = computed((): string => checkIsCssUnit(props.padding, xConfig.unit))
|
||||
const _titleSize = computed((): string => props.titleSize)
|
||||
const _color = computed((): string => getDefaultColor(props.color))
|
||||
const _bgColor = computed((): string => {
|
||||
if(xConfig.dark=='dark'){
|
||||
if(props.darkBgColor!='') return getDefaultColor(props.darkBgColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _btnColor = computed((): string => {
|
||||
if (props.btnColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _btns = computed((): string[] => props.btns)
|
||||
const _subtitle = computed((): string => props.subtitle)
|
||||
const _title = computed((): string => props.title)
|
||||
const _statusIcon = computed((): string => props.statusIcon)
|
||||
const _content = computed((): string => props.content)
|
||||
|
||||
// 方法
|
||||
function actionClick(index : number) {
|
||||
emits('action', index)
|
||||
}
|
||||
function statusClick() {
|
||||
emits('status')
|
||||
}
|
||||
function onClick() {
|
||||
emits('click')
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- boxShadow: shadow, -->
|
||||
<view @click="onClick" class="xCard"
|
||||
<!-- #ifndef APP-HARMONY -->
|
||||
:style="{borderRadius:_round,boxShadow: shadow,backgroundColor:_bgColor}"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
:style="{borderRadius:_round,backgroundColor:_bgColor}"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<!--
|
||||
@slot 图片插槽
|
||||
-->
|
||||
<slot name="image">
|
||||
<image :style="{width:'100%',height:_imageHeight,borderRadius:`${_round} ${_round} 0px 0px`}" :src="_image"
|
||||
v-if="_image!=''"></image>
|
||||
</slot>
|
||||
<view :style="{padding:_padding}">
|
||||
<view class="xCardHeader">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
-->
|
||||
<slot name="title">
|
||||
<view v-if="_title!=''">
|
||||
<x-text :color="_color" :font-size="_titleSize" class="xCardTitle">{{_title}}</x-text>
|
||||
</view>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 状态右边小图标插槽
|
||||
-->
|
||||
<slot name="statusIcon">
|
||||
<view @click.stop="statusClick" v-if="_statusIcon!=''" style="padding: 0rpx 0rpx 0rpx 16px;">
|
||||
<x-icon :font-size="_titleSize" :name="_statusIcon"></x-icon>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="xCardSubtitle">
|
||||
<!--
|
||||
@slot 副标题插槽
|
||||
-->
|
||||
<slot name="subtitle">
|
||||
<x-text font-size="12" v-if="_subtitle!=''" class="xCardSubtitleText">
|
||||
{{subtitle}}
|
||||
</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<!--
|
||||
@slot 默认内容插槽
|
||||
-->
|
||||
<slot>
|
||||
<x-text :color="_color" font-size="16" v-if="_content!=''" class="xCardContent">
|
||||
{{_content}}
|
||||
</x-text>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 底部插槽
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view class="xCardFooter" v-if="_btns.length>0">
|
||||
<x-button round="6" @click.stop="actionClick(index)" v-for="(item,index) in _btns" :key="index"
|
||||
:style="{marginRight:(index!=_btns.length-1)?'12px':'0px'}" :color="_btnColor"
|
||||
:size="btnSize">{{item}}</x-button>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xCardContent {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.xCardFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.xCardSubtitle {}
|
||||
|
||||
.xCardSubtitleText {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
padding: 12rpx 0rpx 24rpx 0rpx;
|
||||
}
|
||||
|
||||
.xCardHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCardTitle {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xCard {
|
||||
/* box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 侧边分类数据
|
||||
*/
|
||||
export type SLIDER_TREE_ITEM = {
|
||||
id : string,
|
||||
title : string,
|
||||
children : SLIDER_TREE_ITEM[],
|
||||
disabled : boolean,
|
||||
/** 当前选中的id数组 */
|
||||
selected : string[],
|
||||
checked:boolean
|
||||
}
|
||||
|
||||
export type NODES_PATH_MENU_TYPE = {
|
||||
indexPath:number[],
|
||||
ids:string[],
|
||||
pathData:SLIDER_TREE_ITEM[]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { CASCADER_TREE_ITEM, CASCADER_PATH_MENU_TYPE } from "../../interface.uts"
|
||||
|
||||
/**
|
||||
* 当前树有多少个被选中了。
|
||||
*/
|
||||
export function getTreeSelectedNum(item : CASCADER_TREE_ITEM[], target : Set<string>) : number {
|
||||
let inx = 0;
|
||||
function jshz(tree : CASCADER_TREE_ITEM[]) {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
let item = tree[i]
|
||||
if (item.children.length > 0) {
|
||||
jshz(item.children)
|
||||
} else if (target.has(item.id)) {
|
||||
inx += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
jshz(item)
|
||||
return inx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目标节点,过滤掉父节点。
|
||||
* @param item
|
||||
* @param target
|
||||
*/
|
||||
export function filterParentNode(item : CASCADER_TREE_ITEM[], target : Set<string>) : string[] {
|
||||
function jshz(tree : CASCADER_TREE_ITEM[]) : string[] {
|
||||
let arr = [] as string[]
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
let item = tree[i]
|
||||
if (item.children.length > 0) {
|
||||
arr = arr.concat(jshz(item.children))
|
||||
} else if (target.has(item.id)) {
|
||||
arr.push(item.id)
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
return jshz(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是父节点
|
||||
* @param item
|
||||
* @param target
|
||||
*/
|
||||
export function isParent(item : CASCADER_TREE_ITEM[], target : string) : boolean {
|
||||
let parent = false;
|
||||
function jshz(tree : CASCADER_TREE_ITEM[]) {
|
||||
if (parent) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
let item = tree[i]
|
||||
if (item.id == target && item.children.length > 0) {
|
||||
parent = true;
|
||||
} else if (item.id != target && item.children.length > 0) {
|
||||
jshz(item.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
jshz(item)
|
||||
return parent
|
||||
}
|
||||
/** 通过节点获取当前节点的索引路径和ids数组 */
|
||||
export function getIndexPathAndIds(tree : CASCADER_TREE_ITEM[], targetId : string) : string[] {
|
||||
const path : string[] = [];
|
||||
|
||||
function dfs(node : CASCADER_TREE_ITEM[]) : boolean {
|
||||
for (let i = 0; i < node.length; i++) {
|
||||
let item = node[i];
|
||||
path.push(item.id);
|
||||
if (item.id === targetId) {
|
||||
return true;
|
||||
}
|
||||
if (dfs(item.children)) {
|
||||
return true;
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dfs(tree)) {
|
||||
return path;
|
||||
} else {
|
||||
return [] as string[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 通过节点获取当前节点的索引路径和ids数组 */
|
||||
export function getTreeNodesPath(tree : CASCADER_TREE_ITEM[], targetId : string) : CASCADER_PATH_MENU_TYPE|null {
|
||||
const path : string[] = [];
|
||||
const indexPaths:number[] = [];
|
||||
const pathData:CASCADER_TREE_ITEM[] = [];
|
||||
function dfs(node : CASCADER_TREE_ITEM[]) : boolean {
|
||||
for (let i = 0; i < node.length; i++) {
|
||||
let item = node[i];
|
||||
path.push(item.id);
|
||||
indexPaths.push(i)
|
||||
pathData.push(item)
|
||||
if (item.id === targetId) {
|
||||
return true;
|
||||
}
|
||||
if (dfs(item.children)) {
|
||||
return true;
|
||||
}
|
||||
path.pop();
|
||||
indexPaths.pop();
|
||||
pathData.pop();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dfs(tree)) {
|
||||
return {
|
||||
indexPath:indexPaths,
|
||||
ids:path,
|
||||
pathData
|
||||
} as CASCADER_PATH_MENU_TYPE;
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,754 @@
|
||||
<script lang="ts">
|
||||
import { PropType, toRaw } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CASCADER_ITEM_INFO,CASCADER_TREE_ITEM } from "../../interface.uts"
|
||||
import { isParent, getTreeNodesPath, getTreeSelectedNum } from "./util.uts"
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @name 极联器 xCascader
|
||||
* @page /pages/index/cascader
|
||||
* @category 表单组件
|
||||
* @description 极联选择器,单选模式。
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
//当前的层级
|
||||
activeId: "",
|
||||
selectedsIds: [] as string[],
|
||||
//当前的层级索引
|
||||
selectedsIdsItem: [] as CASCADER_TREE_ITEM[],
|
||||
menuBarList: [] as CASCADER_TREE_ITEM[],
|
||||
activeIndex: [] as number[],
|
||||
changshowpenl: "0",
|
||||
showCchangshowpenl: true,
|
||||
swiperIndex: 0,
|
||||
_nowListSwiper:[] as CASCADER_TREE_ITEM[][],
|
||||
isAndriod:(uni.getSystemInfoSync().platform == 'android') as boolean,
|
||||
// 添加切换状态管理,防止闪烁
|
||||
isTransitioning: false,
|
||||
nextSwiperIndex: 0,
|
||||
tid:20
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
|
||||
/**
|
||||
* 选中触发时变化,只要路径变化了就会触发
|
||||
* @param {String[]} ids - 当前id路径值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击项目时触发
|
||||
* @param {CASCADER_TREE_ITEM} item - 项目数据
|
||||
* @param {number} parentIndex - 父index
|
||||
* @param {number} childrenIndex - 当前子index
|
||||
*/
|
||||
'cellClick',
|
||||
/**
|
||||
* 最后一项时触发,或者选择本级时触发
|
||||
* @param {String} id - 最后一级选中的值
|
||||
* @param {String[]} ids - 完整的路径id值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 等同v-model,或者选择本级时触发
|
||||
* @param {String} id - 当前id
|
||||
*/
|
||||
'update:modelValue'
|
||||
],
|
||||
props: {
|
||||
/**
|
||||
* 宽,不可为auto。
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 高是必填,不可为auto。
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "150"
|
||||
},
|
||||
|
||||
/**
|
||||
* 选项项目未选中的文字颜色
|
||||
*/
|
||||
itemTextColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 选项项目未选中的暗黑文字颜色,空值是取白色
|
||||
*/
|
||||
darkItemTextColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 选项项目选中的文字颜色,空值取全局主题
|
||||
*/
|
||||
itemActiveColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
/**
|
||||
* 内容区域背景颜色
|
||||
*/
|
||||
sliderContentBgColor: {
|
||||
type: String,
|
||||
default: "transparent"
|
||||
},
|
||||
/**
|
||||
* 提供的数据结构
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<CASCADER_ITEM_INFO[]>,
|
||||
default: () : CASCADER_ITEM_INFO[] => [] as CASCADER_ITEM_INFO[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的id
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 每级是否允许多选
|
||||
* 暂不开放,如需多选请参考组件slider-tree。
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 项目文字大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "18"
|
||||
},
|
||||
/**
|
||||
* 是否在有下级的项目上显示选择本级按钮.
|
||||
* 当用户选中了本级时就同选择最后一项一样会触发confirm及同步vmodel值
|
||||
*/
|
||||
showCurrentBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
_showCurrentBtn() : boolean {
|
||||
return this.showCurrentBtn
|
||||
},
|
||||
|
||||
_width() : string {
|
||||
return checkIsCssUnit(this.width, xConfig.unit);
|
||||
},
|
||||
_height() : string {
|
||||
return checkIsCssUnit(this.height, xConfig.unit);
|
||||
},
|
||||
_fontSize() : string {
|
||||
let fontSize = checkIsCssUnit(this.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
},
|
||||
|
||||
_itemTextColor() : string {
|
||||
let color = this.itemTextColor;
|
||||
if (xConfig.dark == 'dark') {
|
||||
color = this.darkItemTextColor != "" ? this.darkItemTextColor : "#ffffff"
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
},
|
||||
_itemActiveColor() : string {
|
||||
return this.itemActiveColor != "" ? getDefaultColor(this.itemActiveColor) : getDefaultColor(xConfig.color);
|
||||
},
|
||||
|
||||
_sliderContentBgColor() : string {
|
||||
return getDefaultColor(this.sliderContentBgColor);
|
||||
},
|
||||
_multiple() : boolean {
|
||||
return this.multiple
|
||||
},
|
||||
_list() : CASCADER_TREE_ITEM[] {
|
||||
let list = this.list as CASCADER_ITEM_INFO[];
|
||||
function addOptionalFieldsToTree(tree : CASCADER_ITEM_INFO[]) : void {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
node.disabled = node.disabled == null ? false : node.disabled! as boolean;
|
||||
node.selected = node.selected == null ? [] : node.selected! as string[];
|
||||
node.children = node.children == null ? ([] as CASCADER_ITEM_INFO[]) : node.children! as CASCADER_ITEM_INFO[];
|
||||
if ((node.children!).length > 0) {
|
||||
addOptionalFieldsToTree(node.children! as CASCADER_ITEM_INFO[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
function addOptionalFieldsToTreeClolone(tree : CASCADER_ITEM_INFO[]) : CASCADER_TREE_ITEM[] {
|
||||
let nowlist = [] as CASCADER_TREE_ITEM[]
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
node.disabled = node.disabled == null ? false : node.disabled! as boolean;
|
||||
node.selected = node.selected == null ? [] : node.selected! as string[];
|
||||
node.children = node.children == null ? ([] as CASCADER_ITEM_INFO[]) : node.children! as CASCADER_ITEM_INFO[];
|
||||
let item = {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
disabled: node.disabled!,
|
||||
selected: node.selected!,
|
||||
children: [] as CASCADER_TREE_ITEM[],
|
||||
checked: false
|
||||
} as CASCADER_TREE_ITEM
|
||||
if ((node.children!).length > 0) {
|
||||
item.children = addOptionalFieldsToTreeClolone(node.children! as CASCADER_ITEM_INFO[]);
|
||||
}
|
||||
nowlist.push(item)
|
||||
}
|
||||
|
||||
return nowlist
|
||||
}
|
||||
|
||||
addOptionalFieldsToTree(list as CASCADER_ITEM_INFO[]);
|
||||
|
||||
return addOptionalFieldsToTreeClolone(list)
|
||||
},
|
||||
_borderColor() : string {
|
||||
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
|
||||
return '#f5f5f5'
|
||||
},
|
||||
_nextChildren() : CASCADER_TREE_ITEM[] {
|
||||
if (this.menuBarList.length == 0) return this._list
|
||||
let lastChildren = this.menuBarList[this.menuBarList.length -1]
|
||||
if(lastChildren.children.length==0) return [] as CASCADER_TREE_ITEM[];
|
||||
return lastChildren.children
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.oninit();
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
watch: {
|
||||
modelValue(newval : string) {
|
||||
let nowid = ""
|
||||
if (this.selectedsIds.length > 0) {
|
||||
nowid = this.selectedsIds[this.selectedsIds.length - 1]
|
||||
}
|
||||
if (nowid == newval) return;
|
||||
this.oninit();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFontSize(k:string):string{
|
||||
return checkIsCssUnit(k,xConfig.unit)
|
||||
},
|
||||
getNowListSwiper(){
|
||||
if (this.menuBarList.length == 0){
|
||||
// 当菜单为空时,显示根级数据
|
||||
this._nowListSwiper = [this._list]
|
||||
return
|
||||
}
|
||||
let lastId = this.menuBarList[this.menuBarList.length -1].id;
|
||||
let temlist = [] as CASCADER_TREE_ITEM[][]
|
||||
try {
|
||||
temlist = this.findIdToMenuAry(lastId,this._list)
|
||||
} catch (error) {
|
||||
//TODO handle the exception
|
||||
console.error(error)
|
||||
// 出错时回退到根级数据
|
||||
temlist = [this._list]
|
||||
}
|
||||
this._nowListSwiper = temlist
|
||||
},
|
||||
// 根据id返回第一级到当前id级的数据列表
|
||||
findIdToMenuAry(id: string | number, trees: CASCADER_TREE_ITEM[]): CASCADER_TREE_ITEM[][] {
|
||||
// 存储最终结果:每个层级的所有节点(带checked状态)
|
||||
const result: CASCADER_TREE_ITEM[][] = [];
|
||||
// 存储从根到目标节点的路径
|
||||
const selectedPath: CASCADER_TREE_ITEM[] = [];
|
||||
// 标记是否找到目标节点
|
||||
let found = false;
|
||||
|
||||
// 递归查找目标节点并构建路径
|
||||
function findPath(node: CASCADER_TREE_ITEM, path: CASCADER_TREE_ITEM[] = []): boolean {
|
||||
// 当前节点加入临时路径
|
||||
const currentPath = [...path, node] as CASCADER_TREE_ITEM[];
|
||||
|
||||
// 找到目标节点
|
||||
if (node.id == id) {
|
||||
// 复制路径到selectedPath
|
||||
selectedPath.push(...currentPath);
|
||||
found = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 递归搜索子节点
|
||||
if (node.children.length>0) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i]
|
||||
if (findPath(child, currentPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const tree = trees[i]
|
||||
// 直接检查第一级节点
|
||||
if (tree.id == id) {
|
||||
selectedPath.push(tree);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
// 如果第一级不匹配,继续递归查找
|
||||
else if (findPath(tree)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果找到目标节点,构建结果数组
|
||||
if (found) {
|
||||
// 构建每一层级的节点列表
|
||||
let currentLevel = trees;
|
||||
let currentParent = null as null|CASCADER_TREE_ITEM;
|
||||
|
||||
// 遍历路径中的每个节点
|
||||
for (let i = 0; i < selectedPath.length; i++) {
|
||||
const pathNode = selectedPath[i];
|
||||
|
||||
// 为当前层级的所有节点添加checked状态
|
||||
const levelWithChecked = currentLevel
|
||||
|
||||
// 添加到结果数组
|
||||
result.push(levelWithChecked);
|
||||
|
||||
// 更新下一层级的父节点和子节点列表
|
||||
if (i < selectedPath.length - 1) {
|
||||
currentParent = currentLevel.find((node:CASCADER_TREE_ITEM):boolean => node.id == pathNode.id);
|
||||
|
||||
currentLevel = currentParent?.children??[]
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return [trees];
|
||||
},
|
||||
//初始默认选中一个id值。如果为空的话。
|
||||
oninit() {
|
||||
const trees = toRaw(this._list) as CASCADER_TREE_ITEM[]
|
||||
let rulst = getTreeNodesPath(trees, this.modelValue)
|
||||
if (rulst != null) {
|
||||
let indexpath = rulst.indexPath!
|
||||
this.menuBarList = rulst.pathData
|
||||
this.selectedsIds = rulst.ids
|
||||
this.activeIndex = indexpath
|
||||
this.changshowpenl = getUid() as string;
|
||||
this.swiperIndex = this.menuBarList.length - 1
|
||||
}
|
||||
this.getNowListSwiper()
|
||||
},
|
||||
change(isCurrent:boolean) {
|
||||
let idis = this.selectedsIds.slice(0);
|
||||
let empty = ""
|
||||
// 单项模式下,父节点不更新对外更新值,只取最后一个确定的值。
|
||||
if (idis.length > 0) {
|
||||
empty = idis[idis.length - 1]
|
||||
}
|
||||
if (empty != "" && (this._nextChildren.length==0||isCurrent)) {
|
||||
/**
|
||||
* 更新当前的值,等同v-model
|
||||
*/
|
||||
this.$emit('update:modelValue', empty)
|
||||
/**
|
||||
* 最后一项时触发.
|
||||
*/
|
||||
this.$emit('confirm',empty,idis)
|
||||
|
||||
}
|
||||
this.$emit('change', idis)
|
||||
},
|
||||
|
||||
swiperChange(detail:UniSwiperChangeEvent){
|
||||
this.swiperIndex = detail.detail.current;
|
||||
},
|
||||
/**
|
||||
* isCurrent:是否选中本级,不进行下级跳转
|
||||
*/
|
||||
nextOnClick(item : CASCADER_TREE_ITEM, parentIndex : number,childrenIndex:number, isNext : boolean,isCurrent:boolean) {
|
||||
if (item.disabled || this.isTransitioning) return;
|
||||
|
||||
// 设置过渡状态,防止快速点击造成的闪烁
|
||||
this.isTransitioning = true;
|
||||
|
||||
if(isNext){
|
||||
this.menuBarList.push(item)
|
||||
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
|
||||
|
||||
// 先更新数据,再切换swiper
|
||||
this.getNowListSwiper()
|
||||
|
||||
if(item.children.length>0 && !isCurrent){
|
||||
// 使用nextTick确保数据更新完成后再切换
|
||||
this.$nextTick(() => {
|
||||
this.swiperIndex += 1
|
||||
// 延迟重置过渡状态
|
||||
const _this =this;
|
||||
this.tid = setTimeout(() => {
|
||||
_this.isTransitioning = false
|
||||
}, 200)
|
||||
})
|
||||
}else{
|
||||
if(!isCurrent){
|
||||
// #ifdef APP-IOS
|
||||
this.swiperIndex = parentIndex
|
||||
// #endif
|
||||
}
|
||||
this.isTransitioning = false
|
||||
}
|
||||
this.$emit("cellClick",item,parentIndex-1,childrenIndex)
|
||||
this.change(isCurrent)
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuBarList = this.menuBarList.slice(0,this.swiperIndex+1)
|
||||
|
||||
if(item.children.length>0){
|
||||
if(this.swiperIndex == this.menuBarList.length-1){
|
||||
this.menuBarList.splice(this.menuBarList.length-1,1,item)
|
||||
}else{
|
||||
this.menuBarList.push(item)
|
||||
}
|
||||
|
||||
// 先更新数据
|
||||
this.getNowListSwiper()
|
||||
|
||||
if(!isCurrent){
|
||||
// 使用nextTick确保数据更新完成
|
||||
this.$nextTick(() => {
|
||||
this.swiperIndex += 1
|
||||
const _this =this;
|
||||
this.tid = setTimeout(() => {
|
||||
_this.isTransitioning = false
|
||||
}, 200)
|
||||
})
|
||||
} else {
|
||||
this.isTransitioning = false
|
||||
}
|
||||
}else{
|
||||
if(parentIndex>this.menuBarList.length-1){
|
||||
this.menuBarList.push(item)
|
||||
}else{
|
||||
this.menuBarList.splice(this.menuBarList.length-1,1,item)
|
||||
}
|
||||
this.isTransitioning = false
|
||||
}
|
||||
|
||||
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
|
||||
this.$emit("cellClick",item,parentIndex,childrenIndex)
|
||||
this.change(isCurrent)
|
||||
},
|
||||
menuBarClick(index : number) {
|
||||
if(index==-1 || this.isTransitioning) return;
|
||||
|
||||
// 设置过渡状态
|
||||
this.isTransitioning = true;
|
||||
|
||||
if(index==0){
|
||||
this.menuBarList = [] as CASCADER_TREE_ITEM[]
|
||||
}else{
|
||||
this.menuBarList = this.menuBarList.slice(0,index)
|
||||
}
|
||||
|
||||
// 先更新数据,再切换swiper
|
||||
this.getNowListSwiper()
|
||||
|
||||
// 使用nextTick确保数据更新完成
|
||||
this.$nextTick(() => {
|
||||
this.swiperIndex = index
|
||||
this.selectedsIds = this.menuBarList.map((el:CASCADER_TREE_ITEM):string => el.id)
|
||||
this.change(false)
|
||||
|
||||
// 延迟重置过渡状态
|
||||
const _this =this;
|
||||
this.tid = setTimeout(() => {
|
||||
_this.isTransitioning = false
|
||||
}, 200)
|
||||
})
|
||||
},
|
||||
elitext(text : string) : string {
|
||||
let len = text.length;
|
||||
return len <= 7 ? text : (text.substring(0, 7) + '..')
|
||||
},
|
||||
/**
|
||||
* 当前是否选中
|
||||
*/
|
||||
isSelected(item : CASCADER_TREE_ITEM) : boolean {
|
||||
return (this.selectedsIds.includes(item.id)) && item.children.length == 0
|
||||
},
|
||||
/**
|
||||
* 本下级选了几个
|
||||
*/
|
||||
isSelectedNum(item : CASCADER_TREE_ITEM) : number {
|
||||
let ps = new Set(this.selectedsIds)
|
||||
return getTreeSelectedNum(item.children, ps);
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCascaderTree" :style="{width:_width,minHeight:`calc(${_height} + 50px)`}">
|
||||
<scroll-view direction="horizontal" class="xCascaderTreeBar">
|
||||
|
||||
<!--
|
||||
@slot 顶部头菜单导航插槽,你可以完全写自己的导航样式
|
||||
@prop {CASCADER_TREE_ITEM[]} menus - 菜单导航,注意,可能为空
|
||||
-->
|
||||
<slot name="header" :menus="menuBarList" >
|
||||
|
||||
<view @click="menuBarClick(index)" v-if="menuBarList.length>0" class="xCascaderBarTreeItem"
|
||||
v-for="(item,index) in menuBarList" :key="index">
|
||||
<text class="xCascaderTreeItemBarText"
|
||||
:style="{
|
||||
color:_itemActiveColor,
|
||||
fontSize:_fontSize,
|
||||
whiteSpace:'nowrap',
|
||||
border:`1px solid ${_itemActiveColor}`
|
||||
}
|
||||
">
|
||||
{{elitext(item.title)}}
|
||||
</text>
|
||||
<x-icon
|
||||
v-if="index<menuBarList.length-1||(index==0&&_nextChildren.length>0)||_nextChildren.length>0"
|
||||
class="xCascaderBarTreeItemRight" :font-size="_fontSize"
|
||||
name="arrow-right-s-line" :color="_itemActiveColor"></x-icon>
|
||||
</view>
|
||||
|
||||
<view v-if="_nextChildren.length>0||(menuBarList.length==0&&_list.length>0)" @click="menuBarClick(-1)" class="xCascaderBarTreeItem">
|
||||
<text :style="{color:_itemTextColor,fontSize:_fontSize,whiteSpace:'nowrap'}">
|
||||
<!-- 请选择 -->
|
||||
{{i18n!.t('tmui4x.cascader.placeholder')}}
|
||||
</text>
|
||||
<x-icon class="xCascaderBarTreeItemRight" font-size="14" name="arrow-right-s-line"
|
||||
:color="_itemActiveColor"></x-icon>
|
||||
</view>
|
||||
|
||||
|
||||
</slot>
|
||||
</scroll-view>
|
||||
<view :style="{height:'1px',borderBottom:`1px solid ${_borderColor}`}"></view>
|
||||
<swiper :disable-touch="true" :duration="200" @change="swiperChange" :style="{height:_height}" :current="swiperIndex">
|
||||
<swiper-item v-for="(children,childrenIndex) in _nowListSwiper" :key="childrenIndex" class="xCascaderTreeSwiperItem"
|
||||
:style="{height:_height}">
|
||||
<list-view style="width: 100%;height: 100%;" direction="vertical">
|
||||
<list-item v-for="(item,index) in children" :key="index" :style="{height:getFontSize('50')}">
|
||||
<view :hover-start-time="10" :hover-stay-time="100"
|
||||
:hover-class="item.disabled?'':'xCascaderTreeItemHover'"
|
||||
@click="nextOnClick(item,childrenIndex,index,false,false)" class="xCascaderTreeItemRight"
|
||||
:style="{backgroundColor:_sliderContentBgColor,opacity:item.disabled?'0.5':1}">
|
||||
<view style="flex: 1;">
|
||||
<text class="xCascaderTreeItemRightText"
|
||||
:style="{fontSize:_fontSize,color:isSelected(item)?_itemActiveColor : _itemTextColor}">
|
||||
{{item.title}}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
style="display: flex;flex-direction: row;justify-content: flex-end;align-items: center;">
|
||||
<text class="xCascaderTreeItemRightTextBtns" :style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
|
||||
v-if="item.children.length>0&&_showCurrentBtn" @click.stop="nextOnClick(item,swiperIndex+1,index,false,true)">
|
||||
<!-- 选择本级 -->
|
||||
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
|
||||
|
||||
</text>
|
||||
<text :style="{fontSize:_fontSize,color:_itemActiveColor,marginRight:'8px'}"
|
||||
v-if="item.children.length>0&&isSelectedNum(item)>0">已选({{isSelectedNum(item)}})</text>
|
||||
<x-icon v-if="isSelected(item)" :color="_itemActiveColor"
|
||||
name="check-line"></x-icon>
|
||||
<x-icon v-if="item.children.length>0" :color="_itemTextColor"
|
||||
name="arrow-right-s-line"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
|
||||
</list-view>
|
||||
</swiper-item>
|
||||
|
||||
<swiper-item v-if="_nextChildren.length>0||isAndriod" class="xCascaderTreeSwiperItem" :style="{height:_height}">
|
||||
|
||||
|
||||
<list-view style="width: 100%;height: 100%;" direction="vertical">
|
||||
<list-item v-for="(item,index) in _nextChildren" :key="index" :style="{height:getFontSize('50')}">
|
||||
<view :hover-start-time="10" :hover-stay-time="100"
|
||||
:hover-class="item.disabled?'':'xCascaderTreeItemHover'"
|
||||
@click="nextOnClick(item,swiperIndex+1,index,true,false)" class="xCascaderTreeItemRight"
|
||||
:style="{backgroundColor:_sliderContentBgColor,opacity:item.disabled?'0.5':1}">
|
||||
<view style="flex: 1;">
|
||||
<text class="xCascaderTreeItemRightText"
|
||||
:style="{fontSize:_fontSize,color:isSelected(item)?_itemActiveColor : _itemTextColor}">
|
||||
{{item.title}}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
style="display: flex;flex-direction: row;justify-content: flex-end;align-items: center;">
|
||||
|
||||
<text class="xCascaderTreeItemRightTextBtns" :style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
|
||||
v-if="item.children.length>0&&_showCurrentBtn" @click.stop="nextOnClick(item,swiperIndex+1,index,true,true)">
|
||||
<!-- 选择本级 -->
|
||||
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
|
||||
</text>
|
||||
|
||||
<text :style="{fontSize:_fontSize,color:_itemActiveColor,marginRight:'8px'}"
|
||||
v-if="item.children.length>0&&isSelectedNum(item)>0">已选({{isSelectedNum(item)}})</text>
|
||||
<x-icon v-if="isSelected(item)" :color="_itemActiveColor"
|
||||
name="check-line"></x-icon>
|
||||
<x-icon v-if="item.children.length>0" :color="_itemTextColor"
|
||||
name="arrow-right-s-line"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
</swiper-item>
|
||||
|
||||
</swiper>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xCascaderTreeItemRightTextBtns{
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
/* #ifdef APP-ANDROID */
|
||||
padding: 0px 8px;
|
||||
line-height:1.5;
|
||||
/* #endif */
|
||||
/* #ifndef APP-ANDROID */
|
||||
padding: 4px 8px;
|
||||
line-height:1;
|
||||
/* #endif */
|
||||
}
|
||||
.xCascaderTreeItemBarText {
|
||||
text-overflow: ellipsis;
|
||||
lines: 1;
|
||||
border-radius: 3px;
|
||||
/* #ifdef APP */
|
||||
padding: 0px 8px;
|
||||
line-height:1.5;
|
||||
/* #endif */
|
||||
/* #ifndef APP */
|
||||
padding: 4px 8px;
|
||||
line-height:1;
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
/* 限定为3行 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xCascaderTreeBar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
height: 50px;
|
||||
|
||||
}
|
||||
|
||||
.xCascaderBarTreeItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xCascaderBarTreeItemRight {
|
||||
margin: 0px 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.xCascaderTreeSwiperItem {
|
||||
/* #ifdef WEB */
|
||||
cursor: default;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.xCascaderTreeItemHover {
|
||||
background-color: rgba(155, 155, 155, 0.1);
|
||||
}
|
||||
|
||||
.xCascaderTreeItemMoreHeader {
|
||||
padding: 12px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCascaderTreeItemMore {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
|
||||
}
|
||||
|
||||
.xCascaderTreeItem {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xCascaderTreeItemRight {
|
||||
/* padding:0 24rpx; */
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.xCascaderTreeItemRightText {
|
||||
text-align: left;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
/* #ifndef APP */
|
||||
white-space: nowrap;
|
||||
/* 禁止换行 */
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,525 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType, toRaw } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CASCADER_ITEM_INFO, CASCADER_TREE_ITEM } from "../../interface.uts"
|
||||
import { isParent, getTreeNodesPath, getTreeSelectedNum } from "./util.uts"
|
||||
type findNodePathType = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentPath : CASCADER_ITEM_INFO[]) => CASCADER_ITEM_INFO[]|null;
|
||||
type findNodeLayersType = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentLayers : CASCADER_ITEM_INFO[][]) => CASCADER_ITEM_INFO[][]|null
|
||||
|
||||
/**
|
||||
* @name 极联器 xCascader
|
||||
* @page /pages/index/cascader
|
||||
* @category 表单组件
|
||||
* @description 极联选择器,单选模式。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xCascader" })
|
||||
type menuTypeCascaderType = {
|
||||
selected : boolean,
|
||||
item : CASCADER_ITEM_INFO
|
||||
}
|
||||
type xCascaderTreeProps = {
|
||||
/**
|
||||
* 宽,可以为auto
|
||||
*/
|
||||
width : string,
|
||||
/**
|
||||
* 高,不可为auto。
|
||||
*/
|
||||
height : string,
|
||||
/**
|
||||
* 数据结构
|
||||
*/
|
||||
list : CASCADER_ITEM_INFO[],
|
||||
/**
|
||||
* 当前选中项的id
|
||||
*/
|
||||
modelValue : string,
|
||||
/**
|
||||
* 项目文字大小id
|
||||
*/
|
||||
fontSize : string,
|
||||
/**
|
||||
* 选项项目未选中的文字颜色
|
||||
*/
|
||||
itemTextColor : string,
|
||||
/**
|
||||
* 选项项目未选中的暗黑文字颜色,空值是取白色
|
||||
*/
|
||||
darkItemTextColor : string,
|
||||
/**
|
||||
* 选项项目选中的文字颜色,空值取全局主题
|
||||
*/
|
||||
itemActiveColor : string,
|
||||
/**
|
||||
* 内容区域背景颜色
|
||||
*/
|
||||
sliderContentBgColor : string,
|
||||
/**
|
||||
* 是否在有下级的项目上显示选择本级按钮.
|
||||
* 当用户选中了本级时就同选择最后一项一样会触发confirm及同步vmodel值
|
||||
*/
|
||||
showCurrentBtn : boolean,
|
||||
}
|
||||
const props = withDefaults(defineProps<xCascaderTreeProps>(), {
|
||||
width: 'auto',
|
||||
height: '150',
|
||||
list:():CASCADER_ITEM_INFO[] => [] as CASCADER_ITEM_INFO[],
|
||||
fontSize: "18",
|
||||
itemTextColor: "#333333",
|
||||
darkItemTextColor: "",
|
||||
itemActiveColor: "",
|
||||
sliderContentBgColor: 'rgba(0,0,0,0)'
|
||||
})
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 选中触发时变化,只要路径变化了就会触发
|
||||
* @param {String[]} ids - 当前id路径值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击项目时触发
|
||||
* @param {CASCADER_TREE_ITEM} item - 项目数据
|
||||
* @param {number} parentIndex - 父index
|
||||
* @param {number} childrenIndex - 当前子index
|
||||
*/
|
||||
'cellClick',
|
||||
/**
|
||||
* 最后一项时触发,或者选择本级时触发
|
||||
* @param {String} id - 最后一级选中的值
|
||||
* @param {String[]} ids - 完整的路径id值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 等同v-model,或者选择本级时触发
|
||||
* @param {String} id - 当前id
|
||||
*/
|
||||
'update:modelValue'
|
||||
])
|
||||
const _width = computed(() : string => checkIsCssUnit(props.width, xConfig.unit));
|
||||
const _height = computed(() : string => checkIsCssUnit(props.height, xConfig.unit));
|
||||
const _showCurrentBtn = computed(() : boolean => props.showCurrentBtn);
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
});
|
||||
|
||||
const _itemActiveColor = computed(() : string => {
|
||||
return props.itemActiveColor != "" ? getDefaultColor(props.itemActiveColor) : getDefaultColor(xConfig.color);
|
||||
});
|
||||
const _itemTextColor = computed(() : string => {
|
||||
let color = props.itemTextColor;
|
||||
if (xConfig.dark == 'dark') {
|
||||
color = props.darkItemTextColor != "" ? props.darkItemTextColor : "#ffffff"
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
});
|
||||
|
||||
const _sliderContentBgColor = computed(() : string => getDefaultColor(props.sliderContentBgColor));
|
||||
const _borderColor = computed(() : string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
|
||||
return '#f5f5f5'
|
||||
});
|
||||
|
||||
const nowVal = ref('')
|
||||
|
||||
function getNodeArrayPaths(list : CASCADER_ITEM_INFO[], id : string = '') : menuTypeCascaderType[] {
|
||||
if (list.length == 0) {
|
||||
return [] as menuTypeCascaderType[]
|
||||
}
|
||||
// 如果id为空,从第一级第一个节点开始
|
||||
if (id == '') {
|
||||
const firstNode = list[0]
|
||||
const path : CASCADER_ITEM_INFO[] = [firstNode]
|
||||
// 如果第一个节点有子级,继续向下找到最后一级
|
||||
let currentNode = firstNode
|
||||
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
while (children.length > 0) {
|
||||
currentNode = children[0]
|
||||
path.push(currentNode)
|
||||
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
}
|
||||
// 转换为menuTypeCascaderType数组,所有节点都设为未选中
|
||||
return path.map((item, index) : menuTypeCascaderType => ({
|
||||
selected: false,
|
||||
item: item
|
||||
} as menuTypeCascaderType))
|
||||
}
|
||||
let findNodePath : findNodePathType| null = null;
|
||||
// 根据id查找节点路径
|
||||
findNodePath = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentPath : CASCADER_ITEM_INFO[]) : CASCADER_ITEM_INFO[] | null => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
const newPath : CASCADER_ITEM_INFO[] = [...currentPath, node]
|
||||
if (node.id == targetId) {
|
||||
// 找到目标节点,如果有子级则继续向下找到最后一级
|
||||
let currentNode = node
|
||||
let finalPath : CASCADER_ITEM_INFO[] = [...newPath]
|
||||
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
while (children.length > 0) {
|
||||
currentNode = children[0]
|
||||
finalPath.push(currentNode)
|
||||
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
}
|
||||
return finalPath
|
||||
}
|
||||
// 在子节点中继续查找
|
||||
let children = node?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
if (children.length > 0) {
|
||||
let fph = findNodePath!;
|
||||
const result = fph(children, targetId, newPath)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const result = findNodePath(list, id, [] as CASCADER_ITEM_INFO[])
|
||||
if (result != null) {
|
||||
// 转换为menuTypeCascaderType数组,提供的id及之前的节点设为selected,之后的为未选中
|
||||
let targetIndex = -1
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i].id == id) {
|
||||
targetIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return result.map((item, index) : menuTypeCascaderType => ({
|
||||
selected: targetIndex >= 0 && index <= targetIndex,
|
||||
item: item
|
||||
} as menuTypeCascaderType))
|
||||
}
|
||||
return [] as menuTypeCascaderType[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 同getNodeArrayPaths类似,但不同的是,它是返回 id所在层级的数组,形成一个平铺的列表组供用户选择当前组的某一项。
|
||||
* 返回格式是CASCADER_ITEM_INFO[][]
|
||||
*/
|
||||
function getNodeArraySlier(list : CASCADER_ITEM_INFO[], id : string = '') : CASCADER_ITEM_INFO[][]{
|
||||
if (list.length == 0) {
|
||||
return [] as CASCADER_ITEM_INFO[][]
|
||||
}
|
||||
|
||||
// 如果id为空,从第一级开始,继续向下找到最后一级
|
||||
if (id == '') {
|
||||
const layers : CASCADER_ITEM_INFO[][] = [list]
|
||||
let currentNodes = list
|
||||
while (currentNodes.length > 0 && (currentNodes[0]?.children??([] as CASCADER_ITEM_INFO[])).length > 0) {
|
||||
currentNodes = (currentNodes[0]?.children??([] as CASCADER_ITEM_INFO[]))
|
||||
layers.push(currentNodes)
|
||||
}
|
||||
return layers
|
||||
}
|
||||
let findNodeLayers : findNodeLayersType | null = null;
|
||||
// 查找id所在的路径
|
||||
findNodeLayers = (nodes : CASCADER_ITEM_INFO[], targetId : string, currentLayers : CASCADER_ITEM_INFO[][]) : CASCADER_ITEM_INFO[][] | null => {
|
||||
// 当前层级添加到结果中
|
||||
const newLayers : CASCADER_ITEM_INFO[][] = [...currentLayers, nodes]
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.id == targetId) {
|
||||
// 找到目标节点,继续向下找到最后一级
|
||||
let finalLayers : CASCADER_ITEM_INFO[][] = [...newLayers]
|
||||
let currentNode = node
|
||||
let children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
while (children.length > 0) {
|
||||
finalLayers.push(children)
|
||||
currentNode = children[0]
|
||||
children = currentNode?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
}
|
||||
return finalLayers
|
||||
}
|
||||
|
||||
// 在子节点中继续查找
|
||||
let children = node?.children ?? ([] as CASCADER_ITEM_INFO[])
|
||||
if (children.length > 0) {
|
||||
const result = findNodeLayers!(children, targetId, newLayers)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const result = findNodeLayers(list, id, [] as CASCADER_ITEM_INFO[][])
|
||||
|
||||
return result != null ? result : [list] as CASCADER_ITEM_INFO[][]
|
||||
}
|
||||
const currentIndex = ref(0)
|
||||
const _list = computed(() : CASCADER_ITEM_INFO[][] => getNodeArraySlier(props.list, nowVal.value))
|
||||
const _menus = computed(() : menuTypeCascaderType[] => getNodeArrayPaths(props.list, nowVal.value))
|
||||
const _menusList = computed(() : CASCADER_ITEM_INFO[] => {
|
||||
return _menus.value.map((el : menuTypeCascaderType) : CASCADER_ITEM_INFO => el.item)
|
||||
})
|
||||
|
||||
const getIds = () : string[] => {
|
||||
let ids : string[] = []
|
||||
for (let i = 0; i < _menus.value.length; i++) {
|
||||
let item = _menus.value[i]
|
||||
|
||||
if (!item.selected) {
|
||||
break;
|
||||
}
|
||||
ids.push(item.item.id)
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
const elitext = (text : string) : string => {
|
||||
let len = text.length;
|
||||
return len <= 7 ? text : (text.substring(0, 7) + '..')
|
||||
}
|
||||
const isSelected = (item : CASCADER_ITEM_INFO) : boolean => {
|
||||
let ids = _menus.value.findIndex((el : menuTypeCascaderType) : boolean => el.item.id == item.id && el.selected)
|
||||
return ids > -1
|
||||
}
|
||||
const isCurrentNext = computed(() : boolean => {
|
||||
return _menus.value.some((el : menuTypeCascaderType) : boolean => el.selected == false)
|
||||
})
|
||||
const getNowvalIndex = () : number => {
|
||||
let index = 0
|
||||
for (let i = 0; i < _menus.value.length; i++) {
|
||||
let item = _menus.value[i]
|
||||
|
||||
if (!item.selected) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
index = Math.min(_menus.value.length - 1, index)
|
||||
return index;
|
||||
}
|
||||
const menuBarClick = (item : menuTypeCascaderType, index : number) => {
|
||||
let cindex = index
|
||||
if (item.selected) {
|
||||
if (index == 0) {
|
||||
nowVal.value = '';
|
||||
} else {
|
||||
let cureentindex = index - 1;
|
||||
cureentindex = Math.max(0, Math.min(cureentindex, _menus.value.length - 1))
|
||||
nowVal.value = _menus.value[cureentindex].item.id
|
||||
}
|
||||
|
||||
} else {
|
||||
nowVal.value = item.item.id;
|
||||
}
|
||||
nextTick(() => {
|
||||
currentIndex.value = getNowvalIndex();
|
||||
emit('change', getIds())
|
||||
emit('update:modelValue', nowVal.value)
|
||||
})
|
||||
}
|
||||
const selectedCurrentChildren = (item : CASCADER_ITEM_INFO, index : number) => {
|
||||
let disabled = item?.disabled ?? false
|
||||
if (item.id == nowVal.value || disabled) {
|
||||
return;
|
||||
}
|
||||
nowVal.value = item.id;
|
||||
// currentIndex.value = index;
|
||||
emit('change', getIds())
|
||||
}
|
||||
|
||||
const nextCellClick = (item : CASCADER_ITEM_INFO, index : number, childrenIndex : number) => {
|
||||
let disabled = item?.disabled ?? false
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nowVal.value != item.id) {
|
||||
emit('change', getIds())
|
||||
}
|
||||
nowVal.value = item.id;
|
||||
|
||||
nextTick(() => {
|
||||
currentIndex.value = getNowvalIndex();
|
||||
emit('update:modelValue', nowVal.value)
|
||||
emit('cellClick', item, index, childrenIndex)
|
||||
if ((item?.children?.length ?? 0) == 0) {
|
||||
emit('confirm', nowVal.value, getIds())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : any => props.modelValue, () => {
|
||||
nowVal.value = props.modelValue;
|
||||
nextTick(() => {
|
||||
currentIndex.value = getNowvalIndex();
|
||||
})
|
||||
})
|
||||
onMounted(() => {
|
||||
nowVal.value = props.modelValue;
|
||||
nextTick(() => {
|
||||
currentIndex.value = getNowvalIndex();
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCascaderTree" :style="{width:_width,height:_height}">
|
||||
<!-- 导航 -->
|
||||
<scroll-view direction="horizontal" class="xCascaderTreeBar">
|
||||
|
||||
<!--
|
||||
@slot 顶部头菜单导航插槽,你可以完全写自己的导航样式
|
||||
@prop {CASCADER_ITEM_INFO[]} menus - 菜单导航,注意,可能为空
|
||||
-->
|
||||
<slot name="header" :menus="_menusList">
|
||||
<view v-if="_menus.length>0" class="xCascaderBarTreeItem" v-for="(item,index) in _menus" :key="index">
|
||||
<text @click="menuBarClick(item,index)" v-if="item.selected" class="xCascaderTreeItemBarText"
|
||||
:style="{
|
||||
color:_itemActiveColor,
|
||||
fontSize:_fontSize,
|
||||
whiteSpace:'nowrap',
|
||||
border:`1px solid ${_itemActiveColor}`
|
||||
}
|
||||
">
|
||||
{{elitext(item.item.title)}}
|
||||
</text>
|
||||
<x-icon v-if="(item.item?.children??[]).length>0&&item.selected" class="xCascaderBarTreeItemRight"
|
||||
:font-size="_fontSize" name="arrow-right-s-line" :color="_itemActiveColor"></x-icon>
|
||||
</view>
|
||||
<view v-if="isCurrentNext" class="xCascaderBarTreeItem">
|
||||
<text :style="{color:_itemTextColor,fontSize:_fontSize,whiteSpace:'nowrap'}">
|
||||
<!-- 请选择 -->
|
||||
{{i18n!.t('tmui4x.cascader.placeholder')}}
|
||||
</text>
|
||||
<x-icon class="xCascaderBarTreeItemRight" font-size="14" name="arrow-right-s-line"
|
||||
:color="_itemActiveColor"></x-icon>
|
||||
</view>
|
||||
|
||||
</slot>
|
||||
</scroll-view>
|
||||
<view :style="{height:'1px',borderBottom:`1px solid ${_borderColor}`}"></view>
|
||||
<!-- 内容区域 -->
|
||||
<view style="flex: 1;">
|
||||
<swiper :duration="200" :current="currentIndex" class="xCascaderTreeSwiper" :disable-touch="true">
|
||||
<swiper-item v-for="(item,index) in _list" :key="index" class="xCascaderTreeSwiperItem">
|
||||
<list-view class="xCascaderScoll" direction="vertical">
|
||||
<list-item v-for="(item2,index2) in item" :key="item2.id">
|
||||
<view @click="nextCellClick(item2,index,index2)"
|
||||
:class="[(item2?.disabled??false)?'xCascaderItemDisabled':'']" class="xCascaderItem">
|
||||
<text class="xCascaderItemLeft" :style="{
|
||||
fontSize:_fontSize,
|
||||
color:isSelected(item2)?_itemActiveColor : _itemTextColor
|
||||
}">{{item2.title}}{{item2.id}}</text>
|
||||
<view class="xCascaderItemRight">
|
||||
<text class="xCascaderTreeItemRightTextBtns"
|
||||
:style="{color:_itemActiveColor,border:`1px solid ${_itemActiveColor}`}"
|
||||
v-if="(item2?.children??[]).length>0&&_showCurrentBtn"
|
||||
@click.stop="selectedCurrentChildren(item2,index)">
|
||||
<!-- 选择本级 -->
|
||||
{{i18n!.t('tmui4x.cascader.currentPlaceholder')}}
|
||||
</text>
|
||||
<x-icon v-if="(item2?.children??[]).length>0" :color="_itemTextColor"
|
||||
name="arrow-right-s-line"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
|
||||
</list-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xCascaderItemDisabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.xCascaderTreeBar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
height: 50px;
|
||||
|
||||
}
|
||||
|
||||
.xCascaderBarTreeItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xCascaderBarTreeItemRight {
|
||||
margin: 0px 0px;
|
||||
}
|
||||
|
||||
.xCascaderTreeItemBarText {
|
||||
text-overflow: ellipsis;
|
||||
lines: 1;
|
||||
border-radius: 3px;
|
||||
/* #ifdef APP */
|
||||
padding: 0px 8px;
|
||||
line-height: 1.5;
|
||||
/* #endif */
|
||||
/* #ifndef APP */
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
/* 限定为3行 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xCascaderItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.xCascaderItemLeft {
|
||||
lines: 1;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.xCascaderItemRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xCascaderTreeItemRightTextBtns {
|
||||
font-size: 11px;
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.xCascaderTree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.xCascaderTreeSwiper,
|
||||
.xCascaderTreeSwiperItem {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xCascaderScoll {
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,519 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit, fillArrayCssValue } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type xCellItemType = {
|
||||
icon : string,
|
||||
title : string,
|
||||
desc : string,
|
||||
label : string,
|
||||
bottom : boolean,
|
||||
link : boolean,
|
||||
url : string,
|
||||
iconColor : string,
|
||||
labelColor : string,
|
||||
card : boolean
|
||||
}
|
||||
/**
|
||||
* @name 列表 xCell
|
||||
* @page /pages/index/cell
|
||||
* @category 展示组件
|
||||
* @description card为true时,圆角可统一全局配置和动态全局配置,保持所有页面列表样式统一,免于一个一个配置。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xCell" })
|
||||
|
||||
defineSlots<{
|
||||
avatar(props : { icon : string }) : any
|
||||
default() : any
|
||||
desc(props : { desc : string }) : any
|
||||
label(props : { label : string }) : any
|
||||
right() : any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 项目点击
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show'
|
||||
])
|
||||
type xCellPropsType = {
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
icon : string,
|
||||
/**
|
||||
* 左侧图标、头像圆角。默认为8
|
||||
*/
|
||||
avatarRound : string,
|
||||
/**
|
||||
* 背景的主题色
|
||||
*/
|
||||
color : string,
|
||||
/**
|
||||
* 暗黑背景的主题色,空值时取sheetDarkColor
|
||||
*/
|
||||
darkColor : string,
|
||||
/**
|
||||
* 图标色,空值时取全局主题值。
|
||||
*/
|
||||
iconColor : string,
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title : string,
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
titleColor : string,
|
||||
/**
|
||||
* 暗黑标题颜色,如果不填写取白
|
||||
*/
|
||||
darkTitleColor : string,
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleSize : string,
|
||||
/**
|
||||
* 图标大小
|
||||
*/
|
||||
iconSize : string,
|
||||
/**
|
||||
* 右边文本
|
||||
*/
|
||||
label : string,
|
||||
/**
|
||||
* 右边文本颜色
|
||||
*/
|
||||
labelColor : string,
|
||||
/**
|
||||
* 右侧label文字大小
|
||||
*/
|
||||
labelSize : string,
|
||||
/**
|
||||
* 标题正文的简介文本
|
||||
*/
|
||||
desc : string,
|
||||
/**
|
||||
* 是否显示下边线
|
||||
*/
|
||||
showBottomBorder : boolean,
|
||||
/**
|
||||
* 是否让下边线显示居右,不贯穿到左边。
|
||||
*/
|
||||
bottomBorderInsert : boolean,
|
||||
/**
|
||||
* 下边线的颜色。如果你设定了的话。
|
||||
* 暗黑的边颜色失效,采用你自定的颜色。
|
||||
*/
|
||||
bottomBorderColor : string,
|
||||
/**
|
||||
* 是否显示链接状态,有点按效果。包括出现右边跳转指示。
|
||||
* 关闭的话,事件反应和跳转会更快。
|
||||
* 如果true右侧箭头图标会显示
|
||||
*/
|
||||
link : boolean,
|
||||
/**
|
||||
* 右指示图标的颜色
|
||||
*/
|
||||
linkColor : string,
|
||||
/**
|
||||
* 右指示图标的暗黑颜色
|
||||
*/
|
||||
linkDarkColor : string,
|
||||
/**
|
||||
* 需要跳转的页面地址。
|
||||
* 如果填写了右侧箭头图标会显示
|
||||
* 跳转时如果失败会回退到switchTab跳转。
|
||||
*/
|
||||
url : string,
|
||||
/**
|
||||
* 是否是卡片模式
|
||||
*/
|
||||
card : boolean,
|
||||
/**
|
||||
* 卡片模式圆角,不填写采用全局的cardRadius属性值.
|
||||
*/
|
||||
round : string,
|
||||
/**
|
||||
* 左边图标区域宽和高的大小。
|
||||
*/
|
||||
leftSize : string,
|
||||
/**
|
||||
* 最小高度,主要是用来统一风格高度不至于让点击范围过小
|
||||
* 如果你需要紧凑型可以设置为auto
|
||||
*/
|
||||
minHeight : string,
|
||||
/**
|
||||
* 是否禁用url跳转,当link为true或者url需要跳转时
|
||||
* 如果禁用,点击时不会触发跳转。
|
||||
*/
|
||||
disabled : boolean,
|
||||
/**
|
||||
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
padding : string[],
|
||||
/**
|
||||
* margin 同sheet原理
|
||||
* [x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值cellMargin
|
||||
*/
|
||||
margin : string[],
|
||||
/**
|
||||
* 右侧label宽,插槽时,这个属性不会生效
|
||||
* 以你自己布局宽为准。
|
||||
*/
|
||||
rightWidth : string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCellPropsType>(), {
|
||||
icon: "",
|
||||
avatarRound: "8",
|
||||
color: 'white',
|
||||
darkColor: '',
|
||||
iconColor: "",
|
||||
title: "标题",
|
||||
titleColor: "black",
|
||||
darkTitleColor: "white",
|
||||
titleSize: "16",
|
||||
iconSize: "24",
|
||||
label: "",
|
||||
labelColor: "#bfbfbf",
|
||||
labelSize: "13",
|
||||
desc: "",
|
||||
showBottomBorder: true,
|
||||
bottomBorderInsert: false,
|
||||
bottomBorderColor: "",
|
||||
link: true,
|
||||
linkColor: '#bfbfbf',
|
||||
linkDarkColor: '#bfbfbf',
|
||||
url: "",
|
||||
card: true,
|
||||
round: "",
|
||||
leftSize: '32',
|
||||
minHeight: "55",
|
||||
disabled: false,
|
||||
padding: () : string[] => ['12', '0'] as string[],
|
||||
margin: () : string[] => [] as string[],
|
||||
rightWidth: "100"
|
||||
})
|
||||
// 计算属性
|
||||
const _padding = computed(() : string => {
|
||||
if (props.padding.length == 0) {
|
||||
let par = fillArrayCssValue(xConfig.sheetPadding)
|
||||
if (par.length == 0) return "0px 0px 0px 0px";
|
||||
return par.join(" ")
|
||||
}
|
||||
let ar : string[] = fillArrayCssValue(props.padding as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _margin = computed(() : string => {
|
||||
if (props.margin.length == 0) {
|
||||
let par = fillArrayCssValue(xConfig.cellMargin)
|
||||
if (par.length == 0) return "0px 0px 0px 0px";
|
||||
return par.join(" ")
|
||||
}
|
||||
let ar : string[] = fillArrayCssValue(props.margin as string[])
|
||||
if (ar.length == 0) return "0px 0px 0px 0px";
|
||||
return ar.join(" ")
|
||||
})
|
||||
|
||||
const _disabled = computed(() : boolean => {
|
||||
return props.disabled;
|
||||
})
|
||||
|
||||
const _color = computed(() : string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkColor != '') return getDefaultColor(props.darkColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
|
||||
const _titleColor = computed(() : string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkTitleColor != '') return getDefaultColor(props.darkTitleColor)
|
||||
return '#ffffff'
|
||||
}
|
||||
return getDefaultColor(props.titleColor)
|
||||
})
|
||||
|
||||
const _leftSize = computed(() : string => {
|
||||
return checkIsCssUnit(props.leftSize, xConfig.unit);
|
||||
})
|
||||
|
||||
const _rightWidth = computed(() : string => {
|
||||
return checkIsCssUnit(props.rightWidth, xConfig.unit);
|
||||
})
|
||||
|
||||
const _avatarRound = computed(() : string => {
|
||||
return checkIsCssUnit(props.avatarRound, xConfig.unit);
|
||||
})
|
||||
|
||||
const _minHeight = computed(() : string => {
|
||||
return checkIsCssUnit(props.minHeight, xConfig.unit);
|
||||
})
|
||||
|
||||
const _bottomBorderColor = computed(() : string => {
|
||||
if (props.bottomBorderColor != "") return getDefaultColor(props.bottomBorderColor)
|
||||
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
|
||||
return "#f5f5f5"
|
||||
})
|
||||
|
||||
const _icon = computed(() : string => {
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const _allAttr = computed(() : xCellItemType => {
|
||||
let iconColor = props.iconColor;
|
||||
if (iconColor == '') {
|
||||
iconColor = xConfig.color;
|
||||
}
|
||||
let p = {
|
||||
icon: props.icon,
|
||||
title: props.title,
|
||||
desc: props.desc,
|
||||
label: props.label,
|
||||
bottom: props.showBottomBorder,
|
||||
link: props.link,
|
||||
url: props.url,
|
||||
iconColor: getDefaultColor(iconColor),
|
||||
labelColor: getDefaultColor(props.labelColor),
|
||||
card: props.card
|
||||
} as xCellItemType
|
||||
return p
|
||||
})
|
||||
|
||||
const _cardRadius = computed(() : string => {
|
||||
if (props.round == "") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
|
||||
return checkIsCssUnit(xConfig.cellRadius, xConfig.unit)
|
||||
})
|
||||
|
||||
const _titleSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.titleSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _iconSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.iconSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 17
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _rightLableSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.labelSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 13
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _isLinksHover = computed(() : boolean => {
|
||||
return props.link
|
||||
})
|
||||
// 方法
|
||||
function clickLisent() : void {
|
||||
/**
|
||||
* 整个列表被点击
|
||||
*/
|
||||
emits("click");
|
||||
if (props.url != "" && !_disabled.value) {
|
||||
uni.navigateTo({
|
||||
url: props.url,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: props.url,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
|
||||
|
||||
<view @click="clickLisent" <!-- #ifndef APP-HARMONY -->
|
||||
:hover-start-time="_isLinksHover?50:0"
|
||||
:hover-stay-time="_isLinksHover?100:0"
|
||||
:hover-class="_isLinksHover?'cellHover':''"
|
||||
<!-- #endif -->
|
||||
class="xCell "
|
||||
:style="{
|
||||
backgroundColor:_color,
|
||||
borderRadius:_allAttr.card==true?_cardRadius:'0px',
|
||||
minHeight:_minHeight,
|
||||
padding:_padding,
|
||||
margin:_allAttr.card?_margin:'0px',
|
||||
borderBottom:_allAttr.bottom&& !_allAttr.card&&!bottomBorderInsert?`1px solid ${_bottomBorderColor}`:'none'
|
||||
}">
|
||||
|
||||
<view v-if="_icon" class="xCellAvatar" :style="{width:_leftSize,height:_leftSize,borderRadius:_avatarRound}">
|
||||
<!--
|
||||
@slot 头像图标
|
||||
@prop {string} icon - 图标名称
|
||||
-->
|
||||
<slot name="avatar" :icon="_icon">
|
||||
<x-icon :color="_allAttr.iconColor" :font-size="_iconSize" :name="_icon"></x-icon>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="xCellWrap" :style="{
|
||||
borderBottom:_allAttr.bottom&& !_allAttr.card&&bottomBorderInsert?`1px solid ${_bottomBorderColor}`:'none'
|
||||
}">
|
||||
<view class="center">
|
||||
<!--
|
||||
@slot 默认标题插槽
|
||||
-->
|
||||
<slot>
|
||||
<text class="title" :style="{color:_titleColor,fontSize:_titleSize}">{{ _allAttr.title}}</text>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 简介
|
||||
@prop {string} desc - 简介
|
||||
-->
|
||||
<slot name="desc" :desc="_allAttr.desc">
|
||||
<x-text v-if="_allAttr.desc!=''" font-size="12" color='#bfbfbf' dark-color='#bfbfbf'
|
||||
class="desc">{{_allAttr.desc}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="xcellRight">
|
||||
<!--
|
||||
@slot 右边文字
|
||||
@prop {string} label - 标签内容
|
||||
-->
|
||||
<slot name="label" :label="_allAttr.label">
|
||||
<text v-if="_allAttr.label!=''"
|
||||
:style="{marginLeft:'16px',color:_allAttr.labelColor,fontSize:_rightLableSize,width:_rightWidth }"
|
||||
class="rightLabel">{{_allAttr.label}}</text>
|
||||
|
||||
</slot>
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right"></slot>
|
||||
<view v-if="_allAttr.url!=''||_allAttr.link" style="margin-left: 5px;">
|
||||
<x-icon :dark-color="linkDarkColor" :color="linkColor" font-size="20"
|
||||
name="arrow-right-s-line"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.cellHover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.xCell {
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cardInset {
|
||||
// padding: 0 12px;
|
||||
}
|
||||
|
||||
.cellCard {
|
||||
// padding: 0 12px;
|
||||
// margin-bottom: 6px;
|
||||
// margin-left: 12px;
|
||||
// margin-right: 12px;
|
||||
}
|
||||
|
||||
.xCellWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 12px 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
lines: 2;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.xCellAvatar {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xcellRight {
|
||||
// padding-left: 16px;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.rightLabel {
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
|
||||
// 以下是sdk4.51+不支持
|
||||
// max-width: 100px;
|
||||
// width:100px;
|
||||
|
||||
// #ifndef APP
|
||||
white-space: nowrap;
|
||||
/* 防止文本换行 */
|
||||
overflow: hidden;
|
||||
/* 超出部分隐藏 */
|
||||
text-overflow: ellipsis;
|
||||
/* 显示省略号 */
|
||||
// #endif
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, provide } from "vue"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHECKBOX_ITEM_INFO } from '../../interface.uts';
|
||||
|
||||
type XCHECKBOX_LISTITEM_TYPE = {
|
||||
id : string,
|
||||
data : CHECKBOX_ITEM_INFO
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 多选框组 xCheckboxGroup
|
||||
* @page /pages/index/checkbox-group
|
||||
* @category 表单组件
|
||||
* @description 使用时,从1.1.2开始允许是非直接x-checkbox子节点布局,但考虑到性能建议是直接子节点.
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCheckboxGroup"})
|
||||
|
||||
export type xCheckboxGroupPropsType = {
|
||||
/**
|
||||
* 当前选中的值。
|
||||
*/
|
||||
modelValue: Array<string|number|boolean>,
|
||||
/**
|
||||
* 对齐方式
|
||||
*/
|
||||
direction: "row" | "column",
|
||||
/**
|
||||
* 最大选择数量,-1表示不限制。
|
||||
*/
|
||||
max: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCheckboxGroupPropsType>(), {
|
||||
modelValue: ():Array<string|number|boolean> => [] as Array<string|number|boolean>,
|
||||
direction: "row",
|
||||
max: -1
|
||||
})
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 选项变化时触发。
|
||||
* @param {Array<string|number|boolean>} val - 当前选中的值
|
||||
*/
|
||||
'change',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
// 响应式数据
|
||||
const oldvalueList = ref<XCHECKBOX_LISTITEM_TYPE[]>([])
|
||||
const checkvaluelist = ref<Array<string|number|boolean>>([])
|
||||
const tid = ref(0)
|
||||
const isDestroy = ref(false)
|
||||
const id = ref("xCheckboxGroup-" + getUid())
|
||||
|
||||
// 计算属性
|
||||
const oldvalueList_ids = computed((): string[] => {
|
||||
return oldvalueList.value.map((el : XCHECKBOX_LISTITEM_TYPE) : string => el.id)
|
||||
})
|
||||
|
||||
const _max = computed((): number => {
|
||||
return props.max;
|
||||
})
|
||||
|
||||
// 方法
|
||||
//设置当前选中的值
|
||||
function setOldCheckboxValue() {
|
||||
oldvalueList.value.forEach((item : XCHECKBOX_LISTITEM_TYPE) => {
|
||||
if (checkvaluelist.value.includes(item.data.value)) {
|
||||
item.data.nowvalue = item.data.value
|
||||
} else {
|
||||
item.data.nowvalue = item.data.unvalue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//当前选中的值
|
||||
function getcheckvaluelist() {
|
||||
let fl = oldvalueList.value.filter((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.data.nowvalue == el.data.value)
|
||||
let allfill = oldvalueList.value.map((el : XCHECKBOX_LISTITEM_TYPE) : string|number|boolean => el.data.value)
|
||||
let realValue = fl.map((el : XCHECKBOX_LISTITEM_TYPE) : string|number|boolean => el.data.value);
|
||||
// 删除已有的,保留差异的。
|
||||
let psdiff = checkvaluelist.value.filter((el : string|number|boolean) : boolean => !allfill.includes(el))
|
||||
|
||||
checkvaluelist.value = realValue.concat(psdiff)
|
||||
}
|
||||
function addItem(item : CHECKBOX_ITEM_INFO, ischange : boolean) {
|
||||
let index = oldvalueList.value.findIndex((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.id == item.id);
|
||||
let nowitem = item
|
||||
|
||||
let fl = oldvalueList.value.filter((el : XCHECKBOX_LISTITEM_TYPE) : boolean => el.data.nowvalue == el.data.value)
|
||||
|
||||
if (!ischange) {
|
||||
if (checkvaluelist.value.includes(item.value) && item.nowvalue != item.value) {
|
||||
nowitem.nowvalue = nowitem.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > -1) {
|
||||
let oldItem = oldvalueList.value[index]
|
||||
if (fl.length >= _max.value && _max.value > -1 && ischange && oldItem.data.nowvalue != oldItem.data.value) {
|
||||
// "已是最大选择数量"
|
||||
uni.showToast({ title: xConfig.i18n.t("tmui4x.checkbox.tips"), icon: 'none', mask: true })
|
||||
|
||||
return;
|
||||
}
|
||||
oldvalueList.value.splice(index, 1, {
|
||||
id: nowitem.id,
|
||||
data: nowitem
|
||||
} as XCHECKBOX_LISTITEM_TYPE)
|
||||
|
||||
} else {
|
||||
oldvalueList.value.push({
|
||||
id: nowitem.id,
|
||||
data: nowitem
|
||||
} as XCHECKBOX_LISTITEM_TYPE)
|
||||
}
|
||||
|
||||
getcheckvaluelist()
|
||||
|
||||
if (ischange) {
|
||||
/**
|
||||
* 等同v-model=""
|
||||
*/
|
||||
emits("update:modelValue", checkvaluelist.value)
|
||||
/**
|
||||
* 选项变化时触发。
|
||||
* @param val {string[]} 当前选中的值
|
||||
*/
|
||||
emits("change", checkvaluelist.value)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch((): Array<string|number|boolean> => props.modelValue, (newValue : Array<string|number|boolean>) => {
|
||||
let n = newValue.join("")
|
||||
let v = checkvaluelist.value.join("")
|
||||
if (n != v) {
|
||||
checkvaluelist.value = newValue
|
||||
setOldCheckboxValue();
|
||||
}
|
||||
},{deep:true})
|
||||
|
||||
// 生命周期
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
checkvaluelist.value = props.modelValue;
|
||||
isDestroy.value = false;
|
||||
})
|
||||
|
||||
// 提供响应式数据给子组件
|
||||
provide('xCheckboxModelvalue', computed((): Array<string|number|boolean> => checkvaluelist.value))
|
||||
|
||||
defineExpose({
|
||||
/** 添加多选项 **/
|
||||
addItem,
|
||||
/** 获取所有选中的值 **/
|
||||
getAllSelecteds: () => checkvaluelist.value
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCheckboxGroup" :style="{'flex-direction':direction}">
|
||||
<!--
|
||||
@slot 从1.1.2开始允许是非直接x-checkbox子节点布局,但考虑到性能建议是直接子节点.
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xCheckboxGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,422 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, getCurrentInstance, inject } from "vue"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHECKBOX_ITEM_INFO } from '../../interface.uts';
|
||||
|
||||
/**
|
||||
* @name 多选框 xCheckbox
|
||||
* @page /pages/index/checkbox
|
||||
* @category 表单组件
|
||||
* @description 使用时,x-checkbox能单独使用,如果要与x-checkbox-group配合,只能是它的的直接子节点
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCheckbox"})
|
||||
|
||||
defineSlots<{
|
||||
label(props:{
|
||||
checked: boolean,
|
||||
value: string|number|boolean
|
||||
}):any
|
||||
}>()
|
||||
type findeParentCall = (parent:VueComponent|null)=>VueComponent|null
|
||||
export type xCheckboxPropsType = {
|
||||
/**
|
||||
* 当前主题色,空值时取全局
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 当前未选中时主题色,空值时取全局
|
||||
*/
|
||||
unCheckColor: string,
|
||||
/**
|
||||
* 当前未选中时的暗黑主题色
|
||||
*/
|
||||
darkUnCheckColor: string,
|
||||
/**
|
||||
* 当前选中的值,受控时为v-model="x"
|
||||
*/
|
||||
modelValue: string|number|boolean,
|
||||
/**
|
||||
* 非受控下默认选中的状态
|
||||
*/
|
||||
defaultChecked: boolean,
|
||||
/**
|
||||
* 选中的值
|
||||
*/
|
||||
value: string|number|boolean,
|
||||
/**
|
||||
* 未选中的值
|
||||
*/
|
||||
unCheckValue: string|number|boolean,
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 选中的图标名称。
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* 右侧文字。
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* 是否隐藏选中框。然后利用默认插槽自定义选中所有样式和状态。
|
||||
*/
|
||||
hiddenCheckbox: boolean,
|
||||
/**
|
||||
* 半选中
|
||||
*/
|
||||
indeterminate: boolean,
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: string,
|
||||
/**
|
||||
* 中间小图标大小
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
labelFontSize: string,
|
||||
/**
|
||||
* label和选中框间的间距
|
||||
*/
|
||||
labelSpace: string,
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCheckboxPropsType>(), {
|
||||
color: "",
|
||||
unCheckColor: "",
|
||||
darkUnCheckColor: "",
|
||||
modelValue: '',
|
||||
defaultChecked: false,
|
||||
value: '1',
|
||||
unCheckValue: '',
|
||||
disabled: false,
|
||||
icon: "check-line",
|
||||
label: "",
|
||||
hiddenCheckbox: false,
|
||||
indeterminate: false,
|
||||
size: "24",
|
||||
iconSize: "20",
|
||||
labelFontSize: "15px",
|
||||
labelSpace: '10',
|
||||
round: '4'
|
||||
})
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param {Boolean} check - 当前是否选中
|
||||
* @param {string|number|boolean} value - 当前选中的值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy ?? null
|
||||
const checkboxBoxIconRef = ref<UniElement|null>(null)
|
||||
|
||||
// 注入父组件的响应式数据
|
||||
const groupModelValue = inject('xCheckboxModelvalue', computed(():Array<string|number|boolean>|null => null))
|
||||
|
||||
// 响应式数据
|
||||
const nowValue = ref<string|number|boolean>('')
|
||||
const boxId = ref("xCheckbox-" + getUid())
|
||||
const tid = ref(0)
|
||||
const isDestroy = ref(false)
|
||||
const undefaultCheck = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const _color = computed((): string => {
|
||||
if (props.color == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
return checkIsCssUnit(props.round, xConfig.unit);
|
||||
})
|
||||
|
||||
const _unCheckColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark' && props.darkUnCheckColor != '') {
|
||||
return getDefaultColor(props.darkUnCheckColor)
|
||||
}
|
||||
if (props.unCheckColor == "") return getDefaultColor(xConfig.unRadioAndCheckBoxColor)
|
||||
return getDefaultColor(props.unCheckColor)
|
||||
})
|
||||
|
||||
const _isCheck = computed((): boolean => {
|
||||
// 如果在组内,使用组的值判断
|
||||
if (groupModelValue.value != null) {
|
||||
return groupModelValue.value!.includes(props.value)
|
||||
}
|
||||
// 单独使用时,使用自己的值判断
|
||||
return nowValue.value == props.value || undefaultCheck.value || props.indeterminate
|
||||
})
|
||||
|
||||
const _disabled = computed((): boolean => {
|
||||
return props.disabled
|
||||
})
|
||||
|
||||
const _label = computed((): string => {
|
||||
return props.label
|
||||
})
|
||||
|
||||
const _indeterminate = computed((): boolean => {
|
||||
return props.indeterminate
|
||||
})
|
||||
|
||||
const _size = computed((): string => {
|
||||
let size = checkIsCssUnit(props.size, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return size;
|
||||
let sizeNumber = parseInt(size)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 24
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(size)
|
||||
})
|
||||
|
||||
const _labelSpace = computed((): string => {
|
||||
return checkIsCssUnit(props.labelSpace, xConfig.unit)
|
||||
})
|
||||
// 方法
|
||||
let findParent:findeParentCall|null = null;
|
||||
findParent = (parent:VueComponent|null):VueComponent|null => {
|
||||
if(parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS||MP-WEIXIN
|
||||
if((parent.$parent?.$options?.name?.indexOf('xCheckboxGroup')??-1)>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parent.$parent?.$options?.name?.indexOf('xCheckboxGroup')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parent.$parent instanceof XCheckboxGroupComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
|
||||
let findParentCallReal = findParent!
|
||||
let parents = findParentCallReal(parent.$parent)
|
||||
|
||||
// #ifdef WEB||APP-IOS||MP-WEIXIN
|
||||
if((parents?.$options?.name?.indexOf('xCheckboxGroup')??-1)>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parents?.$options?.name?.indexOf('xCheckboxGroup')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parents instanceof XCheckboxGroupComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setAni() {
|
||||
if (props.hiddenCheckbox || isDestroy.value) return;
|
||||
|
||||
try {
|
||||
let el = checkboxBoxIconRef.value!;
|
||||
el.style.setProperty('opacity', _isCheck.value ? 1 : 0)
|
||||
el.style.setProperty('transform', `scale(${_isCheck.value ? 0.74 : 0})`)
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
}
|
||||
|
||||
function pushDataToParent(isChange : boolean) {
|
||||
let pelement = findParent!(proxy);
|
||||
if (pelement == null) return;
|
||||
let parent : XCheckboxGroupComponentPublicInstance = pelement as XCheckboxGroupComponentPublicInstance;
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
if (typeof parent?.addItem != 'function') return;
|
||||
// #endif
|
||||
|
||||
parent.addItem({
|
||||
id: boxId.value as string,
|
||||
nowvalue: nowValue.value,
|
||||
value: props.value,
|
||||
unvalue: props.unCheckValue
|
||||
} as CHECKBOX_ITEM_INFO, isChange)
|
||||
}
|
||||
|
||||
function boxClick() {
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
emits("click")
|
||||
if (_disabled.value) return;
|
||||
/**
|
||||
* 要点提示:当本组件属性半选中状态时,应该继续保持选中状态,
|
||||
* 并触发change。
|
||||
*/
|
||||
if ((_isCheck.value && !props.indeterminate) || undefaultCheck.value) {
|
||||
nowValue.value = props.unCheckValue
|
||||
} else {
|
||||
nowValue.value = props.value
|
||||
}
|
||||
/**
|
||||
* 当前选中的值,等同v-model
|
||||
*/
|
||||
emits("update:modelValue", nowValue.value)
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param check {boolean} 当前是否选中
|
||||
* @param value {string|number|boolean} 当前选中的值
|
||||
*/
|
||||
emits("change", _isCheck.value, nowValue.value)
|
||||
if(undefaultCheck.value){
|
||||
undefaultCheck.value = false
|
||||
}else{
|
||||
pushDataToParent(true);
|
||||
}
|
||||
|
||||
setAni();
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动切换选中状态,这里不会触发change
|
||||
*/
|
||||
function setSelected(val : Array<string|number|boolean>) {
|
||||
if (!Array.isArray(val)) {
|
||||
throw new Error("val must be an array");
|
||||
}
|
||||
const isChecked = val.includes(props.value);
|
||||
if (isChecked) {
|
||||
nowValue.value = props.value;
|
||||
// emits("update:modelValue", nowValue.value);
|
||||
} else {
|
||||
nowValue.value = props.unCheckValue;
|
||||
// emits("update:modelValue", nowValue.value);
|
||||
}
|
||||
setAni()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch((): string|number|boolean => props.modelValue, (newValue : string|number|boolean) => {
|
||||
if (newValue == nowValue.value) return;
|
||||
nowValue.value = newValue;
|
||||
setAni();
|
||||
// pushDataToParent();
|
||||
})
|
||||
|
||||
watch((): boolean => props.indeterminate, (newValue : boolean) => {
|
||||
setAni();
|
||||
})
|
||||
|
||||
// 监听组内值变化
|
||||
if (groupModelValue.value != null) {
|
||||
watch(groupModelValue, (val:Array<string|number|boolean>|null) => {
|
||||
if(val!=null){
|
||||
setSelected(val!);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
isDestroy.value = false;
|
||||
// 如果在group中,默认选中状态将失效.否则数据异常错乱.
|
||||
let pelement = findParent!(proxy);
|
||||
if(pelement!=null){
|
||||
undefaultCheck.value = false
|
||||
}else{
|
||||
undefaultCheck.value = props.defaultChecked
|
||||
}
|
||||
nextTick(() => {
|
||||
nowValue.value = props.modelValue;
|
||||
setAni();
|
||||
pushDataToParent(false);
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view :class="[_disabled?'checkboxDisabled':'']" class="checkbox" @click="boxClick">
|
||||
<view class="checkboxBox" v-if="!hiddenCheckbox" :style="{
|
||||
backgroundColor:_isCheck?_color:'transparent',
|
||||
border: `1px solid ${_isCheck?_color:_unCheckColor}`,
|
||||
borderRadius:_round,
|
||||
width:_size,
|
||||
height:_size
|
||||
}">
|
||||
<view :id="boxId" ref="checkboxBoxIconRef" class="checkboxBoxIcon">
|
||||
<x-icon color="white" :name="_indeterminate?'subtract-line':icon" :font-size="iconSize"></x-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="checkboxLabelBox" :style="{paddingLeft:!hiddenCheckbox?_labelSpace:'0px'}">
|
||||
<!--
|
||||
@slot 默认文本插槽
|
||||
@prop {boolean} checked - 是否选中
|
||||
@prop {number} value - 当前选中的值
|
||||
-->
|
||||
<slot name="label" :checked="_isCheck" :value="nowValue">
|
||||
<x-text :font-size="labelFontSize" class="checkboxLabel">{{_label}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.checkboxLabelBox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkboxDisabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.checkboxBoxIcon {
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transition-property: opacity, transform;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
|
||||
|
||||
}
|
||||
|
||||
.checkboxBox {
|
||||
/* border-radius: 4px; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkboxLabelBoxLeftSpace {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,394 @@
|
||||
<script lang="ts">
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit,getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type OPTIONS = {
|
||||
start : number,
|
||||
end : number,
|
||||
duration : number,
|
||||
run : (current : number) => void,
|
||||
complete : () => void
|
||||
}
|
||||
type ANIMATECALLBACK = {
|
||||
stop : () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 圆形进度环 xCircleProgress
|
||||
* @page /pages/index/circle-progress
|
||||
* @category 展示组件
|
||||
* @description 样式灵活多变。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: 'xCircleProgeress-' + getUid(),
|
||||
nowValue: 0,
|
||||
tid: 0,
|
||||
oladValue: 0,
|
||||
nowback: {
|
||||
stop: () => { },
|
||||
} as ANIMATECALLBACK
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
default: "60"
|
||||
},
|
||||
/**
|
||||
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
|
||||
*/
|
||||
lineWidth: {
|
||||
type: String,
|
||||
default: "3"
|
||||
},
|
||||
/**
|
||||
* 圆环背景颜色,暗黑时取inputDarkColor
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "info"
|
||||
},
|
||||
|
||||
/**
|
||||
* 当前激活的进度颜色,空值读取全局值。
|
||||
*/
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前的值,以百分比为值0-100
|
||||
* 等同v-model=""
|
||||
* 您直接:model-value="xx"也是一样可以改变值。
|
||||
*/
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
/**
|
||||
* 中间文本字号
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: String,
|
||||
default: "16"
|
||||
},
|
||||
/**
|
||||
* 数字的单位。
|
||||
*/
|
||||
labelUnit: {
|
||||
type: String,
|
||||
default: "%"
|
||||
},
|
||||
/**
|
||||
* 是否显示中间文本。
|
||||
*/
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 中间文本的颜色。
|
||||
*/
|
||||
labelColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 中间文本的暗黑颜色。空值是取白色
|
||||
*/
|
||||
darkLabelColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 进度条动画的时间,单位ms。
|
||||
* 请不要设置的过慢,否则会有停顿感。
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
_labelFontSize() : string {
|
||||
let fontSize = checkIsCssUnit(this.labelFontSize, xConfig.unit);
|
||||
if(xConfig.fontScale==1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if(isNaN(sizeNumber)){
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber*xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
},
|
||||
_labelUnit() : string {
|
||||
return this.labelUnit
|
||||
},
|
||||
_labelColor() : string {
|
||||
if(xConfig.dark=='dark'){
|
||||
if(this.darkLabelColor!='') return getDefaultColor(this.darkLabelColor)
|
||||
return "#ffffff"
|
||||
}
|
||||
return getDefaultColor(this.labelColor)
|
||||
},
|
||||
_showLabel() : boolean {
|
||||
return this.showLabel
|
||||
},
|
||||
_width() : number {
|
||||
let p = parseInt(this.size);
|
||||
if (this.size.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p);
|
||||
}
|
||||
return p
|
||||
},
|
||||
_lineWidth() : number {
|
||||
let p = parseInt(this.lineWidth);
|
||||
if (this.lineWidth.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p);
|
||||
}
|
||||
return Math.floor(p)
|
||||
},
|
||||
_color() : string {
|
||||
if(xConfig.dark=='dark') return getDefaultColor(xConfig.inputDarkColor)
|
||||
return getDefaultColor(this.color)
|
||||
},
|
||||
_activeColor() : string {
|
||||
if (this.activeColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(this.activeColor)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(newval : number) {
|
||||
|
||||
if (newval != this.nowValue) {
|
||||
this.oladValue = this.nowValue
|
||||
this.nowValue = newval
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.nowValue = this.modelValue
|
||||
this.play();
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
this.nowback.stop()
|
||||
clearTimeout(this.tid)
|
||||
this.clear();
|
||||
let t = this;
|
||||
this.nowback = this.animate({
|
||||
start: 0,
|
||||
end: 100,
|
||||
duration: t.duration,
|
||||
complete() {
|
||||
|
||||
},
|
||||
run(cureent : number) {
|
||||
let angle = Math.PI * 2;
|
||||
if (t.nowValue >= t.oladValue) {
|
||||
let startAngle = (t.oladValue / 100) * angle
|
||||
let endAngle = ((cureent / 100) * (t.nowValue - t.oladValue) / 100) * angle
|
||||
endAngle = Math.max(endAngle, 0)
|
||||
|
||||
t.dreawer(startAngle, endAngle + startAngle)
|
||||
} else {
|
||||
let startAngle = (t.nowValue / 100) * angle
|
||||
let endAngle = ((cureent / 100) * (t.oladValue - t.nowValue) / 100) * angle
|
||||
let diff = (t.oladValue - t.nowValue) / 100 * angle
|
||||
t.dreawer(startAngle, diff + startAngle - endAngle)
|
||||
}
|
||||
}
|
||||
} as OPTIONS)
|
||||
},
|
||||
animate(options : OPTIONS) : ANIMATECALLBACK {
|
||||
let tid = this.tid;
|
||||
const start = options.start; // 初始值,默认为0
|
||||
const end = options.end; // 目标值,默认为100
|
||||
const duration = options.duration; // 动画时长,默认为1000毫秒
|
||||
let current = 0; // 当前值
|
||||
let startTime = 0; // 动画开始时间
|
||||
let isRunning = true; // 动画运行状态
|
||||
function run() {
|
||||
if (startTime <= 0) {
|
||||
startTime = Date.now(); // 记录动画开始时间
|
||||
}
|
||||
const progress = Math.min((Date.now() - startTime) / duration, 1); // 计算当前进度
|
||||
current = start + (end - start) * progress; // 根据进度计算当前值
|
||||
if (isRunning) {
|
||||
options.run(current); // 执行传入的运行中状态回调函数
|
||||
}
|
||||
if (progress < 1 && isRunning) {
|
||||
tid = setTimeout(() => {
|
||||
run()
|
||||
}, 16); // 递归调用自身,实现动画效果
|
||||
} else {
|
||||
options.complete(); // 动画结束时执行回调函数
|
||||
}
|
||||
}
|
||||
run(); // 开始执行动画
|
||||
return {
|
||||
stop: () => {
|
||||
// 设置动画运行状态为false
|
||||
isRunning = false;
|
||||
}
|
||||
} as ANIMATECALLBACK;
|
||||
},
|
||||
async dreawer(start : number, end : number):Promise<any|null> {
|
||||
this.clear()
|
||||
let canvas = uni.getElementById(this.id as string) as UniElement
|
||||
let ctx = null as DrawableContext | null
|
||||
let ratio = 1;
|
||||
// #ifdef APP
|
||||
ctx = canvas.getDrawableContext()!
|
||||
// #endif
|
||||
|
||||
// #ifdef WEB
|
||||
let crect = canvas.getBoundingClientRect();
|
||||
let dom = canvas as HTMLElement;
|
||||
canvas = dom.querySelector('canvas')
|
||||
ratio = window.devicePixelRatio;
|
||||
let w = crect.width
|
||||
let h = crect.height
|
||||
|
||||
canvas.width = w * ratio
|
||||
canvas.height = h * ratio
|
||||
|
||||
ctx = canvas.getContext('2d')!
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
let ctxnode = await uni.createCanvasContextAsync({
|
||||
id: this.id,
|
||||
component: this,
|
||||
})
|
||||
const canvasContext = ctxnode.getContext('2d')!;
|
||||
canvas = canvasContext.canvas;
|
||||
// 处理高清屏逻辑
|
||||
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
||||
canvas.width = canvas.offsetWidth * dpr;
|
||||
canvas.height = canvas.offsetHeight * dpr;
|
||||
canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
|
||||
ctx = canvasContext
|
||||
// #endif
|
||||
|
||||
|
||||
if (ctx == null) return Promise.resolve(null);
|
||||
let cx = (this._width) / 2
|
||||
let cy = cx
|
||||
let cr = (this._width - this._lineWidth) / 2
|
||||
ctx!.beginPath()
|
||||
ctx!.strokeStyle = this._color
|
||||
ctx!.lineWidth = this._lineWidth
|
||||
ctx!.lineCap = "round"
|
||||
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, Math.PI * 2)
|
||||
ctx!.closePath()
|
||||
ctx!.stroke()
|
||||
// 绘制起始的角度。
|
||||
ctx!.beginPath()
|
||||
ctx!.strokeStyle = this._activeColor
|
||||
ctx!.lineWidth = this._lineWidth
|
||||
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, start)
|
||||
ctx!.stroke()
|
||||
// ctx.update()
|
||||
|
||||
|
||||
ctx!.beginPath()
|
||||
ctx!.strokeStyle = this._activeColor
|
||||
ctx!.lineWidth = this._lineWidth
|
||||
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, start, end)
|
||||
ctx!.stroke()
|
||||
// #ifdef APP
|
||||
ctx!.update()
|
||||
// #endif
|
||||
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
clear(){
|
||||
let canvas = uni.getElementById(this.id as string) as UniElement
|
||||
let ctx = null as DrawableContext | null
|
||||
let ratio = 1;
|
||||
// #ifdef APP
|
||||
ctx = canvas.getDrawableContext()!
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
let dom = canvas as HTMLElement;
|
||||
canvas = dom.querySelector('canvas')
|
||||
ctx = canvas.getContext('2d')!
|
||||
ratio = window.devicePixelRatio;
|
||||
// #endif
|
||||
|
||||
|
||||
|
||||
|
||||
if (canvas == null || ctx == null) return
|
||||
|
||||
// #ifndef APP
|
||||
ctx!.reset()
|
||||
// #endif
|
||||
|
||||
ctx!.fillStyle = 'rgba(0,0,0,0)'
|
||||
ctx!.fillRect(0, 0, this._width * ratio, this._width * ratio)
|
||||
ctx!.fill()
|
||||
// #ifdef APP
|
||||
ctx!.update()
|
||||
// #endif
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view :style="{width:_width+'px',height:_width+'px',position: 'relative'}">
|
||||
<!-- #ifdef APP -->
|
||||
<view class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef WEB || MP -->
|
||||
<canvas class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></canvas>
|
||||
<!-- #endif -->
|
||||
<view v-if="_showLabel" class="xCircleProgressLabel">
|
||||
<!--
|
||||
@slot 默认文本插槽
|
||||
@prop {number} current - 当前的进度值。
|
||||
-->
|
||||
<slot current="nowValue">
|
||||
<text :style="{
|
||||
fontSize:_labelFontSize,
|
||||
color:_labelColor
|
||||
}">{{nowValue}}{{_labelUnit}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xCircleProgress {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.xCircleProgressLabel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,385 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue"
|
||||
import { PropType } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import xSkeleton from "../x-skeleton/x-skeleton.uvue"
|
||||
|
||||
/**
|
||||
* @name 验证码输入框 xCodeInput
|
||||
* @description 验证码输入框,截止4.22安卓会自动拉起系统键盘,ios无法使用系统键盘。目前仅配合我的组件键盘可以全局兼容。已向官方返回Input的bug
|
||||
* @page /pages/index/code-input
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCodeInput"})
|
||||
// emits
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 输入框点击时触发
|
||||
*/
|
||||
"click",
|
||||
/**
|
||||
* 自带键盘上确认或者达到指定长度位数时触发,可能会多次触发
|
||||
* @param {String} value - 值
|
||||
*/
|
||||
"confirm",
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {String} value - 值
|
||||
*/
|
||||
"change",
|
||||
/**
|
||||
* 等同vmodel,可与我的keyborad键盘配合使用。
|
||||
*/
|
||||
"update:modelValue"
|
||||
])
|
||||
|
||||
// props
|
||||
type xCodeInputPropsType = {
|
||||
/**
|
||||
* 进入时自动获取焦点,并弹出系统自带的键盘(需要useSysKeyborad=true)。
|
||||
*/
|
||||
autoFocus: boolean,
|
||||
/**
|
||||
* 是否使用系统自带的键盘。,如果为false你需要自行配置输入键盘
|
||||
* 比如使用我的keyborad键盘组件。
|
||||
*/
|
||||
useSysKeyborad: boolean,
|
||||
/**
|
||||
* 当前输入的值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
maxlength: number,
|
||||
/**
|
||||
* 间距
|
||||
*/
|
||||
gutter: string,
|
||||
/**
|
||||
* 验证码框的宽
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 验证码框的高
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 当前输入项激活时的文字颜色同时也是高亮时的背景色。
|
||||
* 默认取全局主题
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 暗黑时的主题色,不填写等同fontColor
|
||||
*/
|
||||
darkFontColor: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* skin = fill时的背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* skin = fill时的暗黑背景
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* skin = outline时的边线颜色
|
||||
*/
|
||||
borderColor: string,
|
||||
/**
|
||||
* skin = outline时的暗黑边线颜色
|
||||
*/
|
||||
darkBorderColor: string,
|
||||
/**
|
||||
* skin = outline时的边线颜色[非激活时]
|
||||
*/
|
||||
unBorderColor: string,
|
||||
/**
|
||||
* skin = outline时的暗黑边线颜色[非激活时]
|
||||
*/
|
||||
unDarkBorderColor: string,
|
||||
skin: 'fill' | 'outline',
|
||||
/**
|
||||
* 待输入时的占位形状
|
||||
* line线型
|
||||
* round圆形
|
||||
* 空值表示不需要占位符号
|
||||
*/
|
||||
placeShape: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCodeInputPropsType>(), {
|
||||
autoFocus: false,
|
||||
useSysKeyborad: true,
|
||||
modelValue: "",
|
||||
maxlength: 4,
|
||||
gutter: "8",
|
||||
width: "50",
|
||||
height: "50",
|
||||
fontColor: "",
|
||||
darkFontColor: "",
|
||||
fontSize: "21",
|
||||
round: "8",
|
||||
bgColor: "#f0f0f0",
|
||||
darkBgColor: "#272727",
|
||||
borderColor: "",
|
||||
darkBorderColor: "",
|
||||
unBorderColor: "#e3e3e3",
|
||||
unDarkBorderColor: "#2c2b2c",
|
||||
skin: "outline",
|
||||
placeShape: "round",
|
||||
})
|
||||
|
||||
// state
|
||||
const _autoFocus = ref<boolean>(false)
|
||||
const inputvalue = ref<string>("")
|
||||
const tid = ref<number>(0)
|
||||
|
||||
// computed
|
||||
const _fontColor = computed(() : string => {
|
||||
let fontcolor = props.fontColor == "" ? xConfig.color : props.fontColor;
|
||||
let darkFontcolor = props.darkFontColor == "" ? fontcolor : props.darkFontColor;
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(darkFontcolor)
|
||||
}
|
||||
return getDefaultColor(fontcolor)
|
||||
})
|
||||
const _borderColor = computed(() : string => {
|
||||
let outLineColor = props.borderColor == "" ? xConfig.color : props.borderColor;
|
||||
let darkOutlineColor = props.darkBorderColor == "" ? _fontColor.value : props.darkBorderColor;
|
||||
|
||||
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(darkOutlineColor)
|
||||
}
|
||||
return getDefaultColor(outLineColor)
|
||||
})
|
||||
const _unborderColor = computed(() : string => {
|
||||
let unBorderColor = props.unBorderColor == "" ? _fontColor.value : props.unBorderColor;
|
||||
let unDarkBorderColor = props.unDarkBorderColor == "" ? _fontColor.value : props.unDarkBorderColor;
|
||||
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(unDarkBorderColor)
|
||||
}
|
||||
return getDefaultColor(unBorderColor)
|
||||
})
|
||||
const _bgcolor = computed(() : string => {
|
||||
|
||||
if (xConfig.dark == 'dark') {
|
||||
return getDefaultColor(props.darkBgColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 21
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _maxLength = computed(() : number => {
|
||||
return props.maxlength
|
||||
})
|
||||
const _round = computed(() : string => {
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
const _gutter = computed(() : string => {
|
||||
return checkIsCssUnit(props.gutter, xConfig.unit)
|
||||
})
|
||||
const _width = computed(() : string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
const _height = computed(() : string => {
|
||||
return checkIsCssUnit(props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
// methods first (so they can be referenced below)
|
||||
function getValue(index : number) : string {
|
||||
if (index > inputvalue.value.length - 1) return ""
|
||||
return inputvalue.value.split("")[index]
|
||||
}
|
||||
function onconfirm() {
|
||||
emits('update:modelValue',inputvalue.value)
|
||||
nextTick(()=>{
|
||||
/**
|
||||
* 输入长度等于指定长度时触发
|
||||
*/
|
||||
emits('confirm', inputvalue.value)
|
||||
})
|
||||
_autoFocus.value = false;
|
||||
}
|
||||
function borderColorAc(index : number) : string {
|
||||
let isActive = inputvalue.value.split("").length >= (index)
|
||||
if (!isActive) {
|
||||
if (props.skin == 'fill') return "transparent"
|
||||
}
|
||||
return isActive ? _borderColor.value : _unborderColor.value
|
||||
}
|
||||
function blur() {
|
||||
_autoFocus.value = false;
|
||||
}
|
||||
function onFocus(){
|
||||
let len = inputvalue.value.split("").length;
|
||||
if (len > _maxLength.value){
|
||||
inputvalue.value = inputvalue.value.substring(0,_maxLength.value)
|
||||
}
|
||||
}
|
||||
function onClick() {
|
||||
/**
|
||||
* 点击框时触发
|
||||
*/
|
||||
emits('click', inputvalue.value)
|
||||
|
||||
_autoFocus.value = true;
|
||||
}
|
||||
function inputEvent(evt : UniInputEvent):string {
|
||||
let value = evt.detail.value;
|
||||
let len = value.split("").length;
|
||||
if (len > _maxLength.value){
|
||||
inputvalue.value =value
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function() {
|
||||
inputvalue.value = value.substring(0,_maxLength.value)
|
||||
}, 100)
|
||||
return inputvalue.value;
|
||||
}
|
||||
inputvalue.value = value;
|
||||
|
||||
emits('update:modelValue',inputvalue.value)
|
||||
if (len == _maxLength.value) {
|
||||
nextTick(()=>{
|
||||
/**
|
||||
* 输入长度等于指定长度时触发
|
||||
*/
|
||||
emits('confirm', inputvalue.value)
|
||||
_autoFocus.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 变动时触发
|
||||
*/
|
||||
emits('change', inputvalue.value)
|
||||
return inputvalue.value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
_autoFocus.value = props.autoFocus;
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
|
||||
|
||||
watch(():string => props.modelValue, (newval : string) => {
|
||||
if (newval == inputvalue.value) return;
|
||||
inputvalue.value = newval;
|
||||
let len = newval.split("").length;
|
||||
if (len == _maxLength.value) {
|
||||
/**
|
||||
* 输入长度等于指定长度时触发
|
||||
*/
|
||||
emits('confirm', inputvalue.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view @click="onClick" class="xCodeInput">
|
||||
<input
|
||||
@focus="onFocus"
|
||||
v-if="useSysKeyborad"
|
||||
:focus="_autoFocus"
|
||||
:adjust-position="false"
|
||||
:style="{
|
||||
width:'100%',
|
||||
height:_height,
|
||||
}" @confirm="onconfirm" @input="inputEvent" v-model="inputvalue" @blur="blur"
|
||||
:auto-focus="_autoFocus"
|
||||
class="xCodeInputInput" type="number" />
|
||||
|
||||
<view class="xCodeInputItem" :style="{
|
||||
borderRadius:_round,
|
||||
border:`2px solid ${borderColorAc(index)}`,
|
||||
backgroundColor:skin=='fill'?_bgcolor:'transparent',
|
||||
width:_width,
|
||||
height:_height,
|
||||
marginRight:(index==_maxLength-1)?'0px':_gutter}" v-for="(_,index) in _maxLength" :key="index">
|
||||
<text
|
||||
:class="[
|
||||
(index<=inputvalue.length)?'xCodeInputItemTextOn':'xCodeInputItemTextOff'
|
||||
]"
|
||||
class="xCodeInputItemText" :style="{
|
||||
fontWeight:'bold',
|
||||
color:_fontColor,
|
||||
fontSize:_fontSize
|
||||
}">
|
||||
{{getValue(index)}}
|
||||
</text>
|
||||
<x-skeleton v-if="(index<=inputvalue.length)&&getValue(index)==''&&placeShape=='round'" height="5" width="5" :color="_borderColor" :dark-color="_borderColor" ></x-skeleton>
|
||||
<x-skeleton v-if="(index<=inputvalue.length)&&getValue(index)==''&&placeShape=='line'" height="2" width="33%" :color="_borderColor" :dark-color="_borderColor"></x-skeleton>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.xCodeInputItemText{
|
||||
transition-property: transform,opacity;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: linear;
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
.xCodeInputItemTextOn{
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
.xCodeInputItemTextOff{
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
.xCodeInputItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition-property: background-color,border-color;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
.xCodeInput {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xCodeInputInput {
|
||||
/* pointer-events: none;*/
|
||||
opacity: 0;
|
||||
/* transform: translateX(-1000%); */
|
||||
position: absolute;
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 布局子组件 xCol
|
||||
* @description 只能放置中x-row中布局
|
||||
* @page /pages/index/row
|
||||
* @category 常用组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xCol" })
|
||||
export type xColPropsType = {
|
||||
/**
|
||||
* 列宽,它是根据row定义的总列宽计算具体的宽度值。
|
||||
*/
|
||||
span:number,
|
||||
/**
|
||||
* 可以输入%,数字或者带单位的偏移量
|
||||
*/
|
||||
offset:string,
|
||||
/**
|
||||
* 自定义内部标签的style
|
||||
* 请使用_style
|
||||
*/
|
||||
_style:string,
|
||||
/**
|
||||
* 自定义内部标签的class,
|
||||
* 请使用_class
|
||||
*/
|
||||
_class:string
|
||||
}
|
||||
const props = withDefaults(defineProps<xColPropsType>(), {
|
||||
span:3,
|
||||
offset:'0',
|
||||
_style:"",
|
||||
_class:""
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 单元格被点击的事件
|
||||
*/
|
||||
click: []
|
||||
}>()
|
||||
const xRowCol = inject('xRowCol', computed(():number=>12))
|
||||
const _width = computed(():string=>{
|
||||
return `${(props.span / xRowCol.value * 100)}%`
|
||||
})
|
||||
const __style = computed(():string=>props._style)
|
||||
const ___class = computed(():string=>props._class)
|
||||
const _offset = computed(():string=>{
|
||||
return checkIsCssUnit(props.offset, xConfig.unit)
|
||||
})
|
||||
const onClick = () => {
|
||||
emit("click")
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="onClick" :style="{width:_width,transform:`translateX(${_offset})`}">
|
||||
<view :style="[__style]" :class="___class">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,422 @@
|
||||
<script lang="ts">
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit,getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHIDREN_ITEM } from "../x-collapse/interface.uts"
|
||||
|
||||
/**
|
||||
* @name 折叠面板子组件 xCollapseItem
|
||||
* @description 可单,可多开,只可放置在x-collapse直接子节点组件,为了避免重复计算和性能x-collapse-item不能通过响应式修改内容。如果确实需要请通过刷新key解决
|
||||
* @page /pages/index/collapse-item
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
itemHeight: 0,
|
||||
opened: false,
|
||||
id: 'xCollapseItem-' + getUid(),
|
||||
list: [] as string[],
|
||||
resizeObserver: null as UniResizeObserver | null,
|
||||
}
|
||||
},
|
||||
emits:[
|
||||
/**
|
||||
* 点击组件标题时触发
|
||||
* @param {string} name 当前标识
|
||||
* @param {boolean} opened 当前项目打开状态
|
||||
*/
|
||||
'click'
|
||||
],
|
||||
inject: {
|
||||
xCollapseDefaultName: { type: Array, default: [] as string[] },
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 唯一标识
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部边线
|
||||
*/
|
||||
showBottomLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleFontSize: {
|
||||
type: String,
|
||||
default: '16px'
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
/**
|
||||
* 拒绝礼佛标题颜色,如果不填写取白
|
||||
*/
|
||||
darkTitleColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 激活时的颜色,空值读取全局值。
|
||||
*/
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 背景
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
/**
|
||||
* 暗黑时的背景,如果不填写默认取sheetDarkColor
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 左边图标
|
||||
*/
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 标题高度
|
||||
*/
|
||||
titleHeight: {
|
||||
type: String,
|
||||
default: '55'
|
||||
},
|
||||
/**
|
||||
* 标题最多显示几行出现省略号
|
||||
*/
|
||||
titleLines: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_disabled() : boolean {
|
||||
return this.disabled;
|
||||
},
|
||||
_titleFontSize() : string {
|
||||
let fontSize = checkIsCssUnit(this.titleFontSize, xConfig.unit);
|
||||
if(xConfig.fontScale==1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if(isNaN(sizeNumber)){
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber*xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
},
|
||||
_titleHeight() : string {
|
||||
return checkIsCssUnit(this.titleHeight, xConfig.unit);
|
||||
},
|
||||
_titleColor() : string {
|
||||
if(xConfig.dark=='dark'){
|
||||
if(this.darkTitleColor!='') return getDefaultColor(this.darkTitleColor)
|
||||
return '#ffffff'
|
||||
}
|
||||
return getDefaultColor(this.titleColor);
|
||||
},
|
||||
_activeColor() : string {
|
||||
if (this.activeColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(this.activeColor);
|
||||
},
|
||||
_color() : string {
|
||||
if(xConfig.dark=='dark'){
|
||||
if(this.darkColor!='') return getDefaultColor(this.darkColor)
|
||||
return xConfig.sheetDarkColor
|
||||
}
|
||||
return getDefaultColor(this.color);
|
||||
},
|
||||
_leftIcon() : string {
|
||||
return this.leftIcon;
|
||||
},
|
||||
_title() : string {
|
||||
return this.title;
|
||||
},
|
||||
|
||||
_isActive() : boolean {
|
||||
return this.list.includes(this.name);
|
||||
},
|
||||
_textMap() : Map<string, string> {
|
||||
let styleMap = new Map<string, string>()
|
||||
styleMap.set("fontSize", this._titleFontSize)
|
||||
styleMap.set("color", this._isActive ? this._activeColor : this._titleColor)
|
||||
// #ifdef APP
|
||||
styleMap.set("lines", this.titleLines.toString())
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
styleMap.set("-webkit-line-clamp", this.titleLines.toString())
|
||||
// #endif
|
||||
|
||||
return styleMap;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// @ts-ignore
|
||||
this.list = this.xCollapseDefaultName as string[];
|
||||
// @ts-ignore
|
||||
const parent = this.getParent() as XCollapseComponentPublicInstance|null
|
||||
if(parent!=null){
|
||||
parent!.addItem({ id: this.name, ele: this } as CHIDREN_ITEM)
|
||||
}
|
||||
|
||||
if (this._isActive) {
|
||||
this.getNodes()
|
||||
}
|
||||
let t = this;
|
||||
// #ifdef APP || WEB
|
||||
let ele = this.$refs['xCollapseItemContent'] as UniElement
|
||||
if(ele==null) return;
|
||||
if (this.resizeObserver == null) {
|
||||
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target == ele) {
|
||||
if (t._isActive) {
|
||||
t.getNodes()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
this.resizeObserver!.observe(ele!)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
setTimeout(function() {
|
||||
t.getNodes()
|
||||
}, 300);
|
||||
// #endif
|
||||
|
||||
},
|
||||
updated() {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.getNodes()
|
||||
// #endif
|
||||
},
|
||||
beforeUnmount() {
|
||||
// @ts-ignore
|
||||
const parent = this.getParent() as XCollapseComponentPublicInstance|null
|
||||
if(parent!=null){
|
||||
parent!.delItem(this.name)
|
||||
}
|
||||
this.resizeObserver?.disconnect()
|
||||
},
|
||||
methods: {
|
||||
// @ts-ignore
|
||||
getParent():any|null{
|
||||
// @ts-ignore
|
||||
let parent : XCollapseComponentPublicInstance | null = null;
|
||||
try {
|
||||
// @ts-ignore
|
||||
parent = this.$parent as XCollapseComponentPublicInstance
|
||||
} catch (e) {}
|
||||
return parent
|
||||
},
|
||||
setList(items : string[]) {
|
||||
this.list = items
|
||||
this.getNodes()
|
||||
},
|
||||
itemClick() {
|
||||
/**
|
||||
* 项目被点击时触发。
|
||||
* @param name {string} 当前name值
|
||||
*/
|
||||
this.$emit('click', this.name,!this.opened);
|
||||
|
||||
if (!this._disabled) {
|
||||
// @ts-ignore
|
||||
let parent : XCollapseComponentPublicInstance | null = null;
|
||||
try {
|
||||
// @ts-ignore
|
||||
parent = this.$parent as XCollapseComponentPublicInstance
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
if (parent != null) {
|
||||
parent.addChange(this.name)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
getNodes() {
|
||||
// #ifndef MP
|
||||
let _this = this;
|
||||
let ele = this.$refs['xCollapseItemContent'] as UniElement|null;
|
||||
if(ele==null) return;
|
||||
ele.getBoundingClientRectAsync()
|
||||
?.then((rect:DOMRect)=>{
|
||||
_this.itemHeight = rect.height;
|
||||
if (_this._isActive) {
|
||||
_this.open()
|
||||
} else {
|
||||
_this.close()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xCollapseItemContent")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
this.itemHeight = nodeinfo.height!;
|
||||
if (this._isActive) {
|
||||
this.open()
|
||||
} else {
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
open() {
|
||||
this.opened = true;
|
||||
},
|
||||
close() {
|
||||
this.opened = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xCollapseItemBox" :style="{background:_color}">
|
||||
<view @click="itemClick" class="xCollapseItem" :style="{opacity:_disabled?0.5:1}">
|
||||
<view class="xCollapseItemBoxLeft">
|
||||
<x-icon :font-size="_titleFontSize" v-if="_leftIcon" :name="_leftIcon"
|
||||
:color="_isActive?_activeColor:_titleColor" style="margin-right: 12px;"></x-icon>
|
||||
<!--
|
||||
@slot 左边插槽
|
||||
@prop {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="left" :status="opened"></slot>
|
||||
|
||||
<view class="xCollapseItemBoxTextBox" :style="{height:_titleHeight}">
|
||||
<view style="flex:1">
|
||||
<!--
|
||||
@slot 标题插槽,如果你要完全自定标题样式请在此插槽内布局
|
||||
@prop {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="title" :status="opened">
|
||||
<text class="xCollapseItemBoxText" :style="_textMap">
|
||||
{{_title}}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="xCollapseItemBoxRight">
|
||||
<!--
|
||||
@slot 右边插槽
|
||||
@prop {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="right" :status="opened"></slot>
|
||||
<x-icon :color="_isActive?_activeColor:'#bfbfbf'" style="margin-left: 12px;"
|
||||
:name="opened?'arrow-down-s-line':'arrow-right-s-line'"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xCollapseItemWrap" :style="{height:opened?(itemHeight+'px'):'0rpx'}">
|
||||
<view class="xCollapseItemContent" ref="xCollapseItemContent">
|
||||
<!--
|
||||
@slot 默认内容插槽。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<x-divider v-if="showBottomLine"></x-divider>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xCollapseItemBoxTextBox {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCollapseItemBoxText {
|
||||
text-overflow: ellipsis;
|
||||
/* #ifndef APP */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xCollapseItemContent {
|
||||
padding: 12px 0rpx;
|
||||
}
|
||||
|
||||
.xCollapseItemBoxLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xCollapseItemBoxRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xCollapseItemBox {
|
||||
/* background-color: white; */
|
||||
padding: 0px 12px;
|
||||
}
|
||||
|
||||
.xCollapseItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.xCollapseItemWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition-property: height;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export type CHIDREN_ITEM = {
|
||||
id:string,
|
||||
ele:XCollapseItemComponentPublicInstance
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHIDREN_ITEM } from "../x-collapse/interface.uts"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 折叠面板 xCollapse
|
||||
* @description 可单,可多开,内部只可放置x-collapse-item直接子节点组件,为了避免重复计算和性能x-collapse-item不能通过响应式修改内容。如果确实需要请通过刷新key解决
|
||||
* @page /pages/index/collapse
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
list: [] as CHIDREN_ITEM[],
|
||||
activeName: [] as string[],
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 变换时触发
|
||||
* @param {String[]} value - 当前打开的值
|
||||
*/
|
||||
'change',
|
||||
'update:modelValue'],
|
||||
props: {
|
||||
/**
|
||||
* 当前打开的组。可v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 是否允许打开多个。
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
|
||||
},
|
||||
beforeMount() {
|
||||
this.activeName = this.modelValue;
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
xCollapseDefaultName: this.activeName
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue : string[]) {
|
||||
let newstr = newValue.join("")
|
||||
if (newstr == this.activeName.join("")) return;
|
||||
if (this.multiple) {
|
||||
this.activeName = newValue;
|
||||
} else {
|
||||
if (newValue.length >= 1) {
|
||||
this.activeName = [newValue[0]]
|
||||
}else{
|
||||
this.activeName = []
|
||||
}
|
||||
}
|
||||
|
||||
this.pushChildren();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addItem(item : CHIDREN_ITEM) {
|
||||
let index = this.list.findIndex((el : CHIDREN_ITEM) : boolean => el.id == item.id)
|
||||
if (index > -1) {
|
||||
this.list.splice(index, 1, item)
|
||||
} else {
|
||||
this.list.push(item)
|
||||
}
|
||||
if (this.activeName.length > 0) {
|
||||
this.pushChildren()
|
||||
}
|
||||
},
|
||||
delItem(id : string) {
|
||||
let index = this.list.findIndex((el : CHIDREN_ITEM) : boolean => el.id == id)
|
||||
if (index > -1) {
|
||||
this.list.splice(index, 1)
|
||||
}
|
||||
this.pushChildren()
|
||||
},
|
||||
pushChildren() {
|
||||
this.list.forEach((el : CHIDREN_ITEM) => {
|
||||
el.ele.setList(this.activeName)
|
||||
})
|
||||
},
|
||||
|
||||
addChange(id : string) {
|
||||
if (this.multiple) {
|
||||
let index = this.activeName.findIndex((el : string) : boolean => el == id)
|
||||
if (index > -1) {
|
||||
this.activeName.splice(index, 1)
|
||||
} else {
|
||||
this.activeName.push(id)
|
||||
}
|
||||
} else {
|
||||
if (this.activeName.includes(id)) {
|
||||
this.activeName = []
|
||||
} else {
|
||||
this.activeName = [id]
|
||||
}
|
||||
}
|
||||
|
||||
this.pushChildren();
|
||||
/**
|
||||
* 变换时触发
|
||||
* @param value {string[]} 当前打开的值
|
||||
*/
|
||||
this.$emit('change', this.activeName)
|
||||
/**
|
||||
* 等同v-model=""
|
||||
*/
|
||||
this.$emit('update:modelValue', this.activeName)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<!--
|
||||
@slot 默认插槽,仅可放置x-collapse-item子节点
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="colorViewBoxAlphaBox">
|
||||
<x-text _style="opacity:0.5" font-size="13">
|
||||
<!-- 不透明度 -->
|
||||
{{i18n.t('tmui4x.colorView.alpha')}}
|
||||
</x-text>
|
||||
<view class="boxRight">
|
||||
<view :class="[isDark?'dark':'light']" class="barBox " :style="{borderRadius:(barsize+4)+'px'}">
|
||||
<view class="barBoxRealWrap" @touchstart.stop="mStart" @touchmove.stop="mMove" @touchend="mEnd"
|
||||
@touchcancel="mEnd" <!-- #ifdef WEB -->
|
||||
@mousedown="mmStart(($event as UniMouseEvent),index)"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view ref="bar" class="bar"
|
||||
:style="{width:barsize+'px',height:barsize+'px',borderRadius:barsize+'px'}"></view>
|
||||
<view ref="barWrap" class="barWrap" :style="{height:barsize+'px'}"></view>
|
||||
<view class="barPlace" :style="{width:barsize+'px',height:barsize+'px'}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<x-input font-size="14" input-padding="0px" type="number" @input="inputChange" :model-value="rgbvalue"
|
||||
width="70" height="30" align="center" placeholder="">
|
||||
<template v-slot:inputRight>
|
||||
<x-text _style="margin-right:5px">%</x-text>
|
||||
</template>
|
||||
</x-input>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, getCurrentInstance, nextTick } from "vue"
|
||||
import { hexToRgb, hslToRgb, rgbToHexNoAlpha, hslaToRgbCss, rgbToHex, rgbToHsl } from "../../core/util/xCoreColorUtil.uts";
|
||||
import { xConfig } from "../../config/xConfig.uts";
|
||||
const i18n = xConfig.i18n;
|
||||
const props = defineProps({
|
||||
hexValue: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 值变化时触发。
|
||||
* @param {number} alpha - 当前的颜色值
|
||||
*/
|
||||
(e : 'change', alpha : number) : void,
|
||||
}>()
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
const rgbvalue = ref<string>((Math.max(0, Math.min(props.hexValue * 100, 100))).toString())
|
||||
const valuestr = computed(() : number => {
|
||||
let val = (parseInt(rgbvalue.value) / 100) + ''
|
||||
val = val.substring(0, 4)
|
||||
return Math.max(0, Math.min(1, parseFloat(val)))
|
||||
})
|
||||
|
||||
let tid = 0;
|
||||
let _x = 0
|
||||
let _startLeft = 0
|
||||
const barsize = ref(28)
|
||||
const isMoving = ref(false)
|
||||
const barWrapNodes = ref<NodeInfo|null>(null);
|
||||
let xColorviewEventId = ''
|
||||
|
||||
const toChangeEvents = () => {
|
||||
emits('change', valuestr.value)
|
||||
|
||||
}
|
||||
const isDark = computed(() : boolean => xConfig.dark == 'dark')
|
||||
|
||||
const inputChange = (val : string) => {
|
||||
if (val == '') return;
|
||||
let realvalue = parseInt(val)
|
||||
realvalue = isNaN(realvalue) ? 0 : realvalue;
|
||||
let realvalueNew = Math.max(0, Math.min(realvalue, 100));
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let nodes = result[0] as NodeInfo
|
||||
let realwidth = nodes.width!
|
||||
|
||||
let left = Math.floor(realvalueNew / 100 * realwidth)
|
||||
let nowdom = proxy!.$refs['bar'] as UniElement
|
||||
nowdom.style.setProperty('left', left.toString() + 'px')
|
||||
|
||||
rgbvalue.value = realvalue.toString()
|
||||
nextTick(() => {
|
||||
rgbvalue.value = realvalueNew.toString()
|
||||
toChangeEvents()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const setPostionTranslaterStart = (x : number, y : number) => {
|
||||
let dom = proxy!.$refs['bar'] as UniElement
|
||||
let barWrapdom = proxy!.$refs['barWrap'] as UniElement
|
||||
let domleft = dom.style.getPropertyValue('left') as null | string
|
||||
if (isNaN(parseInt(domleft == null ? '0' : domleft!))) {
|
||||
dom.style.setProperty('left', '0px')
|
||||
}
|
||||
|
||||
let barwRound = barWrapNodes.value!
|
||||
|
||||
_x = x
|
||||
_startLeft = (_x - barwRound.left!) - barsize.value / 2
|
||||
_startLeft = Math.ceil(Math.max(0, Math.min(_startLeft, barwRound.width!)))
|
||||
dom.style.setProperty('left', _startLeft + 'px')
|
||||
let val = Math.floor(_startLeft / barwRound.width! * 100).toString()
|
||||
rgbvalue.value = val
|
||||
toChangeEvents()
|
||||
}
|
||||
const setPostionTranslaterMove = (mx : number, my : number) => {
|
||||
let dom = proxy!.$refs['bar'] as UniElement
|
||||
let barWrapdom = proxy!.$refs['barWrap'] as UniElement
|
||||
let barwRound = barWrapNodes.value!
|
||||
|
||||
let x = mx - _x + _startLeft - barsize.value / 2
|
||||
// #ifdef MP||WEB
|
||||
x = mx - _x + _startLeft
|
||||
// #endif
|
||||
x = Math.ceil(Math.max(0, Math.min(x, barwRound.width!)))
|
||||
dom.style.setProperty('left', x + 'px')
|
||||
let val = Math.floor(x / barwRound.width! * 100).toString()
|
||||
if (val == rgbvalue.value) return;
|
||||
rgbvalue.value = val
|
||||
|
||||
toChangeEvents()
|
||||
}
|
||||
|
||||
const mStart = (evt : UniTouchEvent) => {
|
||||
evt.preventDefault()
|
||||
isMoving.value = true;
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let nodes = result[0] as NodeInfo
|
||||
barWrapNodes.value = nodes
|
||||
setPostionTranslaterStart(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
})
|
||||
}
|
||||
const mMove = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return
|
||||
setPostionTranslaterMove(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
}
|
||||
const mEnd = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
}
|
||||
|
||||
// #ifdef WEB
|
||||
const mmStart = (evt : UniMouseEvent, index) => {
|
||||
isMoving.value = true;
|
||||
xColorviewEventId = Math.random().toString(16).substring(2, 16)
|
||||
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY)
|
||||
}
|
||||
const mmMove = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY)
|
||||
}
|
||||
const mmEnd = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
xColorviewEventId = ''
|
||||
|
||||
}
|
||||
document.body.addEventListener('mousemove', mmMove)
|
||||
document.body.addEventListener('mouseup', mmEnd)
|
||||
document.body.addEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
|
||||
const setViewPosition = () => {
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let nodes = result[0] as NodeInfo
|
||||
barWrapNodes.value = nodes
|
||||
let realwidth = nodes.width!
|
||||
let doms = proxy!.$refs['bar'] as UniElement
|
||||
let left = Math.floor(valuestr.value * realwidth)
|
||||
doms.style.setProperty('left', left.toString() + 'px')
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : number => props.hexValue, (newval : number) => {
|
||||
if (valuestr.value == newval) return;
|
||||
rgbvalue.value = (Math.max(0, Math.min(newval * 100, 100))).toString()
|
||||
setViewPosition()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef WEB
|
||||
document.body.removeEventListener('mousemove', mmMove)
|
||||
document.body.removeEventListener('mouseup', mmEnd)
|
||||
document.body.removeEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
})
|
||||
onMounted(() => {
|
||||
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function () {
|
||||
setViewPosition()
|
||||
}, 100);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.colorViewBoxAlphaBox {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
border: 3px solid white;
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
pointer-events: none;
|
||||
/* #ifdef MP-WEIXIN||WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.barPlace {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.box0,
|
||||
.box1 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.boxRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.barBox {
|
||||
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
|
||||
}
|
||||
|
||||
.barBox.light {
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
.barBox.dark {
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(255, 255, 255, 1));
|
||||
}
|
||||
|
||||
.barBoxRealWrap {
|
||||
margin: 2px 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* #ifdef WEB */
|
||||
cursor: cell;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.barWrap {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<view class="colorViewGridBox" ref="colorViewGridBoxRef" :style="{minHeight:boxHeight+'px'}">
|
||||
|
||||
<view class="colorViewGridBoxGrid"
|
||||
:style="{width:gridWidth+'px',height:gridHeight+'px',top:barTop+'px',left:barLeft+'px'}"></view>
|
||||
<canvas :id="canvasUid" @touchstart.stop="mStart" @touchmove.stop="mMove" @touchend="mEnd" @touchcancel="mEnd"
|
||||
v-if="isInitDom" ref="canvas" :style="{width:boxWidth+'px',height:boxHeight+'px'}" <!-- #ifdef WEB -->
|
||||
@mousedown="mmStart"
|
||||
<!-- #endif -->
|
||||
|
||||
></canvas>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, getCurrentInstance } from "vue"
|
||||
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHexNoAlpha } from "../../core/util/xCoreColorUtil.uts";
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 值变化时触发。
|
||||
* @param {string} hexstr - 当前的颜色值
|
||||
*/
|
||||
(e : 'change', hexstr : string) : void,
|
||||
}>()
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
const canvas = ref<UniCanvasElement | null>(null)
|
||||
const colorViewGridBoxRef = ref<UniCanvasElement | null>(null)
|
||||
let tid = 0;
|
||||
let gridWidth = ref(0)
|
||||
let gridHeight = ref(0)
|
||||
let boxWidth = ref(0)
|
||||
let boxHeight = ref(250)
|
||||
let isInitDom = ref(false)
|
||||
let colosXYData = [] as string[][]
|
||||
let barLeft = ref(0)
|
||||
let barTop = ref(0)
|
||||
let windTop = uni.getWindowInfo().windowTop
|
||||
const isMoving = ref(false)
|
||||
let xColorviewEventId = ''
|
||||
const canvasUid = 'xColorView_' + Math.random().toString(16).substring(4, 8)
|
||||
const drawBlackWhite = (ctx : CanvasRenderingContext2D) => {
|
||||
let colors = [] as string[]
|
||||
for (let i = 0; i < 12; i++) {
|
||||
let blv = (11 - i) / 11 * 100
|
||||
let l = Math.floor(blv)
|
||||
let rgba = hslToRgb({ h: 0, s: 0, l: l, a: 1 } as UTSJSONObject)
|
||||
let color = rgbToHexNoAlpha(rgba)
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(i * gridWidth.value, 0, gridWidth.value, gridHeight.value)
|
||||
colors.push(color)
|
||||
}
|
||||
colosXYData.push(colors)
|
||||
}
|
||||
const drawHsla = (ctx : CanvasRenderingContext2D) => {
|
||||
let colorsx = new Array<string[]>()
|
||||
for (let i = 0; i < 9; i++) {
|
||||
let colorsB = [] as string[]
|
||||
for (let j = 0; j < 12; j++) {
|
||||
colorsB.push('')
|
||||
}
|
||||
colorsx.push(colorsB)
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
for (let j = 1; j < 10; j++) {
|
||||
let blv = (j + 0.2) / 10 * 100
|
||||
let l = Math.floor(blv)
|
||||
let rgba = hslToRgb({ h: i * 25 + 180, s: 100 - blv * 0.65, l: l, a: 1 } as UTSJSONObject)
|
||||
let color = rgbToHexNoAlpha(rgba)
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(i * gridWidth.value, j * gridHeight.value, gridWidth.value, gridHeight.value)
|
||||
colorsx[j - 1][i] = color
|
||||
|
||||
}
|
||||
}
|
||||
colosXYData.push(...colorsx)
|
||||
}
|
||||
const getInitDraw = () => {
|
||||
uni.createCanvasContextAsync({
|
||||
id: canvasUid,
|
||||
component: proxy,
|
||||
success(context) {
|
||||
let ctx = context.getContext('2d')!;
|
||||
let canvas = ctx.canvas
|
||||
let dpr = uni.getWindowInfo().pixelRatio
|
||||
canvas.width = canvas.offsetWidth * dpr
|
||||
canvas.height = canvas.offsetHeight * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
gridWidth.value = canvas.offsetWidth / 12
|
||||
gridHeight.value = canvas.offsetHeight / 10
|
||||
drawBlackWhite(ctx)
|
||||
drawHsla(ctx)
|
||||
|
||||
},
|
||||
fail(err) {
|
||||
console.error(err)
|
||||
uni.showToast({ title: '错误', icon: 'none' })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
let nowselectedStr = ref('')
|
||||
const colorViewGridBox = ref<NodeInfo | null>(null)
|
||||
const setPostion = (x : number, y : number) => {
|
||||
let node = colorViewGridBox.value!;
|
||||
// #ifdef MP
|
||||
if(!node?.left){
|
||||
node.left = 0
|
||||
}
|
||||
if(!node?.top){
|
||||
node.top = 0
|
||||
}
|
||||
// #endif
|
||||
let _x = x - node.left!;
|
||||
let _y = y - node.top! - windTop;
|
||||
|
||||
let pos_x = Math.ceil(_x / gridWidth.value)
|
||||
pos_x = Math.max(1, Math.min(12, pos_x))
|
||||
let pos_y = Math.ceil(_y / gridHeight.value)
|
||||
pos_y = Math.max(1, Math.min(10, pos_y))
|
||||
|
||||
let xystr = colosXYData[pos_y - 1][pos_x - 1]
|
||||
barLeft.value = (pos_x - 1) * gridWidth.value
|
||||
barTop.value = (pos_y - 1) * gridHeight.value
|
||||
if (nowselectedStr.value == xystr) return;
|
||||
nowselectedStr.value = xystr
|
||||
|
||||
emits('change', xystr)
|
||||
}
|
||||
const mStart = (evt : UniTouchEvent) => {
|
||||
evt.preventDefault()
|
||||
isMoving.value = true
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
let node = nodes[0] as NodeInfo;
|
||||
|
||||
colorViewGridBox.value = node
|
||||
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
})
|
||||
|
||||
}
|
||||
const mMove = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return;
|
||||
// #ifdef MP-WEIXIN||WEB
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
}
|
||||
const mEnd = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return;
|
||||
isMoving.value = false
|
||||
}
|
||||
|
||||
// #ifdef WEB
|
||||
const mmStart = (evt : UniMouseEvent) => {
|
||||
isMoving.value = true;
|
||||
xColorviewEventId = Math.random().toString(16).substring(2, 16)
|
||||
setPostion(evt.clientX, evt.clientY)
|
||||
}
|
||||
const mmMove = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
setPostion(evt.clientX, evt.clientY)
|
||||
}
|
||||
const mmEnd = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
xColorviewEventId = ''
|
||||
}
|
||||
|
||||
document.body.addEventListener('mousemove', mmMove)
|
||||
document.body.addEventListener('mouseup', mmEnd)
|
||||
document.body.addEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
// #ifdef WEB
|
||||
document.body.removeEventListener('mousemove', mmMove)
|
||||
document.body.removeEventListener('mouseup', mmEnd)
|
||||
document.body.removeEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
})
|
||||
onMounted(() => {
|
||||
clearTimeout(tid)
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
let node = nodes[0] as NodeInfo;
|
||||
colorViewGridBox.value = node
|
||||
boxHeight.value = Math.min(Math.max(250, node.width!), 250)
|
||||
boxWidth.value = Math.max(Math.max(node.width!, 250), 250)
|
||||
isInitDom.value = true;
|
||||
tid = setTimeout(function () {
|
||||
getInitDraw();
|
||||
}, 80);
|
||||
})
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.colorViewGridBox {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
/* #ifdef WEB */
|
||||
cursor: cell;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.colorViewGridBoxGrid {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 255, 255, 0.6);
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
/* #ifdef MP||WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
|
||||
<view class="colorViewGridBox"
|
||||
ref="gridBoxref"
|
||||
@touchstart.stop="mStart"
|
||||
@touchmove.stop="mMove" @touchend="mEnd" @touchcancel="mEnd"
|
||||
:style="{minHeight:boxHeight+'px'}"
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="mmStart"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<!-- #ifndef APP -->
|
||||
<view class="colorViewGridHueBox"
|
||||
:style="{width:boxWidth+'px',height:boxHeight+'px',margin:`${(btnSize/2)}px`,borderRadius:'10px'}">
|
||||
<view class="colorViewGridHueBoxMask">
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef APP -->
|
||||
<canvas v-if="isInitDom" ref="canvas"
|
||||
:style="{width:boxWidth+'px',height:boxHeight+'px',margin:`${(btnSize/2)}px`,borderRadius:'10px'}"></canvas>
|
||||
<!-- #endif -->
|
||||
<view class="colorViewGridBoxGridBar"
|
||||
:style="{width:btnSize+'px',height:btnSize+'px',top:barTop+'px',left:barLeft+'px',backgroundColor:valuestr,opacity:isShowBar?'1':'0'}">
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, getCurrentInstance, watch, onMounted, onBeforeMount, onBeforeUnmount } from "vue"
|
||||
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHsl, rgbToHexNoAlpha } from "../../core/util/xCoreColorUtil.uts";
|
||||
const props = defineProps({
|
||||
hexValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 值变化时触发。
|
||||
* @param {string} hexstr - 当前的颜色值
|
||||
*/
|
||||
(e : 'change', hexstr : string) : void,
|
||||
}>()
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
let boxWidth = ref(0)
|
||||
let boxHeight = ref(300)
|
||||
let tid = 0;
|
||||
const btnSize = ref(24)
|
||||
let barLeft = ref(0)
|
||||
let barTop = ref(0)
|
||||
const isMoving = ref(false)
|
||||
let nowselectedStr = ref('')
|
||||
let windTop = uni.getWindowInfo().windowTop
|
||||
const canvas = ref<UniCanvasElement | null>(null)
|
||||
let isInitDom = ref(false)
|
||||
const valuestr = ref(`rgba(255,255,255,1)`)
|
||||
const isShowBar = ref(false)
|
||||
let xColorviewEventId =''
|
||||
const gridBox = ref<NodeInfo|null>(null);
|
||||
const gridBoxref = ref<UniElement | null>(null)
|
||||
|
||||
const setPostion = (x : number, y : number) => {
|
||||
let node = gridBox.value! as NodeInfo;
|
||||
let bsize = btnSize.value
|
||||
|
||||
// 计算相对于容器的坐标
|
||||
let _x = x - node.left!;
|
||||
let _y = y - node.top! - windTop;
|
||||
|
||||
// 容器的margin
|
||||
let margin = bsize / 2
|
||||
|
||||
// 计算按钮中心点相对于颜色区域的位置
|
||||
let center_x = _x - margin
|
||||
let center_y = _y - margin
|
||||
|
||||
// 限制按钮中心点在颜色区域内(确保按钮不会超出容器)
|
||||
// 按钮中心点的有效范围:从 0 到 boxWidth/boxHeight(因为颜色区域就是boxWidth x boxHeight)
|
||||
center_x = Math.max(0, Math.min(boxWidth.value, center_x))
|
||||
center_y = Math.max(0, Math.min(boxHeight.value, center_y))
|
||||
|
||||
// 设置按钮位置(按钮左上角坐标,相对于整个容器)
|
||||
barLeft.value = center_x
|
||||
barTop.value = center_y
|
||||
|
||||
// 计算颜色采样位置(按钮中心点在颜色区域内的位置)
|
||||
let color_x = center_x
|
||||
let color_y = center_y
|
||||
|
||||
// 计算百分比位置(0-100)
|
||||
let xBlv = Math.max(0, Math.min(100, (color_x / boxWidth.value) * 100))
|
||||
let yBlv = Math.max(0, Math.min(100, (color_y / boxHeight.value) * 100))
|
||||
|
||||
// 计算饱和度s:0-100
|
||||
let s = 0
|
||||
if (xBlv > 0 && xBlv <= 50) {
|
||||
s = xBlv / 50 * 100
|
||||
} else if (xBlv > 50) {
|
||||
s = 100
|
||||
}
|
||||
// 计算亮度
|
||||
let l = 100 - xBlv
|
||||
// 计算色相
|
||||
let h = yBlv / 100 * 360
|
||||
let rgba = hslToRgb({ h: h, s: s, l: l, a: 1 } as UTSJSONObject)
|
||||
let color = rgbToHexNoAlpha(rgba)
|
||||
if (valuestr.value == color) return;
|
||||
|
||||
valuestr.value = color
|
||||
emits('change', color)
|
||||
}
|
||||
const mStart = (evt : UniTouchEvent) => {
|
||||
if(!isInitDom.value) return;
|
||||
evt.preventDefault()
|
||||
// #ifdef MP-WEIXIN||WEB
|
||||
evt.stopPropagation()
|
||||
// #endif
|
||||
isMoving.value = true
|
||||
isShowBar.value = true;
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
gridBox.value = nodes[0] as NodeInfo;
|
||||
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const mMove = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return;
|
||||
evt.preventDefault()
|
||||
// #ifdef MP-WEIXIN||WEB
|
||||
evt.stopPropagation()
|
||||
// #endif
|
||||
setPostion(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY)
|
||||
}
|
||||
const mEnd = (evt : UniTouchEvent) => {
|
||||
if (!isMoving.value) return;
|
||||
isMoving.value = false
|
||||
}
|
||||
|
||||
// #ifdef WEB
|
||||
const mmStart = (evt:UniMouseEvent)=>{
|
||||
isMoving.value = true;
|
||||
xColorviewEventId = Math.random().toString(16).substring(2,16)
|
||||
isShowBar.value = true;
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
gridBox.value = nodes[0] as NodeInfo;
|
||||
setPostion(evt.clientX, evt.clientY)
|
||||
})
|
||||
}
|
||||
const mmMove = (evt:UniMouseEvent)=>{
|
||||
if (!isMoving.value) return
|
||||
// #ifdef MP-WEIXIN||WEB
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
setPostion(evt.clientX, evt.clientY)
|
||||
}
|
||||
const mmEnd = (evt:UniMouseEvent)=>{
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
xColorviewEventId=''
|
||||
}
|
||||
|
||||
document.body.addEventListener('mousemove',mmMove)
|
||||
document.body.addEventListener('mouseup',mmEnd)
|
||||
document.body.addEventListener('mouseleave',mmEnd)
|
||||
// #endif
|
||||
|
||||
|
||||
const drawBgHsla = (ctx : CanvasRenderingContext2D) => {
|
||||
|
||||
// 绘制渐变
|
||||
let liner = ctx.createLinearGradient(boxWidth.value / 2, 0, boxWidth.value / 2, boxHeight.value / 2)
|
||||
liner.addColorStop(0, '#ff0000')
|
||||
liner.addColorStop(0.3333, '#ffff00')
|
||||
liner.addColorStop(0.6666, '#00ff00')
|
||||
liner.addColorStop(1, '#00ffff')
|
||||
ctx.fillStyle = liner
|
||||
ctx.fillRect(0, 0, boxWidth.value, boxHeight.value)
|
||||
|
||||
let liner2 = ctx.createLinearGradient(boxWidth.value / 2, boxHeight.value / 2, boxWidth.value / 2, boxHeight.value)
|
||||
liner2.addColorStop(0, '#00ffff')
|
||||
liner2.addColorStop(0.3333, '#0000ff')
|
||||
liner2.addColorStop(0.6666, '#ff00ff')
|
||||
liner2.addColorStop(1, '#ff0000')
|
||||
ctx.fillStyle = liner2
|
||||
ctx.fillRect(0, boxHeight.value / 2, boxWidth.value, boxHeight.value / 2)
|
||||
|
||||
// 绘制遮罩.
|
||||
let liner3 = ctx.createLinearGradient(0, boxHeight.value / 2, boxWidth.value, boxHeight.value / 2)
|
||||
liner3.addColorStop(0, '#ffffff')
|
||||
liner3.addColorStop(0.5, 'rgba(0,0,0,0)')
|
||||
liner3.addColorStop(1, '#000000')
|
||||
ctx.fillStyle = liner3
|
||||
ctx.fillRect(0, 0, boxWidth.value, boxHeight.value)
|
||||
|
||||
}
|
||||
const getInitDraw = () => {
|
||||
if (canvas.value == null) return;
|
||||
let dom = canvas.value as UniCanvasElement;
|
||||
let dpr = uni.getWindowInfo().pixelRatio
|
||||
dom.width = dom.offsetWidth * dpr;
|
||||
dom.height = dom.offsetHeight * dpr;
|
||||
let ctx = dom.getContext('2d')!
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
drawBgHsla(ctx)
|
||||
}
|
||||
const valueStrToPosition = () => {
|
||||
if (valuestr.value == '') return;
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
let node = nodes[0] as NodeInfo;
|
||||
let hsl = rgbToHsl(hexToRgb(valuestr.value))
|
||||
|
||||
let h = hsl.getNumber('h')!
|
||||
let s = hsl.getNumber('s')!
|
||||
let l = hsl.getNumber('l')!
|
||||
let bsize = btnSize.value
|
||||
|
||||
let xBlv = 0
|
||||
let yBlv = 0
|
||||
|
||||
// 如果饱和度为0,隐藏按钮
|
||||
if (s == 0) {
|
||||
isShowBar.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算X轴位置(饱和度)
|
||||
if (s >= l) {
|
||||
xBlv = 100 - l
|
||||
} else {
|
||||
xBlv = s / 100 * 50
|
||||
}
|
||||
|
||||
// 计算Y轴位置(色相)
|
||||
yBlv = h / 360 * 100
|
||||
|
||||
// 计算按钮中心点在颜色区域内的位置
|
||||
let center_x = (xBlv / 100) * boxWidth.value
|
||||
let center_y = (yBlv / 100) * boxHeight.value
|
||||
|
||||
// 计算按钮位置(考虑margin)
|
||||
let margin = bsize / 2
|
||||
barLeft.value = center_x + margin - bsize / 2
|
||||
barTop.value = center_y + margin - bsize / 2
|
||||
|
||||
isShowBar.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : string => props.hexValue, (newval : string) => {
|
||||
if (newval == '' || valuestr.value == newval) return;
|
||||
valuestr.value = newval
|
||||
valueStrToPosition()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
|
||||
// #ifdef WEB
|
||||
document.body.removeEventListener('mousemove',mmMove)
|
||||
document.body.removeEventListener('mouseup',mmEnd)
|
||||
document.body.removeEventListener('mouseleave',mmEnd)
|
||||
// #endif
|
||||
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
valuestr.value = props.hexValue
|
||||
})
|
||||
onMounted(() => {
|
||||
clearTimeout(tid)
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.colorViewGridBox')
|
||||
.boundingClientRect()
|
||||
.exec(nodes => {
|
||||
let node = nodes[0] as NodeInfo;
|
||||
boxHeight.value = Math.max(Math.min(270, node.width!), 270)
|
||||
boxWidth.value = Math.max(node.width!, 270) - btnSize.value
|
||||
isInitDom.value = true
|
||||
gridBox.value = node
|
||||
tid = setTimeout(function () {
|
||||
valueStrToPosition()
|
||||
getInitDraw()
|
||||
|
||||
}, 80);
|
||||
})
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.colorViewGridHueBox {
|
||||
background-image: linear-gradient(to bottom, red 0, #ff0 16.66666%, #0f0 33.333333%, #0ff 50%, #00f 66.66666%, #f0f 83.333333%, red 100%);
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
/* border-radius: 6px;
|
||||
overflow: hidden; */
|
||||
/* #ifdef MP-WEIXIN||WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
|
||||
.colorViewGridBox {
|
||||
|
||||
/* #ifdef MP-WEIXIN||WEB */
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
cursor: cell;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.colorViewGridHueBoxMask {
|
||||
background-image: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(0, 0, 0, 0), #000000 100%);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.colorViewGridBoxGridBar {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
pointer-events: none;
|
||||
/* #ifdef MP-WEIXIN||WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<view>
|
||||
<view :style="{height:'8px'}"></view>
|
||||
<view class="box" v-for="(item,index) in rgbvalue" :key="index" :class="[`box${index}`]">
|
||||
<x-text _style="opacity:0.5" font-size="13">{{rgbvalueText[index]}}</x-text>
|
||||
<view class="boxRight">
|
||||
<view :class="[
|
||||
index==0?'barR':'',
|
||||
index==1?'barG':'',
|
||||
index==2?'barB':''
|
||||
]" class="barBox " :style="{
|
||||
borderRadius:(barsize+4)+'px',
|
||||
|
||||
}">
|
||||
<view class="barBoxRealWrap" @touchstart.stop="mStart(($event as UniTouchEvent),index)"
|
||||
@touchmove.stop="mMove(($event as UniTouchEvent),index)"
|
||||
@touchend.stop="mEnd(($event as UniTouchEvent),index)"
|
||||
@touchcancel="mEnd(($event as UniTouchEvent),index)" <!-- #ifdef WEB -->
|
||||
@mousedown="mmStart(($event as UniMouseEvent),index)"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view ref="bar" class="bar"
|
||||
:style="{width:barsize+'px',height:barsize+'px',borderRadius:barsize+'px'}"></view>
|
||||
<view ref="barWrap" class="barWrap" :style="{height:barsize+'px'}"></view>
|
||||
<view class="barPlace" :style="{width:barsize+'px',height:barsize+'px'}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<x-input font-size="14" input-padding="0px" type="number" @input="inputChange(($event as string),index)"
|
||||
:model-value="realRgb[index]" width="70" height="30" align="center" placeholder=""></x-input>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { xConfig } from "../../config/xConfig.uts";
|
||||
import { ref, computed, getCurrentInstance, nextTick } from "vue"
|
||||
import { hexToRgb, hslToRgb, rgbToHexNoAlpha, hslaToRgbCss, rgbToHex, rgbToHsl } from "../../core/util/xCoreColorUtil.uts";
|
||||
const i18n = xConfig.i18n;
|
||||
const props = defineProps({
|
||||
hexValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 值变化时触发。
|
||||
* @param {string} hexstr - 当前的颜色值
|
||||
*/
|
||||
(e : 'change', hexstr : string) : void,
|
||||
}>()
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
const rgbvalue = ref<number[]>([0, 0, 0])
|
||||
const rgbvalueText = ref<string[]>([i18n.t('tmui4x.colorView.r'), i18n.t('tmui4x.colorView.g'), i18n.t('tmui4x.colorView.b')])
|
||||
const isMoving = ref(false)
|
||||
const realRgb = computed(() : string[] => {
|
||||
let r = rgbvalue.value[0].toString()
|
||||
let g = rgbvalue.value[1].toString()
|
||||
let b = rgbvalue.value[2].toString()
|
||||
return [r, g, b] as string[]
|
||||
})
|
||||
let tid = 0;
|
||||
let _x = 0
|
||||
let _startLeft = 0
|
||||
const barsize = ref(28)
|
||||
const valuestr = ref('#ffffff')
|
||||
|
||||
let nowIndex = 0
|
||||
let xColorviewEventId = ''
|
||||
const barWrapNodes = ref<NodeInfo | null>(null);
|
||||
|
||||
const toChangeEvents = () => {
|
||||
let str = rgbToHexNoAlpha({ r: rgbvalue.value[0], g: rgbvalue.value[1], b: rgbvalue.value[2] } as UTSJSONObject)
|
||||
if (str == valuestr.value) return;
|
||||
|
||||
valuestr.value = str;
|
||||
emits('change', str)
|
||||
|
||||
}
|
||||
const setPostionTranslaterStart = (x : number, y : number, index : number) => {
|
||||
let doms = proxy!.$refs['bar'] as UniElement[]
|
||||
let dom = doms[index] as UniElement
|
||||
let bardoms = proxy!.$refs['barWrap'] as UniElement[]
|
||||
let barWrapdom = bardoms[0] as UniElement
|
||||
|
||||
let domleft = dom.style.getPropertyValue('left') as null | string
|
||||
if (isNaN(parseInt(domleft == null ? '0' : domleft!))) {
|
||||
dom.style.setProperty('left', '0px')
|
||||
}
|
||||
let barwRound = barWrapNodes.value!;
|
||||
_x = x
|
||||
_startLeft = (_x - barwRound.left!) - barsize.value / 2
|
||||
_startLeft = Math.ceil(Math.max(0, Math.min(_startLeft, barwRound.width!)))
|
||||
dom.style.setProperty('left', _startLeft + 'px')
|
||||
let val = Math.floor(_startLeft / barwRound.width! * 255)
|
||||
rgbvalue.value[index] = val
|
||||
toChangeEvents()
|
||||
}
|
||||
const setPostionTranslaterMove = (mx : number, my : number, index : number) => {
|
||||
let doms = proxy!.$refs['bar'] as UniElement[]
|
||||
let dom = doms[index] as UniElement
|
||||
let bardoms = proxy!.$refs['barWrap'] as UniElement[]
|
||||
let barWrapdom = bardoms[0] as UniElement
|
||||
let barwRound = barWrapNodes.value!;
|
||||
let x = mx - _x + _startLeft - barsize.value / 2
|
||||
// #ifdef MP||WEB
|
||||
x = mx - _x + _startLeft
|
||||
// #endif
|
||||
x = Math.ceil(Math.max(0, Math.min(x, barwRound.width!)))
|
||||
|
||||
dom.style.setProperty('left', x + 'px')
|
||||
let val = Math.floor(x / barwRound.width! * 255)
|
||||
rgbvalue.value[index] = val
|
||||
toChangeEvents()
|
||||
}
|
||||
const inputChange = (val : string, index : number) => {
|
||||
if (val == '') return;
|
||||
let realvalue = parseInt(val)
|
||||
realvalue = isNaN(realvalue) ? 0 : realvalue;
|
||||
let realvalueNew = Math.max(0, Math.min(realvalue, 255));
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let nodes = result[0] as NodeInfo
|
||||
let realwidth = nodes.width!
|
||||
|
||||
let left = Math.floor(realvalueNew / 255 * realwidth)
|
||||
let doms = proxy!.$refs['bar'] as UniElement[]
|
||||
let nowdom = doms[index]
|
||||
nowdom.style.setProperty('left', left.toString() + 'px')
|
||||
|
||||
rgbvalue.value[index] = realvalue
|
||||
nextTick(() => {
|
||||
rgbvalue.value[index] = realvalueNew
|
||||
toChangeEvents()
|
||||
})
|
||||
})
|
||||
}
|
||||
const mStart = (evt : UniTouchEvent, index : number) => {
|
||||
evt.preventDefault()
|
||||
isMoving.value = true;
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let nodes = result[0] as NodeInfo
|
||||
barWrapNodes.value = nodes
|
||||
setPostionTranslaterStart(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY, index)
|
||||
})
|
||||
}
|
||||
const mMove = (evt : UniTouchEvent, index : number) => {
|
||||
if (!isMoving.value) return
|
||||
setPostionTranslaterMove(evt.changedTouches[0].clientX, evt.changedTouches[0].clientY, index)
|
||||
}
|
||||
const mEnd = (evt : UniTouchEvent, index : number) => {
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
}
|
||||
|
||||
// #ifdef WEB
|
||||
const mmStart = (evt : UniMouseEvent, index) => {
|
||||
isMoving.value = true;
|
||||
xColorviewEventId = Math.random().toString(16).substring(2, 16)
|
||||
nowIndex = index
|
||||
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY, index)
|
||||
}
|
||||
const mmMove = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
setPostionTranslaterMove(evt.clientX - barsize.value, evt.clientY, nowIndex)
|
||||
}
|
||||
const mmEnd = (evt : UniMouseEvent) => {
|
||||
if (!isMoving.value) return
|
||||
isMoving.value = false;
|
||||
xColorviewEventId = ''
|
||||
|
||||
}
|
||||
|
||||
document.body.addEventListener('mousemove', mmMove)
|
||||
document.body.addEventListener('mouseup', mmEnd)
|
||||
document.body.addEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
|
||||
const setViewPosition = () => {
|
||||
let colors = hexToRgb(valuestr.value)
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".barWrap")
|
||||
.boundingClientRect()
|
||||
.exec(result => {
|
||||
let r = colors.getNumber('r')!
|
||||
let g = colors.getNumber('g')!
|
||||
let b = colors.getNumber('b')!
|
||||
let rgb = [r, g, b] as number[]
|
||||
|
||||
let nodes = result[0] as NodeInfo
|
||||
barWrapNodes.value = nodes
|
||||
let realwidth = nodes.width!
|
||||
|
||||
let doms = proxy!.$refs['bar'] as UniElement[]
|
||||
|
||||
for (let i = 0; i < rgb.length; i++) {
|
||||
let leftvalue = rgb[i];
|
||||
let left = Math.floor(leftvalue / 255 * realwidth)
|
||||
let nowdom = doms[i]
|
||||
nowdom.style.setProperty('left', left.toString() + 'px')
|
||||
|
||||
}
|
||||
rgbvalue.value = rgb.splice(0)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() : string => props.hexValue, (newval : string) => {
|
||||
if (newval == '' || valuestr.value == newval) return;
|
||||
valuestr.value = newval
|
||||
|
||||
setViewPosition()
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
valuestr.value = props.hexValue
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef WEB
|
||||
document.body.removeEventListener('mousemove', mmMove)
|
||||
document.body.removeEventListener('mouseup', mmEnd)
|
||||
document.body.removeEventListener('mouseleave', mmEnd)
|
||||
// #endif
|
||||
})
|
||||
onMounted(() => {
|
||||
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function () {
|
||||
setViewPosition()
|
||||
}, 60);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
border: 3px solid white;
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
pointer-events: none;
|
||||
/* #ifdef WEB||MP */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
|
||||
.barPlace {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.box0,
|
||||
.box1 {
|
||||
margin-bottom: 15px;
|
||||
|
||||
}
|
||||
|
||||
.boxRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.barBox {
|
||||
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
/* #ifdef WEB||MP */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.barBoxRealWrap {
|
||||
margin: 2px 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* #ifdef WEB||MP */
|
||||
cursor: cell;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.barWrap {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
/* #ifdef WEB||MP */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.barR {
|
||||
background-image: linear-gradient(to right, #000000, #ff0000);
|
||||
}
|
||||
|
||||
.barG {
|
||||
background-image: linear-gradient(to right, #000000, #00ff00);
|
||||
}
|
||||
|
||||
.barB {
|
||||
background-image: linear-gradient(to right, #000000, #0000ff);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<view>
|
||||
<x-radio-button @change="panelChange" height="32" fontSize="14" v-model="menubarId" :list="menubar"></x-radio-button>
|
||||
|
||||
<color-rgb-uvue :hexValue="colorStr" @change="colorChange" v-if="menubarId=='rgb'"></color-rgb-uvue>
|
||||
<color-grid-uvue @change="colorChange" v-if="menubarId=='grid'&&isReadyShow"></color-grid-uvue>
|
||||
<color-hue-uvue :hexValue="colorStr" @change="colorChange" v-if="menubarId=='hue'&&isReadyShow"></color-hue-uvue>
|
||||
<color-alpha-uvue v-if="_showAlpha" :hexValue="aplpha" @change="colorChangeAlpha"></color-alpha-uvue>
|
||||
<view v-if="menubarId=='rgb'||menubarId=='grid'" style="height:16px"></view>
|
||||
<view class="xColorViewFooter">
|
||||
<!--
|
||||
@slot 默认插槽,用于自行布局底部,比如当你需要改造尾部时用.你也可以通过插槽来隐藏底部
|
||||
@prop {string} rgba - 当前的rgbacss颜色值
|
||||
-->
|
||||
<slot name="default" :rgba="realColorRgbaCss">
|
||||
<view class="xColorViewLeftSHapers">
|
||||
<view class="xColorViewLeftSHapersLeft" :style="{backgroundColor:realColorHex}"></view>
|
||||
<view class="xColorViewLeftSHapersRight" :style="{backgroundColor:realColorHex,opacity:aplpha}">
|
||||
</view>
|
||||
</view>
|
||||
<view class="xColorViewFooterRight">
|
||||
<view @click="clickItemColor(item)" class="xColorViewFooterRightItem"
|
||||
v-for="(item,index) in _colorList" :key="index" :style="{backgroundColor:item}"></view>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import colorRgbUvue from './color-rgb.uvue';
|
||||
import colorGridUvue from './color-grid.uvue';
|
||||
import colorHueUvue from './color-hue.uvue';
|
||||
import { RADIO_BUTTON } from '../../interface';
|
||||
import colorAlphaUvue from './color-alpha.uvue';
|
||||
import { hexToRgb, hslToRgb, hslaToRgbCss, rgbToHex, rgbToHsl, rgbToHexNoAlpha, isValidColor, getDefaultColor } from "../../core/util/xCoreColorUtil.uts";
|
||||
const i18n = xConfig.i18n;
|
||||
/**
|
||||
* @name 颜色选择 xColorView
|
||||
* @description 精致且方便用户操作移动颜色选择容器,为你的APP增彩,兼容PC端操作,如果要把组件嵌套在弹层内时,那么在展示的时候需要延迟显示本组件。
|
||||
* @page /pages/index/color-view
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xColorView" })
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前显示的颜色值,可以是合法的值,可以v-model双向绑定
|
||||
* 如:颜色名称,hex,rgb,rgba,hsl,hsla等值
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输出格式:hex,rgba,hsla
|
||||
* 如果输出hex,那么透明度将丢失
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: 'rgba'
|
||||
},
|
||||
/**
|
||||
* 默认显示的面板
|
||||
* hue:光谱,rgb:rgb拖动条,grid:网格面板
|
||||
*/
|
||||
panel: {
|
||||
type: String,
|
||||
default: 'hue'
|
||||
},
|
||||
/**
|
||||
* 是否展示透明度设置
|
||||
*/
|
||||
showAlpha:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
/**
|
||||
* 默认的底部颜色快速选择.
|
||||
*/
|
||||
colorList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => [] as string[]
|
||||
}
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 值变化时触发。
|
||||
* @param {string} hexstr - 当前的颜色值
|
||||
*/
|
||||
(e : 'change', hexstr : string) : void,
|
||||
/**
|
||||
* 当用户切换面板时触发
|
||||
* @param {string} type - 当前面板类型
|
||||
*/
|
||||
(e : 'panelChange', type : string) : void,
|
||||
/**
|
||||
* 值变化时触发。可以v-model绑定.
|
||||
* @param {string} hexstr - 当前的颜色值
|
||||
*/
|
||||
(e : 'update:modelValue', hexstr : string) : void,
|
||||
}>()
|
||||
const slots = defineSlots<{
|
||||
default : {
|
||||
rgba : string
|
||||
}
|
||||
}>()
|
||||
const menubarId = ref(props.panel as string)
|
||||
const menubar = ref<RADIO_BUTTON[]>([
|
||||
{ id: 'rgb', title: i18n.t('tmui4x.colorView.rgb') } as RADIO_BUTTON,
|
||||
{ id: 'hue', title: i18n.t('tmui4x.colorView.hub') } as RADIO_BUTTON,
|
||||
{ id: 'grid', title: i18n.t('tmui4x.colorView.grid') } as RADIO_BUTTON
|
||||
] as RADIO_BUTTON[])
|
||||
const colorStr = ref('')
|
||||
const realColorArrayRgb = ref<string[]>(['0', '0', '0'])
|
||||
const realColorHex = ref<string>('#000000')
|
||||
const aplpha = ref<number>(1)
|
||||
const isReadyShow = ref(false)
|
||||
let tid = 56
|
||||
const _colorList = computed(() : string[] => {
|
||||
let defaults = ['#0579FF', '#002FA7', '#FF0000', '#FF4F00', '#1034A6', '#6C3082', '#1256A7', '#009B3A', '#004225', '#E3A857', '#FFDF00'] as string[]
|
||||
if (props.colorList.length == 0) return defaults;
|
||||
return props.colorList
|
||||
})
|
||||
// @ts-ignore
|
||||
const _showAlpha = computed(():boolean=>props.showAlpha)
|
||||
const changeFormat = (val : string) : string => {
|
||||
if (val == '') {
|
||||
// @ts-ignore
|
||||
if (props.format == 'hex') return '#ffffff'
|
||||
// @ts-ignore
|
||||
if (props.format == 'rgba') return 'rgba(255,255,255,1)'
|
||||
return 'hsla(0,0%,100%,1)'
|
||||
}
|
||||
let rgba = hexToRgb(val);
|
||||
let r = rgba.getNumber('r')!
|
||||
let g = rgba.getNumber('g')!
|
||||
let b = rgba.getNumber('b')!
|
||||
let a = rgba.getNumber('a')!
|
||||
// @ts-ignore
|
||||
if (props.format == 'hex') return rgbToHexNoAlpha(rgba)
|
||||
// @ts-ignore
|
||||
if (props.format == 'rgba') return `rgba(${r},${g},${b},${a})`
|
||||
let hsl = rgbToHsl(rgba)
|
||||
let h = hsl.getNumber('h')!
|
||||
let s = hsl.getNumber('s')!
|
||||
let l = hsl.getNumber('l')!
|
||||
|
||||
return `hsla(${h},${s}%,${l}%)`
|
||||
}
|
||||
const realColorRgbaCss = computed(() : string => {
|
||||
return `rgba(${realColorArrayRgb.value[0]},${realColorArrayRgb.value[1]},${realColorArrayRgb.value[2]},${aplpha.value})`
|
||||
})
|
||||
const changeFormatToArr = (val : string) => {
|
||||
if (val == '' || !isValidColor(val)) {
|
||||
return
|
||||
}
|
||||
|
||||
let color = hexToRgb(val);
|
||||
let r = color.getNumber('r')!
|
||||
let g = color.getNumber('g')!
|
||||
let b = color.getNumber('b')!
|
||||
let a = color.getNumber('a')!
|
||||
aplpha.value = a
|
||||
realColorArrayRgb.value = [r.toString(), g.toString(), b.toString()] as string[]
|
||||
realColorHex.value = rgbToHexNoAlpha(color)
|
||||
colorStr.value = realColorHex.value
|
||||
}
|
||||
const toChanges = (hexstr : string, alphas : number) => {
|
||||
let rgba = hexToRgb(hexstr)
|
||||
let r = rgba.getNumber('r')!
|
||||
let g = rgba.getNumber('g')!
|
||||
let b = rgba.getNumber('b')!
|
||||
let a = alphas
|
||||
|
||||
colorStr.value = hexstr;
|
||||
let rgbastr = `rgba(${r},${g},${b},${a})`
|
||||
let tostring = changeFormat(rgbastr)
|
||||
emits('change', tostring)
|
||||
emits('update:modelValue', tostring)
|
||||
}
|
||||
const colorChange = (hexstr : string) => {
|
||||
if (colorStr.value == hexstr) return;
|
||||
|
||||
toChanges(hexstr, aplpha.value)
|
||||
}
|
||||
const panelChange = (panelId:string,panelIndex:number)=>{
|
||||
emits("panelChange",panelId)
|
||||
}
|
||||
const colorChangeAlpha = (hexAlpha : number) => {
|
||||
if (hexAlpha == aplpha.value) return;
|
||||
aplpha.value = hexAlpha;
|
||||
|
||||
toChanges(colorStr.value, hexAlpha)
|
||||
}
|
||||
|
||||
const clickItemColor = (selectedColor : string) => {
|
||||
let hexstr = getDefaultColor(selectedColor)
|
||||
changeFormatToArr(hexstr)
|
||||
toChanges(realColorHex.value, aplpha.value)
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
// @ts-ignore
|
||||
colorStr.value = getDefaultColor(props.modelValue)
|
||||
changeFormatToArr(colorStr.value)
|
||||
})
|
||||
// @ts-ignore
|
||||
watch(() : string => props.modelValue, (newval : string) => {
|
||||
if (colorStr.value == newval) return;
|
||||
changeFormatToArr(getDefaultColor(newval))
|
||||
})
|
||||
// @ts-ignore
|
||||
watch(() : string => props.panel, (newval : string) => {
|
||||
if (menubarId.value == newval) return;
|
||||
menubarId.value = newval;
|
||||
})
|
||||
onMounted(()=>{
|
||||
// #ifdef APP
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function() {
|
||||
isReadyShow.value = true;
|
||||
}, 150);
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
isReadyShow.value = true;
|
||||
// #endif
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
// #ifdef APP
|
||||
clearTimeout(tid)
|
||||
// #endif
|
||||
})
|
||||
defineExpose({
|
||||
/**
|
||||
* 获取当前颜色的透明度
|
||||
*/
|
||||
getAlpha():number{
|
||||
return aplpha.value
|
||||
},
|
||||
/**
|
||||
* 获取当前颜色,含alpha通道
|
||||
*/
|
||||
getColor():string{
|
||||
return colorStr.value
|
||||
},
|
||||
/**
|
||||
* 获取当前颜色,不含alpha通道
|
||||
*/
|
||||
getColorNoAlpha():string{
|
||||
return rgbToHexNoAlpha(hexToRgb(colorStr.value))
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.xColorViewFooterRight {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
.xColorViewFooter {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xColorViewLeftSHapers {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* margin-right: 20px; */
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #E1E4EE;
|
||||
}
|
||||
|
||||
.xColorViewLeftSHapersLeft {
|
||||
width: 31px;
|
||||
height: 62px;
|
||||
border-radius:32px 0 0 32px;
|
||||
}
|
||||
|
||||
.xColorViewLeftSHapersRight {
|
||||
width: 31px;
|
||||
height: 62px;
|
||||
border-radius:0 32px 32px 0;
|
||||
}
|
||||
|
||||
.xColorViewFooterRightItem {
|
||||
border-radius: 32px;
|
||||
margin-left: 16px;
|
||||
margin-top: 8px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border: 1px solid #E1E4EE;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,298 @@
|
||||
<script lang="ts" setup>
|
||||
import { type PropType, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type TIME_OBJ = {
|
||||
ms : string,
|
||||
ss : string,
|
||||
mm : string,
|
||||
hh : string,
|
||||
dd : string,
|
||||
format : string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 倒计时 xCountdown
|
||||
* @description 倒计时,可以精确到秒,毫秒,记住
|
||||
* @page /pages/index/countdown
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xCountdown"})
|
||||
defineSlots<{
|
||||
default(props: {
|
||||
status: "initial" | "running" | "paused" | "finished",
|
||||
time: number,
|
||||
label: string,
|
||||
ms: string,
|
||||
ss: string,
|
||||
mm: string,
|
||||
hh: string,
|
||||
dd: string,
|
||||
}): any
|
||||
}>()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 时间变化时触发
|
||||
* @param {number} time - 当前剩余的时间
|
||||
*/
|
||||
"change",
|
||||
/**
|
||||
* 暂停时触发
|
||||
*/
|
||||
'pause',
|
||||
/**
|
||||
* 开始时触发
|
||||
*/
|
||||
'start',
|
||||
/**
|
||||
* 结束时触发
|
||||
*/
|
||||
'complete'
|
||||
])
|
||||
|
||||
type xCountdownPropsType = {
|
||||
/** */
|
||||
time: number,
|
||||
/**
|
||||
* 指令,可以通过变动此值来达到暂停,开始,结束的功能,当然也可以通过ref方法控制。"pause" | "play" | "reset" | ""
|
||||
*/
|
||||
actions: "pause" | "play" | "reset" | "",
|
||||
/**
|
||||
* 显示格式
|
||||
* DD天,HH时,MM分,SS秒,MS毫秒
|
||||
*/
|
||||
format: string,
|
||||
autoStart: boolean,
|
||||
/**
|
||||
* 以秒还是毫秒为单位到计时。
|
||||
* ss|ms
|
||||
*/
|
||||
unit: "ss" | "ms",
|
||||
/** 文本大小 */
|
||||
fontSize: string,
|
||||
/** 文本颜色 */
|
||||
color: string,
|
||||
/**
|
||||
* 是否使用验证码模式
|
||||
* 统一倒计时实例
|
||||
*/
|
||||
captcha: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xCountdownPropsType>(), {
|
||||
time: 0,
|
||||
actions: "",
|
||||
format: "DD天HH时MM分SS秒",
|
||||
autoStart: false,
|
||||
unit: "ss",
|
||||
fontSize: "16",
|
||||
color: "#333333",
|
||||
captcha: false
|
||||
})
|
||||
|
||||
// 状态
|
||||
const intervalId = ref<number>(0)
|
||||
const status = ref<"initial" | "running" | "paused" | "finished">("initial")
|
||||
const totalTime = ref<number>(0)
|
||||
function displayTime(): TIME_OBJ {
|
||||
let milliseconds = totalTime.value;
|
||||
let seconds = Math.floor(milliseconds / 1000);
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
milliseconds %= 1000;
|
||||
seconds %= 60;
|
||||
minutes %= 60;
|
||||
hours %= 24;
|
||||
let day_str = days < 10 ? "0" + days.toString() : days.toString()
|
||||
let hours_str = hours < 10 ? "0" + hours.toString() : hours.toString()
|
||||
let minutes_str = minutes < 10 ? "0" + minutes.toString() : minutes.toString()
|
||||
let seconds_str = seconds < 10 ? "0" + seconds.toString() : seconds.toString()
|
||||
let milliseconds_str = milliseconds < 10 ? "0" + milliseconds.toString() : milliseconds.toString()
|
||||
let formattedTime = props.format.replace("DD", day_str)
|
||||
formattedTime = formattedTime.replace("HH", hours_str)
|
||||
formattedTime = formattedTime.replace("MM", minutes_str)
|
||||
formattedTime = formattedTime.replace("SS", seconds_str)
|
||||
formattedTime = formattedTime.replace("MS", milliseconds_str)
|
||||
|
||||
return {
|
||||
ms: milliseconds_str,
|
||||
ss: seconds_str,
|
||||
mm: minutes_str,
|
||||
hh: hours_str,
|
||||
dd: day_str,
|
||||
format: formattedTime
|
||||
} as TIME_OBJ;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const _label = computed((): TIME_OBJ => {
|
||||
return displayTime();
|
||||
})
|
||||
const _time = computed((): number => props.time)
|
||||
const _color = computed((): string => getDefaultColor(props.color))
|
||||
const _fontSize = computed((): string => checkIsCssUnit(props.fontSize, xConfig.unit))
|
||||
|
||||
|
||||
// 工具函数与业务函数(被调用者在前)
|
||||
function setCactcha(): void {
|
||||
if (!props.captcha) return;
|
||||
uni.setStorageSync("timeidLasttime", totalTime.value)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 开始
|
||||
*/
|
||||
function start(): void {
|
||||
let intms = props.unit == "ss" ? 1000 : 16
|
||||
if (status.value === "initial" || status.value === "paused") {
|
||||
status.value = "running";
|
||||
/** 开始时触发 */
|
||||
emits("start")
|
||||
setCactcha()
|
||||
intervalId.value = setInterval(() => {
|
||||
totalTime.value -= intms;
|
||||
if (props.captcha) {
|
||||
uni.setStorageSync("timeidLasttime", totalTime.value)
|
||||
}
|
||||
/**
|
||||
* 时间变化时触发
|
||||
* @param time {number} 当前剩余的时间
|
||||
*/
|
||||
emits("change", totalTime.value)
|
||||
if (totalTime.value < 0) {
|
||||
totalTime.value = 0
|
||||
if (props.captcha) {
|
||||
uni.setStorageSync("timeidLasttime", totalTime.value)
|
||||
}
|
||||
clearInterval(intervalId.value);
|
||||
status.value = "finished"; // 倒计时结束
|
||||
/** 结束时触发 */
|
||||
emits("complete")
|
||||
return;
|
||||
}
|
||||
}, intms);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
function pause(): void {
|
||||
if (status.value === "running") {
|
||||
clearInterval(intervalId.value);
|
||||
status.value = "paused"; // 暂停倒计时
|
||||
setCactcha()
|
||||
/** 暂停时触发 */
|
||||
emits("pause")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function reset(): void {
|
||||
clearInterval(intervalId.value);
|
||||
status.value = "initial"; // 重置倒计时为未开始状态
|
||||
totalTime.value = _time.value
|
||||
if (props.captcha) {
|
||||
uni.setStorageSync("timeidLasttime", 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行状态
|
||||
*/
|
||||
function getStatus(): "initial" | "running" | "paused" | "finished" {
|
||||
return status.value
|
||||
}
|
||||
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
totalTime.value = _time.value;
|
||||
if (props.captcha) {
|
||||
const oldTime = uni.getStorageSync("timeidLasttime")
|
||||
if (oldTime != null && typeof oldTime == 'number') {
|
||||
if (oldTime > 0) {
|
||||
totalTime.value = oldTime;
|
||||
start();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.autoStart) {
|
||||
start();
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(intervalId.value);
|
||||
if (status.value == 'running' || status.value == 'paused') {
|
||||
setCactcha()
|
||||
}
|
||||
})
|
||||
|
||||
watch((): number => props.time, (): void => {
|
||||
reset();
|
||||
})
|
||||
watch((): any => props.actions, (act:"pause" | "play" | "reset" | ""): void => {
|
||||
if (act == 'reset') {
|
||||
reset();
|
||||
} else if (act == 'play') {
|
||||
start();
|
||||
} else if (act == 'pause') {
|
||||
pause();
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露可选方法
|
||||
defineExpose({
|
||||
/**
|
||||
* 开始
|
||||
*/
|
||||
start,
|
||||
/**
|
||||
* 暂停
|
||||
*/
|
||||
pause,
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
reset,
|
||||
/**
|
||||
* 获取当前运行状态,返回值是:"initial" | "running" | "paused" | "finished"
|
||||
*/
|
||||
getStatus
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<!--
|
||||
@slot 插槽
|
||||
@prop {string} status - 状态值可能为:"initial" | "running" | "paused" | "finished"
|
||||
@prop {number} time - 当前剩余的时间:单位为毫秒
|
||||
@prop {string} label - 当前被属性format格式化后的文本
|
||||
@prop {string} ms - 剩余的毫秒数
|
||||
@prop {string} ss - 剩余的秒数
|
||||
@prop {string} mm - 剩余的分钟
|
||||
@prop {string} hh - 剩余的小时
|
||||
@prop {string} dd - 剩余的天数
|
||||
-->
|
||||
<slot :status="status" :time="totalTime" :label="_label.format" :ms="_label.ms" :ss="_label.ss" :mm="_label.mm"
|
||||
:hh="_label.hh" :dd="_label.dd">
|
||||
<text :style="{color:_color,fontSize:_fontSize}">{{_label.format}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,507 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, setPagePullRefresh, getPagePullRefresh } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xDate, xDateTypeTime, createDate } from "../../core/util/xDate.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { PICKER_ITEM_INFO } from "../../interface.uts"
|
||||
type coverValue = {
|
||||
value : string[][],
|
||||
str : string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
/**
|
||||
* @name 嵌入式日期选择器 xDateView
|
||||
* @description 内嵌日期选择,可以控制显示精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
|
||||
* 使用时,建议不要显示过多年份以防卡太多数据。
|
||||
* @page /pages/index/picker-date
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
let startValue = new xDate();
|
||||
let endValue = new xDate();
|
||||
startValue.subtraction(1, 'y')
|
||||
return {
|
||||
nowValue: [] as string[][],
|
||||
nowValueStr: '',
|
||||
startDate: startValue,
|
||||
endDate: endValue,
|
||||
dateList: [] as PICKER_ITEM_INFO[][],
|
||||
changeIndex: 0,
|
||||
nowPull: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
],
|
||||
props: {
|
||||
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。可以提供以下合法格式:
|
||||
* YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "请选择时间"
|
||||
},
|
||||
/**
|
||||
* 开始时间,请提供正确的时间格式
|
||||
*/
|
||||
start: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 结束时间,请提供正确的时间格式
|
||||
*/
|
||||
end: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 精确到的级别
|
||||
* year:年
|
||||
* month:年月
|
||||
* day:年月日
|
||||
* hour:年月日小时
|
||||
* minute:年月日小时分钟
|
||||
* second:年月日小时分钟秒
|
||||
*/
|
||||
type: {
|
||||
type: String as PropType<ModelType>,
|
||||
default: "day"
|
||||
},
|
||||
/**
|
||||
* 输出时间格式,只对v-model:modelStr有效
|
||||
* 如果桢同步对vmodel:modelValue有效需要设置formatSyncValue为true
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD"
|
||||
},
|
||||
/**
|
||||
* 是否将format格式化的v-model:modelStr同步到v-model:modelValue
|
||||
* 默认false,注意:如果开启了同步,你要确保format的值是正常的时间值
|
||||
* 正常兼容以下时间格式:
|
||||
* YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
formatSyncValue:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称,'年', '月', '日', '时', '分', '秒'
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => [] as string[]
|
||||
},
|
||||
|
||||
},
|
||||
computed: {
|
||||
_cellUnits():string[]{
|
||||
if(this.cellUnits.length==0){
|
||||
|
||||
return [
|
||||
this!.i18n.t("tmui4x.pickerDate.year"),
|
||||
this!.i18n.t("tmui4x.pickerDate.month"),
|
||||
this!.i18n.t("tmui4x.pickerDate.day"),
|
||||
this!.i18n.t("tmui4x.pickerDate.hour"),
|
||||
this!.i18n.t("tmui4x.pickerDate.minute"),
|
||||
this!.i18n.t("tmui4x.pickerDate.second"),
|
||||
]
|
||||
}
|
||||
return this.cellUnits;
|
||||
},
|
||||
_start_date() : xDate {
|
||||
if (this.start == "") return this.startDate
|
||||
|
||||
return new xDate(this.start)
|
||||
},
|
||||
_end_date() : xDate {
|
||||
if (this.end == "") return this.endDate
|
||||
return new xDate(this.end)
|
||||
},
|
||||
_getDateType():xDateTypeTime{
|
||||
let isType = 's' as xDateTypeTime
|
||||
if (this.type == 'year') isType = 'y'
|
||||
if (this.type == 'month') isType = 'm'
|
||||
if (this.type == 'day') isType = 'd'
|
||||
if (this.type == 'hour') isType = 'h'
|
||||
if (this.type == 'minute') isType = 'M'
|
||||
return isType;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(newvalue : string) {
|
||||
if (newvalue == '') return;
|
||||
let isType = this._getDateType
|
||||
if (new xDate(newvalue).isBetweenOf(new xDate(this.nowValueStr), '=', isType)) return;
|
||||
this.defaultModelvalue(newvalue,true)
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.nowPull = getPagePullRefresh()
|
||||
let nowValue = new xDate(this.modelValue)
|
||||
this.defaultModelvalue(nowValue.format('YYYY-MM-DD'), this.modelValue != '')
|
||||
|
||||
},
|
||||
methods: {
|
||||
defaultModelvalue(newvalue:string, showStr : boolean){
|
||||
let isType = this._getDateType
|
||||
let nowValue = new xDate(newvalue)
|
||||
if(nowValue.isBetweenOf(this._start_date, '<=', isType)){
|
||||
nowValue = this._start_date;
|
||||
}
|
||||
if(nowValue.isBetweenOf(this._end_date, '>=', isType)){
|
||||
nowValue = this._end_date;
|
||||
}
|
||||
|
||||
let stp = this.getRangByDateTime(nowValue)
|
||||
|
||||
this.nowValue = stp.value;
|
||||
this.nowValueStr = stp.str;
|
||||
this.dateList = this.getTimeTreeByStartAndEnd(this._start_date, this._end_date)
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
this.$emit('update:modelStr', this.formatTimeDate());
|
||||
},
|
||||
getRangByDateTime(d : xDate) : coverValue {
|
||||
let nowRange = [
|
||||
[d.getYear().toString()],
|
||||
[(d.getMonth()).toString()],
|
||||
[d.getDate().toString()],
|
||||
[d.getHours().toString()],
|
||||
[d.getMinutes().toString()],
|
||||
[d.getSeconds().toString()],
|
||||
] as string[][]
|
||||
let nowRangeStr = d.getYear().toString() + "-" + (d.getMonth() + 1).toString()
|
||||
+ "-" + d.getDate().toString()
|
||||
+ " " + d.getHours().toString() + ":"
|
||||
+ d.getMinutes().toString() + ":"
|
||||
+ d.getSeconds().toString()
|
||||
return {
|
||||
value: nowRange as string[][],
|
||||
str: nowRangeStr
|
||||
} as coverValue
|
||||
},
|
||||
|
||||
getNowTypeLenIndex() : number {
|
||||
let index = 6
|
||||
if (this.type == 'year') {
|
||||
index = 1;
|
||||
} else if (this.type == 'month') {
|
||||
index = 2;
|
||||
} else if (this.type == 'day') {
|
||||
index = 3;
|
||||
} else if (this.type == 'hour') {
|
||||
index = 4;
|
||||
} else if (this.type == 'minute') {
|
||||
index = 5;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
indexToHex(i : number) : string {
|
||||
let n = i.toString()
|
||||
if (n.length == 1) return '0' + n;
|
||||
return n;
|
||||
},
|
||||
getTimeTreeByStartAndEnd(start : xDate, end : xDate) : PICKER_ITEM_INFO[][] {
|
||||
let startCopy = start.getClone();
|
||||
let nowDate = new xDate(this.nowValueStr)
|
||||
let endCopy = end.getClone();
|
||||
let years = [] as PICKER_ITEM_INFO[];
|
||||
let months = [] as PICKER_ITEM_INFO[];
|
||||
let days = [] as PICKER_ITEM_INFO[];
|
||||
let hours = [] as PICKER_ITEM_INFO[];
|
||||
let minutes = [] as PICKER_ITEM_INFO[];
|
||||
let seconds = [] as PICKER_ITEM_INFO[];
|
||||
|
||||
for (let i = startCopy.getYear(); i <= endCopy.getYear(); i++) {
|
||||
years.push({
|
||||
id: i.toString(),
|
||||
title: this.indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
let _this = this;
|
||||
function getD(type : string, s : number, n : number) {
|
||||
if (type == 'm') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
months.push({
|
||||
id: i.toString(),
|
||||
title: _this.indexToHex(i + 1)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'd') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
days.push({
|
||||
id: i.toString(),
|
||||
title: _this.indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'h') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
hours.push({
|
||||
id: i.toString(),
|
||||
title: _this.indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'M') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
minutes.push({
|
||||
id: i.toString(),
|
||||
title: _this.indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 's') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
seconds.push({
|
||||
id: i.toString(),
|
||||
title: _this.indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getDnumber(type : xDateTypeTime, target : xDateTypeTime) : number[] {
|
||||
let st = 0;
|
||||
let et = 0
|
||||
|
||||
// 不包含起始, 开始和结束内。
|
||||
if (nowDate.isBetween(startCopy, endCopy, type, '()')) {
|
||||
if (target == 'm') {
|
||||
st = 0
|
||||
et = 11
|
||||
} else if (target == 'd') {
|
||||
st = 1
|
||||
et = nowDate.getMonthCountDay()
|
||||
} else if (target == 'h') {
|
||||
st = 0
|
||||
et = 23
|
||||
} else if (target == 'M' || target == 's') {
|
||||
st = 0
|
||||
et = 59
|
||||
}
|
||||
|
||||
//
|
||||
} else {
|
||||
|
||||
// 开始和结束相等
|
||||
if (startCopy.isBetweenOf(endCopy, '=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
st = startCopy.getMonth()
|
||||
et = endCopy.getMonth()
|
||||
} else if (target == 'd') {
|
||||
st = startCopy.getDate()
|
||||
et = endCopy.getDate()
|
||||
} else if (target == 'h') {
|
||||
st = startCopy.getHours()
|
||||
et = endCopy.getHours()
|
||||
} else if (target == 'M') {
|
||||
st = startCopy.getMinutes()
|
||||
et = endCopy.getMinutes()
|
||||
} else if (target == 's') {
|
||||
st = startCopy.getSeconds()
|
||||
et = endCopy.getSeconds()
|
||||
}
|
||||
|
||||
} else if (nowDate.isBetweenOf(startCopy, '<=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
st = startCopy.getMonth()
|
||||
et = 11
|
||||
} else if (target == 'd') {
|
||||
st = startCopy.getDate()
|
||||
et = startCopy.getMonthCountDay()
|
||||
} else if (target == 'h') {
|
||||
st = startCopy.getHours()
|
||||
et = 23
|
||||
} else if (target == 'M') {
|
||||
st = startCopy.getMinutes()
|
||||
et = 59
|
||||
} else if (target == 's') {
|
||||
st = startCopy.getSeconds()
|
||||
et = 59
|
||||
}
|
||||
} else if (nowDate.isBetweenOf(endCopy, '>=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
|
||||
st = 0
|
||||
et = endCopy.getMonth()
|
||||
} else if (target == 'd') {
|
||||
st = 1
|
||||
et = endCopy.getDate()
|
||||
|
||||
} else if (target == 'h') {
|
||||
st = 0
|
||||
et = endCopy.getHours()
|
||||
} else if (target == 'M') {
|
||||
st = 0
|
||||
et = endCopy.getMinutes()
|
||||
} else if (target == 's') {
|
||||
st = 0
|
||||
et = endCopy.getSeconds()
|
||||
}
|
||||
}
|
||||
}
|
||||
return [st, et] as number[]
|
||||
}
|
||||
let maxlen = this.getNowTypeLenIndex();
|
||||
|
||||
if (maxlen > 1) {
|
||||
let sdate = getDnumber('y', 'm')
|
||||
getD('m', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 2) {
|
||||
let sdate = getDnumber('m', 'd')
|
||||
|
||||
getD('d', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 3) {
|
||||
let sdate = getDnumber('d', 'h')
|
||||
getD('h', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 4) {
|
||||
let sdate = getDnumber('h', 'M')
|
||||
getD('M', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 5) {
|
||||
let sdate = getDnumber('m', 's')
|
||||
getD('s', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
|
||||
|
||||
return [years, months, days, hours, minutes, seconds].slice(0, this.getNowTypeLenIndex()) as PICKER_ITEM_INFO[][];
|
||||
},
|
||||
getRangNumber(start : number, end : number) : string[] {
|
||||
let iar = [] as string[];
|
||||
for (let i = start; i <= end; i++) {
|
||||
iar.push(i.toString());
|
||||
}
|
||||
return iar;
|
||||
},
|
||||
stringArValuCoverToString() : string {
|
||||
if (this.nowValue.length != 6) return "";
|
||||
|
||||
let newsday = new xDate(this.nowValue[0][0] + "-" + (parseInt(this.nowValue[1][0]) + 1).toString() + "-1")
|
||||
let days = parseInt(this.nowValue[2][0])
|
||||
days = days >= newsday.getMonthCountDay() ? newsday.getMonthCountDay() : days
|
||||
this.nowValue.splice(2, 1, [days.toString()])
|
||||
|
||||
return this.fillNumber(this.nowValue[0][0]) + "-" + this.fillNumber((parseInt(this.nowValue[1][0]) + 1).toString())
|
||||
+ "-" + this.fillNumber(this.nowValue[2][0])
|
||||
+ " " + this.fillNumber(this.nowValue[3][0]) + ":"
|
||||
+ this.fillNumber(this.nowValue[4][0]) + ":"
|
||||
+ this.fillNumber(this.nowValue[5][0])
|
||||
},
|
||||
fillNumber(n : string) : string {
|
||||
if (parseInt(n) > 9) return n;
|
||||
return "0" + n
|
||||
},
|
||||
formatTimeDate() : string {
|
||||
if (this.nowValue.length != 6) return "";
|
||||
let sp = this.format;
|
||||
sp = sp.replace(/YYYY/g, this.fillNumber(this.nowValue[0][0]))
|
||||
sp = sp.replace(/MM/g, this.fillNumber((parseInt(this.nowValue[1][0]) + 1).toString()))
|
||||
sp = sp.replace(/DD/g, this.fillNumber(this.nowValue[2][0]))
|
||||
sp = sp.replace(/hh/g, this.fillNumber(this.nowValue[3][0]))
|
||||
sp = sp.replace(/mm/g, this.fillNumber(this.nowValue[4][0]))
|
||||
sp = sp.replace(/ss/g, this.fillNumber(this.nowValue[5][0]))
|
||||
return sp;
|
||||
},
|
||||
|
||||
|
||||
mchange(ids : string[], index : number) {
|
||||
|
||||
this.nowValue.splice(index, 1, ids)
|
||||
this.nowValueStr = this.stringArValuCoverToString()
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date 当前选中时间
|
||||
*/
|
||||
this.$emit('change', this.nowValueStr)
|
||||
|
||||
this.dateList = this.getTimeTreeByStartAndEnd(this._start_date, this._end_date)
|
||||
|
||||
this.$forceUpdate()
|
||||
this.onConfirm()
|
||||
},
|
||||
|
||||
onTouchstart() {
|
||||
setPagePullRefresh(false)
|
||||
},
|
||||
onTouchend() {
|
||||
setPagePullRefresh(this.nowPull)
|
||||
},
|
||||
onConfirm() {
|
||||
const syncValue:string = this.formatSyncValue?this.formatTimeDate():(toRaw(this.nowValueStr) as string)
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
this.$emit('update:modelValue', syncValue);
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
this.$emit('update:modelStr', this.formatTimeDate());
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xPickerDateWrap" @touchstart="onTouchstart" @touchend="onTouchend" @touchcancel="onTouchend">
|
||||
<x-picker-view :cellUnits="[_cellUnits[index]]" @change="mchange($event as string[],index)"
|
||||
:model-value="nowValue[index]" v-for="(item,index) in dateList" :key="index" style="flex: 1;"
|
||||
:list="item"></x-picker-view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xPickerDateWrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit,getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xRequestCall } from "../../core/util/config.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xTableColumns,xRequestHistoryType } from "../..//interface.uts"
|
||||
|
||||
/**
|
||||
* @name 开发组件 xDevtool
|
||||
* @page /pages/index/xrequest
|
||||
* @category 展示组件
|
||||
* @description 需要配合xRequest库,并打开dev模式,才可记录和使用。开发者可以将此组件预埋在某个页面,通过后台请求是否打开 和显示开发模式。这样在app发布后,通过远程调试也可打开请求记录等一些实质的数据。
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
title: '接口',
|
||||
key: 'api',
|
||||
width: "30%"
|
||||
} as xTableColumns,
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: "20%"
|
||||
} as xTableColumns,
|
||||
{
|
||||
title: '时间',
|
||||
key: 'time',
|
||||
width: "30%",
|
||||
desc: true
|
||||
} as xTableColumns,
|
||||
{
|
||||
title: '结果',
|
||||
key: 'result',
|
||||
width: '100%'
|
||||
} as xTableColumns
|
||||
] as xTableColumns[],
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hostUrl() : string {
|
||||
return xRequestCall.hostUrl
|
||||
},
|
||||
list():UTSJSONObject[]{
|
||||
let ls = xRequestCall.history.map((el:xRequestHistoryType):UTSJSONObject=>{
|
||||
return {
|
||||
api:el.api,
|
||||
status:el.status,
|
||||
time:el.time,
|
||||
result:el.result
|
||||
} as UTSJSONObject
|
||||
})
|
||||
|
||||
return ls
|
||||
},
|
||||
_headBgColor():string{
|
||||
return xConfig.dark=='dark'?'#333':'#eee'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<x-float-drawer size="20%" :only-header="true" :disabledScroll="true">
|
||||
<template v-slot:default="{show,height}">
|
||||
<view class="xDevHeader" :style="{backgroundColor:_headBgColor}">
|
||||
<x-text font-size="12" class="xDevHeaderLeft">请求域名</x-text>
|
||||
<view class="xDevHeaderRightWrap">
|
||||
<x-text font-size="12" class="xDevHeaderRight">{{hostUrl}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
<x-divider></x-divider>
|
||||
<view style="flex:1">
|
||||
<x-table :columns="columns" cell-height="100rpx" :list="list" height="100%" max-height="800rpx"></x-table>
|
||||
</view>
|
||||
</template>
|
||||
</x-float-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xDevHeader {
|
||||
padding: 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.xDevHeaderRightWrap{
|
||||
flex:1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.xDevHeaderLeft {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.xDevHeaderRight {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 分割线 xDivider
|
||||
* @description 横和竖向,内容左,中,右。
|
||||
* @page /pages/index/divider
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xDivider"})
|
||||
|
||||
type alignType = "left" | "right" | "center"
|
||||
type modelType = "solid" | "dotted"
|
||||
|
||||
type xDividerPropsType = {
|
||||
/**
|
||||
* 对齐方式
|
||||
*/
|
||||
align: alignType,
|
||||
/**
|
||||
* 文本
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* 线的颜色
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 线的暗黑颜色,如果不提供取全局的borderDarkColor
|
||||
*/
|
||||
darkColor: string,
|
||||
/**
|
||||
* 线粗细度。
|
||||
*/
|
||||
lineWidth: string,
|
||||
/**
|
||||
* 竖向时的高度
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 文本颜色
|
||||
*/
|
||||
labelColor: string,
|
||||
/**
|
||||
* 线条样式
|
||||
*/
|
||||
model: modelType,
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 是否是竖向
|
||||
*/
|
||||
vertical: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xDividerPropsType>(), {
|
||||
align: "center",
|
||||
label: "",
|
||||
color: "#e5e5e5",
|
||||
darkColor: "",
|
||||
lineWidth: "1",
|
||||
height: "10",
|
||||
labelColor: "#a2a2a2",
|
||||
model: "solid",
|
||||
fontSize: "11",
|
||||
vertical: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const id = ref("xDivider" + Date.now())
|
||||
|
||||
// 计算属性
|
||||
const _label = computed((): string => props.label)
|
||||
const _lineWidth = computed((): string => checkIsCssUnit(props.lineWidth, xConfig.unit))
|
||||
const _fontSize = computed((): string => checkIsCssUnit(props.fontSize, xConfig.unit))
|
||||
const _height = computed((): string => checkIsCssUnit(props.height, xConfig.unit))
|
||||
const _color = computed((): string => {
|
||||
if(xConfig.dark == 'dark'){
|
||||
if(props.darkColor != '') return getDefaultColor(props.darkColor)
|
||||
return xConfig.borderDarkColor
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _model = computed((): string => props.model)
|
||||
const _labelColor = computed((): string => getDefaultColor(props.labelColor))
|
||||
const _vertical = computed((): boolean => props.vertical)
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="xDivider"
|
||||
:style="{height:_vertical?_height:'auto','border-left':_vertical?`${_lineWidth} ${_model} ${_color}`:'none'}">
|
||||
<view v-if="!_vertical" class="xDividerLeft"
|
||||
:style="{flex:align=='left'?1:6,'border-bottom':`${_lineWidth} ${_model} ${_color}`}"></view>
|
||||
<!--
|
||||
@slot 默认文本插槽。建议通过属性label填写,如果你有特殊要求可以
|
||||
在插槽中自定义样式和布局。
|
||||
-->
|
||||
<slot>
|
||||
<text v-if="_label!=''&&!_vertical" class="xDividerText"
|
||||
:style="{color:_labelColor,fontSize:_fontSize}">{{_label}}</text>
|
||||
</slot>
|
||||
<view v-if="!_vertical" class="xDividerRight"
|
||||
:style="{flex:align=='right'?1:6,'border-bottom':`${_lineWidth} ${_model} ${_color}`}"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xDivider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.xDividerText {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.xDividerLeft {
|
||||
flex: 6;
|
||||
}
|
||||
|
||||
.xDividerRight {
|
||||
flex: 6;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<script lang="ts">
|
||||
import { type PropType, Ref } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHILDREN_INFO,CHILDREN_SIZE } from "../x-drag/interface.uts"
|
||||
|
||||
/**
|
||||
* @name 拖拽排序子组件 xDragItem
|
||||
* @description 仅可放置在父容器x-drag中,如果在组件上写style时,不可写left,top,width,height等属性来影响组件的高和宽。
|
||||
* 也不可直接写padding,margin来影响组件的位置。你可以在组件中自己写view后,再自由的布局写间隙等。
|
||||
* @page /pages/index/drag
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: ('xDragItem-' + getUid()) as string,
|
||||
cellHeight: 0,
|
||||
cellWidth: 0,
|
||||
nowActiveIndex: -1,
|
||||
targetActiveIndex: -1,
|
||||
orderIndex: -1,
|
||||
nowId: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 索引,vfor时提供index,必填
|
||||
* 而且一定是正确的索引列表顺序,不可随便填写。
|
||||
*/
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 是禁用本项目被拖动,禁用时本项目顺序会被固定。不会被打乱。
|
||||
*/
|
||||
disabled:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 点击时触发
|
||||
*/
|
||||
"click"
|
||||
],
|
||||
mounted() {
|
||||
this.orderIndex = this.order
|
||||
let _this = this;
|
||||
uni.$on("onResize",this.getNodes)
|
||||
// #ifdef APP-ANDROID||MP||WEB
|
||||
this.getNodes()
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
setTimeout(function() {
|
||||
_this.getNodes()
|
||||
}, 20);
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
setTimeout(function() {
|
||||
_this.getNodes()
|
||||
}, 60);
|
||||
// #endif
|
||||
},
|
||||
inject: {
|
||||
XDRAGE_HEIGHT: {
|
||||
type: String,
|
||||
default: '0px'
|
||||
},
|
||||
XDRAGE_COL: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
XDRAGE_MAX_LEN: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
_height() : string {
|
||||
return this.XDRAGE_HEIGHT
|
||||
},
|
||||
_width() : string {
|
||||
return (100 / this.XDRAGE_COL).toString() + '%'
|
||||
},
|
||||
_defaultTop() : number {
|
||||
let rowIndex = Math.floor(this.orderIndex / this.XDRAGE_COL);
|
||||
return this.cellHeight * rowIndex
|
||||
},
|
||||
_defaultLeft() : number {
|
||||
let colindex = this.orderIndex % this.XDRAGE_COL;
|
||||
return this.cellWidth * colindex
|
||||
},
|
||||
_disabled() : boolean {
|
||||
return this.disabled
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
watch:{
|
||||
disabled(){
|
||||
this.getNodes()
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// @ts-ignore
|
||||
let parent : XDragComponentPublicInstance | null = null;
|
||||
try {
|
||||
// @ts-ignore
|
||||
parent = this.$parent as XDragComponentPublicInstance
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
if (parent != null) {
|
||||
parent!.delItem(this.id)
|
||||
}
|
||||
uni.$off("onResize",this.getNodes)
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit("click")
|
||||
console.log(8)
|
||||
},
|
||||
|
||||
|
||||
getNodes() {
|
||||
let ele = this.$refs['xDragItem'] as UniElement|null;
|
||||
if(ele == null) return;
|
||||
ele.getBoundingClientRectAsync()
|
||||
?.then(rect=>{
|
||||
this.cellHeight = rect.height!
|
||||
this.cellWidth = rect.width!
|
||||
this.pushChildren(rect)
|
||||
this.setStylSetProperty('height',this._height)
|
||||
this.setStylSetProperty('width',this._width)
|
||||
this.setStylSetProperty('top',this._defaultTop+'px')
|
||||
this.setStylSetProperty('left',this._defaultLeft+'px')
|
||||
this.setStylSetProperty('z-index','1')
|
||||
this.setStylSetProperty('transition-duration','0s')
|
||||
})
|
||||
.catch(()=>{})
|
||||
|
||||
},
|
||||
|
||||
pushChildren(node : DOMRect) {
|
||||
// @ts-ignore
|
||||
let parent : XDragComponentPublicInstance | null = null;
|
||||
try {
|
||||
// @ts-ignore
|
||||
parent = this.$parent as XDragComponentPublicInstance
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
if (parent != null) {
|
||||
parent!.addItem({
|
||||
id: this.id,
|
||||
index: this.order,
|
||||
oldindex: this.order,
|
||||
ele: this,
|
||||
disabled:this._disabled,
|
||||
node: node
|
||||
} as CHILDREN_INFO)
|
||||
}
|
||||
},
|
||||
|
||||
updateForce() {
|
||||
this.$forceUpdate()
|
||||
},
|
||||
setOrderIndex(index : number) {
|
||||
this.orderIndex = index;
|
||||
},
|
||||
setActivdId(id : string) {
|
||||
this.nowId = id;
|
||||
},
|
||||
setStylSetProperty(name:string,value:any|null){
|
||||
let ele = this.$refs['xDragItem'] as UniElement
|
||||
ele.style.setProperty(name,value)
|
||||
},
|
||||
getStylSetProperty(name:string): any | null{
|
||||
let ele = this.$refs['xDragItem'] as UniElement
|
||||
return ele.style.getPropertyValue(name);
|
||||
},
|
||||
updatePos(){
|
||||
this.setStylSetProperty('height',this._height)
|
||||
this.setStylSetProperty('width',this._width)
|
||||
this.setStylSetProperty('top',this._defaultTop+'px')
|
||||
this.setStylSetProperty('left',this._defaultLeft+'px')
|
||||
this.setStylSetProperty('transition-duration','0.4s')
|
||||
this.setStylSetProperty('z-index','1')
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view
|
||||
|
||||
<!-- #ifdef APP||MP -->
|
||||
@click="onClick"
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mouseup="onClick"
|
||||
<!-- #endif -->
|
||||
|
||||
ref="xDragItem"
|
||||
class="xDragItem"
|
||||
:style="{
|
||||
height:_height,
|
||||
width:_width,
|
||||
top:_defaultTop+'px',
|
||||
left:_defaultLeft+'px',
|
||||
zIndex:nowId==id&&nowId!=''?'5':'1',
|
||||
transitionDuration:nowId!=id&&nowId!=''?'0.4s':'0s',
|
||||
}">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot :order="orderIndex"></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xDragItem {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
transition-timing-function: ease;
|
||||
transition-property: top, left;
|
||||
/* #ifdef WEB */
|
||||
cursor: grab;
|
||||
/* #endif */
|
||||
z-index: 1;
|
||||
}
|
||||
/* #ifdef WEB */
|
||||
.xDragItem:active{
|
||||
cursor: grabbing;
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
export type CHILDREN_INFO = {
|
||||
id:string,
|
||||
index:number,
|
||||
oldindex:number,
|
||||
node:DOMRect,
|
||||
disabled:boolean,
|
||||
ele:XDragItemComponentPublicInstance
|
||||
}
|
||||
export type CHILDREN_SIZE = {
|
||||
width:number,
|
||||
height:number,
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
<script lang="ts">
|
||||
import { type PropType, ref, computed } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { CHILDREN_INFO, CHILDREN_SIZE } from "../x-drag/interface.uts"
|
||||
import { vibrator } from "@/uni_modules/x-vibrate-s"
|
||||
|
||||
|
||||
type POSITION = {
|
||||
col : number,
|
||||
row : number,
|
||||
index : number
|
||||
}
|
||||
type POSITION_XY = {
|
||||
x : number,
|
||||
y : number
|
||||
}
|
||||
|
||||
type XDRAG_DOMRECT = {
|
||||
width : number,
|
||||
height : number,
|
||||
left : number,
|
||||
top : number,
|
||||
right : number,
|
||||
bottom : number,
|
||||
}
|
||||
/**
|
||||
* @name 拖拽排序 xDrag
|
||||
* @description 自由布局拖拽排序组件,列或者宫格都支持。使用时,需要将需要拖拽排序的子元素设置为x-drag-item。
|
||||
* 并且子元素和父元素不允许通过style来动态设置宽高,否则拖拽排序会失效。并且list及子元素中的order不允许动态设置,否则拖拽排序会失效。
|
||||
* 想要动态修改数据可以通过vif切换重新渲染下。web支持响应式屏幕。本插件:引用了原生插件x-vibrate-s
|
||||
* @page /pages/index/drag
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
domlist: [] as CHILDREN_INFO[],
|
||||
backckList: [] as string[],
|
||||
oldList: [] as UTSJSONObject[],
|
||||
activeIndex: -1,
|
||||
targetIndex: -1,
|
||||
|
||||
isApplongStartMove:false,
|
||||
|
||||
|
||||
cellHeight: 0,
|
||||
cellWidth: 0,
|
||||
isMoveing: false,
|
||||
|
||||
_x: 0,
|
||||
_y: 0,
|
||||
tid: 0,
|
||||
oragie_x: 0,
|
||||
oragie_y: 0,
|
||||
scrollDiffTopJuli: 0,
|
||||
tid2: 12,
|
||||
oldStartXy: { col: 0, row: 0, index: 0 } as POSITION,
|
||||
xdragRect: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
} as XDRAG_DOMRECT,
|
||||
// 拖动检测相关
|
||||
hasMoved: false,
|
||||
moveThreshold: 1, // 移动阈值,超过这个距离才认为是拖动
|
||||
startTouchX: 0,
|
||||
startTouchY: 0
|
||||
}
|
||||
},
|
||||
|
||||
emits: [
|
||||
/**
|
||||
* 排序变动时触发
|
||||
* @param {UTSJSONObject[]} list - 当前变动后的数据列表
|
||||
*/
|
||||
"change",
|
||||
/**
|
||||
* 拖动的时候触发,可以根据参数进行对你超出页面屏幕高时进行一个滚动.
|
||||
* @param {number} diff - 滚动的距离向上是-diff,向下是正
|
||||
*/
|
||||
"move",
|
||||
"end",
|
||||
"start",
|
||||
],
|
||||
props: {
|
||||
/**
|
||||
* 项目的高度,不要动态更改。
|
||||
*/
|
||||
itemHeight: {
|
||||
type: String,
|
||||
default: "50"
|
||||
},
|
||||
|
||||
/**
|
||||
* 列数,默认1即列表布局,不要动态更改。
|
||||
* 如果是1以上就是宫格布局了。
|
||||
*/
|
||||
col: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* 你的排序数据list,变动后,请通过change事件来取得
|
||||
* 主要是用来骗编译器用的。
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<UTSJSONObject[]>,
|
||||
default: () : UTSJSONObject[] => [] as UTSJSONObject[],
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 当组件放置在scroll页面中,如果拖动时项目需要滚动时自动滚动的进步值
|
||||
* 比如组件在屏幕被底部或者顶部遮挡一部分,当拖动靠近底部时,会向下滚动的值.
|
||||
* 这个值是你外部自己通过设置滚动页面的scrollTop来达成的,具体见demo,已经为你写了示例.
|
||||
*/
|
||||
scrollDiff: {
|
||||
type: Number,
|
||||
default: 25
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_rows() : number {
|
||||
let row = Math.ceil(this.domlist.length / this.col)
|
||||
return row
|
||||
},
|
||||
_cols() : number {
|
||||
return this.col
|
||||
},
|
||||
_totalHeight() : number {
|
||||
return this._rows * this.cellHeight
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
XDRAGE_HEIGHT: checkIsCssUnit(this.itemHeight, xConfig.unit),
|
||||
XDRAGE_COL: this.col,
|
||||
XDRAGE_MAX_LEN: this.list.length,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.oldList = this.list.slice(0)
|
||||
this.getXdrageDomRect();
|
||||
},
|
||||
updated() {
|
||||
this.getXdrageDomRect()
|
||||
},
|
||||
methods: {
|
||||
updataResize(resize : CHILDREN_SIZE) {
|
||||
this.cellHeight = resize.height
|
||||
this.cellWidth = resize.width
|
||||
},
|
||||
addItem(item : CHILDREN_INFO) {
|
||||
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == item.id)
|
||||
if (index > -1) {
|
||||
this.domlist.splice(index, 1, item)
|
||||
} else {
|
||||
this.domlist.push(item)
|
||||
}
|
||||
|
||||
this.cellHeight = item.node.height!;
|
||||
this.cellWidth = item.node.width!;
|
||||
|
||||
},
|
||||
delItem(id : string) {
|
||||
let index = this.domlist.findIndex((el : CHILDREN_INFO) : boolean => el.id == id)
|
||||
if (index > -1) {
|
||||
this.domlist.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
indexTransformer(originalIndex : number) : number {
|
||||
let detail = this.targetIndex - this.activeIndex;
|
||||
if (detail > 0) {
|
||||
|
||||
return originalIndex - 1
|
||||
}
|
||||
|
||||
return originalIndex + 1
|
||||
},
|
||||
getLastMaxCol() : number {
|
||||
// 计算总行数,虽然这里主要关注最后一行,但计算总行数是理解过程的一部分
|
||||
const totalRows = Math.ceil(this.domlist.length / this.col);
|
||||
|
||||
// 计算最后一行的列数,通过求余数得到
|
||||
const lastRowColumns = this.domlist.length % this.col;
|
||||
|
||||
// 如果最后一行是满列的(即余数为0),直接返回列数;否则返回实际的列数(余数)
|
||||
return lastRowColumns === 0 ? this.col : lastRowColumns;
|
||||
},
|
||||
coverXy(x : number, y : number) : POSITION {
|
||||
let maxRow = Math.floor((this.domlist.length - 1) / this.col);
|
||||
let lastMacCol = this.getLastMaxCol();
|
||||
let el = this.$refs["xDrag"] as UniElement;
|
||||
let bounce = this.xdragRect;
|
||||
// #ifdef APP||WEB
|
||||
bounce = this.getByAppDomRect(el)
|
||||
// #endif
|
||||
let row = Math.floor((y - bounce.top!) / this.cellHeight)
|
||||
let col = Math.floor((x - bounce.left!) / this.cellWidth)
|
||||
|
||||
row = Math.min(Math.max(0, row), maxRow)
|
||||
|
||||
col = Math.min(Math.max(0, col), this.col - 1)
|
||||
|
||||
let index = (row + 1) * this.col - (this.col - col);
|
||||
|
||||
col = maxRow == row ? Math.min(lastMacCol - 1, col) : col;
|
||||
|
||||
index = Math.min(Math.max(0, index), this.domlist.length - 1)
|
||||
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
index
|
||||
} as POSITION
|
||||
},
|
||||
|
||||
eventTransformer_start(evt : POSITION_XY) {
|
||||
let t = this;
|
||||
if (t.domlist.length == 0) return;
|
||||
this.backckList = this.domlist.map((el) : string => el.id)
|
||||
|
||||
let x = evt.x;
|
||||
let y = evt.y;
|
||||
const startPos = this.coverXy(x, y);
|
||||
this.activeIndex = startPos.index
|
||||
this.targetIndex = this.activeIndex;
|
||||
|
||||
|
||||
this.oldStartXy = startPos
|
||||
let childrenIndex = this.domlist.findIndex((el) : boolean => el.id == this.backckList[this.activeIndex])
|
||||
if (childrenIndex == -1) return;
|
||||
let children = this.domlist[childrenIndex]!;
|
||||
let childrenEle = children!.ele
|
||||
|
||||
let childrenTop = parseFloat(childrenEle!.getStylSetProperty('top') as string);
|
||||
let childrenLeft = parseFloat(childrenEle!.getStylSetProperty('left') as string);
|
||||
childrenEle!.setStylSetProperty('transition-duration', '0s')
|
||||
childrenEle!.setStylSetProperty('z-index', '5')
|
||||
|
||||
this._x = x - childrenLeft;
|
||||
this._y = y - childrenTop;
|
||||
for (let i = 0; i < t.backckList.length; i++) {
|
||||
let temdom = t.backckList[i]
|
||||
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
|
||||
let el = t.domlist[elIndex];
|
||||
el.ele!.setActivdId(children.id)
|
||||
}
|
||||
// #ifdef MP
|
||||
t.domlist.forEach((el, index) : boolean => {
|
||||
if (index != childrenIndex) {
|
||||
el!.ele.updatePos()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
eventTransformer_move(evt : POSITION_XY) {
|
||||
let t = this;
|
||||
if (t.domlist.length == 0 || t.backckList.length == 0) return;
|
||||
let x = evt.x;
|
||||
let y = evt.y;
|
||||
let el = this.$refs["xDrag"] as UniElement;
|
||||
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
|
||||
if (childrenIndex == -1) return
|
||||
let children = t.domlist[childrenIndex]!;
|
||||
let childrenEle = children!.ele
|
||||
|
||||
let offsetY = y - t._y
|
||||
let offsetX = x - t._x
|
||||
|
||||
childrenEle!.setStylSetProperty('top', offsetY.toString() + 'px')
|
||||
childrenEle!.setStylSetProperty('left', offsetX.toString() + 'px')
|
||||
childrenEle!.setStylSetProperty('transition-duration', '0s')
|
||||
childrenEle!.setStylSetProperty('z-index', '5')
|
||||
|
||||
|
||||
|
||||
|
||||
let target = t.coverXy(x, y)
|
||||
let targetIndex = target.index
|
||||
let targetChildren = t.domlist[targetIndex]!;
|
||||
// 如果目标位置是禁用项目,则不进行任何位置交换操作,直接返回
|
||||
if (targetChildren.disabled) {
|
||||
return;
|
||||
}
|
||||
// 如果目标位置与当前位置相同,也不需要进行交换
|
||||
if (targetIndex == t.activeIndex) {
|
||||
return;
|
||||
}
|
||||
this.oldStartXy = target
|
||||
|
||||
let backChildrent = t.backckList.slice(0)[t.activeIndex];
|
||||
let backTargChildren = t.backckList.slice(0)[targetIndex];
|
||||
|
||||
t.targetIndex = targetIndex;
|
||||
let backChildrent_model = t.oldList.slice(0)[t.activeIndex];
|
||||
let backTargChildren_model = t.oldList.slice(0)[t.targetIndex];
|
||||
|
||||
t.backckList.splice(t.activeIndex, 1, backTargChildren)
|
||||
t.backckList.splice(t.targetIndex, 1, backChildrent)
|
||||
|
||||
t.oldList.splice(t.activeIndex, 1, backTargChildren_model)
|
||||
t.oldList.splice(t.targetIndex, 1, backChildrent_model)
|
||||
|
||||
for (let i = 0; i < t.backckList.length; i++) {
|
||||
let temdom = t.backckList[i]
|
||||
let elIndex = t.domlist.findIndex((el) : boolean => el.id == temdom)
|
||||
let el = t.domlist[elIndex];
|
||||
el.oldindex = el.index;
|
||||
el.index = i;
|
||||
el.ele!.setOrderIndex(i)
|
||||
}
|
||||
// #ifdef MP
|
||||
t.domlist.forEach((el, index) : boolean => {
|
||||
if (index != childrenIndex) {
|
||||
el!.ele.updatePos()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
t.activeIndex = t.targetIndex
|
||||
},
|
||||
eventTransformer_end(evt : POSITION_XY) {
|
||||
let t = this;
|
||||
|
||||
if (t.domlist.length == 0 || t.backckList.length == 0) return;
|
||||
|
||||
let x = evt.x;
|
||||
let y = evt.y;
|
||||
|
||||
let childrenIndex = t.domlist.findIndex((el) : boolean => el.id == t.backckList[t.activeIndex])
|
||||
if (childrenIndex == -1) return
|
||||
let children = t.domlist[childrenIndex]!;
|
||||
let childrenEle = children!.ele
|
||||
|
||||
let result = t.coverXy(x, y);
|
||||
let targetChildren = t.domlist[result.index]!;
|
||||
|
||||
|
||||
if (targetChildren.disabled) {
|
||||
result = this.oldStartXy
|
||||
|
||||
}
|
||||
|
||||
|
||||
let col = result.col;
|
||||
let row = result.row;
|
||||
|
||||
|
||||
childrenEle!.setStylSetProperty('top', (this.cellHeight * row) + 'px')
|
||||
childrenEle!.setStylSetProperty('left', (this.cellWidth * col) + 'px')
|
||||
|
||||
t.domlist.sort((ela, elb) : number => ela.index - elb.index)
|
||||
for (let i = 0; i < t.domlist.length; i++) {
|
||||
let temdom = t.domlist[i]
|
||||
temdom.oldindex = temdom.index
|
||||
temdom.index = i
|
||||
temdom.ele!.setOrderIndex(i)
|
||||
temdom.ele!.setActivdId("")
|
||||
temdom.ele!.updateForce()
|
||||
|
||||
}
|
||||
// #ifdef MP
|
||||
t.domlist.forEach((el, index) : boolean => {
|
||||
if (index != childrenIndex) {
|
||||
el!.ele.updatePos()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// this.activeIndex = -1;
|
||||
// this.targetIndex = -1;
|
||||
this.$emit('change', JSON.parseArray<UTSJSONObject>(JSON.stringify(this.oldList)!)!)
|
||||
},
|
||||
mLongStart(evt : UniTouchEvent) {
|
||||
this.isApplongStartMove = true;
|
||||
this.mStart(evt)
|
||||
},
|
||||
mStart(evt : UniTouchEvent) {
|
||||
|
||||
let x = evt.changedTouches[0].clientX;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
|
||||
// 记录初始触摸位置和重置拖动状态
|
||||
this.startTouchX = x;
|
||||
this.startTouchY = y;
|
||||
this.hasMoved = false;
|
||||
|
||||
this.oragie_x = x
|
||||
this.oragie_y = y
|
||||
let tempActiveIndex = this.coverXy(x, y).index
|
||||
if (tempActiveIndex == -1) return;
|
||||
let children = this.domlist[tempActiveIndex]!;
|
||||
if (children.disabled) return;
|
||||
this.isMoveing = true;
|
||||
this.getXdrageDomRect()
|
||||
this.scrollDiffTopJuli = 0
|
||||
clearTimeout(this.tid2)
|
||||
|
||||
vibrator(100)
|
||||
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
|
||||
this.$emit('start')
|
||||
|
||||
// 暂时不阻止事件传播,等检测到真正拖动时再阻止
|
||||
|
||||
},
|
||||
getXdrageDomRect() {
|
||||
// #ifdef MP-WEIXIN
|
||||
let t = this;
|
||||
let el = this.$refs["xDrag"] as UniElement;
|
||||
el.getBoundingClientRectAsync()
|
||||
.then(res => {
|
||||
t.xdragRect = {
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
left: res.left,
|
||||
right: res.right,
|
||||
top: res.top,
|
||||
bottom: res.bottom,
|
||||
} as XDRAG_DOMRECT
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
getByAppDomRect(ele : UniElement) : XDRAG_DOMRECT {
|
||||
|
||||
let rect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
} as XDRAG_DOMRECT
|
||||
// #ifdef APP || WEB
|
||||
let res = ele.getBoundingClientRect();
|
||||
rect = {
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
left: res.left,
|
||||
right: res.right,
|
||||
top: res.top,
|
||||
bottom: res.bottom,
|
||||
} as XDRAG_DOMRECT
|
||||
// #endif
|
||||
return rect
|
||||
},
|
||||
mMove(evt : UniTouchEvent) {
|
||||
|
||||
if (!this.isMoveing || this.activeIndex == -1) return;
|
||||
let x = evt.changedTouches[0].clientX + this.scrollDiffTopJuli;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
|
||||
// 检测是否真正发生了拖动
|
||||
if (!this.hasMoved) {
|
||||
let deltaX = Math.abs(x - this.startTouchX);
|
||||
let deltaY = Math.abs(y - this.startTouchY);
|
||||
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance > this.moveThreshold) {
|
||||
this.hasMoved = true;
|
||||
// 一旦检测到真正拖动,就开始阻止事件传播
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
} else {
|
||||
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
|
||||
return;
|
||||
}
|
||||
} else if(this.isApplongStartMove||this.hasMoved) {
|
||||
// 已经确认是拖动状态,继续阻止事件传播
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
|
||||
let el = this.$refs["xDrag"] as UniElement;
|
||||
let bounce = this.xdragRect;
|
||||
// #ifdef APP||WEB
|
||||
bounce = this.getByAppDomRect(el)
|
||||
// #endif
|
||||
let maxheight = uni.getWindowInfo().windowHeight
|
||||
let diffBottom = Math.abs(bounce.bottom - maxheight)
|
||||
let _this = this;
|
||||
// #ifdef APP||WEB
|
||||
clearTimeout(_this.tid2)
|
||||
_this.tid2 = setTimeout(() => {
|
||||
// 可见区域尾部在下面,向下滚动
|
||||
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
|
||||
this.$emit('move', _this.scrollDiff)
|
||||
_this.scrollDiffTopJuli += _this.scrollDiff
|
||||
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
|
||||
this.$emit('move', _this.scrollDiff * -1)
|
||||
_this.scrollDiffTopJuli -= _this.scrollDiff
|
||||
}
|
||||
}, 200)
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
clearTimeout(_this.tid2)
|
||||
_this.tid2 = setTimeout(async () => {
|
||||
bounce = await el.getBoundingClientRectAsync();
|
||||
diffBottom = Math.abs(bounce.bottom - maxheight)
|
||||
|
||||
// 可见区域尾部在下面,向下滚动
|
||||
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
|
||||
this.$emit('move', _this.scrollDiff)
|
||||
_this.scrollDiffTopJuli += _this.scrollDiff
|
||||
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
|
||||
this.$emit('move', _this.scrollDiff * -1)
|
||||
_this.scrollDiffTopJuli -= _this.scrollDiff
|
||||
}
|
||||
}, 200)
|
||||
|
||||
// #endif
|
||||
|
||||
},
|
||||
mEnd(evt : UniTouchEvent) {
|
||||
this.isApplongStartMove = false;
|
||||
this.scrollDiffTopJuli = 0
|
||||
clearTimeout(this.tid2)
|
||||
this.$emit('end')
|
||||
if (!this.isMoveing) return;
|
||||
this.isMoveing = false;
|
||||
|
||||
// 重置拖动状态
|
||||
this.hasMoved = false;
|
||||
this.startTouchX = 0;
|
||||
this.startTouchY = 0;
|
||||
|
||||
let x = evt.changedTouches[0].clientX;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
|
||||
this.eventTransformer_end({ x: x, y: y } as POSITION_XY)
|
||||
},
|
||||
// #ifdef WEB
|
||||
mmStart(evt : UniMouseEvent) {
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
// 记录初始鼠标位置和重置拖动状态
|
||||
this.startTouchX = x;
|
||||
this.startTouchY = y;
|
||||
this.hasMoved = false;
|
||||
|
||||
let tempActiveIndex = this.coverXy(x, y).index
|
||||
if (tempActiveIndex == -1) return;
|
||||
let children = this.domlist[tempActiveIndex]!;
|
||||
if (children.disabled) return;
|
||||
|
||||
this.isMoveing = true;
|
||||
this.eventTransformer_start({ x: x, y: y } as POSITION_XY)
|
||||
this.$emit('start')
|
||||
},
|
||||
mmMove(evt : UniMouseEvent) {
|
||||
if (!this.isMoveing || this.activeIndex == -1) return;
|
||||
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
// 检测是否真正发生了拖动
|
||||
if (!this.hasMoved) {
|
||||
let deltaX = Math.abs(x - this.startTouchX);
|
||||
let deltaY = Math.abs(y - this.startTouchY);
|
||||
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance > this.moveThreshold) {
|
||||
this.hasMoved = true;
|
||||
// 一旦检测到真正拖动,就开始阻止事件传播
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
} else {
|
||||
// 如果移动距离不够,不阻止事件传播,允许子组件响应点击
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 已经确认是拖动状态,继续阻止事件传播
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
console.log(777)
|
||||
}
|
||||
|
||||
this.eventTransformer_move({ x: x, y: y } as POSITION_XY)
|
||||
|
||||
let el = this.$refs["xDrag"] as UniElement;
|
||||
let bounce = this.xdragRect;
|
||||
bounce = this.getByAppDomRect(el)
|
||||
let maxheight = uni.getWindowInfo().windowHeight
|
||||
let diffBottom = Math.abs(bounce.bottom - maxheight)
|
||||
let _this = this;
|
||||
clearTimeout(_this.tid2)
|
||||
_this.tid2 = setTimeout(() => {
|
||||
// 可见区域尾部在下面,向下滚动
|
||||
if (bounce.bottom - maxheight > 0 && diffBottom > 25) {
|
||||
this.$emit('move', _this.scrollDiff)
|
||||
_this.scrollDiffTopJuli += _this.scrollDiff
|
||||
} else if (bounce.top < 0 && Math.abs(bounce.top) > 25) {
|
||||
this.$emit('move', _this.scrollDiff * -1)
|
||||
_this.scrollDiffTopJuli -= _this.scrollDiff
|
||||
}
|
||||
}, 200)
|
||||
|
||||
},
|
||||
mmEnd(evt : UniMouseEvent) {
|
||||
this.$emit('end')
|
||||
if (!this.isMoveing) return;
|
||||
this.isMoveing = false;
|
||||
|
||||
// 重置拖动状态
|
||||
this.hasMoved = false;
|
||||
this.startTouchX = 0;
|
||||
this.startTouchY = 0;
|
||||
|
||||
this.eventTransformer_end({ x: evt.clientX, y: evt.clientY } as POSITION_XY)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- @longpress="mStart" @longpress="mLongStart @touchstart="mStart"" -->
|
||||
<view
|
||||
class="xDrag"
|
||||
ref="xDrag"
|
||||
|
||||
<!-- #ifdef APP || MP -->
|
||||
@longpress="mLongStart"
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
@touchstart="mStart"
|
||||
<!-- #endif -->
|
||||
|
||||
@touchmove="mMove"
|
||||
@touchend="mEnd"
|
||||
@touchcancel="mEnd"
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown.parent="mmStart"
|
||||
@mousemove.stop="mmMove"
|
||||
@mouseup.parent="mmEnd"
|
||||
@mouseleave="mmEnd"
|
||||
<!-- #endif -->
|
||||
|
||||
:style="{height:_totalHeight+'px'}">
|
||||
<!--
|
||||
@slot 只可放置子组件x-drag-item
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xDrag {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,985 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig,xProvitae } from "../../config/xConfig.uts"
|
||||
|
||||
type callbackType = ()=>Promise<boolean>;
|
||||
type positionType = "top" | "bottom" | "left" | "right"
|
||||
|
||||
/**
|
||||
* @name 抽屉 xDrawer
|
||||
* @description 提供四个方向的弹出。
|
||||
* @page /pages/index/drawer
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xDrawer"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
defineSlots<{
|
||||
trigger(props: { show: boolean }): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show',
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
'confirm'
|
||||
])
|
||||
|
||||
type xDrawerPropsType = {
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: string,
|
||||
/**
|
||||
* 自定义容器背景层样式
|
||||
*/
|
||||
customWrapStyle: string,
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 显示底部操作栏
|
||||
*/
|
||||
showFooter: boolean,
|
||||
/**
|
||||
* 是否显示标题
|
||||
*/
|
||||
showTitle: boolean,
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showClose: boolean,
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: boolean,
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: boolean,
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showCancel: boolean,
|
||||
/**
|
||||
* 取消按钮的文本
|
||||
*/
|
||||
cancelText: string,
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
*/
|
||||
confirmText: string,
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: number,
|
||||
/**
|
||||
* 打开dom的延迟量,如果你打开 弹窗在ios正常。
|
||||
* 请不要修改此值。如果遇到打不开,或者 打开 后没动画,关闭不了等可能是sdk bug导致
|
||||
* 此时需要加大值来避免。具体加多少以你弹窗内的节点复杂度有关,需要你自行压力测试。
|
||||
* 此值仅在ios下生效。
|
||||
*/
|
||||
watiDuration: number,
|
||||
/**
|
||||
* 打开方向。
|
||||
*/
|
||||
position: positionType,
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 左右时为内容宽,
|
||||
* 上下时为内容高
|
||||
* 百分比,数字字符或者带单位,或者为auto(根据内容自动高度或者宽高)
|
||||
*/
|
||||
size: string,
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: string,
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 暗黑背景颜色,如果不提供默认读取全局的sheet配置
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 遮罩的背景色
|
||||
*/
|
||||
overflayBgColor: string,
|
||||
/**
|
||||
* 是否禁用内部的scroll标签
|
||||
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
|
||||
* 但如果没有指定高,内容自动的话,高是自动的。
|
||||
*/
|
||||
disabledScroll: boolean,
|
||||
/**
|
||||
* 内容区域左右和下的边距。
|
||||
*/
|
||||
contentMargin: string,
|
||||
/**
|
||||
* 宽屏时是否让内容剧中显示
|
||||
* 并限制其宽为屏幕宽,只展示中间内容以适应宽屏。
|
||||
* 注意只有top,bottom才会生效。
|
||||
*/
|
||||
widthCoverCenter: boolean,
|
||||
/**
|
||||
* 滑动左右或者上下关闭弹出层
|
||||
* 注意如果设置为0就表示关闭该功能。
|
||||
* 默认drawer嵌套了scroll-view,再你滚动到顶或者底时,如果继续滑动的距离大于此值关闭层。
|
||||
* 但如果你是禁用了内部scroll-view,而是采用自己的scorll-view,此时该功能会与你的滚动手势冲突,请自行考虑。
|
||||
* 建议要打开时设置为80-100比较合理
|
||||
*/
|
||||
swiperLenClose: number,
|
||||
/**
|
||||
* 距离顶部的偏移量,如果你布局顶会遮罩弹层可以考虑使用此值
|
||||
*/
|
||||
offsetTop: string,
|
||||
/**
|
||||
* 距离底部的偏移量,如果你布局底会遮罩弹层可以考虑使用此值
|
||||
*/
|
||||
offsetBottom: string,
|
||||
/**
|
||||
* 弹层的层级
|
||||
*/
|
||||
zIndex: number,
|
||||
/**
|
||||
* 懒加载
|
||||
* 为了解决业务布局节点超多时,你可能需要内容延迟加载以免阻塞动画流畅度.
|
||||
* 如果你启用了lazy,每次打开时,动画执行后才会显示内容.这样动画就流畅,不会因为节点过多造成的卡.
|
||||
* 开启了此属性后ios端前面的watiDuration属性可以不用再设置了.
|
||||
*/
|
||||
lazy: boolean,
|
||||
/**
|
||||
* 是否禁用确认按钮
|
||||
*/
|
||||
disabledConfirm: boolean,
|
||||
/**
|
||||
* 底部按钮操作的主题色,空取全局
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 关闭前异步执行的函数,如果返回false阻止关闭,返回true允许关闭
|
||||
* 必须返回的是Promise异步函数,且类型返回值必须是Promise<boolean>,不然会报错。
|
||||
*/
|
||||
beforeClose: callbackType,
|
||||
/**
|
||||
* 关闭图标的颜色
|
||||
*/
|
||||
closeColor: string,
|
||||
/**
|
||||
* 关闭图标的暗黑颜色
|
||||
*/
|
||||
closeDarkColor: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xDrawerPropsType>(), {
|
||||
customStyle: "",
|
||||
customWrapStyle: "",
|
||||
title: "",
|
||||
showFooter: false,
|
||||
showTitle: true,
|
||||
showClose: false,
|
||||
overlayClick: true,
|
||||
show: false,
|
||||
showCancel: true,
|
||||
cancelText: "",
|
||||
confirmText: "",
|
||||
duration: 300,
|
||||
watiDuration: 120,
|
||||
position: "bottom",
|
||||
round: "",
|
||||
size: "50%",
|
||||
maxHeight: "",
|
||||
bgColor: 'white',
|
||||
darkBgColor: '',
|
||||
overflayBgColor: 'rgba(0, 0, 0, 0.4)',
|
||||
disabledScroll: false,
|
||||
contentMargin: '16',
|
||||
widthCoverCenter: false,
|
||||
swiperLenClose: 0,
|
||||
offsetTop: '0',
|
||||
offsetBottom: '0',
|
||||
zIndex: 1100,
|
||||
lazy: false,
|
||||
disabledConfirm: false,
|
||||
btnColor: "",
|
||||
beforeClose: () : Promise<boolean> => {
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
closeColor: "#e6e6e6",
|
||||
closeDarkColor: "#545454"
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const actioning = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("xDrawer" + getUid())
|
||||
const wrapId = ref("xDrawerWrap" + getUid())
|
||||
const first = ref(true)
|
||||
const tid = ref(0)
|
||||
const windtop = ref(0)
|
||||
const windowBottom = ref(0)
|
||||
const start_move_x = ref(0)
|
||||
const start_move_y = ref(0)
|
||||
const move_x = ref(0)
|
||||
const move_y = ref(0)
|
||||
const move_end_x = ref(0)
|
||||
const move_end_y = ref(0)
|
||||
const scrollTop = ref(-1)
|
||||
const isTopOrBottomByScroll = ref(false)
|
||||
const xDrawerContentHeight = ref(0)
|
||||
const safeFooterHeight = ref(0)
|
||||
const lezyShowModal = ref(true)
|
||||
const isOpenedDefault = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const anitid = ref(23)
|
||||
// #ifdef H5
|
||||
const teleportElH5 = ref("uni-app")
|
||||
const teleportTarget = ref<string | null>(null)
|
||||
const getTeleportTarget = () => {
|
||||
try {
|
||||
if(status.value == ''||status.value=='close') return 'uni-app'
|
||||
// 优先尝试 uni-page
|
||||
if (document.querySelector('uni-page')) {
|
||||
return 'uni-page'
|
||||
}
|
||||
// 优先尝试 uni-app
|
||||
if (document.querySelector('uni-app')) {
|
||||
return 'uni-app'
|
||||
}
|
||||
// 备用方案:尝试 app
|
||||
if (document.querySelector('#app')) {
|
||||
return '#app'
|
||||
}
|
||||
// 最后备用:body
|
||||
return 'body'
|
||||
} catch (error) {
|
||||
// console.warn('Failed to get teleport target:', error)
|
||||
return 'body'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查teleport目标是否可用
|
||||
const isTeleportTargetValid = (target: string) => {
|
||||
try {
|
||||
return !!document.querySelector(target)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 计算属性
|
||||
const _disabledConfirm = computed((): boolean => props.disabledConfirm)
|
||||
const _lazy = computed((): boolean => props.lazy)
|
||||
const _customStyle = computed((): string => props.customStyle)
|
||||
const _customWrapStyle = computed((): string => props.customWrapStyle)
|
||||
const _show = computed((): boolean => props.show)
|
||||
const _widthCoverCenter = computed((): boolean => props.widthCoverCenter)
|
||||
const _showClose = computed((): boolean => props.showClose)
|
||||
const _duration = computed((): number => props.duration)
|
||||
const _position = computed((): string => props.position)
|
||||
const _showTitle = computed((): boolean => props.showTitle)
|
||||
|
||||
const _round = computed((): string => {
|
||||
let round = props.round;
|
||||
if (round == "") {
|
||||
round = xConfig.drawerRadius
|
||||
}
|
||||
let radius = checkIsCssUnit(round, xConfig.unit);
|
||||
|
||||
let _r = "none"
|
||||
if (props.position == 'top') {
|
||||
_r = `0px 0px ${radius} ${radius}`
|
||||
}
|
||||
if (props.position == 'bottom') {
|
||||
_r = `${radius} ${radius} 0px 0px`
|
||||
}
|
||||
return _r
|
||||
})
|
||||
|
||||
const _offset = computed((): number => {
|
||||
let offset = checkIsCssUnit(props.offsetTop, xConfig.unit);
|
||||
let x = parseFloat(offset)
|
||||
let unit = getUnit(offset)
|
||||
if (unit == 'rpx') {
|
||||
x = uni.rpx2px(x)
|
||||
}
|
||||
return x;
|
||||
})
|
||||
|
||||
const _offsetBottom = computed((): number => {
|
||||
let offset = checkIsCssUnit(props.offsetBottom, xConfig.unit);
|
||||
let x = parseFloat(offset)
|
||||
let unit = getUnit(offset)
|
||||
if (unit == 'rpx') {
|
||||
x = uni.rpx2px(x)
|
||||
}
|
||||
return x;
|
||||
})
|
||||
|
||||
const _size = computed((): string => checkIsCssUnit(props.size, xConfig.unit))
|
||||
const _contentMargin = computed((): string => checkIsCssUnit(props.contentMargin, xConfig.unit))
|
||||
const _showFooter = computed((): boolean => props.showFooter)
|
||||
|
||||
const _maxHeight = computed((): string => {
|
||||
if (props.maxHeight == "") return ""
|
||||
if (props.position == 'left' || props.position == 'right') return ""
|
||||
return checkIsCssUnit(props.maxHeight, xConfig.unit);
|
||||
})
|
||||
|
||||
const _showCancel = computed((): boolean => props.showCancel)
|
||||
|
||||
const _title = computed((): string => {
|
||||
if(props.title==''){
|
||||
return i18n.t("tmui4x.modal.title")
|
||||
}
|
||||
return props.title
|
||||
})
|
||||
|
||||
const _cancelText = computed((): string => {
|
||||
if(props.cancelText==''){
|
||||
return i18n.t("tmui4x.cancel")
|
||||
}
|
||||
return props.cancelText
|
||||
})
|
||||
|
||||
const _confirmText = computed((): string => {
|
||||
if(props.confirmText==''){
|
||||
return i18n.t("tmui4x.confirm")
|
||||
}
|
||||
return props.confirmText
|
||||
})
|
||||
|
||||
const _animationFun = computed((): string => xConfig.animationFun)
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
let bgcolor = props.bgColor;
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor != '') {
|
||||
|
||||
}
|
||||
bgcolor = props.darkBgColor != '' ? props.darkBgColor : xConfig.sheetDarkColor
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
})
|
||||
|
||||
const _btnColor = computed((): string => {
|
||||
if(props.btnColor == '') return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
|
||||
const __height = computed((): string => {
|
||||
let h = '100%';
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtop.value}px - ${_offsetBottom.value}px)`
|
||||
// #endif
|
||||
// #ifdef APP || MP-WEIXIN
|
||||
if (_offset.value > 0 || _offsetBottom.value > 0) {
|
||||
h = (_height.value - _offsetBottom.value) + 'px'
|
||||
}
|
||||
// #endif
|
||||
return h;
|
||||
})
|
||||
|
||||
const _titleFontSize = computed((): string => (xConfig.fontScale * 16).toString() + 'px')
|
||||
const _isDark = computed((): boolean => xConfig.dark == 'dark')
|
||||
const _closeIcon = computed((): string => xConfig.closeIcon)
|
||||
function onEnd() {
|
||||
actioning.value = false;
|
||||
if (status.value == 'close') {
|
||||
showOverflay.value = false;
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
emits('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
emits('update:show', false)
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = false
|
||||
}
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
emits('open')
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setStyleAni() {
|
||||
try {
|
||||
let sys = uni.getWindowInfo()
|
||||
_height.value = sys.windowHeight - _offset.value
|
||||
let watiDuration = 60;
|
||||
// #ifdef APP-IOS
|
||||
watiDuration = props.watiDuration
|
||||
// #endif
|
||||
|
||||
if (status.value == 'open') {
|
||||
showOverflay.value = true;
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function () {
|
||||
let element = proxy!.$refs['xDrawerWrap'] as UniElement | null
|
||||
let elementWrap = proxy!.$refs['xDrawerWrapContent'] as UniElement | null
|
||||
if (element== null || elementWrap == null) return;
|
||||
|
||||
element!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
elementWrap!.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
|
||||
element!.style.setProperty('opacity', 1)
|
||||
elementWrap!.style.setProperty('transform', `translate(0%,0%)`)
|
||||
}, watiDuration);
|
||||
} else if (status.value == 'close') {
|
||||
let element = proxy!.$refs['xDrawerWrap'] as UniElement | null
|
||||
let elementWrap = proxy!.$refs['xDrawerWrapContent'] as UniElement | null
|
||||
if (element== null || elementWrap == null) return;
|
||||
|
||||
element.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
elementWrap.style.setProperty("transition-duration", _duration.value.toString() + 'ms')
|
||||
|
||||
element.style.setProperty('opacity', 0)
|
||||
if (_position.value == 'bottom') {
|
||||
elementWrap.style.setProperty('transform', `translate(0%,100%)`)
|
||||
} else if (_position.value == 'top') {
|
||||
elementWrap.style.setProperty('transform', `translate(0%,-100%)`)
|
||||
} else if (_position.value == 'left') {
|
||||
elementWrap.style.setProperty('transform', `translate(-100%,0%)`)
|
||||
} else if (_position.value == 'right') {
|
||||
elementWrap.style.setProperty('transform', `translate(100%,0%)`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
}
|
||||
|
||||
function overflayMoveTouch(evt : TouchEvent) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
function closeAlert() {
|
||||
if (actioning.value) return;
|
||||
if (status.value == 'close') return;
|
||||
actioning.value = true;
|
||||
status.value = 'close'
|
||||
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emits('beforeClose')
|
||||
setStyleAni();
|
||||
anitid.value = setTimeout(function() {
|
||||
onEnd()
|
||||
}, _duration.value);
|
||||
}
|
||||
|
||||
function showAlert() {
|
||||
if (actioning.value) return;
|
||||
if (status.value == 'open') return;
|
||||
|
||||
showOverflay.value = true;
|
||||
actioning.value = true;
|
||||
status.value = 'open'
|
||||
|
||||
// #ifdef WEB
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emits('beforeOpen')
|
||||
setStyleAni();
|
||||
anitid.value = setTimeout(function() {
|
||||
onEnd()
|
||||
}, _duration.value+60);
|
||||
}
|
||||
|
||||
function onClickOverflowy(evt : Event) {
|
||||
evt.stopPropagation()
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
emits("click")
|
||||
if (!props.overlayClick||isLoading.value) return;
|
||||
closeAlert();
|
||||
}
|
||||
|
||||
// 方法
|
||||
function cancelEvt() {
|
||||
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
|
||||
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
|
||||
// #ifdef APP-IOS
|
||||
actioning.value = false;
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
emits('cancel')
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
async function confirmEvt(): Promise<any> {
|
||||
isLoading.value = true;
|
||||
let isCanClose = await props.beforeClose()
|
||||
isLoading.value = false;
|
||||
if(!isCanClose){
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
// ios渲染有时会造成无法触发onEnd事件,导致无法关闭。这是ios渲染的bug造成,无力修复,已向官方反馈
|
||||
// 但这牵涉到底层问题。一时无法修复,故在ios特殊处理。后期修复,需要删除此值。
|
||||
// #ifdef APP-IOS
|
||||
actioning.value = false;
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
emits('confirm')
|
||||
closeAlert()
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
|
||||
function openDrawer() {
|
||||
showAlert();
|
||||
}
|
||||
|
||||
|
||||
function swiperClose() {
|
||||
let offsetX = move_end_x.value - start_move_x.value
|
||||
let offsetY = move_end_y.value - start_move_y.value
|
||||
|
||||
if (props.swiperLenClose == 0 || (actioning.value && status.value == 'close')) return;
|
||||
|
||||
if (props.position == 'left' && offsetX < props.swiperLenClose * -1 && Math.abs(offsetX) >= Math.abs(offsetY)) {
|
||||
closeAlert()
|
||||
}
|
||||
if (props.position == 'right' && offsetX > props.swiperLenClose && Math.abs(offsetX) >= Math.abs(offsetY)) {
|
||||
closeAlert()
|
||||
}
|
||||
if (props.position == 'top' && offsetY < props.swiperLenClose * -1 && Math.abs(offsetY) >= Math.abs(offsetX)) {
|
||||
closeAlert()
|
||||
}
|
||||
if (props.position == 'bottom' && offsetY > props.swiperLenClose && Math.abs(offsetY) >= Math.abs(offsetX)) {
|
||||
closeAlert()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function maskerMove(evt : UniTouchEvent) {
|
||||
// #ifdef WEB
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function mStart(evt : UniTouchEvent) {
|
||||
if (props.swiperLenClose == 0) return;
|
||||
start_move_x.value = evt.changedTouches[0].clientX
|
||||
start_move_y.value = evt.changedTouches[0].clientY
|
||||
}
|
||||
|
||||
function mMove(evt : UniTouchEvent) {
|
||||
if (props.swiperLenClose == 0) return;
|
||||
// #ifdef WEB
|
||||
evt.preventDefault()
|
||||
// #endif
|
||||
|
||||
if (evt.changedTouches.length == 0) return;
|
||||
move_x.value = evt.changedTouches[0].clientX
|
||||
move_y.value = evt.changedTouches[0].clientY
|
||||
}
|
||||
|
||||
function mEnd(evt : UniTouchEvent) {
|
||||
if (props.swiperLenClose == 0) return;
|
||||
if (evt.changedTouches.length == 0) return;
|
||||
let x = evt.changedTouches[0].clientX
|
||||
let y = evt.changedTouches[0].clientY
|
||||
move_end_x.value = x
|
||||
move_end_y.value = y;
|
||||
swiperClose();
|
||||
}
|
||||
|
||||
|
||||
function onScroll(evt : UniScrollEvent) {
|
||||
if (props.position == 'bottom') {
|
||||
if (evt.detail.scrollTop > 0) {
|
||||
start_move_x.value = move_x.value
|
||||
start_move_y.value = move_y.value
|
||||
}
|
||||
}
|
||||
if (props.position == 'top') {
|
||||
let ele = proxy!.$refs['xDrawerContent'] as UniElement | null;
|
||||
if (ele == null) return;
|
||||
let height = ele.getBoundingClientRect().height;
|
||||
let maxheight = evt.detail.scrollHeight - evt.detail.scrollTop
|
||||
if (evt.detail.scrollTop < maxheight - 1) {
|
||||
start_move_x.value = move_x.value
|
||||
start_move_y.value = move_y.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onScrollTop(evt : UniScrollToUpperEvent) {
|
||||
start_move_x.value = move_x.value
|
||||
start_move_y.value = move_y.value
|
||||
}
|
||||
|
||||
function onScrollBottom(evt : UniScrollToLowerEvent) {
|
||||
start_move_x.value = move_x.value
|
||||
start_move_y.value = move_y.value
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch(():boolean => props.show, (newval : boolean) => {
|
||||
if (newval) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
lezyShowModal.value = _lazy.value ? false : true;
|
||||
// #ifdef H5
|
||||
nextTick(() => {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
})
|
||||
// #endif
|
||||
|
||||
function oninitready(){
|
||||
isOpenedDefault.value = true;
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifdef WEB
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight;
|
||||
windtop.value = sys.windowTop + _offset.value;
|
||||
// #endif
|
||||
// #ifdef APP|| MP-WEIXIN
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight + 44;
|
||||
windtop.value = _offset.value;
|
||||
// #endif
|
||||
safeFooterHeight.value = sys.safeAreaInsets.bottom == 0 ? 16 : sys.safeAreaInsets.bottom
|
||||
if (_show.value) {
|
||||
showAlert();
|
||||
}
|
||||
}
|
||||
|
||||
oninitready()
|
||||
})
|
||||
|
||||
// 监听页面变化,重新获取teleport目标
|
||||
onUpdated(() => {
|
||||
// #ifdef H5
|
||||
if (teleportTarget.value && !isTeleportTargetValid(teleportTarget.value)) {
|
||||
teleportTarget.value = getTeleportTarget()
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** 打开 **/
|
||||
open:()=>showAlert(),
|
||||
/** 关闭 **/
|
||||
close:()=>closeAlert()
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer">
|
||||
<!--
|
||||
@slot 标签触发显示遮罩,免于使用变量控制
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="show"></slot>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport :to="teleportTarget || teleportElH5" :disabled="!teleportTarget">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal >
|
||||
<!-- #endif -->
|
||||
|
||||
<view @click="onClickOverflowy" @touchmove="maskerMove" v-if="showOverflay" :id="id" ref="xDrawerWrap"
|
||||
class="xDrawerWrap" :class="[
|
||||
(_position=='top'||_position=='bottom')&&_widthCoverCenter?'xDrawerWrapContentMinwidthWrapDir':'',
|
||||
'xDrawerWrap_'+_position
|
||||
]" :style="[{backgroundColor:overflayBgColor,width:'100%',
|
||||
top:windtop+'px',
|
||||
height:__height,
|
||||
zIndex:zIndex,
|
||||
'transition-timing-function':_animationFun},_customStyle]">
|
||||
<view v-if="showOverflay&&_position == 'bottom'&&!actioning">
|
||||
<!--
|
||||
@slot 内容顶部的额外插槽,仅为下向上弹出(position=bottom)下才会显示。
|
||||
-->
|
||||
<slot name="contentTop"></slot>
|
||||
</view>
|
||||
<!-- @transitionend="onEnd" -->
|
||||
<view @click.stop="" ref="xDrawerWrapContent" class="xDrawerWrapContent"
|
||||
@touchstart="mStart" @touchmove="mMove" @touchend="mEnd" :class="[
|
||||
(_position=='top'||_position=='bottom')&&_widthCoverCenter?'xDrawerWrapContentMinwidth':'',
|
||||
'xDrawerWrapContent_'+_position
|
||||
]" :id="wrapId"
|
||||
:style="[
|
||||
{
|
||||
width:_position=='left'||_position=='right'?_size:'100%',
|
||||
height:_position=='left'||_position=='right'?'100%':_size,
|
||||
borderRadius:_round,
|
||||
maxHeight:_maxHeight!=''?_maxHeight:'100%',
|
||||
'transition-timing-function':_animationFun,
|
||||
backgroundColor:_bgColor,
|
||||
},
|
||||
customWrapStyle
|
||||
]">
|
||||
<view v-if="_showClose" class="xDrawerXclose">
|
||||
<x-icon @click="cancelEvt" :color="closeColor" :dark-color="closeDarkColor" font-size="24px"
|
||||
:name="_closeIcon"></x-icon>
|
||||
</view>
|
||||
|
||||
<view v-if="_showTitle">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="show">
|
||||
<view class="xDrawerTitleBox">
|
||||
<text :style="{fontSize:_titleFontSize,color:_isDark?'white':'black',opacity:'0.64'}"
|
||||
class="xDrawertitleBox">{{_title}}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<x-loading v-if="!lezyShowModal"><text></text></x-loading>
|
||||
<view ref="xDrawerContent" class="xDrawerContent"
|
||||
:style="{flex:'1',margin:`0px 0px ${_contentMargin} 0px`}">
|
||||
<scroll-view v-if="!disabledScroll&&lezyShowModal" @scroll="onScroll"
|
||||
@scrolltoupper="onScrollTop" @scrolltolower="onScrollBottom"
|
||||
|
||||
<!-- #ifdef APP||WEB -->
|
||||
:style="{flex:'1'}"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP -->
|
||||
:style="{position:'absolute',width:'100%',height:'100%'}"
|
||||
<!-- #endif -->
|
||||
|
||||
:scroll-y="true" :rebound="false">
|
||||
<view :style="{padding:`0px ${_contentMargin} 0px ${_contentMargin}`}">
|
||||
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot name="default"></slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="disabledScroll&&lezyShowModal"
|
||||
:style="{flex:'1',padding:`0px ${_contentMargin} 0px ${_contentMargin}`}">
|
||||
<!--
|
||||
默认插槽
|
||||
-->
|
||||
<slot name="default"></slot>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view v-if="showFooter&&lezyShowModal" class="xDrawerFooter" :style="{backgroundColor:_bgColor}">
|
||||
<!--
|
||||
@slot 底部操作栏
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view style="flex-direction: row;align-items: center;justify-content: center;display: flex;">
|
||||
<x-button :disabled="isLoading" :color="_btnColor" @click="cancelEvt" v-if="_showCancel" skin="thin" width="0px" :block="true"
|
||||
style="margin-right: 16rpx;flex:1">{{_cancelText}}</x-button>
|
||||
<x-button :loading="isLoading" :color="_btnColor" @click="confirmEvt" width="0px" :disabled="_disabledConfirm" :block="true"
|
||||
style="flex:1">{{_confirmText}}</x-button>
|
||||
</view>
|
||||
</slot>
|
||||
<view :style="{height:safeFooterHeight+'px'}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xDrawerContent{
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.xDrawerFooter {
|
||||
width: 100%;
|
||||
/* background-color: white; */
|
||||
padding: 0 16px 0px 16px;
|
||||
/* #ifdef MP-WEIXIN||WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xDrawerXclose {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 9px;
|
||||
z-index: 100;
|
||||
}
|
||||
.xDrawerXcloseOutter {
|
||||
padding-right: 16px;
|
||||
}
|
||||
.xDrawerTitleBox {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xDrawertitleBox {
|
||||
max-width: 175px;
|
||||
overflow: hidden;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
}
|
||||
|
||||
.xDrawerWrap_bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.xDrawerWrap_top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.xDrawerWrap_left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.xDrawerWrap_right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.xDrawerWrapContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.xDrawerWrapContentMinwidth {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.xDrawerWrapContentMinwidthWrapDir {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xDrawerWrapContent_bottom {
|
||||
transform: translate(0%, 100%);
|
||||
}
|
||||
|
||||
.xDrawerWrapContent_top {
|
||||
transform: translate(0%, -100%);
|
||||
}
|
||||
|
||||
.xDrawerWrapContent_left {
|
||||
transform: translate(-100%, 0%);
|
||||
}
|
||||
|
||||
.xDrawerWrapContent_right {
|
||||
transform: translate(100%, 0%);
|
||||
}
|
||||
|
||||
.xDrawerWrap {
|
||||
/* background: rgba(0, 0, 0, 0.4); */
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
/* #ifdef APP-HARMONY */
|
||||
transition-duration: 0ms;
|
||||
/* #endif */
|
||||
/* #ifndef APP-HARMONY */
|
||||
transition-duration: 350ms;
|
||||
/* #endif */
|
||||
transition-property: opacity;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<script lang="ts">
|
||||
import { checkIsCssUnit, getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { XDROPDOWN_LISTITEM_INFO_TYPE } from '../../interface.uts';
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 下拉菜单子组件 xDropdownItem
|
||||
* @description 注意只能放置在父组件x-dropdown-menu中
|
||||
* @page /pages/index/dropdown-menu
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: ("xDropMenuItem" + getUid()) as string,
|
||||
show: false,
|
||||
tid: 0
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 菜单标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题"
|
||||
},
|
||||
/**
|
||||
* 标识,变换或者点击时,会通过事件传回。
|
||||
*/
|
||||
keyName: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 未选中时的图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "arrow-down-s-fill"
|
||||
},
|
||||
/**
|
||||
* 激活时的图标
|
||||
*/
|
||||
activeIcon: {
|
||||
type: String,
|
||||
default: "arrow-up-s-fill"
|
||||
},
|
||||
/**
|
||||
* 默认的文字及图标颜色
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的默认的文字及图标颜色,
|
||||
* 空取时白色
|
||||
*/
|
||||
darkFontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 文字及图标大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "16"
|
||||
},
|
||||
/**
|
||||
* 激活的文字及图标颜色
|
||||
* 空值时取全局统一的主题色。
|
||||
*/
|
||||
activeFontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否是按钮选项。
|
||||
*/
|
||||
isBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 内容背景颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的内容背景颜色,空值取sheetDarkColor
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 渲染,如果设置为true,内部使用vif切换渲染
|
||||
* 注意它不会影响其它菜单只影响本菜单.vif切换会导致内容重绘,但
|
||||
* 可以解决sdk的一些异常组件嵌套的问题.
|
||||
* 当您嵌套的组件在此内出现异常,闪退,报错时请设置为true,否则不用理会本属性
|
||||
* 本属性存在就是为了绕开sdk bug.
|
||||
*/
|
||||
render:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.pushDataToParent();
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.removeSelf();
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
watch: {
|
||||
title() { this.pushDataToParent() },
|
||||
keyName() { this.pushDataToParent() },
|
||||
icon() { this.pushDataToParent() },
|
||||
activeIcon() { this.pushDataToParent() },
|
||||
fontColor() { this.pushDataToParent() },
|
||||
activeFontColor() { this.pushDataToParent() },
|
||||
fontSize() { this.pushDataToParent() }
|
||||
},
|
||||
computed: {
|
||||
_color() : string {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkColor != '') return getDefaultColor(this.darkColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(this.color)
|
||||
},
|
||||
_activeFontColor() : string {
|
||||
if (this.activeFontColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(this.activeFontColor)
|
||||
},
|
||||
_fontColor() : string {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkFontColor != '') return getDefaultColor(this.darkFontColor)
|
||||
return "#ffffff"
|
||||
}
|
||||
return getDefaultColor(this.fontColor)
|
||||
},
|
||||
|
||||
_fontSize() : string {
|
||||
return checkIsCssUnit(this.fontSize, xConfig.unit)
|
||||
},
|
||||
_showrender():boolean{
|
||||
if(this.render){
|
||||
return this.show
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pushDataToParent() {
|
||||
let t = this;
|
||||
let parent : XDropdownMenuComponentPublicInstance | null = null;
|
||||
try {
|
||||
parent = this.$parent as XDropdownMenuComponentPublicInstance | null
|
||||
} catch (_e) {
|
||||
console.error("x-dropdown-item:本组件必须放置在x-dropdown-menu中的直接子节点,不可单独或者嵌套使用。")
|
||||
}
|
||||
|
||||
if (parent == null) return;
|
||||
|
||||
// #ifdef WEB
|
||||
if (typeof parent?.addMenu != 'function') return;
|
||||
// #endif
|
||||
|
||||
clearTimeout(this.tid)
|
||||
this.tid = setTimeout(function () {
|
||||
parent!.addMenu(t as XDropdownItemComponentPublicInstance, {
|
||||
id: t.id as string,
|
||||
title: t.title as string,
|
||||
keyName: t.keyName as string,
|
||||
icon: t.icon as string,
|
||||
activeIcon: t.activeIcon as string,
|
||||
fontColor: t._fontColor as string,
|
||||
activeFontColor: t._activeFontColor as string,
|
||||
fontSize: t._fontSize as string,
|
||||
isBtn: t.isBtn as boolean
|
||||
} as XDROPDOWN_LISTITEM_INFO_TYPE)
|
||||
}, 5);
|
||||
},
|
||||
removeSelf() {
|
||||
let parent : XDropdownMenuComponentPublicInstance | null = null;
|
||||
try {
|
||||
parent = this.$parent as XDropdownMenuComponentPublicInstance | null
|
||||
} catch (_e) {
|
||||
console.error("x-dropdown-item:本组件必须放置在x-dropdown-menu中的直接子节点,不可单独或者嵌套使用。")
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
// #ifdef WEB
|
||||
if (typeof parent?.delMenu != 'function') return;
|
||||
// #endif
|
||||
parent!.delMenu(this.id as string)
|
||||
}
|
||||
},
|
||||
open() {
|
||||
this.show = true;
|
||||
},
|
||||
close() {
|
||||
this.show = false;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- :class="show?'x-dropdown-itemOn':'x-dropdown-itemOff'" -->
|
||||
<!-- :class="[
|
||||
show&&!render?'xDrodownItemOn':'',
|
||||
!show&&!render?'xDrodownItemOff':'',
|
||||
]" -->
|
||||
<view v-if="_showrender" @click.stop="" :ref="id" :id="id" class="x-dropdown-item" :class="show?'x-dropdown-itemOn':'x-dropdown-itemOff'">
|
||||
<view class="x-dropdown-itemWrap"
|
||||
:style="{backgroundColor:_color}"
|
||||
>
|
||||
<!--
|
||||
@slot 默认插槽,弹层内容。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.x-dropdown-item {
|
||||
transition-duration: 250ms;
|
||||
transition-property: transform , opacity;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transform: translateY(-100%);
|
||||
position: absolute;
|
||||
top:0px;
|
||||
left:0;
|
||||
width:100%;
|
||||
}
|
||||
.x-dropdown-itemOn{
|
||||
transform: translateY(0%);
|
||||
}
|
||||
.x-dropdown-itemOff{
|
||||
transition-duration: 50ms;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
.x-dropdown-itemWrap{
|
||||
transition-duration: 250ms;
|
||||
transition-property: height;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
pointer-events: auto;
|
||||
padding: 12px;
|
||||
border-radius: 0px 0px 12px 12px;
|
||||
}
|
||||
.xDrodownItemOn {
|
||||
display: flex;
|
||||
}
|
||||
.xDrodownItemOff {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,524 @@
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { XDROPDOWN_LISTITEM_INFO_TYPE, NODE_INFO } from '../../interface.uts';
|
||||
import { checkIsCssUnit, getUid, rpx2px, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
type XDROPDOWN_LISTITEM_TYPE = {
|
||||
ele : XDropdownItemComponentPublicInstance,
|
||||
id : string,
|
||||
data : XDROPDOWN_LISTITEM_INFO_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 下拉菜单 xDropdownMenu
|
||||
* @description 下拉菜单,标签内只能放置子项目x-dropdown-item
|
||||
* @page /pages/index/dropdown-menu
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
name: "x-dropdown-menu",
|
||||
data() {
|
||||
return {
|
||||
id: ("xDropMenu" + getUid()) as string,
|
||||
_width: 0,
|
||||
_height: 0,
|
||||
menubarNodeino: null as null | NODE_INFO,
|
||||
nowIndex: -1,
|
||||
opended: false,
|
||||
cacheListItem: [] as XDROPDOWN_LISTITEM_TYPE[],
|
||||
windtop: 0,
|
||||
tid: 0,
|
||||
maskMoveX:0,
|
||||
maskMoveY:0,
|
||||
maskTouchTime:0
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 切换菜单时触发
|
||||
* @param index {number} - 当前被切换的索引值
|
||||
* @param keyName {string} - 菜单keyName标识
|
||||
* @param status {boolean} - 当前切换的状态:false表示关闭,true表示打开。
|
||||
*/
|
||||
'change',
|
||||
'update:modelValue'],
|
||||
props: {
|
||||
/**
|
||||
* 位置,
|
||||
* static | fixed
|
||||
* 静态 | 展开后悬浮在顶部。
|
||||
*/
|
||||
position: {
|
||||
type: String as PropType<"static" | 'fixed'>,
|
||||
default: "fixed"
|
||||
},
|
||||
/**
|
||||
* 顶部的偏移量,针对你们自定标题导航时可能需要让出顶部位置。
|
||||
* 数字字符,prx,px等单位
|
||||
* 如果position=static此属性失效。
|
||||
*/
|
||||
offsetTop: {
|
||||
type: String,
|
||||
default: "0"
|
||||
},
|
||||
/**
|
||||
* 当前激活的索引
|
||||
* -1表示关闭。提供值时请大于-1,
|
||||
* 如果不想要变量控制,些值 可不提供。交由内部自行处理。
|
||||
* 当你想要用变量控制开关时可v-model="索引"来控制关闭和打开。
|
||||
*/
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
/**
|
||||
* 菜单栏的高度
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "44"
|
||||
},
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的背景颜色
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 88
|
||||
},
|
||||
/**
|
||||
* 对于要把此组件嵌套在fiexd中时,并且position为static时
|
||||
* 请一定设置为true,否则在web端会出现层级混乱(这是css dom规则所定)
|
||||
* 为了全平台对齐请嵌套组件时一定注意使用事项.
|
||||
*/
|
||||
hidnMask:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
_cutomhHeight() : string {
|
||||
return checkIsCssUnit(this.height, xConfig.unit)
|
||||
},
|
||||
_cutomWidth() : string {
|
||||
return checkIsCssUnit(this.width, xConfig.unit)
|
||||
},
|
||||
_color() : string {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkColor != '') return getDefaultColor(this.darkColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(this.color)
|
||||
},
|
||||
_offsetTop() : number {
|
||||
if (this.position == 'static') return this.windtop
|
||||
let height = checkIsCssUnit(this.offsetTop, xConfig.unit)
|
||||
let unit = getUnit(height)
|
||||
let realheight = parseInt(height)
|
||||
if (unit == 'rpx') {
|
||||
realheight = rpx2px(realheight)
|
||||
}
|
||||
|
||||
return realheight + this.windtop;
|
||||
},
|
||||
menuLeft() : string {
|
||||
if (this.menubarNodeino == null) return ""
|
||||
if (this.position == 'fixed') {
|
||||
return "0px";
|
||||
}
|
||||
return (this.menubarNodeino!.left).toString() + 'px';
|
||||
},
|
||||
menuWidth() : string {
|
||||
if (this.menubarNodeino == null) return ""
|
||||
if (this.position == 'fixed') {
|
||||
return "100%"
|
||||
}
|
||||
return (this.menubarNodeino!.width).toString() + 'px';
|
||||
},
|
||||
menuTop() : string {
|
||||
if (this.menubarNodeino == null) return ""
|
||||
if (this.position == 'fixed') {
|
||||
return this.windtop+"px";
|
||||
}
|
||||
// + this.menubarNodeino!.height
|
||||
// let top = this.menubarNodeino!.top
|
||||
let parentTop = 0
|
||||
// #ifdef WEB
|
||||
if(this.$parent?.$el?.getBoundingClientRect){
|
||||
parentTop = this.$parent?.$el?.getBoundingClientRect()?.top??0
|
||||
}
|
||||
parentTop = this.menubarNodeino!.top - parentTop + this.menubarNodeino!.height;
|
||||
|
||||
// #endif
|
||||
// #ifdef APP||MP-WEIXIN
|
||||
parentTop = this.menubarNodeino!.top
|
||||
// #endif
|
||||
let top = parentTop
|
||||
return top.toString() + 'px';
|
||||
},
|
||||
nowItemIsBtn() : boolean {
|
||||
if (this.nowIndex <= -1 || this.nowIndex >= this.cacheListItem.length || this.cacheListItem.length == 0) return false;
|
||||
let item = this.cacheListItem[this.nowIndex];
|
||||
return item.data.isBtn
|
||||
},
|
||||
__height() : string {
|
||||
let h = '100%';
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${this.windtop}px)`
|
||||
// #endif
|
||||
|
||||
return h;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue : number) {
|
||||
if (this.nowIndex == newValue) return;
|
||||
if (newValue == -1) {
|
||||
this.nowIndex = -1;
|
||||
this.opended = false;
|
||||
this.closeMenuContent();
|
||||
return;
|
||||
}
|
||||
this.nowIndex = newValue;
|
||||
this.openMenuContent();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let t = this;
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifdef WEB
|
||||
this._width = sys.windowWidth
|
||||
this._height = sys.windowHeight;
|
||||
this.windtop = sys.windowTop;
|
||||
// #endif
|
||||
// #ifdef APP || MP-WEIXIN
|
||||
this._width = sys.windowWidth
|
||||
this._height = sys.windowHeight + 44;
|
||||
// #endif
|
||||
|
||||
|
||||
this.nowIndex = this.modelValue;
|
||||
this.getNodes();
|
||||
|
||||
uni.$on("onResize", this.getNodes)
|
||||
},
|
||||
beforeUnmount() {
|
||||
uni.$off("onResize", this.getNodes)
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
methods: {
|
||||
addMenu(insCom : XDropdownItemComponentPublicInstance, obj : XDROPDOWN_LISTITEM_INFO_TYPE) {
|
||||
let index = this.cacheListItem.findIndex((el : XDROPDOWN_LISTITEM_TYPE) : boolean => el.id == obj.id)
|
||||
if (index > -1) {
|
||||
this.cacheListItem[index] = {
|
||||
ele: insCom,
|
||||
id: obj.id,
|
||||
data: obj
|
||||
} as XDROPDOWN_LISTITEM_TYPE
|
||||
} else {
|
||||
this.cacheListItem.push({
|
||||
ele: insCom,
|
||||
id: obj.id,
|
||||
data: obj
|
||||
} as XDROPDOWN_LISTITEM_TYPE);
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.openMenuContent();
|
||||
})
|
||||
},
|
||||
delMenu(id : string) {
|
||||
if (this.cacheListItem.length == 0) return;
|
||||
let index : number = this.cacheListItem.findIndex((el : XDROPDOWN_LISTITEM_TYPE) : boolean => el.id == id);
|
||||
if (index > -1) {
|
||||
if (index == this.nowIndex) {
|
||||
this.nowIndex = -1;
|
||||
}
|
||||
this.cacheListItem.splice(index, 1)
|
||||
}
|
||||
},
|
||||
openMenuContent() {
|
||||
|
||||
this.cacheListItem.forEach((item : XDROPDOWN_LISTITEM_TYPE, index : number) => {
|
||||
if (index != this.nowIndex) {
|
||||
item.ele.close();
|
||||
} else {
|
||||
item.ele.open();
|
||||
this.opended = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
closeMenuContent() {
|
||||
this.cacheListItem.forEach((item : XDROPDOWN_LISTITEM_TYPE) => {
|
||||
item.ele.close();
|
||||
})
|
||||
},
|
||||
maskerMove(evt : UniTouchEvent) {
|
||||
// evt.preventDefault();
|
||||
},
|
||||
closeMenu() {
|
||||
let nkey = ""
|
||||
try {
|
||||
nkey = this.cacheListItem[this.nowIndex].data.keyName
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
/**
|
||||
* 切换菜单时触发
|
||||
* @param index {number} 当前被切换的索引值
|
||||
* @param keyName {string} 菜单keyName标识
|
||||
* @param status {boolean} 当前切换的状态:false表示关闭,true表示打开。
|
||||
*/
|
||||
this.$emit("change", this.nowIndex, nkey, false)
|
||||
this.nowIndex = -1;
|
||||
this.opended = false;
|
||||
/**
|
||||
* 等同v-model=""
|
||||
*/
|
||||
this.$emit("update:modelValue", this.nowIndex)
|
||||
this.closeMenuContent();
|
||||
// #ifdef WEB
|
||||
document.body.style.removeProperty("overflow")
|
||||
// #endif
|
||||
},
|
||||
openMenu(index : number) {
|
||||
this.getNodes();
|
||||
this.nowIndex = index;
|
||||
this.opended = false;
|
||||
/**
|
||||
* 等同v-model=""
|
||||
*/
|
||||
this.$emit("update:modelValue", index)
|
||||
this.openMenuContent();
|
||||
let nkey = ""
|
||||
try {
|
||||
nkey = this.cacheListItem[index].data.keyName
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
|
||||
this.$emit("change", index, nkey, true)
|
||||
// #ifdef WEB
|
||||
document.body.style.setProperty("overflow","hidden")
|
||||
// #endif
|
||||
},
|
||||
menuClick(index : number) {
|
||||
|
||||
if (index == this.nowIndex) {
|
||||
this.closeMenu();
|
||||
} else {
|
||||
this.openMenu(index);
|
||||
}
|
||||
console.log(this.nowIndex)
|
||||
},
|
||||
|
||||
maskmStart(evt:UniTouchEvent){
|
||||
evt.preventDefault()
|
||||
this.maskMoveX = evt.changedTouches[0].clientX
|
||||
this.maskMoveY = evt.changedTouches[0].clientY
|
||||
this.maskTouchTime = Date.now()
|
||||
|
||||
|
||||
},
|
||||
maskmMove(evt:UniTouchEvent){
|
||||
// #ifdef WEB||MP-WEIXIN
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
// #endif
|
||||
},
|
||||
maskmEnd(evt:UniTouchEvent){
|
||||
let diffx = evt.changedTouches[0].clientX - this.maskMoveX
|
||||
let diffy = evt.changedTouches[0].clientY - this.maskMoveY
|
||||
let difftime = Date.now() - this.maskTouchTime;
|
||||
if(diffx==diffy&&difftime>50&&difftime<250){
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
getNodes() {
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select('.xDropMenu')
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
this.menubarNodeino = {
|
||||
left: nodeinfo.left!,
|
||||
width: nodeinfo.width!,
|
||||
height: nodeinfo.height!,
|
||||
bottom: nodeinfo.bottom!,
|
||||
right: nodeinfo.right!,
|
||||
top: nodeinfo.top!,
|
||||
} as NODE_INFO
|
||||
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view :id="id" class="xDropMenu" :style="{
|
||||
width:_cutomWidth,
|
||||
height:_cutomhHeight,
|
||||
backgroundColor:_color
|
||||
}">
|
||||
|
||||
<view class="xDropMenuBarStatic"
|
||||
:style="{height:_cutomhHeight,visibility:!opended||nowItemIsBtn?'visible':'hidden'}">
|
||||
<view @click.stop="menuClick(index)" class="xDropMenuBaritem" v-for="(item,index) in cacheListItem"
|
||||
:key="index">
|
||||
<text :style="{
|
||||
color:nowIndex==index? item.data.activeFontColor:item.data.fontColor,
|
||||
fontSize:item.data.fontSize
|
||||
}">{{item.data.title}}</text>
|
||||
<x-icon style="margin-left:5px"
|
||||
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
|
||||
v-if="nowIndex==index&&item.data.activeIcon!=''" :name="item.data.activeIcon"></x-icon>
|
||||
<x-icon style="margin-left:5px"
|
||||
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
|
||||
v-if="nowIndex!=index&&item.data.icon!=''" :name="item.data.icon"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef WEB -->
|
||||
<teleport to="uni-app">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
|
||||
<view v-if="!hidnMask"
|
||||
@touchstart="maskmStart"
|
||||
@touchmove="maskmMove"
|
||||
@touchend="maskmEnd"
|
||||
|
||||
class="xDropMenuWrap" :style="{
|
||||
width:'100%',
|
||||
height:__height,
|
||||
display:opended&&!nowItemIsBtn?'flex':'none',
|
||||
top:_offsetTop+'px',
|
||||
zIndex:zIndex.toString(),
|
||||
position:'fixed'
|
||||
}
|
||||
">
|
||||
</view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef WEB -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
<view @click.stop="closeMenu" class="xDropMenuWrapContent" :style="{
|
||||
width:menuWidth,
|
||||
height:'100%',
|
||||
left:menuLeft,
|
||||
top:menuTop,
|
||||
zIndex:(zIndex+1).toString(),
|
||||
display:opended&&!nowItemIsBtn?'flex':'none',
|
||||
position: 'fixed'
|
||||
}">
|
||||
<view class="xDropMenuBarStatic xDropMenuBarAbs" :style="{
|
||||
width:_cutomWidth,
|
||||
height:_cutomhHeight,
|
||||
backgroundColor:_color,
|
||||
|
||||
}">
|
||||
<view v-if="opended" @click.stop="menuClick(index)" class="xDropMenuBaritem"
|
||||
v-for="(item,index) in cacheListItem" :key="index">
|
||||
<text :style="{
|
||||
color:nowIndex==index? item.data.activeFontColor:item.data.fontColor,
|
||||
fontSize:item.data.fontSize
|
||||
}">{{item.data.title}}</text>
|
||||
<x-icon style="margin-left:5px"
|
||||
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
|
||||
v-if="nowIndex==index&&item.data.activeIcon!=''" :name="item.data.activeIcon"></x-icon>
|
||||
<x-icon style="margin-left:5px"
|
||||
:color="(nowIndex==index? item.data.activeFontColor:item.data.fontColor)"
|
||||
v-if="nowIndex!=index&&item.data.icon!=''" :name="item.data.icon"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xDropMenuBgColor" :style="{'background-color': opended?`rgba(0, 0, 0, 0.4)`:`rgba(0, 0, 0, 0)`}">
|
||||
<!--
|
||||
@slot 默认插槽,只能放置子项目x-dropdown-item
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xDropMenuBgColor{
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color;
|
||||
transition-timing-function: linear;
|
||||
overflow: hidden;position: relative;
|
||||
background-color:rgba(0,0,0,0);
|
||||
transition-delay: 50ms;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.xDropMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.xDropMenuBaritem {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.xDropMenuBarStatic {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.xDropMenuWrap {
|
||||
position: fixed;
|
||||
/* z-index: 88; */
|
||||
background-color: transparent;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
|
||||
}
|
||||
|
||||
.xDropMenuWrapContent {
|
||||
/* position: fixed; */
|
||||
/* z-index: 89; */
|
||||
/* background-color: rgba(0, 0, 0, 0.4); */
|
||||
left: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
// @ts-nocheck
|
||||
export default class WxCanvas {
|
||||
constructor(ctx, canvasId, isNew, canvasNode) {
|
||||
this.ctx = ctx;
|
||||
this.canvasId = canvasId;
|
||||
this.chart = null;
|
||||
this.isNew = isNew
|
||||
if (isNew) {
|
||||
this.canvasNode = canvasNode;
|
||||
}
|
||||
else {
|
||||
this._initStyle(ctx);
|
||||
}
|
||||
|
||||
// this._initCanvas(zrender, ctx);
|
||||
|
||||
this._initEvent();
|
||||
}
|
||||
|
||||
getContext(contextType) {
|
||||
if (contextType === '2d') {
|
||||
return this.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// canvasToTempFilePath(opt) {
|
||||
// if (!opt.canvasId) {
|
||||
// opt.canvasId = this.canvasId;
|
||||
// }
|
||||
// return wx.canvasToTempFilePath(opt, this);
|
||||
// }
|
||||
|
||||
setChart(chart) {
|
||||
this.chart = chart;
|
||||
}
|
||||
|
||||
attachEvent() {
|
||||
// noop
|
||||
}
|
||||
|
||||
detachEvent() {
|
||||
// noop
|
||||
}
|
||||
|
||||
_initCanvas(zrender, ctx) {
|
||||
zrender.util.getContext = function () {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
zrender.util.$override('measureText', function (text, font) {
|
||||
ctx.font = font || '12px sans-serif';
|
||||
return ctx.measureText(text);
|
||||
});
|
||||
}
|
||||
|
||||
_initStyle(ctx) {
|
||||
// @ts-ignore
|
||||
ctx.createRadialGradient = () => {
|
||||
return ctx.createCircularGradient(arguments);
|
||||
};
|
||||
}
|
||||
|
||||
_initEvent() {
|
||||
this.event = {};
|
||||
const eventNames = [{
|
||||
wxName: 'touchStart',
|
||||
ecName: 'mousedown'
|
||||
}, {
|
||||
wxName: 'touchMove',
|
||||
ecName: 'mousemove'
|
||||
}, {
|
||||
wxName: 'touchEnd',
|
||||
ecName: 'mouseup'
|
||||
}, {
|
||||
wxName: 'touchEnd',
|
||||
ecName: 'click'
|
||||
}];
|
||||
|
||||
eventNames.forEach(name => {
|
||||
this.event[name.wxName] = e => {
|
||||
console.log(e)
|
||||
const touch = e.touches[0];
|
||||
this.chart.getZr().handler.dispatch(name.ecName, {
|
||||
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
|
||||
zrY: name.wxName === 'tap' ? touch.clientY : touch.y
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
addEventListener(el, name, handler, opt) {
|
||||
// el?.addEventListener(name, handler, opt);
|
||||
}
|
||||
set width(w) {
|
||||
if (this.canvasNode) this.canvasNode.width = w
|
||||
}
|
||||
set height(h) {
|
||||
if (this.canvasNode) this.canvasNode.height = h
|
||||
}
|
||||
|
||||
get width() {
|
||||
if (this.canvasNode)
|
||||
return this.canvasNode.width
|
||||
return 0
|
||||
}
|
||||
get height() {
|
||||
if (this.canvasNode)
|
||||
return this.canvasNode.height
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
// #ifdef MP-WEIXIN
|
||||
import WxCanvas from './canvasinit.uts';
|
||||
let echarts : any = null
|
||||
// #endif
|
||||
type eventsType = (data : any) => void;
|
||||
|
||||
/**
|
||||
* @name 图表 xEchart
|
||||
* @description 是百度图表6.0.0,全量版本
|
||||
* 传递正常的百度对象数据且需要将数据JSON.stringify化
|
||||
* 图表文档:https://echarts.apache.org/zh/index.html
|
||||
* 编译微信版本:https://echarts.apache.org/zh/builder.html
|
||||
* 微信版本请使用1.1.18下dmeo qita/echarts.esm.min.js文件。或者自己下载[Echart下载](https://github.com/apache/echarts/tree/6.0.0/dist)
|
||||
* 注意的是:如果你的配置中函数函数,需要自己转换为字符串【如果是微信端建议直接传对象,不要转为字符串这样兼容性更好。】
|
||||
* 比如:函数对象请参考我demo页面的示例规则分平台写否则无法实现函数对象。
|
||||
* @page /pages/index/echart
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xEchart" })
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy ?? null
|
||||
|
||||
type xEchartPropsType = {
|
||||
/**
|
||||
* 容器宽
|
||||
*/
|
||||
width : string,
|
||||
/**
|
||||
* 容器高
|
||||
*/
|
||||
height : string,
|
||||
/**
|
||||
* hbx sdk4.76+后建议不要使用此属性,请改用ref方法setOptions
|
||||
*/
|
||||
opts : string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xEchartPropsType>(), {
|
||||
width: 'auto',
|
||||
height: '250px',
|
||||
opts: ''
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 当图表初始化完成后触发,此时可以使用ref或者参数chart来来设置图表数据了。
|
||||
*/
|
||||
(e : 'init', chart : any | null) : void
|
||||
}>()
|
||||
|
||||
|
||||
const id = ref<string>("xEchart-" + getUid())
|
||||
const webviewContext = ref<WebviewContext | null>(null)
|
||||
const isLoaded = ref<boolean>(false)
|
||||
const boxWidth = ref<number>(10)
|
||||
const boxHeight = ref<number>(10)
|
||||
const tid = ref<number>(0)
|
||||
const tid2 = ref<number>(0)
|
||||
const realLoaded = ref<boolean>(false)
|
||||
const dipcatchEvents = ref<Map<string, eventsType>>(new Map())
|
||||
// #ifdef MP-WEIXIN
|
||||
const chart = ref<any | null>(null)
|
||||
const echartCanvasObj = ref<any>({})
|
||||
// #endif
|
||||
|
||||
|
||||
const _width = computed(() : string => checkIsCssUnit(props.width, xConfig.unit))
|
||||
const _height = computed(() : string => checkIsCssUnit(props.height, xConfig.unit))
|
||||
const _options = computed(() : string => props.opts)
|
||||
|
||||
|
||||
|
||||
function cahrtActions(fun : string, opts : string, evt : eventsType | null) {
|
||||
let filterevents = ["click"]
|
||||
let eventsid = ('x-' + getUid()) as string;
|
||||
if (evt != null && filterevents.includes(fun)) {
|
||||
dipcatchEvents.value.set(eventsid, evt!)
|
||||
}
|
||||
// #ifdef WEB
|
||||
var iframe = document.getElementById(id.value) as any;
|
||||
if (!iframe) return;
|
||||
iframe.contentWindow['chart_call'](fun, opts, eventsid);
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
let wb = webviewContext.value!;
|
||||
wb.evalJS(`chart_call('${fun}','${opts}','${eventsid}')`)
|
||||
// #endif
|
||||
|
||||
}
|
||||
function eventJsCall(callfun : string, str : string) {
|
||||
// #ifdef WEB
|
||||
var iframe = document.getElementById(id.value) as any;
|
||||
if (!iframe) return;
|
||||
iframe.contentWindow[callfun](str);
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
let wb = webviewContext.value!;
|
||||
wb.evalJS(`${callfun}(${str})`)
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
(chart.value as any)[callfun](str)
|
||||
// #endif
|
||||
}
|
||||
function drawer() {
|
||||
if (!realLoaded.value) {
|
||||
uni.showToast({ title: "未初始化完成", icon: 'none' })
|
||||
return;
|
||||
}
|
||||
// #ifdef WEB
|
||||
eventJsCall('chart_setOption', `${_options.value}`)
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
eventJsCall('chart_setOption', `'${_options.value}'`)
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
setoptsByWx(_options.value)
|
||||
// #endif
|
||||
}
|
||||
function onResizeChart() {
|
||||
if (realLoaded.value) {
|
||||
cahrtActions('resize', '', null)
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeInfo() {
|
||||
uni.createSelectorQuery().in(proxy as any)
|
||||
.select(".xEchart")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
boxWidth.value = nodeinfo.width!
|
||||
boxHeight.value = nodeinfo.height!
|
||||
if (webviewContext.value != null) return;
|
||||
isLoaded.value = true;
|
||||
tid.value = setTimeout(function () {
|
||||
// #ifdef APP
|
||||
webviewContext.value = uni.createWebviewContext(id.value, proxy)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
webviewContext.value = document.getElementById(id.value) as HTMLElement
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.createCanvasContextAsync({
|
||||
id: id.value,
|
||||
component: proxy as any,
|
||||
success(canvascontext) {
|
||||
const pixelRatio = uni.getWindowInfo().pixelRatio
|
||||
const context = canvascontext.getContext('2d')
|
||||
const canvas = context?.canvas;
|
||||
canvas.width = canvas?.offsetWidth * pixelRatio
|
||||
canvas.height = canvas?.offsetHeight * pixelRatio
|
||||
context.scale(pixelRatio, pixelRatio)
|
||||
let echartCanvas = {}
|
||||
echartCanvas = new WxCanvas(context, id.value, true, canvas)
|
||||
echarts.setPlatformAPI({
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
}
|
||||
});
|
||||
chart.value = echarts.init(echartCanvas, null, {
|
||||
width: boxWidth.value,
|
||||
height: boxHeight.value,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
echartCanvas.setChart(chart.value);
|
||||
chart.value.on('mousedown', (e) => {
|
||||
dipcatchEvents.value.forEach(fun => {
|
||||
const datas = e;
|
||||
delete (datas as any).event;
|
||||
delete (datas as any).encode;
|
||||
delete (datas as any).encode;
|
||||
fun(datas)
|
||||
})
|
||||
})
|
||||
echartCanvasObj.value = echartCanvas
|
||||
realLoaded.value = true;
|
||||
drawer()
|
||||
emits("init", chart.value)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}, 50);
|
||||
})
|
||||
}
|
||||
|
||||
function onAddlisentMesage() {
|
||||
// #ifdef WEB
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.data.iframeId == id.value && event.data.action == 'onJSBridgeReady') {
|
||||
clearTimeout(tid2.value)
|
||||
tid2.value = setTimeout(function () {
|
||||
realLoaded.value = true;
|
||||
drawer()
|
||||
emits("init", null)
|
||||
}, 50);
|
||||
}
|
||||
if (event.data.iframeId == id.value && event.data.action == 'click') {
|
||||
let eventId = event.data.eventId
|
||||
let filterEvents = dipcatchEvents.value.get(eventId)
|
||||
if (filterEvents != null) {
|
||||
let evt = filterEvents!;
|
||||
evt(JSON.parse(event.data.data))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
function onMessage(event : WebViewMessageEvent) {
|
||||
let msgdatas = event.detail.data
|
||||
// #ifdef APP-ANDROID
|
||||
if (msgdatas.length > 0) {
|
||||
let msg = msgdatas[0]! as UTSJSONObject
|
||||
|
||||
let ac = msg!.getString("action") as string;
|
||||
if (ac == 'img') {
|
||||
let imgbase64 = msg!.getString("url") as string;
|
||||
console.log(imgbase64)
|
||||
} else if (ac == "onJSBridgeReady") {
|
||||
|
||||
|
||||
} else if (ac == 'click') {
|
||||
|
||||
let eventId = msg!.getString("eventId") as string
|
||||
let filterEvents = dipcatchEvents.value.get(eventId)
|
||||
if (filterEvents != null) {
|
||||
let handler = filterEvents! as eventsType;
|
||||
let datas = msg!.getString("data")
|
||||
|
||||
if (datas == null || datas == '') {
|
||||
handler({} as UTSJSONObject)
|
||||
} else {
|
||||
let eventData = JSON.parse(datas!)! as UTSJSONObject
|
||||
handler(eventData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-IOS || APP-HARMONY
|
||||
if (msgdatas.length > 0) {
|
||||
let msg = msgdatas[0]
|
||||
let ac = msg["action"] as string;
|
||||
if (ac == 'img') {
|
||||
let imgbase64 = msg['url'] as string;
|
||||
console.log(imgbase64)
|
||||
} else if (ac == "onJSBridgeReady") {
|
||||
|
||||
} else if (ac == 'click') {
|
||||
|
||||
let eventId = msg['eventId'] as string
|
||||
let filterEvents = dipcatchEvents.value.get(eventId)
|
||||
if (filterEvents != null) {
|
||||
let handler = filterEvents! as eventsType;
|
||||
let datas = msg['data']
|
||||
|
||||
if (datas == null || datas == '') {
|
||||
handler({} as UTSJSONObject)
|
||||
} else {
|
||||
let eventData = JSON.parse(datas!)! as UTSJSONObject
|
||||
handler(eventData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function wrapTouch(event) {
|
||||
for (let i = 0; i < event.touches.length; ++i) {
|
||||
const touch = event.touches[i];
|
||||
touch.offsetX = touch.x;
|
||||
touch.offsetY = touch.y;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
function touchStart(e) {
|
||||
if (chart.value && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.value.getZr().handler;
|
||||
handler.dispatch('mousedown', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopImmediatePropagation: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopImmediatePropagation: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'start');
|
||||
}
|
||||
}
|
||||
|
||||
function touchMove(e) {
|
||||
if (chart.value && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.value.getZr().handler;
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopImmediatePropagation: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'change');
|
||||
}
|
||||
}
|
||||
|
||||
function touchEnd(e) {
|
||||
if (chart.value) {
|
||||
const touch = e.changedTouches ? e.changedTouches[0] : {};
|
||||
var handler = chart.value.getZr().handler;
|
||||
handler.dispatch('mouseup', {
|
||||
zrX: (touch as any).x,
|
||||
zrY: (touch as any).y,
|
||||
preventDefault: () => { },
|
||||
stopImmediatePropagation: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('click', {
|
||||
zrX: (touch as any).x,
|
||||
zrY: (touch as any).y,
|
||||
preventDefault: () => { },
|
||||
stopImmediatePropagation: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'end');
|
||||
}
|
||||
}
|
||||
const touchmove = touchMove
|
||||
const touchend = touchEnd
|
||||
|
||||
function setEcharts(e : any) {
|
||||
echarts = e;
|
||||
echarts.registerPreprocessor((option : any) => {
|
||||
if (option && option.series) {
|
||||
if (option.series.length > 0) {
|
||||
option.series.forEach((series : any) => {
|
||||
series.progressive = 0;
|
||||
});
|
||||
}
|
||||
else if (typeof option.series === 'object') {
|
||||
(option.series as any).progressive = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
getNodeInfo()
|
||||
}
|
||||
|
||||
function parseJsonWithFunction(jsonString) {
|
||||
if (typeof jsonString !== 'string') {
|
||||
return jsonString;
|
||||
}
|
||||
const obj = JSON.parse(jsonString, function (k, v) {
|
||||
var isFunctionStr =
|
||||
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*\}\s*$/.test(v) ||
|
||||
/^\s*\([^)]*\)\s*=>\s*\{[\s\S]*\}\s*$/.test(v) ||
|
||||
/^\s*\([^)]*\)\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
|
||||
/^\s*[a-zA-Z0-9_$]+\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
|
||||
/^\s*[a-zA-Z0-9_$]+\s*=>\s*\{[\s\S]*\}\s*$/.test(v);
|
||||
if (typeof v === 'string' && isFunctionStr) {
|
||||
const funcMatch = v.match(/function\s*\(([^)]*)\)\s*\{([\s\S]*)\}\s*$/);
|
||||
if (funcMatch) {
|
||||
const params = funcMatch[1].split(',').map(p => p.trim());
|
||||
const body = funcMatch[2].trim();
|
||||
v = new Function(...params, body);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
function setoptsByWx(opts : any) {
|
||||
if (!opts) return;
|
||||
if (typeof opts == 'string') {
|
||||
const dataopts = parseJsonWithFunction(opts);
|
||||
if (dataopts && dataopts != null && typeof dataopts == 'object') {
|
||||
chart.value.setOption(dataopts)
|
||||
}
|
||||
} else {
|
||||
chart.value.setOption(opts)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
function setOptions(opts : any) {
|
||||
if (!realLoaded.value) {
|
||||
uni.showToast({ title: "未初始化完成", icon: 'none' })
|
||||
return;
|
||||
}
|
||||
// #ifdef WEB
|
||||
eventJsCall('chart_setOption', `${opts}`)
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
eventJsCall('chart_setOption', `'${opts as string}'`)
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
setoptsByWx(opts)
|
||||
// #endif
|
||||
}
|
||||
|
||||
function getImg() {
|
||||
eventJsCall('EchartImg', '')
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function appWebViewLoaded() {
|
||||
// #ifdef APP-ANDROID||APP-IOS
|
||||
realLoaded.value = true;
|
||||
drawer()
|
||||
emits("init", null)
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
setTimeout(function () {
|
||||
realLoaded.value = true;
|
||||
drawer()
|
||||
emits("init", null)
|
||||
}, 150);
|
||||
// #endif
|
||||
}
|
||||
|
||||
watch(() : string => props.opts, () => {
|
||||
drawer()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef WEB||APP-ANDROID||APP-IOS
|
||||
getNodeInfo()
|
||||
onAddlisentMesage()
|
||||
uni.$on('onResize', onResizeChart)
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
setTimeout(function () {
|
||||
getNodeInfo()
|
||||
onAddlisentMesage()
|
||||
uni.$on('onResize', onResizeChart)
|
||||
}, 100)
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
uni.$off('onResize', onResizeChart)
|
||||
})
|
||||
defineExpose({
|
||||
/**
|
||||
* 设置图表数据,小程序可以直接传图表对象数据。非小程序,需要序列化为字符串再赋值。
|
||||
* @param {string|object} data 小程序是object,非小程序是json序列化的字符串。
|
||||
*/
|
||||
setOptions,
|
||||
/**
|
||||
* 暂未开放
|
||||
*/
|
||||
getImg,
|
||||
// #ifdef MP-WEIXIN
|
||||
/**
|
||||
* 设置图表实例
|
||||
* @param {Echart} ins 微信专用函数。Echart实例
|
||||
*/
|
||||
setEcharts,
|
||||
// #endif
|
||||
eventJsCall,
|
||||
/**
|
||||
* chart对象函数操作
|
||||
* @param {string} funName 第一个参数是方法名如:resize
|
||||
* @param {string} args 方法参数
|
||||
* @param {null} arg 固定为null
|
||||
*/
|
||||
cahrtActions
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xEchart" :style="{ width: _width, height: _height }">
|
||||
<view v-if="!isLoaded"
|
||||
style="width:100%;height:100%;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<x-icon color="primary" :spin="true" name="loader-4-line"></x-icon>
|
||||
</view>
|
||||
<!-- #ifdef APP||WEB -->
|
||||
<web-view @load="appWebViewLoaded" v-else :id="id" :src="`/hybrid/html/local.html?id=${id}`"
|
||||
:style="{ width: '100%', height: '100%', opacity: isLoaded ? 1 : 0 }" @message="onMessage"></web-view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas v-else :id="id" @touchstart="touchStart" @touchmove="touchmove" @touchend="touchend"
|
||||
:style="{ width: boxWidth + 'px', height: boxHeight + 'px' }"></canvas>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,785 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
// #ifdef MP
|
||||
import { Marked } from "./marked.uts"
|
||||
// #endif
|
||||
type DATATYP = {
|
||||
action : string
|
||||
}
|
||||
type xEditeListType = "ordered" | "bullet" | "unchecked" | "checked" | ""
|
||||
type xEditeAlign = "center" | "left" | "right" | ""
|
||||
type xEditeOptsType = {
|
||||
img : string,
|
||||
link : string,
|
||||
b : boolean,
|
||||
i : boolean,
|
||||
s : boolean,
|
||||
u : boolean,
|
||||
align : xEditeAlign,
|
||||
list : xEditeListType,
|
||||
indent : number,
|
||||
header : number,
|
||||
color : string,
|
||||
background : string,
|
||||
size : string
|
||||
}
|
||||
type xEditeListItemType = {
|
||||
name : xEditeListType,
|
||||
icon : string
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 富文本编辑器 xEditor
|
||||
* @description 传递正常markdown或者html内容即可,传递markdown时会自动转换为html,如果直接传递html不会转换直接赋值.
|
||||
* 值得注意的是:在微信小程序端它是没有样式高亮显示的.其它平台有样式指示,这是因为受限于微信官方本身就不支持.
|
||||
* 另外我测试发现HBX4.53 sdk ios端的输入框焦点有问题,导致无法设置样式,待官方修复。
|
||||
* @page /pages/biaodan/editor
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: ("xEdte-" + getUid()) as string,
|
||||
webviewContext: null as WebviewContext | null,
|
||||
isLoaded: false,
|
||||
boxWidth: 10,
|
||||
boxHeight: 0,
|
||||
tid: 0,
|
||||
tid2: 0,
|
||||
realLoaded: false,
|
||||
isMp: false,
|
||||
|
||||
optsStatus: {
|
||||
img: '',
|
||||
link: '',
|
||||
b: false,
|
||||
i: false,
|
||||
s: false,
|
||||
u: false,
|
||||
align: '',
|
||||
list: '',
|
||||
indent: 0,
|
||||
header: 0,
|
||||
color: '',
|
||||
background: '',
|
||||
size: ''
|
||||
} as xEditeOptsType,
|
||||
|
||||
listDataItems: [
|
||||
{ icon: 'list-ordered', name: 'ordered' },
|
||||
{ icon: 'list-unordered', name: 'bullet' },
|
||||
{ icon: 'list-check-2', name: 'unchecked' },
|
||||
{ icon: 'list-check-3', name: 'checked' },
|
||||
] as xEditeListItemType[],
|
||||
|
||||
|
||||
// #ifdef MP
|
||||
markdownObj: new Marked(),
|
||||
htmlMpContent: "",
|
||||
editorCtx: null
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 特定的a,img标签被点击触发,小程序不支持,其它平台支持.
|
||||
* @return {Object<{text,tag,attr}>}
|
||||
*/
|
||||
'tagClick',
|
||||
/**
|
||||
* 是否初始化成功
|
||||
*/
|
||||
'init',
|
||||
/**
|
||||
* 需要通过ref函数调用getHtml才会触发此函数
|
||||
*/
|
||||
'getValue'
|
||||
],
|
||||
props: {
|
||||
/**
|
||||
* 窗口宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 窗口高,可以传递所支持的任意单位高.
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: '350'
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否启用纯html渲染。如果你的内容含有特殊字符比如:%,^&%这种不要出现在里面
|
||||
* 此时你启用isHtml会经过数据处理直接跳过插件,直接赋值内容到html.就不要启用Markdown了.
|
||||
*/
|
||||
isHtml: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 富文本的style样式,不可以动态更改.
|
||||
* 为了对齐所有端,默认已经把所有平台的样式删除.因此你可以自己设置默认样式来对齐所有平台.
|
||||
*/
|
||||
nodeStyle: {
|
||||
type: String,
|
||||
default: "line-height:1.6;color:#000"
|
||||
},
|
||||
/**
|
||||
* 同上,暗黑时的样式.
|
||||
*/
|
||||
nodeDarkStyle: {
|
||||
type: String,
|
||||
default: "line-height:1.6;color:#fff"
|
||||
},
|
||||
/**
|
||||
* 默认的按钮背景色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "#f5f5f5"
|
||||
},
|
||||
/**
|
||||
* 激活时的选中背景色
|
||||
* 空值取全局
|
||||
*/
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 自定义默认的文字/背景颜色
|
||||
*/
|
||||
customColos: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => ['#ff0000', '#ff00ff', '#00ff00', '#00ffff', '#ffff00', '#ffffff', '#000000'] as string[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_customColos() : string[] {
|
||||
return this.customColos
|
||||
},
|
||||
_color() : string {
|
||||
if (this._isDark) return "#222222"
|
||||
return getDefaultColor(this.color)
|
||||
},
|
||||
_activeColor() : string {
|
||||
return getDefaultColor(this.activeColor == '' ? 'primary' : this.activeColor)
|
||||
},
|
||||
|
||||
_width() : string {
|
||||
return checkIsCssUnit(this.width, xConfig.unit)
|
||||
},
|
||||
_height() : string {
|
||||
return checkIsCssUnit(this.height, xConfig.unit)
|
||||
},
|
||||
_value() : string {
|
||||
return this.value
|
||||
},
|
||||
_nodeStyle() : string {
|
||||
return xConfig.dark == 'dark' ? this.nodeDarkStyle : this.nodeStyle
|
||||
},
|
||||
_isDark() : boolean {
|
||||
return xConfig.dark == 'dark'
|
||||
}
|
||||
|
||||
|
||||
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
// #ifdef APP||WEB
|
||||
this.drawer(this._value, this.isHtml)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
this.setContent(this.value);
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let t = this;
|
||||
// #ifdef MP
|
||||
t.isMp = true;
|
||||
// #endif
|
||||
t.isLoaded = true;
|
||||
this.tid = setTimeout(function () {
|
||||
// #ifdef APP
|
||||
t.webviewContext = uni.createWebviewContext(t.id, t);
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
t.webviewContext = document.getElementById(t.id) as HTMLElement;
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
t.mpOnInit()
|
||||
// #endif
|
||||
}, 50);
|
||||
|
||||
this.onAddlisentMesage();
|
||||
|
||||
|
||||
},
|
||||
beforeMount() {
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
beforeUnmount() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
// #ifdef MP
|
||||
|
||||
mpOnInit() {
|
||||
let _this = this;
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#editor').context((res) => {
|
||||
console.log(res)
|
||||
_this.editorCtx = res.context
|
||||
_this.realLoaded = true;
|
||||
_this.setContent(_this.value);
|
||||
/**
|
||||
* 图表加载初始化完成后触发此事件。
|
||||
*/
|
||||
this.$emit("init")
|
||||
}).exec()
|
||||
},
|
||||
setContent(str) {
|
||||
if (!this.realLoaded) {
|
||||
uni.showToast({ title: "未初始化完成", icon: 'none' })
|
||||
return;
|
||||
}
|
||||
if (this.isHtml) {
|
||||
this.htmlMpContent = str
|
||||
} else {
|
||||
const htmlcontent = this.markdownObj.parse(str)
|
||||
this.htmlMpContent = htmlcontent
|
||||
}
|
||||
console.log(this.htmlMpContent)
|
||||
this.editorCtx.setContents({ html: this.htmlMpContent })
|
||||
},
|
||||
onItemClick(event) {
|
||||
console.log(event)
|
||||
},
|
||||
|
||||
onfocus(event) {
|
||||
// console.log(this.editorCtx)
|
||||
},
|
||||
onstatuschange(event) {
|
||||
const detail = event.detail || {}
|
||||
let key = ''
|
||||
let value = ''
|
||||
|
||||
|
||||
if (Object(detail).hasOwnProperty('bold')) {
|
||||
this.optsStatus.b = value as boolean;
|
||||
} else if (Object(detail).hasOwnProperty('underline')) {
|
||||
this.optsStatus.u = value as boolean;
|
||||
} else if (Object(detail).hasOwnProperty('italic')) {
|
||||
this.optsStatus.italic = value as boolean;
|
||||
} else if (Object(detail).hasOwnProperty('header')) {
|
||||
this.optsStatus.header = value as number;
|
||||
console.log(detail)
|
||||
} else if (Object(detail).hasOwnProperty('align')) {
|
||||
this.optsStatus.align = value;
|
||||
} else if (Object(detail).hasOwnProperty('list')) {
|
||||
this.optsStatus.list = value as xEditeListType;
|
||||
} else if (Object(detail).hasOwnProperty('background')) {
|
||||
this.optsStatus.list = value as string;
|
||||
} else if (Object(detail).hasOwnProperty('size')) {
|
||||
this.optsStatus.size = value as string;
|
||||
}
|
||||
},
|
||||
setFormart(key, value) {
|
||||
const eobj = {
|
||||
'b': 'bold', //true,false
|
||||
'i': 'italic', //true,false
|
||||
's': 'strike', //true,false
|
||||
'u': 'underline', //true,false
|
||||
'link': 'link',
|
||||
'img': 'image',
|
||||
'video': 'video',
|
||||
'align': 'align', //left,right,center
|
||||
/**
|
||||
* ordered:有序列表(编号列表)
|
||||
* bullet:无序列表(项目符号列表)
|
||||
* unchecked:未选中的复选框列表项
|
||||
* checked:选中的复选框列表项
|
||||
*/
|
||||
'list': 'list',
|
||||
'blockquote': 'blockquote', //true,false
|
||||
'indent': 'indent', //缩进值0-8
|
||||
'color': 'color',
|
||||
'background': 'backgroundColor',
|
||||
'size': 'fontSize',
|
||||
'script': 'script', //super,sub
|
||||
'header': 'header', //1-6
|
||||
}
|
||||
console.log(eobj[key], value)
|
||||
this.editorCtx?.format(eobj[key], value)
|
||||
},
|
||||
// #endif
|
||||
// h5端
|
||||
onAddlisentMesage() {
|
||||
// #ifdef WEB
|
||||
let t = this;
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.data.iframeId == t.id && event.data.action == 'onJSBridgeReady') {
|
||||
clearTimeout(t.tid2)
|
||||
t.tid2 = setTimeout(function () {
|
||||
t.realLoaded = true;
|
||||
t.drawer(t._value, t.isHtml)
|
||||
t.$emit("init")
|
||||
t.eventJsCall('setBodyStyle', t._nodeStyle)
|
||||
}, 50);
|
||||
}
|
||||
|
||||
if (event.data.iframeId == t.id && event.data.action == 'offsetHeight') {
|
||||
t.boxHeight = event.data.data + 20
|
||||
|
||||
} else if (event.data.iframeId == t.id && event.data.action == 'toValue') {
|
||||
t.$emit('getValue', event.data.data)
|
||||
} else if (event.data.iframeId == t.id && event.data.action == 'click') {
|
||||
let dataStr = JSON.stringify(event.data.data);
|
||||
let dataJson = JSON.parseObject(dataStr)!
|
||||
t.$emit('tagClick', dataJson)
|
||||
} else if (event.data.iframeId == t.id && event.data.action == 'fontStyleOpts') {
|
||||
let localOptsNow = event.data.data as xEditeOptsType;
|
||||
t.optsStatus = localOptsNow;
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
|
||||
onMessage(event : WebViewMessageEvent) {
|
||||
let t = this;
|
||||
|
||||
let msgdatas = event.detail.data
|
||||
|
||||
if (msgdatas.length == 0) return;
|
||||
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
if (msgdatas.length > 0) {
|
||||
let dataStr = JSON.stringify(event.detail);
|
||||
let dataJson = JSON.parseObject(dataStr)!
|
||||
let msgeAr = dataJson.getArray<UTSJSONObject>('data')!
|
||||
|
||||
let msg = msgeAr[0]!
|
||||
|
||||
let ac = msg["action"] as string;
|
||||
if (ac == 'offsetHeight') {
|
||||
// const h = msg["data"]! as number;
|
||||
// t.boxHeight = h + 25
|
||||
} else if (ac == 'toValue') {
|
||||
t.$emit('getValue', msg["data"]! as string)
|
||||
} else if (ac == 'click') {
|
||||
|
||||
t.$emit('tagClick', msg['data']! as UTSJSONObject)
|
||||
} else if (ac == 'fontStyleOpts') {
|
||||
|
||||
let datamsg = JSON.stringify(msg['data']!)!;
|
||||
let localOptsNow = JSON.parseObject<xEditeOptsType>(datamsg! as string)! as xEditeOptsType;
|
||||
t.optsStatus = localOptsNow;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-IOS
|
||||
if (msgdatas.length > 0) {
|
||||
let msg = msgdatas[0]
|
||||
let ac = msg['action'] as string;
|
||||
if (ac == 'offsetHeight') {
|
||||
// t.boxHeight = (msg['data']! as Number) + 25
|
||||
} else if (ac == 'toValue') {
|
||||
t.$emit('getValue', msg['data'])
|
||||
} else if (ac == 'click') {
|
||||
let dataStr = JSON.stringify(msg['data']);
|
||||
let dataJson = JSON.parseObject(dataStr)!
|
||||
t.$emit('tagClick', dataJson)
|
||||
} else if (ac == 'fontStyleOpts') {
|
||||
let localOptsNow = msg['data']! as xEditeOptsType;
|
||||
t.optsStatus = localOptsNow;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
},
|
||||
|
||||
drawer(value : string, isHtml : boolean) {
|
||||
if (!this.realLoaded) {
|
||||
uni.showToast({ title: "未初始化完成", icon: 'none' })
|
||||
return;
|
||||
}
|
||||
// #ifdef WEB
|
||||
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(value), render: true }))
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
this.eventJsCall('markdown', JSON.stringify({ value: encodeURIComponent(value), render: true }))
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
|
||||
this.eventJsCall('markdown', JSON.stringify({ value: isHtml ? btoa(encodeURIComponent(value)!) : value, render: isHtml ? 'android' : '' }))
|
||||
// #endif
|
||||
|
||||
},
|
||||
|
||||
eventJsCall(callfun : string, str : string) {
|
||||
// #ifdef WEB
|
||||
var iframe = document.getElementById(this.id);
|
||||
if (!iframe) return;
|
||||
if ((iframe.contentWindow[callfun] || null)) {
|
||||
iframe.contentWindow[callfun](str, this.isHtml);
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
this.webviewContext?.evalJS(`${callfun}(${str},${this.isHtml})`)
|
||||
// #endif
|
||||
|
||||
},
|
||||
|
||||
setEventContent(callanem : string, value : string, ishtml : boolean) {
|
||||
if (!this.realLoaded) {
|
||||
uni.showToast({ title: "未初始化完成", icon: 'none' })
|
||||
return;
|
||||
}
|
||||
const _this = this;
|
||||
function evenjscall(callfun : string, str : string, isHtml : boolean) {
|
||||
// #ifdef WEB
|
||||
var iframe = document.getElementById(_this.id);
|
||||
if (!iframe) return;
|
||||
if ((iframe.contentWindow[callfun] || null)) {
|
||||
iframe.contentWindow[callfun](str, isHtml);
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_this.webviewContext?.evalJS(`${callfun}(${str},${isHtml})`)
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
// #ifdef WEB
|
||||
evenjscall(callanem, JSON.stringify({ value: encodeURIComponent(value), render: true }), ishtml)
|
||||
// #endif
|
||||
// #ifdef APP-IOS || APP-HARMONY
|
||||
evenjscall(callanem, JSON.stringify({ value: encodeURIComponent(value), render: true }), ishtml)
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
let rendervalue = (ishtml ? 'android' : '') as string
|
||||
let cvalue = (ishtml ? btoa(encodeURIComponent(value)!) : value) as any
|
||||
let resultValue = JSON.stringify({ value: cvalue, render: rendervalue })! as string
|
||||
evenjscall(callanem, resultValue, ishtml)
|
||||
// #endif
|
||||
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取html内容。注意本函数不会返回内容,你要通过事件getValue得到html内容.
|
||||
* @public
|
||||
*/
|
||||
getHtml() {
|
||||
// #ifdef WEB
|
||||
var iframe = document.getElementById(this.id);
|
||||
if (!iframe) return;
|
||||
|
||||
iframe.contentWindow['getHtml']()
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
this.webviewContext?.evalJS(`getHtml()`)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
this.$emit('getValue', this.htmlMpContent)
|
||||
// #endif
|
||||
},
|
||||
appWebViewLoaded() {
|
||||
this.realLoaded = true;
|
||||
this.drawer(this._value, this.isHtml)
|
||||
/**
|
||||
* 图表加载初始化完成后触发此事件。
|
||||
*/
|
||||
this.$emit("init")
|
||||
// setBodyStyle
|
||||
this.eventJsCall('setBodyStyle', this._nodeStyle)
|
||||
},
|
||||
/**
|
||||
* 获取选区
|
||||
*/
|
||||
getSelected() {
|
||||
|
||||
},
|
||||
setFontStyle(key : string, value : boolean | number | string) {
|
||||
// #ifndef MP
|
||||
this.setEventContent('setSelectedStyle', JSON.stringify({ key: key, value: value }), true)
|
||||
this.$nextTick(() => {
|
||||
if (key == 'b') {
|
||||
this.optsStatus.b = value as boolean;
|
||||
} else if (key == 'u') {
|
||||
this.optsStatus.u = value as boolean;
|
||||
} else if (key == 'i') {
|
||||
this.optsStatus.i = value as boolean;
|
||||
} else if (key == 's') {
|
||||
this.optsStatus.s = value as boolean;
|
||||
} else if (key == 'header') {
|
||||
this.optsStatus.header = value as number;
|
||||
} else if (key == 'align') {
|
||||
this.optsStatus.align = value as xEditeAlign;
|
||||
} else if (key == 'list') {
|
||||
this.optsStatus.list = value as xEditeListType;
|
||||
} else if (key == 'background') {
|
||||
this.optsStatus.list = value as string;
|
||||
} else if (key == 'size') {
|
||||
this.optsStatus.size = value as string;
|
||||
}
|
||||
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
this.setFormart(key, value)
|
||||
console.log(key)
|
||||
// #endif
|
||||
|
||||
},
|
||||
|
||||
getBgColor(isActive : boolean) : string {
|
||||
if (isActive) return this._activeColor;
|
||||
return this._color
|
||||
},
|
||||
getFontColor(isActive : boolean) : string {
|
||||
if (isActive || this._isDark) return "#ffffff";
|
||||
return "#333333"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xEdite" :style="{width:_width,height:_height}">
|
||||
<view v-if="!isLoaded"
|
||||
style="width:100%;height:100%;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<x-icon color="primary" :spin="true" name="loader-4-line"></x-icon>
|
||||
</view>
|
||||
|
||||
<view class="FontTabsGroup">
|
||||
<view class="fontTabs" style="flex:1;margin-right: 2px;">
|
||||
<view @click="setFontStyle('b',!optsStatus.b)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.b)}">
|
||||
<x-icon :color="getFontColor(optsStatus.b)" name="bold"></x-icon>
|
||||
</view>
|
||||
<view @click="setFontStyle('i',!optsStatus.i)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.i)}">
|
||||
<x-icon :color="getFontColor(optsStatus.i)" name="italic"></x-icon>
|
||||
</view>
|
||||
<view @click="setFontStyle('u',!optsStatus.u)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.u)}">
|
||||
<x-icon :color="getFontColor(optsStatus.u)" name="underline"></x-icon>
|
||||
</view>
|
||||
<view @click="setFontStyle('s',!optsStatus.s)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.s)}">
|
||||
<x-icon :color="getFontColor(optsStatus.s)" name="strikethrough"></x-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="fontTabs" style="width: 36px;align-self: stretch;margin-right: 2px;">
|
||||
<x-popover position="bc" style="flex:1;align-self: stretch;">
|
||||
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.size!='')}">
|
||||
<x-icon :color="getFontColor(optsStatus.size!='')" name="font-size"></x-icon>
|
||||
</view>
|
||||
<template #menu>
|
||||
<view style="width:110px">
|
||||
<view v-for="item in ['12px', '14px', '16px', '18px', '24px']" :key="item"
|
||||
@click="setFontStyle('size',optsStatus.size==item?'':item)" class="fontTabsCirlItem"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.size==item)}">
|
||||
<x-text :color="getFontColor(optsStatus.size==item)">{{item}}</x-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
</x-popover>
|
||||
</view>
|
||||
|
||||
<view class="fontTabs" style="width: 120px;align-self: stretch;">
|
||||
|
||||
<x-popover position="bc" style="flex:1;align-self: stretch;">
|
||||
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.background!='')}">
|
||||
<view class="fontTabsCirl"
|
||||
:style="{backgroundColor:optsStatus.background==''?'transparent':optsStatus.background}">
|
||||
</view>
|
||||
<x-icon :color="getFontColor(optsStatus.background!='')" name="drop-fill"></x-icon>
|
||||
</view>
|
||||
<template #menu>
|
||||
<view style="width:110px">
|
||||
<view @click="setFontStyle('background',item)" v-for="(item,index) in _customColos"
|
||||
:key="index" class="fontTabsCirlItem"
|
||||
:style="{backgroundColor:_isDark?'#222222':'#ffffff'}">
|
||||
<view class="fontTabsCirl2" :style="{backgroundColor:item}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
</x-popover>
|
||||
|
||||
<x-popover position="br" style="flex:1;align-self: stretch;">
|
||||
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.color!='')}">
|
||||
<view class="fontTabsCirl"
|
||||
:style="{backgroundColor:optsStatus.color==''?'transparent':optsStatus.color}"></view>
|
||||
<x-icon :color="getFontColor(optsStatus.color!='')" name="font-color"></x-icon>
|
||||
</view>
|
||||
<template #menu>
|
||||
<view style="width:110px">
|
||||
<view @click="setFontStyle('color',item)" v-for="(item,index) in _customColos" :key="index"
|
||||
class="fontTabsCirlItem" :style="{backgroundColor:_isDark?'#222222':'#ffffff'}">
|
||||
<view class="fontTabsCirl2" :style="{backgroundColor:item}"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
</x-popover>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="FontTabsGroup">
|
||||
|
||||
<view class="fontTabs" style="width: 36px;align-self: stretch;margin-right: 2px;">
|
||||
<x-popover position="bl" style="flex:1;align-self: stretch;">
|
||||
<view class="xEditorBtns" :style="{backgroundColor:getBgColor(optsStatus.header!=0)}">
|
||||
<x-icon :color="getFontColor(optsStatus.header!=0)" name="heading"></x-icon>
|
||||
</view>
|
||||
<template #menu>
|
||||
<view style="width:110px">
|
||||
<view v-for="item in 6" :key="item"
|
||||
@click="setFontStyle('header',optsStatus.header==item?0:item)" class="fontTabsCirlItem"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.header==item)}">
|
||||
<x-icon :color="getFontColor(optsStatus.s)" :name="`h-${item}`"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
</x-popover>
|
||||
|
||||
</view>
|
||||
<view class="fontTabs" style="flex:1;margin-right: 2px;">
|
||||
<view v-for="(item,index) in ['left','center','right']" :key="index"
|
||||
@click="setFontStyle('align',optsStatus.align==item?'':item)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.align==item)}">
|
||||
<x-icon :color="getFontColor(optsStatus.align==item)" :name="`align-${item}`"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fontTabs" style="align-self: stretch;flex:1">
|
||||
<view v-for="(item,index) in listDataItems" :key="index"
|
||||
@click="setFontStyle('list',optsStatus.list==item.name?'':item.name)" class="xEditorBtns"
|
||||
:style="{backgroundColor:getBgColor(optsStatus.list==item.name)}">
|
||||
<x-icon :color="getFontColor(optsStatus.list==item.name)" :name="item.icon"></x-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: 8px;"></view>
|
||||
|
||||
<view style="flex:1;">
|
||||
|
||||
<!-- #ifdef APP||WEB -->
|
||||
<web-view v-if="isLoaded" :horizontalScrollBarAccess="true" :verticalScrollBarAccess="false"
|
||||
class="xMarkdownNoevents" @load="appWebViewLoaded" :id="id" src="/hybrid/html/edite.html"
|
||||
:style="{width:'100%',height:'100%',opacity:isLoaded?1:0}" @message="onMessage"></web-view>
|
||||
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP -->
|
||||
<editor class="xMarkdownNoevents" id="editor" @ready="mpOnInit" @itemclick="onItemClick" @focus="onfocus"
|
||||
@statuschange="onstatuschange" :selectable="true" placeholder="请输入"
|
||||
:style="[{width:'100%',height:'100%'},_nodeStyle]"></editor>
|
||||
<!-- #endif -->
|
||||
<!-- 兼容安卓。webview到4.19+页面无法滚动 -->
|
||||
<!-- <view v-if="!_edite" class="xMarkdownAndrod"></view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xEdite{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.xEditorBtns {
|
||||
height: 40px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.fontTabsCirl {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #b2b2b2;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fontTabsCirl2 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #b2b2b2;
|
||||
}
|
||||
|
||||
.fontTabsCirlItem {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.fontTabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.FontTabsGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xMarkdownAndrod {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.xMarkdownNoevents {
|
||||
/* pointer-events: none; */
|
||||
width: 100%;
|
||||
/* height: 100%; */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { colors, getDefaultColor, getDefaultColorObj, getTextColorObj, getThinColorObj, getOutlineColorObj } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { toFillMarginAr, checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
/**
|
||||
* @name 空状态 xEmpty
|
||||
* @description 主要用于列表加载页面或者空状态页面时使用。
|
||||
* @page /pages/index/action-menu
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xEmpty"})
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 刷新按钮被点击时触发
|
||||
*/
|
||||
'click'
|
||||
])
|
||||
|
||||
type xEmptyPropsType = {
|
||||
/**
|
||||
* 加载状态
|
||||
*/
|
||||
loading: boolean,
|
||||
/**
|
||||
* 是否为空
|
||||
*/
|
||||
empty: boolean,
|
||||
/**
|
||||
* 错误状态
|
||||
*/
|
||||
error: boolean,
|
||||
/**
|
||||
* 是否有更多数据状态
|
||||
*/
|
||||
more: boolean,
|
||||
/**
|
||||
* 没有数据时的提示,用于加载更多数据时
|
||||
* ,没有更多数据啦
|
||||
*/
|
||||
moreLabel: string,
|
||||
/**
|
||||
* 列表加载出错时,出错啦~
|
||||
*/
|
||||
errorLabel: string,
|
||||
/**
|
||||
* 按钮文本,点击重试
|
||||
*/
|
||||
btnLabel: string,
|
||||
/**
|
||||
* 按钮颜色,默认取全局值
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 按钮文本颜色,默认自动
|
||||
*/
|
||||
btnTextColor: string,
|
||||
/**
|
||||
* 空或者加载出错时的标语,当前没有数据
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 图片路径
|
||||
*/
|
||||
src: string,
|
||||
/**
|
||||
* 是否显示重试按钮
|
||||
*/
|
||||
showBtn: boolean,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xEmptyPropsType>(), {
|
||||
loading: true,
|
||||
empty: false,
|
||||
error: false,
|
||||
more: false,
|
||||
moreLabel: "",
|
||||
errorLabel: "",
|
||||
btnLabel: "",
|
||||
btnColor: "",
|
||||
btnTextColor: "",
|
||||
title: "",
|
||||
src: "/static/tmui4xLibs/static/empty.png",
|
||||
showBtn: true,
|
||||
})
|
||||
|
||||
const _showBtn = computed(() : boolean => {
|
||||
return props.showBtn
|
||||
})
|
||||
const _loading = computed(() : boolean => {
|
||||
return props.loading
|
||||
})
|
||||
const _error = computed(() : boolean => {
|
||||
return props.error
|
||||
})
|
||||
const _more = computed(() : boolean => {
|
||||
return props.more
|
||||
})
|
||||
const _empty = computed(() : boolean => {
|
||||
return props.empty
|
||||
})
|
||||
const _moreLabel = computed(() : string => {
|
||||
if(props.moreLabel == '') return i18n.t("tmui4x.empty.moreLabel")
|
||||
return props.moreLabel
|
||||
})
|
||||
const _errorLabel = computed(() : string => {
|
||||
if(props.errorLabel == '') return i18n.t("tmui4x.empty.errorLabel")
|
||||
return props.errorLabel
|
||||
})
|
||||
const _btnLabel = computed(() : string => {
|
||||
if(props.btnLabel == '') return i18n.t("tmui4x.empty.btnLabel")
|
||||
return props.btnLabel
|
||||
})
|
||||
const _btnColor = computed(() : string => {
|
||||
if (props.btnColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _btnTextColor = computed(() : string => {
|
||||
return getDefaultColor(props.btnTextColor)
|
||||
})
|
||||
const _title = computed(() : string => {
|
||||
if(props.title == '') return i18n.t("tmui4x.empty.title")
|
||||
return props.title
|
||||
})
|
||||
|
||||
function onclick() {
|
||||
/**
|
||||
* 刷新按钮被点击时触发
|
||||
*/
|
||||
emits('click')
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xEmpty">
|
||||
<x-loading v-if="_loading"></x-loading>
|
||||
<view class="xEmptyWrap" v-if="!_loading&&!_more&&(_empty||_error)">
|
||||
<image style="width: 200px;height:200px" :src="src" />
|
||||
<x-text font-size="16" v-if="_empty" style="opacity: 0.5;">{{_title}}</x-text>
|
||||
<x-text font-size="16" v-if="_error" style="opacity: 0.5;">{{_errorLabel}}</x-text>
|
||||
<!--
|
||||
@slot 按钮位置的插槽
|
||||
-->
|
||||
<slot>
|
||||
<view v-if="_showBtn" style="padding-top: 21px;">
|
||||
<x-button @click="onclick" width="150px" :color="_btnColor"
|
||||
:font-color="_btnTextColor">{{_btnLabel}}</x-button>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
<x-text font-size="16" v-if="_more&&!_loading&&!_error&&!_empty"
|
||||
style="padding: 16px;opacity: 0.5;">{{_moreLabel}}</x-text>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xEmpty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.xEmptyWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,899 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
||||
import { getUid } from '../../core/util/xCoreUtil.uts';
|
||||
|
||||
type CHECKPOINT_XY = { x : number | null, y : number | null }
|
||||
type DRect = {
|
||||
left:number,
|
||||
right:number,
|
||||
top:number,
|
||||
bottom:number,
|
||||
width:number,
|
||||
height:number,
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 手势库 xFinger
|
||||
* @description 多方向的手势库,包括旋转,捏合,轻扫,双击等手势
|
||||
* @page /pages/index/finger
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xFinger"})
|
||||
type Props = {
|
||||
/**
|
||||
* 滑动多长距离,识别并触发滑动的方向
|
||||
*/
|
||||
swiperDiff: number
|
||||
/**
|
||||
* 定义双击的时间间隔触发时机
|
||||
*/
|
||||
dbClickDiff: number
|
||||
/**
|
||||
* 定义单击click的时间间隔触发时机
|
||||
*/
|
||||
clickDiff: number
|
||||
/**
|
||||
* 定义长按的时间间隔触发时机
|
||||
*/
|
||||
longDiff: number
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: boolean
|
||||
/**
|
||||
* 移动事件的节流间隔(毫秒),0表示不节流
|
||||
*/
|
||||
throttleDelay: number
|
||||
/**
|
||||
* 是否启用防抖,防止快速连续触发
|
||||
*/
|
||||
debounce: boolean
|
||||
/**
|
||||
* 最小移动距离,小于此距离不触发移动事件
|
||||
*/
|
||||
minMoveDistance: number
|
||||
/**
|
||||
* 连续性坐标初始位置x
|
||||
*/
|
||||
accOffsetX:number,
|
||||
/**
|
||||
* 连续性坐标初始位置y
|
||||
*/
|
||||
accOffsetY:number,
|
||||
/**
|
||||
* 是否启用无障碍访问支持
|
||||
*/
|
||||
accessibility: boolean
|
||||
/**
|
||||
* 无障碍标签
|
||||
*/
|
||||
ariaLabel: string
|
||||
}
|
||||
defineSlots<{
|
||||
/**
|
||||
* 默认插槽default
|
||||
*/
|
||||
default(props:{
|
||||
/**
|
||||
* 当前x坐标
|
||||
*/
|
||||
x:number,
|
||||
/**
|
||||
* 当前y坐标
|
||||
*/
|
||||
y:number,
|
||||
/**
|
||||
* 累积x坐标(连续定位)
|
||||
*/
|
||||
accX:number,
|
||||
/**
|
||||
* 累积y坐标(连续定位)
|
||||
*/
|
||||
accY:number,
|
||||
/**
|
||||
* 当前事件类型click,start,end,dbclick,longPress,swiper,pinch,rotate
|
||||
*/
|
||||
type:string
|
||||
}):any
|
||||
}>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
swiperDiff: 50,
|
||||
dbClickDiff: 300,
|
||||
clickDiff: 50,
|
||||
longDiff: 800,
|
||||
disabled: false,
|
||||
throttleDelay: 16, // 约60fps
|
||||
debounce: false,
|
||||
minMoveDistance: 1,
|
||||
accessibility: true,
|
||||
accOffsetX:0,
|
||||
accOffsetY:0,
|
||||
ariaLabel: '手势识别区域'
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
start: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 触摸移动时触发
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
move: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
end: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 触摸中断
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
cancel: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* doubleClick
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
doubleClick: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 长按事件
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
longPress: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 滑动时触发
|
||||
* @param evt - {x,y,type,width,height,direction:方向UP|DOWN|LEFT|RIGHT}
|
||||
*/
|
||||
swiper: [evt: {x: number, y: number, diffX: number, diffY: number, direction: string, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 单击
|
||||
* @param evt - {x,y,type,width,height}
|
||||
*/
|
||||
click: [evt: {x: number, y: number, type: string, width: number, height: number}]
|
||||
/**
|
||||
* 缩放事件
|
||||
* @description len为两点间的距离,scale为当前的缩放比例(最小为0.1)
|
||||
* @param evt - {x,y,x1,y1,len,scale,type,width,height}
|
||||
*/
|
||||
pinch: [evt: {x: number, y: number, x1: number, y2: number, type: string, width: number, height: number, len: number, scale: number}]
|
||||
/**
|
||||
* 旋转事件
|
||||
* @description len为两点间的距离,angle为当前当前旋转的角度
|
||||
* @param evt - {x,y,x1,y1,len,angle,type,width,height}
|
||||
*/
|
||||
rotate: [evt: {x: number, y: number, x1: number, y2: number, type: string, width: number, height: number, len: number, angle: number}]
|
||||
}>()
|
||||
|
||||
// Reactive data
|
||||
const isMouseDown = ref(false)
|
||||
const wheelScale = ref(1)
|
||||
const wheelDelta = ref(0.1) // 滚轮缩放系数
|
||||
const dubleTime = ref(0)
|
||||
const tid = ref(56)
|
||||
const _x = ref(0)
|
||||
const _y = ref(0)
|
||||
const _start_x = ref(0)
|
||||
const _start_y = ref(0)
|
||||
const eventName = ref('')
|
||||
const mX = ref(0)
|
||||
const mY = ref(0)
|
||||
const swipeDirection = ref("")
|
||||
// 累积定位坐标,保持连续性
|
||||
const accumulatedX = ref(props.accOffsetX)
|
||||
const accumulatedY = ref(props.accOffsetY)
|
||||
// 本次手势开始时的累积坐标
|
||||
const startAccumulatedX = ref(0)
|
||||
const startAccumulatedY = ref(0)
|
||||
const xFingerRef = ref<UniElement | null>(null)
|
||||
const left = ref(0)
|
||||
const top = ref(0)
|
||||
const id = ref("xFinGer" + getUid())
|
||||
const zoomFactor = ref(0.55) // 缩放速率
|
||||
const zoomFactorAb = ref(0.03) // 旋转速率
|
||||
const pinchStartLen = ref(0)
|
||||
const scale = ref(0)
|
||||
const angle = ref(0)
|
||||
const pinth_x = ref(0)
|
||||
const pinth_y = ref(0)
|
||||
const preV = ref<CHECKPOINT_XY>({ x: null, y: null })
|
||||
const parentRect = ref<DRect>({
|
||||
left:0,
|
||||
right:0,
|
||||
top:0,
|
||||
bottom:0,
|
||||
width:0,
|
||||
height:0,
|
||||
})
|
||||
|
||||
// 性能优化相关
|
||||
const lastMoveTime = ref(0)
|
||||
const throttleTimer = ref<number | null>(null)
|
||||
const debounceTimer = ref<number | null>(null)
|
||||
|
||||
// Computed
|
||||
const _disabled = computed(() => props.disabled)
|
||||
|
||||
// Get current instance
|
||||
const instance = getCurrentInstance()?.proxy;
|
||||
|
||||
|
||||
// 工具函数
|
||||
const throttle = (func: Function, delay: number) => {
|
||||
return (...args: any[]) => {
|
||||
const now = Date.now()
|
||||
if (now - lastMoveTime.value >= delay) {
|
||||
lastMoveTime.value = now
|
||||
func.apply(null, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const debounce = (func: Function, delay: number) => {
|
||||
return (...args: any[]) => {
|
||||
if (debounceTimer.value) {
|
||||
clearTimeout(debounceTimer.value)
|
||||
}
|
||||
debounceTimer.value = setTimeout(() => {
|
||||
func.apply(null, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {
|
||||
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
||||
}
|
||||
|
||||
const isGestureValid = (deltaX: number, deltaY: number): boolean => {
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
return distance >= props.minMoveDistance
|
||||
}
|
||||
|
||||
|
||||
// #ifdef WEB
|
||||
// 处理鼠标滚轮事件,模拟缩放功能
|
||||
const handleWheel = (evt: WheelEvent) => {
|
||||
if (_disabled.value) return
|
||||
const delta = evt.deltaY > 0 ? -wheelDelta.value : wheelDelta.value;
|
||||
wheelScale.value = Math.max(0.1, wheelScale.value + delta);
|
||||
emit("pinch", {
|
||||
x: evt.clientX,
|
||||
y: evt.clientY,
|
||||
type: 'pinch',
|
||||
width: parentRect.value.width,
|
||||
height: parentRect.value.height,
|
||||
len: 0,
|
||||
scale: wheelScale.value
|
||||
});
|
||||
}
|
||||
|
||||
// 鼠标事件处理函数
|
||||
const mouseStart = (evt: MouseEvent) => {
|
||||
if (_disabled.value) return
|
||||
isMouseDown.value = true;
|
||||
let rectBox = parentRect.value;
|
||||
_x.value = evt.clientX - rectBox.left;
|
||||
_y.value = evt.clientY - rectBox.top;
|
||||
|
||||
// 重置移动距离,开始新的手势
|
||||
mX.value = 0
|
||||
mY.value = 0
|
||||
|
||||
// 记录本次手势开始时的累积坐标
|
||||
startAccumulatedX.value = accumulatedX.value
|
||||
startAccumulatedY.value = accumulatedY.value
|
||||
|
||||
_start_x.value = evt.clientX - rectBox.left;
|
||||
_start_y.value = evt.clientY - rectBox.top;
|
||||
swipeDirection.value = "";
|
||||
|
||||
emit("start", {
|
||||
x: _start_x.value,
|
||||
y: _start_y.value,
|
||||
type: 'start',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'start';
|
||||
|
||||
// 处理双击
|
||||
let difftime = new Date().getTime() - dubleTime.value;
|
||||
if (difftime > 0 && difftime <= props.dbClickDiff) {
|
||||
emit("doubleClick", {
|
||||
x: _start_x.value,
|
||||
y: _start_y.value,
|
||||
type: 'doubleClick',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'doubleClick';
|
||||
}
|
||||
dubleTime.value = new Date().getTime();
|
||||
|
||||
// 处理长按
|
||||
clearTimeout(tid.value);
|
||||
tid.value = setTimeout(() => {
|
||||
if (isMouseDown.value) {
|
||||
emit("longPress", {
|
||||
x: _start_x.value,
|
||||
y: _start_y.value,
|
||||
type: 'longPress',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'longPress';
|
||||
}
|
||||
}, props.longDiff);
|
||||
}
|
||||
|
||||
const mouseMove = (evt: MouseEvent) => {
|
||||
if (_disabled.value || !isMouseDown.value) return
|
||||
let rectBox = parentRect.value;
|
||||
let x = evt.clientX - rectBox.left;
|
||||
let y = evt.clientY - rectBox.top;
|
||||
|
||||
let deltaX = Math.abs(x);
|
||||
let deltaY = Math.abs(y);
|
||||
|
||||
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
// 在移动过程中实时更新累积坐标(用于插槽输出)
|
||||
accumulatedX.value = startAccumulatedX.value + mX.value
|
||||
accumulatedY.value = startAccumulatedY.value + mY.value
|
||||
|
||||
|
||||
if (deltaX > deltaY && deltaX > props.swiperDiff) {
|
||||
swipeDirection.value = (_x.value > x) ? "left" : "right";
|
||||
} else if (deltaY > deltaX && deltaY > props.swiperDiff) {
|
||||
swipeDirection.value = (_y.value < y) ? "down" : "up";
|
||||
}
|
||||
|
||||
if (swipeDirection.value != "") {
|
||||
emit("swiper", {
|
||||
x: mX.value,
|
||||
y: mY.value,
|
||||
diffX: deltaX,
|
||||
diffY: deltaY,
|
||||
direction: swipeDirection.value,
|
||||
type: 'swiper',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'swiper';
|
||||
}
|
||||
|
||||
emit("move", {
|
||||
x: mX.value,
|
||||
y: mY.value,
|
||||
type: 'move',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'move';
|
||||
clearTimeout(tid.value);
|
||||
}
|
||||
|
||||
const mouseEnd = (evt: MouseEvent) => {
|
||||
if (_disabled.value) return
|
||||
|
||||
let rectBox = parentRect.value;
|
||||
let x = evt.clientX - rectBox.left;
|
||||
let y = evt.clientY - rectBox.top;
|
||||
|
||||
let deltaX = Math.abs(x);
|
||||
let deltaY = Math.abs(y);
|
||||
|
||||
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
// 更新累积坐标(手势结束时累加本次移动的偏移量)
|
||||
accumulatedX.value = startAccumulatedX.value + mX.value
|
||||
accumulatedY.value = startAccumulatedY.value + mY.value
|
||||
|
||||
emit("end", {
|
||||
x: mX.value,
|
||||
y: mY.value,
|
||||
type: 'end',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
|
||||
eventName.value = 'end';
|
||||
if (new Date().getTime() - dubleTime.value > props.clickDiff) {
|
||||
emit("click", {
|
||||
x: mX.value,
|
||||
y: mY.value,
|
||||
type: 'click',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
});
|
||||
eventName.value = 'click';
|
||||
}
|
||||
|
||||
isMouseDown.value = false;
|
||||
|
||||
// 重置所有状态,为下次触摸做准备
|
||||
swipeDirection.value = ""
|
||||
scale.value = 0
|
||||
angle.value = 0
|
||||
}
|
||||
|
||||
const mouseCancel = (evt: MouseEvent) => {
|
||||
if (_disabled.value) return
|
||||
let rectBox = parentRect.value;
|
||||
let x = evt.clientX - rectBox.left;
|
||||
let y = evt.clientY - rectBox.top;
|
||||
|
||||
let deltaX = Math.abs(x);
|
||||
let deltaY = Math.abs(y);
|
||||
|
||||
// mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
// mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
// emit("cancel", {
|
||||
// x: mX.value,
|
||||
// y: mY.value,
|
||||
// type: 'cancel',
|
||||
// width: rectBox.width,
|
||||
// height: rectBox.height
|
||||
// });
|
||||
eventName.value = 'cancel';
|
||||
isMouseDown.value = false;
|
||||
|
||||
// 重置所有状态,为下次触摸做准备
|
||||
swipeDirection.value = ""
|
||||
scale.value = 0
|
||||
angle.value = 0
|
||||
}
|
||||
|
||||
// #endif
|
||||
const getLen = (v : CHECKPOINT_XY) : number => {
|
||||
if (v.x == null || v.y == null) return 0
|
||||
return Math.hypot(v.x!, v.y!)
|
||||
}
|
||||
|
||||
const dot = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
|
||||
if (v1.x == null || v1.y == null || v2.x == null || v2.y == null) return 0
|
||||
return v1.x! * v2.x! + v1.y! * v2.y!;
|
||||
}
|
||||
|
||||
const getRectBox = (call:(rect:DRect)=>void) => {
|
||||
try {
|
||||
if (xFingerRef.value == null) {
|
||||
console.warn('x-finger: Element not found')
|
||||
return
|
||||
}
|
||||
|
||||
let rectBox = {
|
||||
left:0,
|
||||
right:0,
|
||||
top:0,
|
||||
bottom:0,
|
||||
width:0,
|
||||
height:0,
|
||||
} as DRect
|
||||
|
||||
xFingerRef.value?.getBoundingClientRectAsync()?.then((res:DOMRect)=>{
|
||||
rectBox = {
|
||||
left:res.left,
|
||||
right:res.right,
|
||||
top:res.top,
|
||||
bottom:res.bottom,
|
||||
width:res.width,
|
||||
height:res.height,
|
||||
} as DRect
|
||||
call(rectBox)
|
||||
}).catch(err => {
|
||||
console.error('x-finger: getBoundingClientRectAsync error', err)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('x-finger: getRectBox error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getAngle = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
|
||||
let mr = getLen(v1) * getLen(v2);
|
||||
if (mr == 0) return 0;
|
||||
let r = dot(v1, v2) / mr;
|
||||
if (r > 1) r = 1;
|
||||
let jd = Math.acos(r);
|
||||
jd = jd * (180 / Math.PI);
|
||||
return (jd + 360) % 360;;
|
||||
}
|
||||
|
||||
const cross = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
|
||||
if (v1.x == null || v1.y == null || v2.x == null || v2.y == null) return 0
|
||||
return v1.x! * v2.y! - v2.x! * v1.y!;
|
||||
}
|
||||
|
||||
const getRotateAngle = (v1 : CHECKPOINT_XY, v2 : CHECKPOINT_XY) : number => {
|
||||
let angle = getAngle(v1, v2);
|
||||
if (cross(v1, v2) > 0) {
|
||||
angle *= -1;
|
||||
}
|
||||
return angle * 180 / Math.PI;
|
||||
}
|
||||
const mStart = (evt : UniTouchEvent) => {
|
||||
if (_disabled.value) return
|
||||
let event = evt.changedTouches[0]
|
||||
|
||||
clearTimeout(tid.value)
|
||||
|
||||
let rectBox = parentRect.value
|
||||
_x.value = event.clientX - rectBox.left;
|
||||
_y.value = event.clientY - rectBox.top;
|
||||
|
||||
// 重置移动距离,开始新的手势
|
||||
mX.value = 0
|
||||
mY.value = 0
|
||||
|
||||
// 记录本次手势开始时的累积坐标
|
||||
startAccumulatedX.value = accumulatedX.value
|
||||
startAccumulatedY.value = accumulatedY.value
|
||||
|
||||
_start_x.value = event.clientX - rectBox.left
|
||||
_start_y.value = event.clientY - rectBox.top
|
||||
|
||||
swipeDirection.value = ""
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("start", { x: _start_x.value, y: _start_y.value, type: 'start', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'start'
|
||||
let difftime = new Date().getTime() - dubleTime.value;
|
||||
if (difftime > 0 && difftime <= props.dbClickDiff) {
|
||||
/**
|
||||
* 双击事件
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("doubleClick", { x: _start_x.value, y: _start_y.value, type: 'doubleClick', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'doubleClick'
|
||||
}
|
||||
dubleTime.value = new Date().getTime();
|
||||
|
||||
if (evt.changedTouches.length >= 2) {
|
||||
pinth_x.value = evt.touches[0].pageX;
|
||||
pinth_y.value = evt.touches[0].pageY;
|
||||
|
||||
let otx = evt.touches[1].pageX;
|
||||
let oty = evt.touches[1].pageY;
|
||||
let preVs = { x: otx - pinth_x.value, y: oty - pinth_y.value } as CHECKPOINT_XY;
|
||||
preV.value = preVs
|
||||
pinchStartLen.value = getLen(preVs);
|
||||
|
||||
}
|
||||
|
||||
tid.value = setTimeout(function () {
|
||||
/**
|
||||
* 长按事件
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("longPress", { x: _start_x.value, y: _start_y.value, type: 'longPress', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'longPress'
|
||||
}, props.longDiff);
|
||||
}
|
||||
|
||||
// 键盘事件处理(无障碍访问)
|
||||
// #ifdef WEB
|
||||
const handleKeyDown = (evt: KeyboardEvent) => {
|
||||
if (!props.accessibility || _disabled.value) return
|
||||
|
||||
const rectBox = parentRect.value
|
||||
const centerX = rectBox.width / 2
|
||||
const centerY = rectBox.height / 2
|
||||
|
||||
switch (evt.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
evt.preventDefault()
|
||||
emit("click", {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
type: 'click',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
})
|
||||
break
|
||||
case 'ArrowUp':
|
||||
evt.preventDefault()
|
||||
emit("swiper", {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
diffX: 0,
|
||||
diffY: props.swiperDiff,
|
||||
direction: 'up',
|
||||
type: 'swiper',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
})
|
||||
break
|
||||
case 'ArrowDown':
|
||||
evt.preventDefault()
|
||||
emit("swiper", {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
diffX: 0,
|
||||
diffY: props.swiperDiff,
|
||||
direction: 'down',
|
||||
type: 'swiper',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
})
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
evt.preventDefault()
|
||||
emit("swiper", {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
diffX: props.swiperDiff,
|
||||
diffY: 0,
|
||||
direction: 'left',
|
||||
type: 'swiper',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
})
|
||||
break
|
||||
case 'ArrowRight':
|
||||
evt.preventDefault()
|
||||
emit("swiper", {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
diffX: props.swiperDiff,
|
||||
diffY: 0,
|
||||
direction: 'right',
|
||||
type: 'swiper',
|
||||
width: rectBox.width,
|
||||
height: rectBox.height
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
const mMove = (evt : UniTouchEvent) => {
|
||||
if (_disabled.value) return
|
||||
let event = evt.changedTouches[0]
|
||||
let rectBox = parentRect.value
|
||||
let x = event.clientX - rectBox.left;
|
||||
let y = event.clientY - rectBox.top;
|
||||
|
||||
let deltaX = Math.abs(x - _x.value);
|
||||
let deltaY = Math.abs(y - _y.value);
|
||||
|
||||
// 检查最小移动距离
|
||||
if (!isGestureValid(deltaX, deltaY)) {
|
||||
return
|
||||
}
|
||||
|
||||
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
// 在移动过程中实时更新累积坐标(用于插槽输出)
|
||||
accumulatedX.value = startAccumulatedX.value + mX.value
|
||||
accumulatedY.value = startAccumulatedY.value + mY.value
|
||||
|
||||
if (deltaX > deltaY && deltaX > props.swiperDiff) {
|
||||
swipeDirection.value = (_x.value > x) ? "left" : "right";
|
||||
} else if (deltaY > deltaX && deltaY > props.swiperDiff) {
|
||||
swipeDirection.value = (_y.value < y) ? "down" : "up";
|
||||
}
|
||||
if (swipeDirection.value != "") {
|
||||
/**
|
||||
* 滑动时触发
|
||||
* @param evt {object} {x,y,type,width,height,direction:方向UP|DOWN|LEFT|RIGHT}
|
||||
*/
|
||||
emit("swiper", { x: mX.value, y: mY.value, diffX: deltaX, diffY: deltaY, direction: swipeDirection.value, type: 'swiper', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'swiper'
|
||||
}
|
||||
// _x.value = x;
|
||||
// _y.value = y;
|
||||
/**
|
||||
* 触摸移动时触发
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("move", { x: mX.value, y: mY.value, type: 'move', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'move'
|
||||
clearTimeout(tid.value)
|
||||
|
||||
if (evt.changedTouches.length >= 2) {
|
||||
let currentX = evt.touches[0].pageX;
|
||||
let currentY = evt.touches[0].pageY;
|
||||
|
||||
let otx = evt.touches[1].pageX;
|
||||
let oty = evt.touches[1].pageY;
|
||||
let v = { x: otx - currentX, y: oty - currentY } as CHECKPOINT_XY;
|
||||
|
||||
if (preV.value.x !== null) {
|
||||
let nowLenPitch = getLen(v);
|
||||
if (pinchStartLen.value > 0) {
|
||||
let temsc = (nowLenPitch / pinchStartLen.value);
|
||||
// 计算缩放比例
|
||||
const deltaScale = (temsc - 1) * zoomFactor.value + scale.value;
|
||||
// 最小值为0.1
|
||||
scale.value = Math.max(deltaScale, 0.1)
|
||||
|
||||
|
||||
/**
|
||||
* 缩放事件
|
||||
* @param evt {object} {x,y,x1,y1,len,scale,type,width,height}
|
||||
* @description len为两点间的距离,scale为当前的缩放比例(最小为0.1)
|
||||
*/
|
||||
emit("pinch", {
|
||||
x: currentX, y: currentY,
|
||||
x1: otx, y2: oty,
|
||||
type: 'pinch',
|
||||
width: rectBox.width, height: rectBox.height,
|
||||
len: nowLenPitch,
|
||||
scale: scale.value
|
||||
})
|
||||
|
||||
}
|
||||
let testjd = getRotateAngle(v, preV.value);
|
||||
angle.value = Math.floor((testjd - 1) * zoomFactorAb.value) + angle.value;
|
||||
pinchStartLen.value = nowLenPitch;
|
||||
|
||||
/**
|
||||
* 旋转事件
|
||||
* @param evt {object} {x,y,x1,y1,len,angle,type,width,height}
|
||||
* @description len为两点间的距离,angle为当前当前旋转的角度
|
||||
*/
|
||||
emit("rotate", {
|
||||
x: currentX, y: currentY,
|
||||
x1: otx, y2: oty,
|
||||
type: 'rotate',
|
||||
width: rectBox.width, height: rectBox.height,
|
||||
len: nowLenPitch,
|
||||
angle: angle.value
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
preV.value = v;
|
||||
}
|
||||
}
|
||||
const mEnd = (evt : UniTouchEvent) => {
|
||||
clearTimeout(tid.value)
|
||||
getRectBox((rect:DRect)=>{
|
||||
parentRect.value = rect
|
||||
})
|
||||
if (_disabled.value) return
|
||||
let event = evt.changedTouches[0]
|
||||
|
||||
let rectBox = parentRect.value
|
||||
let x = event.clientX - rectBox.left;
|
||||
let y = event.clientY - rectBox.top;
|
||||
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("end", { x: mX.value, y: mY.value, type: 'end', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'end'
|
||||
if (new Date().getTime() - dubleTime.value > props.clickDiff) {
|
||||
/**
|
||||
* 单击
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("click", { x: mX.value, y: mY.value, type: 'click', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'click'
|
||||
}
|
||||
|
||||
// 重置所有状态,为下次触摸做准备
|
||||
preV.value = { x: 0, y: 0 } as CHECKPOINT_XY;
|
||||
pinchStartLen.value = 0
|
||||
swipeDirection.value = ""
|
||||
scale.value = 0
|
||||
angle.value = 0
|
||||
}
|
||||
const mCancel = (evt : UniTouchEvent) => {
|
||||
clearTimeout(tid.value)
|
||||
getRectBox((rect:DRect)=>{
|
||||
parentRect.value = rect
|
||||
})
|
||||
if (_disabled.value) return
|
||||
let event = evt.changedTouches[0]
|
||||
|
||||
let rectBox = parentRect.value
|
||||
let x = event.clientX - rectBox.left;
|
||||
let y = event.clientY - rectBox.top;
|
||||
mX.value = Math.max(0, Math.min(rectBox.width, x)) - _start_x.value
|
||||
mY.value = Math.max(0, Math.min(rectBox.height, y)) - _start_y.value
|
||||
|
||||
// 更新累积坐标(手势取消时也要累加本次移动的偏移量)
|
||||
accumulatedX.value = startAccumulatedX.value + mX.value
|
||||
accumulatedY.value = startAccumulatedY.value + mY.value
|
||||
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param evt {object} {x,y,type,width,height}
|
||||
*/
|
||||
emit("cancel", { x: mX.value, y: mY.value, type: 'end', width: rectBox.width, height: rectBox.height })
|
||||
eventName.value = 'cancel'
|
||||
|
||||
// 重置所有状态,为下次触摸做准备
|
||||
preV.value = { x: 0, y: 0 } as CHECKPOINT_XY;
|
||||
pinchStartLen.value = 0
|
||||
swipeDirection.value = ""
|
||||
scale.value = 0
|
||||
angle.value = 0
|
||||
}
|
||||
|
||||
// Methods
|
||||
const initFunc = () => {
|
||||
|
||||
getRectBox((rect:DRect)=>{
|
||||
parentRect.value = rect;
|
||||
top.value = rect.top;
|
||||
left.value = rect.left;
|
||||
})
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
// #ifdef APP-HARMONY
|
||||
setTimeout(function() {
|
||||
initFunc()
|
||||
}, 120);
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-HARMONY
|
||||
initFunc()
|
||||
// #endif
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
if (throttleTimer.value!=null) {
|
||||
clearTimeout(throttleTimer.value!)
|
||||
}
|
||||
if (debounceTimer.value!=null) {
|
||||
clearTimeout(debounceTimer.value!)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view :id="id" ref="xFingerRef" class="finger"
|
||||
@touchstart="mStart" @touchmove="mMove" @touchend="mEnd"
|
||||
@touchcancel="mCancel"
|
||||
:aria-label="props.ariaLabel"
|
||||
:role="props.accessibility ? 'button' : null"
|
||||
:tabindex="props.accessibility ? 0 : null"
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
|
||||
@mousedown="mouseStart"
|
||||
@mousemove="mouseMove"
|
||||
@mouseup="mouseEnd"
|
||||
@mouseleave="mouseCancel"
|
||||
@wheel="handleWheel"
|
||||
@keydown="handleKeyDown"
|
||||
|
||||
<!-- #endif -->
|
||||
|
||||
>
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {number} x - 触摸的位置x
|
||||
@prop {number} y - 触摸的位置y
|
||||
@prop {number} accX - 累积x坐标(连续定位)
|
||||
@prop {number} accY - 累积y坐标(连续定位)
|
||||
@prop {string} type - 事件类型click,start,end,dbclick,longPress,swiper,pinch,rotate
|
||||
-->
|
||||
<slot :x="mX" :y="mY" :accX="accumulatedX" :accY="accumulatedY" :type="eventName"></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,489 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue"
|
||||
import { type PropType } from "vue"
|
||||
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
type POSITION_TYPE_XY = {
|
||||
x : number,
|
||||
y : number
|
||||
}
|
||||
/**
|
||||
* @name 浮球 xFloatButton
|
||||
* @description 可以左右四个角定位放置,自动靠边吸咐,也可自由拖动放置。
|
||||
* @page /pages/index/float-button
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xFloatButton"})
|
||||
const emits = defineEmits([
|
||||
|
||||
/**
|
||||
* 点击组件时触发
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 长按组件时触发大于500ms
|
||||
*/
|
||||
'longpress',
|
||||
/**
|
||||
* 坐标改变时触发
|
||||
* @param {number[]} x,y坐标信息
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 动态修改当前的位置
|
||||
* 等同v-model:offset具体见offset属性那。
|
||||
*/
|
||||
'update:offset'
|
||||
])
|
||||
|
||||
type xFloatButtonPropsType = {
|
||||
duration: number,
|
||||
/**
|
||||
* 松开后,如果是吸附adsorption为true的话,吸附在两边的位置时的距离边界的距离。
|
||||
* 单位为px,左右的安全距离.
|
||||
*/
|
||||
threshold: number,
|
||||
/**
|
||||
* 单位为px,顶部的安全距离
|
||||
*/
|
||||
thresholdTop: number,
|
||||
/**
|
||||
* 单位为px,底部的安全距离
|
||||
*/
|
||||
thresholdBottom: number,
|
||||
/**
|
||||
* 圆角,空值时取全局的drawr圆角。
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 自己定义位置:以可视范围内的左上角算起。如何自己定义位置时
|
||||
* 需要计算屏幕坐标时请使用uni.getWindowInfo()
|
||||
* 来获取可视屏幕的宽和高定位你自己需要的自由位置
|
||||
* 可以v-model:offset="[x,y]"来动态更改其位置。
|
||||
* 我预置了以下几种常见模式:
|
||||
* [-1,-1]会在右下角。会让出threshold边界距离
|
||||
* [-2,-2]会在左下角。会让出threshold边界距离
|
||||
* [-3,-3]会在左上角。会让出threshold边界距离
|
||||
* [-4,-4]会在右上角。会让出threshold边界距离
|
||||
* [-5,-5]会在底部居中。会让出threshold边界距离
|
||||
*/
|
||||
offset: number[],
|
||||
/**
|
||||
* 背景,支持渐变值如:linear-gradient(to left, #FFED46, #FF7EC7)
|
||||
* 默认空值,取全局主题值。
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 高
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 是否开启吸附在两边。
|
||||
* 如果设置为false,可以自由拖动在屏幕上。
|
||||
*/
|
||||
adsorption: boolean,
|
||||
/**
|
||||
* 是否禁止拖动。
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
zIndex: number,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xFloatButtonPropsType>(), {
|
||||
duration: 650,
|
||||
threshold: 12,
|
||||
thresholdTop: 0,
|
||||
thresholdBottom: 12,
|
||||
round: "64",
|
||||
offset: (): number[] => [-1, -1] as number[],
|
||||
bgColor: "",
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
adsorption: true,
|
||||
disabled: false,
|
||||
zIndex: 87,
|
||||
})
|
||||
|
||||
// refs (data)
|
||||
const id = ref<string>(("xFloatButtonId-" + getUid()) as string)
|
||||
const _x = ref(0)
|
||||
const _y = ref(0)
|
||||
const winHeight = ref(0)
|
||||
const winWidth = ref(0)
|
||||
const nowXy = ref<number[]>([0, 0])
|
||||
const windtop = ref(0)
|
||||
const isMoveing = ref(false)
|
||||
const dateTime = ref(0)
|
||||
const diffX = ref(0)
|
||||
const _real_X = ref(0)
|
||||
const _real_Y = ref(0)
|
||||
const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
const first = ref(true)
|
||||
const longtimeid = ref<number>(22)
|
||||
const isReady = ref(false)
|
||||
const xFloatButton = ref<UniElement | null>(null)
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
// computed
|
||||
const _diffLen = computed(():number=>{
|
||||
let p = parseInt(props.width);
|
||||
if (props.width.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p);
|
||||
}
|
||||
return Math.floor(p)
|
||||
})
|
||||
const _round = computed(() : string => {
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
const _width = computed(() : number => {
|
||||
let p = parseInt(props.width);
|
||||
if (props.width.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p);
|
||||
}
|
||||
return Math.floor(p)
|
||||
})
|
||||
const _height = computed(() : number => {
|
||||
let p = parseInt(props.height);
|
||||
if (props.height.lastIndexOf('rpx') > -1) {
|
||||
p = rpx2px(p);
|
||||
}
|
||||
return Math.floor(p)
|
||||
})
|
||||
const _bgColor = computed(() : object => {
|
||||
if (props.bgColor.indexOf('linear-gradient') > -1) {
|
||||
return {
|
||||
backgroundImage: props.bgColor
|
||||
}
|
||||
}
|
||||
let color = props.bgColor == "" ? getDefaultColor(xConfig.color) : getDefaultColor(props.bgColor)
|
||||
return {
|
||||
backgroundColor: color
|
||||
};
|
||||
})
|
||||
const _disabled = computed(() : boolean => {
|
||||
return props.disabled
|
||||
})
|
||||
|
||||
function onClick() {
|
||||
emits('click')
|
||||
}
|
||||
function setProperty(x : number, y : number) {
|
||||
let node = xFloatButton.value as UniElement
|
||||
node.style.setProperty("transition-duration", first.value ? '0' : props.duration.toString() + 'ms')
|
||||
if (x == -1 || x == -4) {
|
||||
x = winWidth.value - _width.value - props.threshold;
|
||||
} else if (x == -2 || x == -3) {
|
||||
x = props.threshold;
|
||||
} else if (x == -5) {
|
||||
x = (winWidth.value - _width.value) / 2;
|
||||
}
|
||||
if (y == -1 || y == -2 || y == -5) {
|
||||
y = winHeight.value - _height.value - props.thresholdBottom;
|
||||
} else if (y == -3 || y == -4) {
|
||||
y = props.thresholdTop;
|
||||
}
|
||||
|
||||
x = Math.max(props.threshold, Math.min(winWidth.value - _width.value - props.threshold, x))
|
||||
|
||||
|
||||
if(y>winHeight.value/2){
|
||||
y = Math.max(props.thresholdBottom, Math.min(winHeight.value - _height.value - props.thresholdBottom, y))
|
||||
}else{
|
||||
y = Math.max(props.thresholdTop, Math.min(winHeight.value - _height.value - props.thresholdTop, y))
|
||||
}
|
||||
|
||||
node.style.setProperty("left", `${x}px`)
|
||||
node.style.setProperty("top", `${y + windtop.value}px`)
|
||||
nowXy.value = [x, y];
|
||||
lastX.value = x;
|
||||
lastY.value = y;
|
||||
/**
|
||||
* 当前的位置,等同v-model:offset
|
||||
* @param postion {number[]} [x,y]位置
|
||||
*/
|
||||
emits('change', nowXy.value)
|
||||
|
||||
first.value = false
|
||||
}
|
||||
function eventTrasform_start(evt : POSITION_TYPE_XY) {
|
||||
isMoveing.value = true;
|
||||
diffX.value = 0
|
||||
dateTime.value = new Date().getTime()
|
||||
let node = xFloatButton.value as Element
|
||||
let leftpos = parseInt(node.style.getPropertyValue("left")! as string)
|
||||
let toppos = parseInt(node.style.getPropertyValue("top")! as string)
|
||||
|
||||
_x.value = evt.x - leftpos
|
||||
_y.value = evt.y - toppos
|
||||
_real_X.value = evt.x
|
||||
_real_Y.value = evt.y
|
||||
node.style.setProperty("transition-duration", '0ms')
|
||||
|
||||
let realx = Math.floor(evt.x - _real_X.value)
|
||||
let realy = Math.floor(evt.y - _real_Y.value)
|
||||
clearTimeout(longtimeid.value)
|
||||
longtimeid.value = setTimeout(function() {
|
||||
emits('longpress')
|
||||
}, 500);
|
||||
}
|
||||
function eventTrasform_move(evt : POSITION_TYPE_XY) {
|
||||
clearTimeout(longtimeid.value)
|
||||
let x = evt.x - _x.value
|
||||
let y = evt.y - _y.value
|
||||
let diff_x = evt.x - _real_X.value
|
||||
let diff_y = evt.y - _real_Y.value
|
||||
diffX.value = Math.max(Math.abs(diff_x), Math.abs(diff_y))
|
||||
let node = xFloatButton.value as Element
|
||||
let maxX = winWidth.value - _width.value;
|
||||
let maxY = winHeight.value - _height.value + windtop.value;
|
||||
|
||||
x = Math.max(Math.min(maxX, x), 0)
|
||||
y = Math.max(Math.min(maxY, y), 0)
|
||||
|
||||
node.style.setProperty("left", `${x}px`)
|
||||
node.style.setProperty("top", `${y}px`)
|
||||
nowXy.value = [x, y];
|
||||
/**
|
||||
* 当前的位置,等同v-model:offset
|
||||
* @param postion {number[]} [x,y]位置
|
||||
*/
|
||||
emits('change', [x, y])
|
||||
}
|
||||
|
||||
function eventTrasform_end(evt : POSITION_TYPE_XY) {
|
||||
isMoveing.value = false;
|
||||
let node = xFloatButton.value as Element
|
||||
let x = evt.x - _x.value
|
||||
let y = evt.y - _y.value - windtop.value
|
||||
let maxX = winWidth.value - _width.value;
|
||||
let maxY = winHeight.value - _height.value;
|
||||
x = Math.max(Math.min(maxX, x), 0)
|
||||
y = Math.max(Math.min(maxY, y), 0)
|
||||
|
||||
if (props.adsorption) {
|
||||
y = Math.max(Math.min(maxY - props.threshold, y), props.threshold)
|
||||
if (x >= (winWidth.value - _width.value) / 2) {
|
||||
x = winWidth.value - _width.value - props.threshold;
|
||||
} else {
|
||||
x = props.threshold;
|
||||
}
|
||||
nowXy.value = [x, y];
|
||||
|
||||
setProperty(x, y)
|
||||
}
|
||||
let diffTiff = new Date().getTime() - dateTime.value
|
||||
// sdk的click和touch会同时触发,呃。目前只能以触发时间自行判断是单击还是滑动。
|
||||
let realx = Math.floor(evt.x - _real_X.value)
|
||||
let realy = Math.floor(evt.y - _real_Y.value)
|
||||
|
||||
if(realx==0&&realy==0&&realx==realy){
|
||||
if (diffTiff > 50 && diffTiff<250){
|
||||
onClick()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
function mStart(evt : TouchEvent) {
|
||||
// evt.preventDefault()
|
||||
if (_disabled.value) return
|
||||
// #ifdef WEB||MP
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
// #endif
|
||||
let x = evt.changedTouches[0].clientX;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
_real_X.value = x;
|
||||
_real_Y.value = x;
|
||||
eventTrasform_start({ x, y } as POSITION_TYPE_XY)
|
||||
}
|
||||
function mMove(evt : TouchEvent) {
|
||||
|
||||
// #ifdef WEB||MP
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
// #endif
|
||||
if (_disabled.value) return
|
||||
let x = evt.changedTouches[0].clientX;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
eventTrasform_move({ x, y } as POSITION_TYPE_XY)
|
||||
|
||||
}
|
||||
function mEnd(evt : TouchEvent) {
|
||||
if (_disabled.value) return
|
||||
let x = evt.changedTouches[0].clientX;
|
||||
let y = evt.changedTouches[0].clientY;
|
||||
|
||||
eventTrasform_end({ x, y } as POSITION_TYPE_XY)
|
||||
}
|
||||
|
||||
|
||||
function getNodes() : Promise<boolean> {
|
||||
return new Promise((res, rej) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".xFloatButtonBox")
|
||||
.boundingClientRect()
|
||||
.exec((nodes) => {
|
||||
let node = nodes[0] as NodeInfo;
|
||||
winWidth.value = node.width!;
|
||||
winHeight.value = node.height! - windtop.value;
|
||||
res(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onresizeOffsetXy() {
|
||||
getNodes().then(() => {
|
||||
setProperty(props.offset[0], props.offset[1])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
watch(():Array<number> => props.offset, (newValue : Array<number>) => {
|
||||
if (newValue.length == 2 && nowXy.value.join("") != newValue.join("")) {
|
||||
setProperty(newValue[0], newValue[1])
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
isReady.value = false;
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifndef APP
|
||||
winWidth.value = sys.windowWidth
|
||||
winHeight.value = sys.windowHeight;
|
||||
windtop.value = sys.windowTop;
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
winWidth.value = sys.windowWidth
|
||||
winHeight.value = sys.windowHeight + 44;
|
||||
// #endif
|
||||
|
||||
// 兼容电脑端
|
||||
// #ifdef WEB
|
||||
window.addEventListener('mouseup', mmEnd);
|
||||
window.addEventListener('mousemove', mmMove);
|
||||
// #endif
|
||||
let t = this;
|
||||
getNodes().then(() => {
|
||||
setProperty(props.offset[0], props.offset[1])
|
||||
isReady.value = true;
|
||||
})
|
||||
|
||||
uni.$on("onResize", onresizeOffsetXy)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 兼容电脑端
|
||||
// #ifdef WEB
|
||||
window.removeEventListener('mouseup', mmEnd);
|
||||
window.removeEventListener('mousemove', mmMove);
|
||||
// #endif
|
||||
uni.$off("onResize", onresizeOffsetXy)
|
||||
})
|
||||
|
||||
// #ifdef WEB
|
||||
function mmStart(evt : UniMouseEvent) {
|
||||
;(window as any).xFloatButtonId = id.value;
|
||||
if (_disabled.value) return
|
||||
evt.preventDefault();
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
_real_X.value = x;
|
||||
_real_Y.value = x;
|
||||
eventTrasform_start({ x, y } as POSITION_TYPE_XY)
|
||||
}
|
||||
function mmMove(evt : UniMouseEvent) {
|
||||
evt.preventDefault();
|
||||
// evt.stopPropagation();
|
||||
if (_disabled.value || !isMoveing.value || (window as any).xFloatButtonId != id.value) return
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
eventTrasform_move({ x, y } as POSITION_TYPE_XY)
|
||||
|
||||
}
|
||||
function mmEnd(evt : UniMouseEvent) {
|
||||
if (_disabled.value || !isMoveing.value || (window as any).xFloatButtonId != id.value) return
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
eventTrasform_end({ x, y } as POSITION_TYPE_XY)
|
||||
}
|
||||
// #endif
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view
|
||||
:id="id"
|
||||
@touchstart="mStart"
|
||||
@touchmove.stop="mMove"
|
||||
@touchend="mEnd"
|
||||
ref="xFloatButton"
|
||||
<!--#ifdef WEB -->
|
||||
@mousedown="mmStart"
|
||||
<!-- #endif -->
|
||||
|
||||
:style="[{
|
||||
width:_width+'px',
|
||||
height:_height+'px',
|
||||
borderRadius:_round,
|
||||
zIndex:(zIndex+1),
|
||||
opacity:isReady?'1':'0'
|
||||
},_bgColor]" class="xFloatButton"
|
||||
>
|
||||
<!--
|
||||
@slot 请在插槽内自由布局你的样式及功能块。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="xFloatButtonBox" :style="{zIndex:zIndex}"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xFloatButtonBox {
|
||||
pointer-events: none;
|
||||
/* z-index: 87; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
transform: translate(-100%, -100%);
|
||||
}
|
||||
|
||||
.xFloatButton {
|
||||
/* z-index: 88; */
|
||||
transition-duration: 0ms;
|
||||
transition-property: left, right, top, bottom;
|
||||
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: fixed;
|
||||
/* #ifndef APP-HARMONY */
|
||||
/* box-shadow: 0 5px 24px rgba(0, 0, 0, 0.06); */
|
||||
/* #endif */
|
||||
/* #ifdef WEB */
|
||||
cursor: grab;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB */
|
||||
.xFloatButton:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,919 @@
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- @vue-docgen-ignore-next-line -->
|
||||
<script module="xfs" lang="wxs" src="./xfs.wxs"></script>
|
||||
<!-- #endif -->
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue"
|
||||
import { checkIsCssUnit, rpx2px, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type POSITION_TYPE = "bottom" | "top" | "left" | "right"
|
||||
type POSITION_EVENT = {
|
||||
x : number,
|
||||
y : number,
|
||||
classList : Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 浮动面板 FloatDrawer
|
||||
* @description 提供流畅的拖拉阻尼效果,回弹丝滑。右滑关闭逻辑已经实现,但在app体验不好,主要是scoll与事件冲突需要官方优化.
|
||||
* @page /pages/index/float-drawer
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
|
||||
*/
|
||||
defineOptions({name:"xFloatDrawer"})
|
||||
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
|
||||
defineSlots<{
|
||||
default(props: { show: boolean, height: number }): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 高度位置变化时触发这个差值.返回参数evt是个百分比,0%是最低下,100%代表是在最顶部.
|
||||
*/
|
||||
'heightChange',
|
||||
/**
|
||||
* 开始拖动
|
||||
*/
|
||||
'movestart',
|
||||
/**
|
||||
* 结束拖动
|
||||
*/
|
||||
'moveend',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show'
|
||||
])
|
||||
|
||||
export type xFloatDrawerPropsType = {
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
* 默认是打开还是放置在底部。
|
||||
*/
|
||||
show: boolean,
|
||||
/**
|
||||
* 是否仅允许通过标题栏拖动。
|
||||
*/
|
||||
onlyHeader: boolean,
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: number,
|
||||
/**
|
||||
* 向上的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 百分比,数字字符或者带单位,
|
||||
* 默认露出的内容高度
|
||||
*/
|
||||
size: string,
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: string,
|
||||
/**
|
||||
* 当拖动时,触发打开和关闭时的临界值,单位是px
|
||||
* 如果没有达到此临界值时,将会回弹至原始位置。
|
||||
*/
|
||||
triggerDy: number,
|
||||
/**
|
||||
* 当拖动时,如果已经达到了关闭和打开时的临界值时
|
||||
* 可以继续拖拉时缓动阻尼值
|
||||
*/
|
||||
threshold: number,
|
||||
/**
|
||||
* 内容层的背景色
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 暗黑的背景色,空时,取全局的sheetDarkColor
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 拖动标题栏的横线背景色
|
||||
*/
|
||||
actionColor: string,
|
||||
/**
|
||||
* 禁用内部的容器并采用view容器
|
||||
*/
|
||||
disabledScroll: boolean,
|
||||
/**
|
||||
* 没有禁用disabledScroll生效
|
||||
* 容器内部使用的类型
|
||||
* scroll :scroll-view
|
||||
* list : list-view
|
||||
*/
|
||||
containerType: string,
|
||||
/**
|
||||
* 是否禁用用户滚动等来触发关闭或者打开。
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
zIndex: number,
|
||||
/**
|
||||
* 控制内容的边跑,有时需要自定布局时非常有用.
|
||||
* 请直接使用style css规则写margin,
|
||||
*/
|
||||
contentMargin: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xFloatDrawerPropsType>(), {
|
||||
show: false,
|
||||
onlyHeader: false,
|
||||
duration: 350,
|
||||
round: "",
|
||||
size: "15%",
|
||||
maxHeight: "80%",
|
||||
triggerDy: 180,
|
||||
threshold: 0.045,
|
||||
bgColor: "white",
|
||||
darkBgColor: "",
|
||||
actionColor: "#888888",
|
||||
disabledScroll: false,
|
||||
containerType: 'scroll',
|
||||
disabled: false,
|
||||
zIndex: 100,
|
||||
contentMargin: "0 16px 16px 16px"
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const elementWrap = ref<UniElement | null>(null)
|
||||
//是否动画中
|
||||
const actioning = ref(false)
|
||||
const status = ref("")
|
||||
const wrapId = ref("xFloatDrawerWrap" + getUid())
|
||||
const _y = ref(0)
|
||||
const _realY = ref(0)
|
||||
const _realYDiff = ref(0)
|
||||
const isHover = ref(false)
|
||||
const disabledMove = ref(true)
|
||||
const netId = ref('xFloatDrawerScrollIds-' + getUid())
|
||||
const tid = ref(0)
|
||||
const startMoveWEB = ref(false)
|
||||
const first = ref(true)
|
||||
const _test_x = ref(0)
|
||||
const _test_y = ref(0)
|
||||
const _scrollDetail_y = ref(0)
|
||||
const moveDy = ref(0)
|
||||
const endtid = ref(12)
|
||||
const isClicking = ref(false)
|
||||
const _heade_x = ref(0)
|
||||
const _heade_y = ref(0)
|
||||
const _scrollDetail_y_start = ref(0)
|
||||
const disabledScolly = ref(false)
|
||||
const isMoving = ref(false)
|
||||
const touchmoevIsOver = ref(false)
|
||||
const tid2 = ref(11)
|
||||
const nowTouchType = ref('body')
|
||||
const moveDir = ref("none")
|
||||
const _reeal_x = ref(0)
|
||||
const _reeal_y = ref(0)
|
||||
const scrollTopDiff = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const _contentMargin = computed((): string => props.contentMargin)
|
||||
const _show = computed((): boolean => props.show)
|
||||
const _disabled = computed((): boolean => props.disabled)
|
||||
const _onlyHeader = computed((): boolean => props.onlyHeader)
|
||||
const _duration = computed((): number => props.duration)
|
||||
|
||||
const _round = computed((): string => {
|
||||
let round = props.round;
|
||||
if (round == "") {
|
||||
round = xConfig.drawerRadius
|
||||
}
|
||||
let radius = checkIsCssUnit(round, 'rpx');
|
||||
let _r = `${radius} ${radius} 0px 0px`
|
||||
return _r
|
||||
})
|
||||
|
||||
const _size = computed((): number => {
|
||||
if (props.size == "") {
|
||||
return _height.value * 0.8
|
||||
}
|
||||
if (props.size.lastIndexOf('rpx') > -1) {
|
||||
let sz = rpx2px(parseFloat(props.size))
|
||||
return _height.value - sz
|
||||
}
|
||||
if (props.size.lastIndexOf('px') > -1) {
|
||||
let sz = parseFloat(props.size)
|
||||
return _height.value - sz
|
||||
}
|
||||
if (props.size.lastIndexOf('%') > -1) {
|
||||
let sz = parseFloat(props.size) / 100 * _height.value
|
||||
return _height.value - sz
|
||||
}
|
||||
let sz = uni.rpx2px(parseFloat(props.size))
|
||||
return _height.value - sz
|
||||
})
|
||||
|
||||
const _maxHeight = computed((): number => {
|
||||
if (props.size == "") {
|
||||
return _height.value * 0.2
|
||||
}
|
||||
if (props.maxHeight.lastIndexOf('rpx') > -1) {
|
||||
let sz = rpx2px(parseFloat(props.maxHeight))
|
||||
return _height.value - sz
|
||||
}
|
||||
if (props.maxHeight.lastIndexOf('px') > -1) {
|
||||
let sz = parseFloat(props.maxHeight)
|
||||
return _height.value - sz
|
||||
}
|
||||
if (props.maxHeight.lastIndexOf('%') > -1) {
|
||||
let sz = parseFloat(props.maxHeight) / 100 * _height.value
|
||||
return _height.value - sz
|
||||
}
|
||||
let sz = rpx2px(parseFloat(props.maxHeight))
|
||||
return _height.value - sz
|
||||
})
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
|
||||
const _actionColor = computed((): string => getDefaultColor(props.actionColor))
|
||||
const _animationFun = computed((): string => xConfig.animationFun)
|
||||
|
||||
// 方法
|
||||
function onEnd() {
|
||||
if (status.value == 'close') {
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
emits('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
emits('update:show', false)
|
||||
emits('heightChange', 0)
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
emits('open')
|
||||
emits('update:show', true)
|
||||
emits('heightChange', 100)
|
||||
}
|
||||
actioning.value = false;
|
||||
_realYDiff.value = 0
|
||||
}
|
||||
|
||||
function setStyleAni() {
|
||||
try {
|
||||
let duration = _duration.value;
|
||||
if (first.value == true) {
|
||||
duration = 0
|
||||
}
|
||||
if (status.value == 'open') {
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function () {
|
||||
elementWrap.value!.style.setProperty("transition-duration", duration.toString() + 'ms')
|
||||
elementWrap.value!.style.setProperty('transform', `translate(0%,${_maxHeight.value}px)`)
|
||||
}, 50);
|
||||
} else if (status.value == 'close') {
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(function () {
|
||||
elementWrap.value!.style.setProperty("transition-duration", duration.toString() + 'ms')
|
||||
elementWrap.value!.style.setProperty('transform', `translate(0%,${_size.value}px)`)
|
||||
}, 50);
|
||||
}
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
first.value = false;
|
||||
}
|
||||
|
||||
function closeAlert() {
|
||||
if (actioning.value || status.value == 'close') return
|
||||
|
||||
actioning.value = true;
|
||||
status.value = 'close'
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emits('beforeClose')
|
||||
emits('heightChange', 0)
|
||||
// #ifndef MP-WEIXIN
|
||||
setStyleAni();
|
||||
// #endif
|
||||
}
|
||||
|
||||
function showAlert() {
|
||||
if (actioning.value) return;
|
||||
if (status.value == 'open') return;
|
||||
actioning.value = true;
|
||||
status.value = 'open'
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emits('beforeOpen')
|
||||
emits('heightChange', 100)
|
||||
// #ifndef MP-WEIXIN
|
||||
setStyleAni();
|
||||
// #endif
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
showAlert();
|
||||
}
|
||||
|
||||
function headerClickClose() {
|
||||
if (_disabled.value) return;
|
||||
if (moveDy.value != 0) return;
|
||||
actioning.value = false;
|
||||
if (status.value == 'open') {
|
||||
closeAlert();
|
||||
} else {
|
||||
showAlert();
|
||||
}
|
||||
onEnd()
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function setOpts(opts: any) {
|
||||
status.value = opts.status;
|
||||
}
|
||||
|
||||
function callEmits(args: any) {
|
||||
if(!args.name) return;
|
||||
if(args.args!=null&&args.args!=undefined){
|
||||
emits(args.name as any, args.args)
|
||||
}else{
|
||||
emits(args.name as any)
|
||||
}
|
||||
}
|
||||
|
||||
function setDisabledScolly(opts: any) {
|
||||
disabledScolly.value = opts.disabledScolly
|
||||
isMoving.value = opts.isMoving
|
||||
}
|
||||
// #endif
|
||||
|
||||
function scrollTopChange() {
|
||||
// 空实现
|
||||
}
|
||||
|
||||
function scollChange(evt: UniScrollEvent) {
|
||||
const stop = Math.max(0,Math.floor(evt.detail.scrollTop));
|
||||
scrollTopDiff.value = stop
|
||||
// #ifdef APP-ANDROID||WEB||APP-HARMONY
|
||||
if(!isMoving.value){
|
||||
_scrollDetail_y.value = stop
|
||||
}
|
||||
disabledScolly.value = _scrollDetail_y.value<=0;
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
_scrollDetail_y.value = stop
|
||||
//当手指已经离开了屏幕,再置顶不需要再禁用滚动了。如果手指一直在划动则需要禁用。
|
||||
if(!touchmoevIsOver.value){
|
||||
disabledScolly.value = _scrollDetail_y.value<=0;
|
||||
}else{
|
||||
disabledScolly.value = false
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
function eventTransform_start(evt: POSITION_EVENT) {
|
||||
try {
|
||||
let ele = elementWrap.value!;
|
||||
if (status.value == 'open') {
|
||||
_y.value = evt.y - (_maxHeight.value)
|
||||
} else if (status.value == 'close') {
|
||||
_y.value = evt.y - (_size.value)
|
||||
}
|
||||
_realY.value = evt.y
|
||||
ele.style.setProperty("transition-duration", '0ms')
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
}
|
||||
|
||||
function eventTransform_move(evt: POSITION_EVENT) {
|
||||
try {
|
||||
if (!disabledMove.value) return;
|
||||
|
||||
let ele = elementWrap.value! as UniElement
|
||||
|
||||
if(moveDir.value == 'v'||status.value=='close'){
|
||||
let movey = evt.y - _y.value - (nowTouchType.value == 'header'?0:_scrollDetail_y_start.value )
|
||||
if (movey <= _maxHeight.value) {
|
||||
movey = _maxHeight.value - (_maxHeight.value - movey) * props.threshold
|
||||
} else if (movey >= _size.value) {
|
||||
movey = _size.value + (movey - _size.value) * props.threshold
|
||||
}
|
||||
ele.style.setProperty("transform", `translate(0%, ${movey.toString()}px)`)
|
||||
_realYDiff.value = evt.y - _realY.value
|
||||
let ratioValue = (movey - _size.value) / (_maxHeight.value - _size.value) * 100;
|
||||
emits('heightChange',ratioValue )
|
||||
}else if(moveDir.value == 'h'&&status.value=='open'){
|
||||
disabledScolly.value = true;
|
||||
let traslateX = evt.x - _test_x.value
|
||||
traslateX = Math.max(0,Math.min(traslateX,_width.value))
|
||||
|
||||
ele.style.setProperty("transform", `translate(${traslateX.toString()}px,${_maxHeight.value}px)`)
|
||||
let ratioValue = (1-traslateX / _width.value) * 100
|
||||
emits('heightChange',ratioValue )
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("floatDrawer出现意外错误:",e,"不用担心,这是兼容性错误,不会影响使用!!仅供参考.")
|
||||
}
|
||||
}
|
||||
|
||||
function eventTransform_end(evt: POSITION_EVENT) {
|
||||
const oldStatus = status.value
|
||||
|
||||
try {
|
||||
if (!disabledMove.value) return;
|
||||
isHover.value = false;
|
||||
let ele = elementWrap.value! as UniElement
|
||||
ele.style.setProperty("transition-duration", _duration.value.toString() + 'ms');
|
||||
if(moveDir.value == 'v'||status.value=='close'){
|
||||
if (_realYDiff.value >= props.triggerDy) {
|
||||
ele.style.setProperty("transform", `translate(0%, ${_size.value.toString()}px)`);
|
||||
status.value = 'close';
|
||||
emits('beforeClose');
|
||||
emits('heightChange', 0);
|
||||
} else if (_realYDiff.value < (props.triggerDy * -1)) {
|
||||
ele.style.setProperty("transform", `translate(0%, ${_maxHeight.value.toString()}px)`);
|
||||
status.value = 'open';
|
||||
emits('heightChange', 100);
|
||||
} else {
|
||||
if (status.value === 'open') {
|
||||
ele.style.setProperty("transform", `translate(0%, ${_maxHeight.value.toString()}px)`);
|
||||
emits('heightChange', 100);
|
||||
} else {
|
||||
ele.style.setProperty("transform", `translate(0%, ${_size.value.toString()}px)`);
|
||||
emits('heightChange', 0);
|
||||
}
|
||||
}
|
||||
}else if(moveDir.value == 'h'&&status.value=='open'){
|
||||
let traslateX = evt.x - _test_x.value
|
||||
traslateX = Math.max(0,Math.min(traslateX,_width.value))
|
||||
if(traslateX>=50){
|
||||
status.value = 'close';
|
||||
emits('heightChange',0 )
|
||||
ele.style.setProperty("transform", `translate(0px,${_size.value}px)`)
|
||||
}else{
|
||||
status.value = 'open';
|
||||
emits('heightChange',100 )
|
||||
ele.style.setProperty("transform", `translate(0px,${_maxHeight.value}px)`)
|
||||
disabledScolly.value = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 处理异常
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if(oldStatus != status.value){
|
||||
onEnd()
|
||||
}
|
||||
}
|
||||
|
||||
function mStart(evt: UniTouchEvent, type: string) {
|
||||
if (type == 'header'){
|
||||
isHover.value = true;
|
||||
scrollTopDiff.value = -1
|
||||
// 这里的意思,是让这个值引用界面的变化,这样可以避免可能的界面不渲染。
|
||||
nextTick((()=>{
|
||||
scrollTopDiff.value = 0
|
||||
}))
|
||||
}
|
||||
nextTick(()=>{
|
||||
emits('movestart');
|
||||
nowTouchType.value = type
|
||||
touchmoevIsOver.value = true;
|
||||
if (_disabled.value) return;
|
||||
|
||||
if (_onlyHeader.value && type == 'body') return;
|
||||
moveDy.value = 0;
|
||||
_test_x.value = evt.changedTouches[0].clientX;
|
||||
_reeal_x.value = evt.changedTouches[0].clientX
|
||||
_reeal_y.value = evt.changedTouches[0].clientY
|
||||
_scrollDetail_y_start.value = _scrollDetail_y.value; // 记录内部滚动的起始位置
|
||||
disabledScolly.value = false
|
||||
isMoving.value = false
|
||||
// 按头拖时,不要记录起始的位置,否则body与头会出现heady的位移。
|
||||
if (type == 'header'){
|
||||
disabledScolly.value = true
|
||||
}else{
|
||||
_test_y.value = evt.changedTouches[0].clientY;
|
||||
}
|
||||
eventTransform_start({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY,
|
||||
classList: [] as string[],
|
||||
} as POSITION_EVENT);
|
||||
})
|
||||
}
|
||||
|
||||
function mMove(evt: UniTouchEvent, type: string) {
|
||||
if (_disabled.value) return;
|
||||
if (_onlyHeader.value && nowTouchType.value == 'body') return;
|
||||
|
||||
const currentY = evt.changedTouches[0].clientY;
|
||||
const currentX = evt.changedTouches[0].clientX;
|
||||
const dy = currentY - _test_y.value;
|
||||
const dx_real = currentX - _reeal_x.value
|
||||
const dy_real = currentY - _reeal_y.value
|
||||
|
||||
moveDir.value = 'v'
|
||||
|
||||
if (_scrollDetail_y.value>0&& nowTouchType.value == 'body'&&moveDir.value=='v') return;
|
||||
// #ifdef WEB
|
||||
evt.preventDefault();
|
||||
// #endif
|
||||
|
||||
if(nowTouchType.value == 'body'){
|
||||
if(dy<0){
|
||||
disabledScolly.value = false;
|
||||
isMoving.value = false;
|
||||
}else{
|
||||
_scrollDetail_y.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
let scrollDetail_y_start = nowTouchType.value == 'header'?0:_scrollDetail_y_start.value
|
||||
if (scrollDetail_y_start > 0 && dy < 0) {
|
||||
// 如果内部可以向上滚动,并且拖动是向上的
|
||||
// 计算需要滚动的内部距离
|
||||
const internalScrollDelta = -dy;
|
||||
if (_scrollDetail_y.value - internalScrollDelta >= 0) {
|
||||
// 内部可以滚动,更新内部滚动位置
|
||||
_scrollDetail_y.value -= internalScrollDelta;
|
||||
} else {
|
||||
// 内部已经滚动到顶部,开始拖动整个弹层
|
||||
const excess = internalScrollDelta - scrollDetail_y_start;
|
||||
isMoving.value = true;
|
||||
eventTransform_move({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY - excess,
|
||||
classList: [] as string[],
|
||||
} as POSITION_EVENT);
|
||||
}
|
||||
} else {
|
||||
isMoving.value = true;
|
||||
// 其他情况,允许拖动整个弹层
|
||||
eventTransform_move({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY,
|
||||
classList: [] as string[],
|
||||
} as POSITION_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
function mEnd(evt: UniTouchEvent, type: string) {
|
||||
emits('moveend');
|
||||
isMoving.value = false;
|
||||
touchmoevIsOver.value = false;
|
||||
if (_disabled.value){
|
||||
return;
|
||||
}
|
||||
if (nowTouchType.value == 'header'||_scrollDetail_y.value<0){
|
||||
disabledScolly.value = false
|
||||
}
|
||||
|
||||
let dy = evt.changedTouches[0].clientY - _test_y.value;
|
||||
let dx = evt.changedTouches[0].clientX - _test_x.value;
|
||||
if (dy == dx) return;
|
||||
|
||||
moveDy.value = dy;
|
||||
eventTransform_end({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY,
|
||||
classList: [] as string[],
|
||||
});
|
||||
|
||||
disabledScolly.value = false;
|
||||
moveDir.value = 'none'
|
||||
}
|
||||
|
||||
// #ifdef WEB
|
||||
function mmStart(evt: UniMouseEvent, type: string) {
|
||||
// @ts-ignore
|
||||
window.FloatDrawerId = wrapId.value
|
||||
emits('movestart')
|
||||
_heade_x.value = evt.clientX
|
||||
_heade_y.value = evt.clientY
|
||||
_test_y.value = evt.clientY;
|
||||
_test_x.value = evt.clientX;
|
||||
if (_disabled.value) return;
|
||||
if (type == 'header'){
|
||||
isHover.value = true;
|
||||
}
|
||||
if (_onlyHeader.value && type == 'body') return
|
||||
startMoveWEB.value = true;
|
||||
eventTransform_start({
|
||||
x: evt.clientX,
|
||||
y: evt.clientY,
|
||||
classList: [] as string[],
|
||||
} as POSITION_EVENT)
|
||||
}
|
||||
|
||||
function mmMove(evt: UniMouseEvent, type: string) {
|
||||
// @ts-ignore
|
||||
if(window.FloatDrawerId!=wrapId.value) return
|
||||
if (_onlyHeader.value && type == 'body') return
|
||||
if (!startMoveWEB.value) return;
|
||||
|
||||
const currentY = evt.clientY;
|
||||
const currentX = evt.clientX;
|
||||
const dy = currentY - _test_y.value;
|
||||
const dx = currentX - _test_x.value
|
||||
if(moveDir.value=='none'&&(Math.abs(dx) - Math.abs(dy))>0){
|
||||
moveDir.value = 'h'
|
||||
}else if(moveDir.value=='none'&&(Math.abs(dx) - Math.abs(dy))<=0){
|
||||
moveDir.value = 'v'
|
||||
}
|
||||
|
||||
eventTransform_move({
|
||||
x: evt.clientX,
|
||||
y: evt.clientY,
|
||||
classList: [] as string[],
|
||||
} as POSITION_EVENT)
|
||||
}
|
||||
|
||||
function mmEnd(evt: UniMouseEvent, type: string) {
|
||||
emits('moveend')
|
||||
let diffx = evt.clientX - _heade_x.value
|
||||
let diffy = evt.clientY - _heade_y.value
|
||||
// @ts-ignore
|
||||
window.FloatDrawerId = ''
|
||||
if (diffx == diffy) return;
|
||||
|
||||
if (_disabled.value) return;
|
||||
|
||||
startMoveWEB.value = false;
|
||||
eventTransform_end({
|
||||
x: evt.clientX,
|
||||
y: evt.clientY,
|
||||
classList: [] as string[],
|
||||
})
|
||||
moveDir.value = 'none'
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 监听器
|
||||
watch((): boolean => props.show, (newval: boolean) => {
|
||||
if (newval && status.value == 'close') {
|
||||
actioning.value = false;
|
||||
showAlert()
|
||||
onEnd()
|
||||
} else if(!newval && status.value == 'open') {
|
||||
actioning.value = false;
|
||||
closeAlert()
|
||||
onEnd()
|
||||
}
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
let yachi = 0
|
||||
// #ifdef APP
|
||||
yachi = 250
|
||||
// #endif
|
||||
|
||||
tid.value = setTimeout(function () {
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifndef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight;
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight + 44;
|
||||
// #endif
|
||||
if (_show.value) {
|
||||
showAlert();
|
||||
actioning.value = false;
|
||||
status.value = 'open'
|
||||
} else {
|
||||
closeAlert()
|
||||
actioning.value = false;
|
||||
status.value = 'close'
|
||||
}
|
||||
}, yachi);
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
clearTimeout(tid2.value)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** 打开 **/
|
||||
open: () => showAlert(),
|
||||
/** 关闭 **/
|
||||
close: () => closeAlert()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- @transitionend="onEnd" -->
|
||||
<view @click.stop=""
|
||||
ref="elementWrap"
|
||||
class="xFloatDrawerWrapContent" :id="wrapId" :style="{
|
||||
width:'100%',
|
||||
height:_height+'px',
|
||||
borderRadius:_round,
|
||||
backgroundColor:_bgColor,
|
||||
'transition-timing-function':_animationFun,
|
||||
zIndex:props.zIndex
|
||||
}"
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
|
||||
@touchstart="xfs.mStart"
|
||||
@touchend="xfs.mEnd"
|
||||
@touchmove="xfs.mMove"
|
||||
:change:prop="xfs.propObserver"
|
||||
:prop="status"
|
||||
:data-opts="{
|
||||
maxHeight:_maxHeight,
|
||||
size:_size,height:_height,
|
||||
status:status,
|
||||
threshold:props.threshold,
|
||||
duration:_duration,
|
||||
triggerDy:props.triggerDy,
|
||||
_scrollDetail_y:_scrollDetail_y
|
||||
}"
|
||||
|
||||
<!-- #endif -->
|
||||
|
||||
>
|
||||
|
||||
|
||||
<view
|
||||
<!-- #ifdef APP||H5 -->
|
||||
@touchstart="mStart($event as TouchEvent,'header')"
|
||||
@touchend="mEnd($event as TouchEvent,'header')"
|
||||
@touchmove="mMove($event as TouchEvent,'header')"
|
||||
<!-- #endif -->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="mmStart($event as MouseEvent,'header')"
|
||||
@mousemove="mmMove($event as MouseEvent,'header')"
|
||||
@mouseup="mmEnd($event as MouseEvent,'header')"
|
||||
@mouseleave="mmEnd"
|
||||
<!-- #endif -->
|
||||
class="xFloatDrawerBar"
|
||||
>
|
||||
<view @click="headerClickClose" style="height:100%" class="xFloatDrawerBarSop">
|
||||
<view class="xFloatDrawerBarLine" :style="{'background-color':_actionColor,opacity: isHover?1:0.5}">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="xFLoatViewWrap"
|
||||
style="flex:1"
|
||||
<!-- #ifdef APP||H5 -->
|
||||
@touchstart.stop="mStart($event as TouchEvent,'body')"
|
||||
@touchend.stop="mEnd($event as TouchEvent,'body')"
|
||||
@touchmove.stop="mMove($event as TouchEvent,'body')"
|
||||
<!-- #endif -->
|
||||
|
||||
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="mmStart($event as MouseEvent,'body')"
|
||||
@mousemove="mmMove($event as MouseEvent,'body')"
|
||||
@mouseup="mmEnd($event as MouseEvent,'body')"
|
||||
@mouseleave="mmEnd"
|
||||
@mousewheel="disabledScolly = false"
|
||||
<!-- #endif -->
|
||||
>
|
||||
|
||||
<scroll-view
|
||||
:scroll-top="scrollTopDiff"
|
||||
v-if="!props.disabledScroll&&props.containerType=='scroll'"
|
||||
:direction="status=='close'||disabledScolly?'none':'vertical'"
|
||||
:scroll-y="status=='close'||disabledScolly?false:true"
|
||||
@scroll="scollChange"
|
||||
@scrolltoupper="scrollTopChange" :style="{flex:'1',margin:_contentMargin,height:'100px'}"
|
||||
:bounces="false">
|
||||
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
@prop {Number} height - 容器的高度
|
||||
-->
|
||||
<slot name="default" :show="_show" :height="_maxHeight"></slot>
|
||||
<view :style="{height:_maxHeight+'px'}"></view>
|
||||
</scroll-view>
|
||||
|
||||
<list-view v-else-if="!props.disabledScroll&&props.containerType=='list'"
|
||||
:scroll-top="scrollTopDiff"
|
||||
:scroll-y="status=='close'||disabledScolly?'none':'vertical'"
|
||||
:direction="status=='close'||disabledScolly?'none':'vertical'" @scroll="scollChange"
|
||||
@scrolltoupper="scrollTopChange" :style="{flex:'1',margin:_contentMargin}" :bounces="false">
|
||||
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
@prop {Number} height - 容器的高度
|
||||
-->
|
||||
<slot name="default" :show="_show" :height="_maxHeight"></slot>
|
||||
<list-item>
|
||||
<view :style="{height:_maxHeight+'px'}"></view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
|
||||
|
||||
<view v-else :style="{flex:'1',margin:_contentMargin}">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
@prop {Number} height - 容器的高度
|
||||
-->
|
||||
<slot name="default" :show="_show" :height="_maxHeight"></slot>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.xFloatDrawerBarSop {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xFloatDrawerBar {
|
||||
height: 44px;
|
||||
|
||||
/* #ifdef WEB */
|
||||
cursor: grab;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB */
|
||||
.xFloatDrawerBar:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
|
||||
.xFloatDrawerBarLine {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.xFloatDrawerWrapContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* #ifndef APP-HARMONY */
|
||||
transition-duration: 250ms;
|
||||
/* #endif */
|
||||
/* #ifdef APP-HARMONY */
|
||||
transition-duration: 0ms;
|
||||
/* #endif */
|
||||
/* transition-timing-function: cubic-bezier(.18,.89,.32,1); */
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translate(0%, 100%);
|
||||
position: fixed;
|
||||
/* z-index: 100; */
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,837 @@
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- @vue-docgen-ignore-next-line -->
|
||||
<script module="xfs" lang="wxs" src="./xfs.wxs"></script>
|
||||
<!-- #endif -->
|
||||
<script lang="ts">
|
||||
import { checkIsCssUnit, rpx2px, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
type POSITION_TYPE = "bottom" | "top" | "left" | "right"
|
||||
type POSITION_EVENT = {
|
||||
x : number,
|
||||
y : number,
|
||||
classList : Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 浮动面板 FloatDrawer
|
||||
* @description 提供流畅的拖拉阻尼效果,回弹丝滑。右滑关闭逻辑已经实现,但在app体验不好,主要是scoll与事件冲突需要官方优化.
|
||||
* @page /pages/index/float-drawer
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
_width: 0,
|
||||
_height: 0,
|
||||
status:'close' as 'close'|'open',
|
||||
disabledScrolling:false,
|
||||
actioning:false,
|
||||
diffY:0,
|
||||
first:true,
|
||||
tid:5,
|
||||
_lastTouchY:0,
|
||||
_isTouching:false,
|
||||
_scrollTop:0,
|
||||
_lastScrollTop:0,
|
||||
_topEpsilon:2,
|
||||
_translateY:0,
|
||||
_currentTranslateY:0
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 高度位置变化时触发这个差值.返回参数evt是个百分比,0%是最低下,100%代表是在最顶部.
|
||||
*/
|
||||
'heightChange',
|
||||
/**
|
||||
* 开始拖动
|
||||
*/
|
||||
'movestart',
|
||||
/**
|
||||
* 结束拖动
|
||||
*/
|
||||
'moveend',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show'],
|
||||
props: {
|
||||
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
* 默认是打开还是放置在底部。
|
||||
*/
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否仅允许通过标题栏拖动。
|
||||
*/
|
||||
onlyHeader: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 350
|
||||
},
|
||||
/**
|
||||
* 向上的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 百分比,数字字符或者带单位,
|
||||
* 默认露出的内容高度
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
default: "15%"
|
||||
},
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: "80%"
|
||||
},
|
||||
/**
|
||||
* 当拖动时,触发打开和关闭时的临界值,单位是px
|
||||
* 如果没有达到此临界值时,将会回弹至原始位置。
|
||||
*/
|
||||
triggerDy: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
/**
|
||||
* 当拖动时,如果已经达到了关闭和打开时的临界值时
|
||||
* 可以继续拖拉时缓动阻尼值
|
||||
*/
|
||||
threshold: {
|
||||
type: Number,
|
||||
default: 0.045
|
||||
},
|
||||
/**
|
||||
* 内容层的背景色
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 暗黑的背景色,空时,取全局的sheetDarkColor
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 拖动标题栏的横线背景色
|
||||
*/
|
||||
actionColor: {
|
||||
type: String,
|
||||
default: "#888888"
|
||||
},
|
||||
/**
|
||||
* 禁用内部的容器并采用view容器
|
||||
*/
|
||||
disabledScroll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 没有禁用disabledScroll生效
|
||||
* 容器内部使用的类型
|
||||
* scroll :scroll-view
|
||||
* list : list-view
|
||||
*/
|
||||
containerType: {
|
||||
type: String,
|
||||
default: 'scroll'
|
||||
},
|
||||
/**
|
||||
* 是否禁用用户滚动等来触发关闭或者打开。
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 层级
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
/**
|
||||
* 控制内容的边跑,有时需要自定布局时非常有用.
|
||||
* 请直接使用style css规则写margin,
|
||||
*/
|
||||
contentMargin: {
|
||||
type: String,
|
||||
default: "0 16px 16px 16px"
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(_newval : boolean) {
|
||||
if (_newval&&this.status == 'close') {
|
||||
this.actioning = false;
|
||||
this.open()
|
||||
this.onEnd()
|
||||
} else if(!_newval&&this.status == 'open') {
|
||||
this.actioning = false;
|
||||
this.close()
|
||||
this.onEnd()
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
computed: {
|
||||
_contentMargin() : string {
|
||||
return this.contentMargin
|
||||
},
|
||||
_show() : boolean {
|
||||
return this.show
|
||||
},
|
||||
_disabled() : boolean {
|
||||
return this.disabled
|
||||
},
|
||||
|
||||
_onlyHeader() : boolean {
|
||||
return this.onlyHeader
|
||||
},
|
||||
_duration() : number {
|
||||
return this.duration
|
||||
},
|
||||
|
||||
_round() : string {
|
||||
let round = this.round;
|
||||
if (round == "") {
|
||||
round = xConfig.drawerRadius
|
||||
}
|
||||
let radius = checkIsCssUnit(round, 'rpx');
|
||||
|
||||
let _r = `${radius} ${radius} 0px 0px`
|
||||
return _r
|
||||
},
|
||||
_size() : number {
|
||||
|
||||
if (this.size == "") {
|
||||
return this._height * 0.8
|
||||
}
|
||||
if (this.size.lastIndexOf('rpx') > -1) {
|
||||
let sz = rpx2px(parseFloat(this.size))
|
||||
return this._height - sz
|
||||
}
|
||||
if (this.size.lastIndexOf('px') > -1) {
|
||||
let sz = parseFloat(this.size)
|
||||
|
||||
return this._height - sz
|
||||
}
|
||||
if (this.size.lastIndexOf('%') > -1) {
|
||||
let sz = parseFloat(this.size) / 100 * this._height
|
||||
|
||||
return this._height - sz
|
||||
}
|
||||
|
||||
let sz = uni.rpx2px(parseFloat(this.size))
|
||||
|
||||
return this._height - sz
|
||||
},
|
||||
|
||||
_maxHeight() : number {
|
||||
if (this.size == "") {
|
||||
return this._height * 0.2
|
||||
}
|
||||
if (this.maxHeight.lastIndexOf('rpx') > -1) {
|
||||
let sz = rpx2px(parseFloat(this.maxHeight))
|
||||
return this._height - sz
|
||||
}
|
||||
if (this.maxHeight.lastIndexOf('px') > -1) {
|
||||
let sz = parseFloat(this.maxHeight)
|
||||
return this._height - sz
|
||||
}
|
||||
if (this.maxHeight.lastIndexOf('%') > -1) {
|
||||
let sz = parseFloat(this.maxHeight) / 100 * this._height
|
||||
|
||||
return this._height - sz
|
||||
}
|
||||
|
||||
let sz = rpx2px(parseFloat(this.maxHeight))
|
||||
return this._height - sz
|
||||
},
|
||||
_bgColor() : string {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkBgColor != '') return getDefaultColor(this.darkBgColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(this.bgColor)
|
||||
},
|
||||
_actionColor() : string {
|
||||
return getDefaultColor(this.actionColor)
|
||||
},
|
||||
_animationFun() : string {
|
||||
return xConfig.animationFun
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let t = this;
|
||||
let yachi = 0
|
||||
// #ifdef APP
|
||||
yachi = 250
|
||||
// #endif
|
||||
|
||||
|
||||
this.tid = setTimeout(function () {
|
||||
let sys = uni.getWindowInfo()
|
||||
t._width = sys.windowWidth
|
||||
t._height = sys.windowHeight;
|
||||
if (t._show) {
|
||||
t.status = 'close'
|
||||
t.open();
|
||||
} else {
|
||||
t.status = 'open'
|
||||
t.close()
|
||||
}
|
||||
}, yachi);
|
||||
},
|
||||
beforeUnmount() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
setOpts(opts){
|
||||
this.status = opts.status;
|
||||
},
|
||||
callEmits(args){
|
||||
if(!args.name) return;
|
||||
if(args.name!='open'&&args.name!='close'){
|
||||
if(args.args!=null&&args.args!=undefined){
|
||||
this.$emit(args.name,args.args)
|
||||
}else{
|
||||
this.$emit(args.name)
|
||||
}
|
||||
}else{
|
||||
if(args.name == 'open'){
|
||||
this.status = 'open';
|
||||
}
|
||||
if(args.name == 'close'){
|
||||
this.status = 'close';
|
||||
}
|
||||
this.onEnd()
|
||||
}
|
||||
},
|
||||
setDisabledScolly(opts){
|
||||
this.disabledScrolling = opts.disabledScolly
|
||||
this._isTouching = opts.isMoving
|
||||
},
|
||||
// #endif
|
||||
|
||||
getCurrentHeightPercentByTranslate(translateY:number) : number {
|
||||
const minY = this._maxHeight
|
||||
const maxY = this._size
|
||||
const total = maxY - minY
|
||||
if (total <= 0) return 0
|
||||
let clamped = translateY
|
||||
if (clamped < minY) clamped = minY
|
||||
if (clamped > maxY) clamped = maxY
|
||||
const percent = (maxY - clamped) / total * 100
|
||||
return percent
|
||||
},
|
||||
setStyleAni() {
|
||||
let t = this;
|
||||
|
||||
try {
|
||||
const xFLoatDrawerRef = this.$refs['xFLoatDrawerRef'] as UniElement|null;
|
||||
if(xFLoatDrawerRef == null) return;
|
||||
let duration = t._duration;
|
||||
if (t.first == true) {
|
||||
duration = 0
|
||||
}
|
||||
if (this.status == 'open') {
|
||||
clearTimeout(this.tid)
|
||||
this.tid = setTimeout(function () {
|
||||
xFLoatDrawerRef.style.setProperty("transition-duration", duration.toString() + 'ms')
|
||||
xFLoatDrawerRef.style.setProperty('transform', `translate(0%,${t._maxHeight}px)`)
|
||||
}, 50);
|
||||
} else if (this.status == 'close') {
|
||||
clearTimeout(this.tid)
|
||||
this.tid = setTimeout(function () {
|
||||
xFLoatDrawerRef.style.setProperty("transition-duration", duration.toString() + 'ms')
|
||||
xFLoatDrawerRef.style.setProperty('transform', `translate(0%,${t._size}px)`)
|
||||
|
||||
}, 50);
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
t.first = false;
|
||||
},
|
||||
onEnd(){
|
||||
let _this = this;
|
||||
clearTimeout(this._duration)
|
||||
_this.tid = setTimeout(function() {
|
||||
if (_this.status == 'close') {
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
_this.$emit('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
_this.$emit('update:show', false)
|
||||
_this.$emit('heightChange', 0)
|
||||
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
_this.$emit('open')
|
||||
_this.$emit('update:show', true)
|
||||
_this.$emit('heightChange', 100)
|
||||
}
|
||||
|
||||
_this.actioning = false;
|
||||
_this.diffY = 0
|
||||
}, _this._duration);
|
||||
},
|
||||
open(){
|
||||
if (this.actioning) return;
|
||||
|
||||
if (this.status == 'open') return;
|
||||
this.actioning = true;
|
||||
this.status = 'open'
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
this.$emit('beforeOpen')
|
||||
this.$emit('heightChange', 100)
|
||||
this.setStyleAni();
|
||||
},
|
||||
close(){
|
||||
if (this.actioning || this.status == 'close') return
|
||||
this.actioning = true;
|
||||
this.status = 'close'
|
||||
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
this.$emit('beforeClose')
|
||||
this.$emit('heightChange', 0)
|
||||
this.setStyleAni();
|
||||
},
|
||||
scrollTouchStart(){
|
||||
if (this.status == 'open') {
|
||||
this.disabledScrolling = false
|
||||
}
|
||||
},
|
||||
headerClickClose(){
|
||||
if(this._isTouching) return;
|
||||
if(this.status == 'open'){
|
||||
this.actioning = false;
|
||||
this.close()
|
||||
}else if(this.status == 'close'){
|
||||
this.actioning = false;
|
||||
this.open()
|
||||
}
|
||||
},
|
||||
diffStart(eventx:number,eventy:number){
|
||||
this._isTouching = true
|
||||
this._lastTouchY = eventy
|
||||
this.diffY = 0
|
||||
let ele = this.$refs['xFLoatDrawerRef'] as UniElement
|
||||
if (this.status == 'open') {
|
||||
this._translateY = eventy - (this._maxHeight)
|
||||
this._currentTranslateY = this._maxHeight
|
||||
} else if (this.status == 'close') {
|
||||
this._translateY = eventy - (this._size)
|
||||
this._currentTranslateY = this._size
|
||||
}
|
||||
|
||||
ele.style.setProperty("transition-duration", '0ms')
|
||||
this.$emit('heightChange', this.getCurrentHeightPercentByTranslate(this._currentTranslateY))
|
||||
},
|
||||
diffMove(eventx:number,eventy:number){
|
||||
if(!this._isTouching) return
|
||||
const dy = eventy - this._lastTouchY
|
||||
this.diffY = dy
|
||||
// 手指向上滑动(内容希望上滚)
|
||||
if (dy < 0) {
|
||||
|
||||
// 允许滚动
|
||||
if (this.status == 'open') this.disabledScrolling = false
|
||||
} else if (dy > 0) {
|
||||
// 手指向下滑(内容希望下滚)。若已在顶部(含回弹阈值),禁用滚动交由外层处理
|
||||
const atTop = this._scrollTop <= this._topEpsilon
|
||||
if (atTop) {
|
||||
if (!this.disabledScrolling) {
|
||||
this.disabledScrolling = true
|
||||
// 切换为外层拖拽时,重置拖拽基线,避免位置跳变
|
||||
const base = this.status == 'open' ? this._maxHeight : this._size
|
||||
this._translateY = eventy - base
|
||||
this._lastTouchY = eventy
|
||||
this._currentTranslateY = base
|
||||
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
|
||||
ele.style.setProperty("transition-duration", '0ms')
|
||||
}
|
||||
} else {
|
||||
if (this.disabledScrolling) this.disabledScrolling = false
|
||||
}
|
||||
}
|
||||
// 拖拽位移与阻尼:
|
||||
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
|
||||
const base = this.status == 'open' ? this._maxHeight : this._size
|
||||
const rawTranslate = eventy - this._translateY
|
||||
let newTranslate = base
|
||||
const minY = this._maxHeight
|
||||
const maxY = this._size
|
||||
if (this.status == 'open') {
|
||||
const atTop = this._scrollTop <= this._topEpsilon
|
||||
if (atTop) {
|
||||
if (dy > 0) {
|
||||
// 向下:在 [minY, maxY] 线性;超过 maxY 才强阻尼
|
||||
if (rawTranslate <= maxY) {
|
||||
newTranslate = Math.max(minY, Math.min(rawTranslate, maxY))
|
||||
} else {
|
||||
const over = rawTranslate - maxY
|
||||
const damp = over * this.threshold
|
||||
newTranslate = maxY + Math.min(this.triggerDy, damp)
|
||||
}
|
||||
} else if (dy < 0) {
|
||||
// 向上:仅越过顶部时强阻尼
|
||||
if (rawTranslate >= minY) {
|
||||
newTranslate = minY
|
||||
} else {
|
||||
const overUp = minY - rawTranslate
|
||||
const dampUp = overUp * this.threshold
|
||||
newTranslate = minY - Math.min(this.triggerDy, dampUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (dy < 0) {
|
||||
// 向上:在 [minY, maxY] 线性;超过 minY 才强阻尼
|
||||
if (rawTranslate >= minY) {
|
||||
newTranslate = Math.max(minY, Math.min(rawTranslate, maxY))
|
||||
} else {
|
||||
const overUp = minY - rawTranslate
|
||||
const dampUp = overUp * this.threshold
|
||||
newTranslate = minY - Math.min(this.triggerDy, dampUp)
|
||||
}
|
||||
} else if (dy > 0) {
|
||||
// 向下:仅越过底部时强阻尼
|
||||
if (rawTranslate <= maxY) {
|
||||
newTranslate = maxY
|
||||
} else {
|
||||
const overDown = rawTranslate - maxY
|
||||
const dampDown = overDown * this.threshold
|
||||
newTranslate = maxY + Math.min(this.triggerDy, dampDown)
|
||||
}
|
||||
}
|
||||
}
|
||||
this._currentTranslateY = newTranslate
|
||||
|
||||
ele.style.setProperty('transform', `translate(0%,${newTranslate}px)`)
|
||||
this.$emit('heightChange', this.getCurrentHeightPercentByTranslate(newTranslate))
|
||||
},
|
||||
diffEnd(eventx:number,eventy:number){
|
||||
this._isTouching = false
|
||||
const ele = this.$refs['xFLoatDrawerRef'] as UniElement
|
||||
const base = this.status == 'open' ? this._maxHeight : this._size
|
||||
const moved = this._currentTranslateY - base
|
||||
|
||||
const absMoved = Math.abs(moved)
|
||||
|
||||
if (absMoved >= this.triggerDy) {
|
||||
if (this.status == 'open' && moved > 0) {
|
||||
this.status = 'close'
|
||||
this.onEnd()
|
||||
}
|
||||
if (this.status == 'close' && moved < 0) {
|
||||
this.status = 'open'
|
||||
this.onEnd()
|
||||
}
|
||||
}
|
||||
// 复位
|
||||
const fuweiBase = this.status == 'open' ? this._maxHeight : this._size
|
||||
ele.style.setProperty("transition-duration", this._duration.toString() + 'ms')
|
||||
ele.style.setProperty('transform', `translate(0%,${fuweiBase}px)`)
|
||||
if (this.status == 'open') {
|
||||
this.$emit('heightChange', 100)
|
||||
} else if (this.status == 'close') {
|
||||
this.$emit('heightChange', 0)
|
||||
}
|
||||
|
||||
this.diffY = 0
|
||||
},
|
||||
scrolltoTop(){
|
||||
this.disabledScrolling = true;
|
||||
// #ifdef APP
|
||||
let el = this.$refs['xFloatScrollRef']! as UniElement;
|
||||
// el.scrollTop = 0
|
||||
// this._scrollTop = 0
|
||||
// this._lastTouchY = 0
|
||||
// #endif
|
||||
console.log('top....')
|
||||
},
|
||||
onscroll(evt:UniScrollEvent){
|
||||
const scrollTop = evt.detail.scrollTop
|
||||
this._lastScrollTop = this._scrollTop
|
||||
this._scrollTop = scrollTop
|
||||
// 在滚动过程中,如果向上滚动,则始终允许滚动
|
||||
if (this._isTouching) {
|
||||
const delta = this._scrollTop - this._lastScrollTop
|
||||
if (delta > 0) {
|
||||
// 内容向上滚动
|
||||
if (this.disabledScrolling) this.disabledScrolling = false
|
||||
} else if (delta < 0) {
|
||||
// 内容向下滚动
|
||||
const atTop = this._scrollTop <= this._topEpsilon
|
||||
if (atTop) {
|
||||
if (!this.disabledScrolling) this.disabledScrolling = true
|
||||
} else {
|
||||
if (this.disabledScrolling) this.disabledScrolling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mStart(evt:UniTouchEvent){
|
||||
const x = evt.changedTouches[0].clientX
|
||||
const y = evt.changedTouches[0].clientY
|
||||
this.diffStart(x,y)
|
||||
},
|
||||
mMove(evt:UniTouchEvent){
|
||||
const x = evt.changedTouches[0].clientX
|
||||
const y = evt.changedTouches[0].clientY
|
||||
this.diffMove(x,y)
|
||||
},
|
||||
mEnd(evt:UniTouchEvent){
|
||||
const x = evt.changedTouches[0].clientX
|
||||
const y = evt.changedTouches[0].clientY
|
||||
this.diffEnd(x,y)
|
||||
},
|
||||
// #ifdef WEB
|
||||
mmStart(evt:UniMouseEvent){
|
||||
const x = evt.clientX
|
||||
const y = evt.clientY
|
||||
this.diffStart(x,y)
|
||||
},
|
||||
mmMove(evt:UniMouseEvent){
|
||||
const x = evt.clientX
|
||||
const y = evt.clientY
|
||||
this.diffMove(x,y)
|
||||
},
|
||||
mmEnd(evt:UniMouseEvent){
|
||||
const x = evt.clientX
|
||||
const y = evt.clientY
|
||||
this.diffEnd(x,y)
|
||||
},
|
||||
// #endif
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- @transitionend="onEnd" -->
|
||||
<view
|
||||
ref="xFLoatDrawerRef"
|
||||
class="xFloatDrawerWrapContent"
|
||||
:style="{
|
||||
width:'100%',
|
||||
height:_height+'px',
|
||||
borderRadius:_round,
|
||||
backgroundColor:_bgColor,
|
||||
'transition-timing-function':_animationFun,
|
||||
zIndex:zIndex
|
||||
}"
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
|
||||
@touchstart="xfs.mStart"
|
||||
@touchend="xfs.mEnd"
|
||||
@touchcancel="xfs.mEnd"
|
||||
@touchmove="xfs.mMove"
|
||||
:change:prop="xfs.propObserver"
|
||||
:prop="status"
|
||||
:data-opts="{
|
||||
maxHeight:_maxHeight,size:_size,height:_height,
|
||||
status:status,
|
||||
threshold:threshold,
|
||||
duration:_duration,
|
||||
triggerDy:triggerDy,
|
||||
_scrollDetail_y:_lastScrollTop
|
||||
}"
|
||||
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef APP||H5 -->
|
||||
@touchstart="mStart"
|
||||
@touchmove="mMove"
|
||||
@touchend="mEnd"
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="mmStart"
|
||||
@mousemove="mmMove"
|
||||
@mouseup="mmEnd"
|
||||
<!-- #endif -->
|
||||
|
||||
>
|
||||
|
||||
<view class="xFloatDrawerBar">
|
||||
<view @click.stop="headerClickClose" style="height:100%" class="xFloatDrawerBarSop">
|
||||
<view class="xFloatDrawerBarLine"
|
||||
:disable="_disabled"
|
||||
:style="{'background-color':_actionColor,opacity: _disabled?0.5:1}">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="xFLoatViewWrap" :style="{flex:'1',padding:_contentMargin}">
|
||||
<scroll-view
|
||||
ref="xFloatScrollRef"
|
||||
@scrolltoupper="scrolltoTop"
|
||||
@scroll="onscroll"
|
||||
@touchstart="scrollTouchStart"
|
||||
v-if="!disabledScroll&&containerType=='scroll'"
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
:scroll-y="!(status=='close'||disabledScrolling)"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
:scroll-y="!(status=='close'||disabledScrolling)"
|
||||
<!-- #endif -->
|
||||
:style="{flex:'1'}"
|
||||
:bounces="false">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="default" :show="show"></slot>
|
||||
<view :style="{height:_maxHeight+'px'}"></view>
|
||||
</scroll-view>
|
||||
|
||||
<list-view
|
||||
ref="xFloatScrollRef"
|
||||
@scroll="onscroll"
|
||||
@touchstart="scrollTouchStart"
|
||||
@scrolltoupper="scrolltoTop"
|
||||
v-else-if="!disabledScroll&&containerType=='list'"
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
:direction="status=='close'||disabledScrolling?'none':'vertical'"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
:scroll-y="!(status=='close'||disabledScrolling)"
|
||||
<!-- #endif -->
|
||||
:style="{flex:'1',margin:_contentMargin}" :bounces="false">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="default" :show="show"></slot>
|
||||
<list-item>
|
||||
<view :style="{height:_maxHeight+'px'}"></view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
|
||||
<view v-else :style="{flex:'1',margin:_contentMargin}">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
@prop {Boolean} show - 当前是否已显示
|
||||
@prop {Number} height - 容器的高度
|
||||
-->
|
||||
<slot name="default" :show="show" :height="_maxHeight"></slot>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.xFloatDrawerBarSop {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xFloatDrawerBar {
|
||||
height: 44px;
|
||||
|
||||
/* #ifdef WEB */
|
||||
cursor: grab;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB */
|
||||
.xFloatDrawerBar:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
|
||||
.xFloatDrawerBarLine {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
border-radius: 8px;
|
||||
/* #ifdef WEB */
|
||||
&:not[disable=true]:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xFloatDrawerWrapContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* #ifndef APP-HARMONY */
|
||||
transition-duration: 250ms;
|
||||
/* #endif */
|
||||
/* #ifdef APP-HARMONY */
|
||||
transition-duration: 0ms;
|
||||
/* #endif */
|
||||
/* transition-timing-function: cubic-bezier(.18,.89,.32,1); */
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translate(0%, 100%);
|
||||
position: fixed;
|
||||
/* z-index: 100; */
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
var nowTouchType = 'head'
|
||||
var touchmoevIsOver = true;
|
||||
var isHover = true;
|
||||
var isMoving = false;
|
||||
var _test_x = 0
|
||||
var _test_y = 0
|
||||
var _x = 0
|
||||
var _y = 0
|
||||
var _realY = 0
|
||||
var _realYDiff = 0
|
||||
var _scrollDetail_y_start = 0
|
||||
var disabledScolly = false;
|
||||
var defaultTranformYValue = 'matrix(1, 0, 0, 1, 0, 0) '
|
||||
var opts = {
|
||||
maxHeight: 650,
|
||||
size: 480,
|
||||
height: 0,
|
||||
threshold: 0,
|
||||
duration: 300,
|
||||
status: 'close',
|
||||
triggerDy: 180,
|
||||
_scrollDetail_y:0
|
||||
}
|
||||
|
||||
function eventTransform_start(evt, ins) {
|
||||
isHover = true;
|
||||
_scrollDetail_y_start = opts._scrollDetail_y
|
||||
disabledScolly = false
|
||||
isMoving = false
|
||||
|
||||
if (opts.status == 'open') {
|
||||
_y = evt.y - opts.maxHeight
|
||||
|
||||
} else if (opts.status == 'close') {
|
||||
_y = evt.y - opts.size
|
||||
}
|
||||
_realY = evt.y
|
||||
_test_y = evt.y;
|
||||
|
||||
ins.selectComponent('.xFloatDrawerWrapContent').setStyle({
|
||||
'transition-duration': '0ms',
|
||||
'transform': defaultTranformYValue
|
||||
})
|
||||
ins.callMethod('callEmits', {
|
||||
disabledScolly: disabledScolly,
|
||||
isMoving: isMoving
|
||||
})
|
||||
ins.callMethod('setDisabledScolly', {
|
||||
disabledScolly: disabledScolly,
|
||||
isMoving: isMoving
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function eventTransform_move(evt, ins) {
|
||||
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
|
||||
|
||||
var dy = evt.y - _test_y;
|
||||
|
||||
if(dy<0){
|
||||
disabledScolly = false;
|
||||
isMoving = false;
|
||||
opts._scrollDetail_y = 0
|
||||
}else{
|
||||
opts._scrollDetail_y = 0
|
||||
}
|
||||
|
||||
|
||||
ins.callMethod('callEmits', {
|
||||
disabledScolly: disabledScolly,
|
||||
isMoving: isMoving
|
||||
})
|
||||
ins.callMethod('setDisabledScolly', {
|
||||
disabledScolly: disabledScolly,
|
||||
isMoving: isMoving
|
||||
})
|
||||
|
||||
|
||||
if(disabledScolly) return true;
|
||||
var movey = evt.y - _y
|
||||
if (movey <= opts.maxHeight) {
|
||||
movey = opts.maxHeight - (opts.maxHeight - movey) * opts.threshold
|
||||
} else if (movey >= opts.size) {
|
||||
movey = opts.size + (movey - opts.size) * opts.threshold
|
||||
}
|
||||
|
||||
ele.setStyle({
|
||||
'transition-duration': '0ms',
|
||||
'transform': 'matrix(1, 0, 0, 1, 0, ' + movey + ')'
|
||||
})
|
||||
|
||||
_realYDiff = evt.y - _realY
|
||||
var ratioValue = (movey - opts.size) / (opts.maxHeight - opts.size) * 100;
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'heightChange',
|
||||
args: ratioValue
|
||||
})
|
||||
}
|
||||
|
||||
function eventTransform_end(evt, ins) {
|
||||
isHover = false;
|
||||
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
|
||||
var duration = opts.duration
|
||||
var offset = 0
|
||||
if (_realY - evt.y == 0) return;
|
||||
if (_realYDiff >= opts.triggerDy) {
|
||||
offset = opts.size
|
||||
opts.status = 'close';
|
||||
ins.callMethod('callEmits', 'beforeClose')
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'heightChange',
|
||||
args: 0
|
||||
})
|
||||
ins.callMethod('callEmits', {name:'close'})
|
||||
} else if (_realYDiff < (opts.triggerDy * -1)) {
|
||||
offset = opts.maxHeight
|
||||
opts.status = 'open';
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'heightChange',
|
||||
args: 100
|
||||
})
|
||||
ins.callMethod('callEmits', {name:'open'})
|
||||
} else {
|
||||
if (opts.status === 'open') {
|
||||
offset = opts.maxHeight
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'heightChange',
|
||||
args: 100
|
||||
})
|
||||
|
||||
} else {
|
||||
offset = opts.size
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'heightChange',
|
||||
args: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ins.callMethod('setOpts', opts)
|
||||
ele.setStyle({
|
||||
'transition-duration': duration + 'ms',
|
||||
'transform': 'matrix(1, 0, 0, 1, 0, ' + offset + ')'
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
function setTatus(status,instance){
|
||||
var ele = instance.selectComponent('.xFloatDrawerWrapContent')
|
||||
var offset = 0
|
||||
var duration = opts.duration
|
||||
if (status === 'open') {
|
||||
offset = opts.maxHeight
|
||||
} else {
|
||||
offset = opts.size
|
||||
}
|
||||
|
||||
ele.setStyle({
|
||||
'transition-duration': duration + 'ms',
|
||||
'transform': 'matrix(1, 0, 0, 1, 0, ' + offset + ')'
|
||||
})
|
||||
}
|
||||
function mStartHead(evt, ins) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function mMoveHead(evt, ins) {
|
||||
|
||||
}
|
||||
|
||||
function mEndHead(evt, ins) {
|
||||
|
||||
}
|
||||
|
||||
function mStart(evt, ins) {
|
||||
_test_x = evt.changedTouches[0].clientX;
|
||||
_test_y = evt.changedTouches[0].clientY;
|
||||
// var otps = ins.callMethod('getNowOpts')
|
||||
var ele = ins.selectComponent('.xFloatDrawerWrapContent')
|
||||
opts = ele.getDataset().opts;
|
||||
defaultTranformYValue = ele.getComputedStyle(['transform']).transform
|
||||
isMoving = false
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'movestart',
|
||||
args: null
|
||||
})
|
||||
eventTransform_start({
|
||||
x: _test_x,
|
||||
y: _test_y
|
||||
}, ins);
|
||||
}
|
||||
|
||||
function mMove(evt, ins) {
|
||||
isMoving = true
|
||||
|
||||
eventTransform_move({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY
|
||||
}, ins);
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function mEnd(evt, ins) {
|
||||
ins.callMethod('callEmits', {
|
||||
name: 'moveend',
|
||||
args: null
|
||||
})
|
||||
eventTransform_end({
|
||||
x: evt.changedTouches[0].clientX,
|
||||
y: evt.changedTouches[0].clientY
|
||||
}, ins)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mStart: mStart,
|
||||
mMove: mMove,
|
||||
mEnd: mEnd,
|
||||
mStartHead: mStartHead,
|
||||
mMoveHead: mMoveHead,
|
||||
mEndHead: mEndHead,
|
||||
propObserver: function(newValue, oldValue, ownerInstance, instance) {
|
||||
var ele = ownerInstance.selectComponent('.xFloatDrawerWrapContent')
|
||||
opts = ele.getDataset().opts;
|
||||
if(newValue!=undefined&&oldValue!=undefined&&newValue!=oldValue){
|
||||
setTatus(newValue,ownerInstance)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
<script lang="ts">
|
||||
import { PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { FORM_RULE, FORM_SUBMIT_OBJECT} from "../../interface.uts"
|
||||
import { FORM_ITEM } from "../x-form/interface.uts"
|
||||
import { formVaild} from "../x-form/util.uts"
|
||||
|
||||
/**
|
||||
* @name 表单子组件 xFormItem
|
||||
* @description 从1.1.2开始允许非xform直接子节点了,也就是在xform可以嵌套view进行form-item布局了,但建议不要嵌套太深,影响性能.
|
||||
* 1.1.17开始支持嵌套对象字段,key必须/分割如'user/id',sdk不支持以.分割符。
|
||||
* @page /pages/index/form
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
name:"xFormItem",
|
||||
data() {
|
||||
return {
|
||||
id: ("xFormItem-" + getUid()) as string,
|
||||
errorText: "请正确填写",
|
||||
isError: false,
|
||||
first: true,
|
||||
tid: 0
|
||||
}
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 表单名称
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示标题
|
||||
*/
|
||||
showLabel: {
|
||||
type: [Boolean, null] as PropType<boolean | null>,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 校验的字段名称
|
||||
* 字段名称一定要存在于form表单数据中。否则报错。
|
||||
*/
|
||||
field: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否是必填项.
|
||||
* 设置了此值,才会执行校验。
|
||||
*/
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否显示校验必填项前面的红*符号
|
||||
*/
|
||||
showRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 校验规则对象,uniappx暂不支持对象中嵌套泛型函数
|
||||
* 从1.1.10开始rule对象中增加了trigger:change,blur校验时机,blur主要是用在input组件上的
|
||||
* 如果你内部没有input而使用blur,会造成不校验的问题,请认真阅读文档.
|
||||
*/
|
||||
rule: {
|
||||
type: Array as PropType<FORM_RULE[]>,
|
||||
default: () : FORM_RULE[] => [] as FORM_RULE[]
|
||||
},
|
||||
/**
|
||||
* 标签宽(横向表单时有效)
|
||||
* 默认空值取父form统一的设置。
|
||||
*/
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 默认空值取父form统一的设置。
|
||||
* vertical|horizontal
|
||||
*/
|
||||
labelDirection: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 默认空值取父form统一的设置。
|
||||
* 标签的文本颜色
|
||||
*/
|
||||
labelFontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 默认空值取父form统一的设置。
|
||||
* 标签的文本颜色
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标签标题对齐方式
|
||||
* left,right,center
|
||||
*/
|
||||
labelAlign:{
|
||||
type:String,
|
||||
default:"left"
|
||||
},
|
||||
/**
|
||||
* 是否显示底部边框
|
||||
*/
|
||||
showBottomBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 排版布局上和下的间隙。
|
||||
* 数组必须是2长度
|
||||
* 第一个是上间隙,第二个是下间隙,
|
||||
* 如果showBottomBorder为false时下间隙第二个参数会被取消。
|
||||
* 如果label为horizontal横向时,第一个参数的上间隙生效,如果是上下排列不生效。
|
||||
*/
|
||||
cellPadding: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => ['10', '10'] as string[]
|
||||
},
|
||||
/**
|
||||
* 排版布局上和下的间隙。
|
||||
* 数组必须是2长度
|
||||
* 第一个是上间隙,第二个是下间隙,
|
||||
* 如果label为horizontal横向时,些参数失效,只有竖向时才生效。
|
||||
*/
|
||||
labelPadding: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () : string[] => ['12', '12'] as string[]
|
||||
},
|
||||
/**
|
||||
* 校验时,是否显示下边的出错信息
|
||||
*/
|
||||
showError:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
/**
|
||||
* 默认区域内的自定样式
|
||||
*/
|
||||
contentStyle:{
|
||||
type:String,
|
||||
default:""
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
XFORMITEM_TOP: { type: Number, default: 0 },
|
||||
XFORMITEM_LABEL_WIDTH: { type: String, default: '100' },
|
||||
XFORMITEM_LABEL_Direction: { type: String, default: 'horizontal' },
|
||||
XFORMITEM_LABEL_FontColor: { type: String, default: '#333333' },
|
||||
XFORMITEM_LABEL_SCROLL: { type: Boolean, default: true },
|
||||
XFORMITEM_LABEL_FontSize: { type: String, default: '16' },
|
||||
XFORMITEM_SHOWLABEL: { type: Boolean, default: true },
|
||||
XFORMITEM_ERROR_ALIGN: { type: String, default: 'right' },
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.pushDataToParent();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.removeSelf();
|
||||
clearTimeout(this.tid);
|
||||
},
|
||||
computed: {
|
||||
_contentStyle():string{
|
||||
return this.contentStyle
|
||||
},
|
||||
_showError():boolean{
|
||||
return this.showError
|
||||
},
|
||||
_cellPadding() : string[] {
|
||||
let mb = checkIsCssUnit(this.cellPadding[0], xConfig.unit)
|
||||
let pb = checkIsCssUnit(this.cellPadding[1], xConfig.unit)
|
||||
return [mb, pb]
|
||||
},
|
||||
_labelPadding() : string[] {
|
||||
let mb = checkIsCssUnit(this.labelPadding[0], xConfig.unit)
|
||||
let pb = checkIsCssUnit(this.labelPadding[1], xConfig.unit)
|
||||
return [mb, pb]
|
||||
},
|
||||
_parentTop() : number {
|
||||
return this.XFORMITEM_TOP
|
||||
},
|
||||
_labelWidth() : string {
|
||||
if (this.labelWidth != "") return checkIsCssUnit(this.labelWidth, xConfig.unit)
|
||||
return checkIsCssUnit(this.XFORMITEM_LABEL_WIDTH, xConfig.unit)
|
||||
},
|
||||
_labelFontSize() : string {
|
||||
let labelsize = checkIsCssUnit(this.XFORMITEM_LABEL_FontSize, xConfig.unit)
|
||||
if (this.labelFontSize != "") {
|
||||
labelsize = checkIsCssUnit(this.labelFontSize, xConfig.unit)
|
||||
}
|
||||
return labelsize
|
||||
},
|
||||
_labelDirection() : string {
|
||||
if (this.labelDirection != "") return this.labelDirection
|
||||
return this.XFORMITEM_LABEL_Direction
|
||||
},
|
||||
_labelFontColor() : string {
|
||||
if (this.labelFontColor != "") return getDefaultColor(this.labelFontColor)
|
||||
return getDefaultColor(this.XFORMITEM_LABEL_FontColor)
|
||||
},
|
||||
_label() : string {
|
||||
return this.label
|
||||
},
|
||||
_showLabel() : boolean {
|
||||
let show = this.XFORMITEM_SHOWLABEL;
|
||||
if (this.showLabel != null) {
|
||||
show = this.showLabel! as boolean;
|
||||
}
|
||||
return show
|
||||
},
|
||||
|
||||
_showRequired() : boolean {
|
||||
return this.showRequired
|
||||
},
|
||||
_rule() : FORM_RULE[] {
|
||||
return this.rule;
|
||||
},
|
||||
_showBottomBoder() : boolean {
|
||||
return this.showBottomBorder
|
||||
},
|
||||
_required() : boolean {
|
||||
return this.required
|
||||
},
|
||||
_errorFontsize() : string {
|
||||
return (xConfig.fontScale * 14).toString() + 'px'
|
||||
},
|
||||
_bordrBottomSolid() : string {
|
||||
if (!this.showBottomBorder) return "border-bottom:none";
|
||||
let lightSolid = "border-bottom: 1px solid #f5f5f5";
|
||||
let darkSolid = `border-bottom: 1px solid ${xConfig.borderDarkColor}`
|
||||
return xConfig.dark == 'dark' ? darkSolid : lightSolid
|
||||
},
|
||||
_labelAlign():string{
|
||||
let dq = 'flex-start'
|
||||
if(this.labelAlign=='right'){
|
||||
dq = 'flex-end'
|
||||
}else if(this.labelAlign=='center'){
|
||||
dq = 'center'
|
||||
}
|
||||
return dq;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
field() {
|
||||
this.pushDataToParent();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
pushDataToParent() {
|
||||
let pelement = this.findParent(this);
|
||||
|
||||
if (pelement == null) return;
|
||||
// @ts-ignore
|
||||
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
|
||||
|
||||
// 预先传递,怕子组件内的内容复杂导致查询延误,导致顺序错乱。
|
||||
parent.pushAdd({
|
||||
id: this.id,
|
||||
ele: this,
|
||||
top: 0,
|
||||
name: this.field,
|
||||
} as FORM_ITEM)
|
||||
let t = this;
|
||||
uni.createSelectorQuery().in(t)
|
||||
.select(".xFormItem")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
parent.pushAdd({
|
||||
id: t.id,
|
||||
ele: t,
|
||||
top: nodeinfo.top!,
|
||||
name: t.field,
|
||||
} as FORM_ITEM)
|
||||
})
|
||||
},
|
||||
removeSelf() {
|
||||
let pelement = this.findParent(this);
|
||||
if (pelement == null) return;
|
||||
// @ts-ignore
|
||||
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
|
||||
|
||||
parent.delItem(this.id)
|
||||
},
|
||||
getParentRules():FORM_RULE[] {
|
||||
let pelement = this.findParent(this);
|
||||
if (pelement == null) return [] as FORM_RULE[];
|
||||
// @ts-ignore
|
||||
let parent : XFormComponentPublicInstance = pelement as XFormComponentPublicInstance;
|
||||
|
||||
return parent.getRules(this.field) as FORM_RULE[]
|
||||
},
|
||||
vaildCompele(val : any|null,isSkipBlur:boolean|null = true) : FORM_SUBMIT_OBJECT {
|
||||
this.first = false;
|
||||
|
||||
if (!this._required) {
|
||||
return {
|
||||
valid: true,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: ""
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
}
|
||||
const rulesList = [...this._rule,...this.getParentRules()] as FORM_RULE[]
|
||||
|
||||
if (rulesList.length == 0 && this._required) {
|
||||
let isSuccess = formVaild(val,{} as FORM_RULE)
|
||||
this.isError = !isSuccess
|
||||
|
||||
return {
|
||||
valid: isSuccess,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: isSuccess ? "" : '请正确填写/选择'
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
}
|
||||
let isSuccess = true;
|
||||
for (let i = 0; i < rulesList.length; i++) {
|
||||
let item = rulesList[i]
|
||||
if(item.trigger == 'blur'&&isSkipBlur==true) continue;
|
||||
isSuccess = formVaild(val,item)
|
||||
this.isError = !isSuccess
|
||||
if (!isSuccess) {
|
||||
if (item.errorMessage != "" && typeof item.errorMessage == 'string') {
|
||||
this.errorText = item.errorMessage! as string
|
||||
} else {
|
||||
this.errorText = '请正确填写/选择'
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: isSuccess,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: isSuccess ? "" : this.errorText
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
|
||||
},
|
||||
|
||||
clearValid(){
|
||||
this.isError=false;
|
||||
this.first = true;
|
||||
},
|
||||
/**
|
||||
* 实时校验结果。form中要开启,如果没有这种需求可以不开启,会损耗性能。
|
||||
*/
|
||||
getVaildStatus(val : any|null) : FORM_SUBMIT_OBJECT {
|
||||
|
||||
if (!this._required) {
|
||||
return {
|
||||
valid: true,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: ""
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
}
|
||||
const rulesList = [...this._rule,...this.getParentRules()] as FORM_RULE[]
|
||||
if (rulesList.length == 0 && this._required) {
|
||||
let isSuccess = formVaild(val,{} as FORM_RULE)
|
||||
return {
|
||||
valid: isSuccess,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: isSuccess ? "" : '请正确填写/选择'
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
}
|
||||
let isSuccess = true;
|
||||
let errotips = ''
|
||||
for (let i = 0; i < rulesList.length; i++) {
|
||||
let item = rulesList[i]
|
||||
isSuccess = formVaild(val,item)
|
||||
if (!isSuccess) {
|
||||
if (item.errorMessage != "" && typeof item.errorMessage == 'string') {
|
||||
errotips = item.errorMessage! as string
|
||||
} else {
|
||||
errotips = '请正确填写/选择'
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: isSuccess,
|
||||
key: this.field,
|
||||
value: val,
|
||||
errorMessage: isSuccess ? "" : errotips
|
||||
} as FORM_SUBMIT_OBJECT;
|
||||
|
||||
},
|
||||
/**
|
||||
* 为Input定制校验函数.
|
||||
*/
|
||||
validByblur(value:any){
|
||||
this.vaildCompele(value,false)
|
||||
},
|
||||
findParent(parent:VueComponent|null):VueComponent|null{
|
||||
|
||||
if(parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS|| MP-WEIXIN
|
||||
if(parent.$parent?.id?.indexOf('xForm')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parent.$parent?.$options?.name?.indexOf('xForm')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parent.$parent instanceof XFormComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
|
||||
let parents = this.findParent(parent.$parent)
|
||||
|
||||
// #ifdef WEB||APP-IOS || MP-WEIXIN
|
||||
if(parents?.id?.indexOf('xForm')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parents?.$options?.name?.indexOf('xForm')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parents instanceof XFormComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xFormItem" ref="xFormItem" :style="[
|
||||
_bordrBottomSolid,
|
||||
{
|
||||
paddingTop: _cellPadding[0],
|
||||
paddingBottom: _cellPadding[1]
|
||||
}
|
||||
]">
|
||||
<view class="xFormIitemWrap" :style="{flexDirection:_labelDirection=='horizontal'?'row':'column'}">
|
||||
<view v-if="_showLabel" class="xFormIteLabel" :style="{
|
||||
width:_labelDirection=='horizontal'?_labelWidth:'auto',
|
||||
paddingBottom:_labelDirection=='horizontal'?'0':_labelPadding[1],
|
||||
paddingTop:_labelDirection=='horizontal'?'0':_labelPadding[0],
|
||||
'justify-content': _labelAlign
|
||||
}">
|
||||
<!--
|
||||
@slot 标题
|
||||
-->
|
||||
<slot name="label">
|
||||
<text v-if="_showRequired&&_required"
|
||||
:style="{fontSize:_labelFontSize,color:'red',paddingRight:'8rpx'}">*</text>
|
||||
<x-text v-if="_label" :font-size="_labelFontSize" :color="_labelFontColor">
|
||||
{{_label}}
|
||||
</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="xFormIteContent" :style="[{flex:_labelDirection=='horizontal'?'1':'auto'},_contentStyle]">
|
||||
<!--
|
||||
@slot 默认内容区域
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="isError&&!first&&_required&&_showError" class="xFormItemError">
|
||||
<!--
|
||||
@slot 出错提示的插槽
|
||||
-->
|
||||
<slot name="error">
|
||||
<text class="xFormItemErrorText" :style="{fontSize:_errorFontsize,textAlign:XFORMITEM_ERROR_ALIGN}">{{errorText}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xFormIteLabel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xFormItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.xFormItemErrorText {
|
||||
color: red;
|
||||
padding-top: 5px;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
.xFormIitemWrap{
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FORM_RULE_TYPE } from "../../interface.uts"
|
||||
export type FORM_ITEM = {
|
||||
id : string,
|
||||
ele : XFormItemComponentPublicInstance,
|
||||
top : number,
|
||||
name : string
|
||||
}
|
||||
export type FORM_VAILD_FUN = (val : any | null) => boolean
|
||||
export type FORM_RULE_SELF = {
|
||||
/**
|
||||
* 字段类型,空值为自动判断 ,不填写默认为空,自动判断.
|
||||
*/
|
||||
type : FORM_RULE_TYPE,
|
||||
/**
|
||||
* 校验函数。默认根据泛型判断字段值,如果不填写默认为自带的校验函数校验.
|
||||
*/
|
||||
valid : FORM_VAILD_FUN|null,
|
||||
/**
|
||||
* 错误信息,默认为自动根据类型编写提示
|
||||
*/
|
||||
errorMessage : string,
|
||||
/** 最大值,如果为空值或者为-1表示不限制大小,(数组和字符串是长度,数字是值,utsjson是key字段数量) */
|
||||
max : number,
|
||||
/** 最小值,如果为空默认为1,(数组,字符,utsjson为1,长不能小于1) */
|
||||
min : number,
|
||||
/**
|
||||
* 触发校验时机,只针对input有效.
|
||||
*/
|
||||
trigger : string
|
||||
}
|
||||
|
||||
export type FORM_SUBMIT_SELF_OBJECT = {
|
||||
errorMessage : string,
|
||||
valid : boolean
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { FORM_RULE_SELF, FORM_VAILD_FUN } from "./interface.uts";
|
||||
import { FORM_RULE, FORM_RULE_TYPE } from "../../interface.uts";
|
||||
|
||||
function getDefaultRule(rule : FORM_RULE) : FORM_RULE_SELF {
|
||||
let type : FORM_RULE_TYPE = rule?.type ?? ''
|
||||
let errorMessage : string = rule?.errorMessage ?? '请正确填写/选择'
|
||||
let max : number = rule?.max ?? -1
|
||||
let min : number = rule?.min ?? 1
|
||||
let selfvaild:FORM_VAILD_FUN|null = rule.valid
|
||||
let validfaun : FORM_VAILD_FUN | null = selfvaild
|
||||
let trigger:string = rule?.trigger??'change'
|
||||
return {
|
||||
type, errorMessage, max, min, valid: validfaun,trigger
|
||||
} as FORM_RULE_SELF
|
||||
}
|
||||
function vaild(val : any | null, rule : FORM_RULE_SELF) : boolean {
|
||||
if (rule.type != '') return vaildBytType(val, rule)
|
||||
if (val == null) return false;
|
||||
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
|
||||
if (val == undefined) return false;
|
||||
// #endif
|
||||
if (typeof val === 'string') {
|
||||
let vallen = (val as string).trim().split('').length
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
let vallen = (val as any[]).length
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
if (typeof val === 'boolean') {
|
||||
return val as boolean;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
if (isNaN(val as number)) {
|
||||
return false;
|
||||
}
|
||||
let vallen = val as number
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
// #ifdef APP-ANDROID
|
||||
if (typeof val == 'Int') {
|
||||
let vallen = val as Int
|
||||
if (rule.max == -1) return vallen >= rule.min.toInt()
|
||||
return vallen >= rule.min.toInt() && vallen <= rule.max.toInt();
|
||||
}
|
||||
if (typeof val == 'Float') {
|
||||
let vallen = val as Float
|
||||
if (rule.max == -1) return vallen >= rule.min.toFloat()
|
||||
return vallen >= rule.min.toFloat() && vallen <= rule.max.toFloat();
|
||||
}
|
||||
if (typeof val == 'Double') {
|
||||
let vallen = val as Double
|
||||
if (rule.max == -1) return vallen >= rule.min.toDouble()
|
||||
return vallen >= rule.min.toDouble() && vallen <= rule.max.toDouble();
|
||||
}
|
||||
if (typeof val == 'Long') {
|
||||
let vallen = val as Long
|
||||
if (rule.max == -1) return vallen >= rule.min.toLong()
|
||||
return vallen >= rule.min.toLong() && vallen <= rule.max.toLong();
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
|
||||
if (val == undefined) {
|
||||
return false;
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function formVaild(val : any | null, rule : FORM_RULE) : boolean {
|
||||
let rulereal = getDefaultRule(rule)
|
||||
let selfVaildFun = rulereal?.valid??null;
|
||||
let isPass = true;
|
||||
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
|
||||
isPass = (selfVaildFun == null||selfVaildFun == undefined) ? vaild(val, rulereal) : selfVaildFun(val)
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
|
||||
isPass = selfVaildFun == null ? vaild(val, rulereal) : selfVaildFun(val)
|
||||
// #endif
|
||||
|
||||
return isPass
|
||||
}
|
||||
/** 指定了type的情况下. */
|
||||
export function vaildBytType(val : any | null, rule : FORM_RULE_SELF) : boolean {
|
||||
|
||||
if (val == null) return false;
|
||||
// #ifdef APP-IOS || WEB || MP-WEIXIN || APP-HARMONY
|
||||
if (val == undefined) return false;
|
||||
// #endif
|
||||
if (rule.type == 'string') {
|
||||
let vallen = (val as string).trim().split('').length
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
if (rule.type == 'array') {
|
||||
let vallen = (val as any[]).length
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
if (rule.type == 'boolean') {
|
||||
return val as boolean;
|
||||
}
|
||||
if (rule.type == 'number') {
|
||||
let v = 0;
|
||||
if(typeof val != 'number'&&typeof val != 'string') return false;
|
||||
|
||||
// #ifdef APP
|
||||
if (typeof val == 'Int') {
|
||||
let vallen = val as Int
|
||||
v = vallen + 0
|
||||
}
|
||||
if (typeof val == 'Float') {
|
||||
let vallen = val as Float
|
||||
v = vallen + 0
|
||||
}
|
||||
if (typeof val == 'Double') {
|
||||
let vallen = val as Double
|
||||
v = vallen + 0
|
||||
}
|
||||
if (typeof val == 'Long') {
|
||||
let vallen = val as Long
|
||||
v = vallen + 0
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
if(typeof val == 'number'){
|
||||
v = val as number;
|
||||
}else if(typeof val == 'string'){
|
||||
let pv = val as string;
|
||||
let vazhi = parseFloat(pv)
|
||||
if (isNaN(vazhi)) {
|
||||
return false;
|
||||
}
|
||||
v = vazhi;
|
||||
}
|
||||
|
||||
let vallen =v
|
||||
if (rule.max == -1) return vallen >= rule.min
|
||||
return vallen >= rule.min && vallen <= rule.max;
|
||||
}
|
||||
|
||||
if(rule.type == 'phone'){
|
||||
let vallen = ''
|
||||
if(typeof val == 'string'){
|
||||
vallen = val as string;
|
||||
}else if(typeof val == 'number'){
|
||||
vallen = (val as number).toString()
|
||||
}
|
||||
let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
|
||||
return reg.test(vallen)
|
||||
}
|
||||
if(rule.type == 'email'){
|
||||
let vallen = val as string;
|
||||
let reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return reg.test(vallen)
|
||||
}
|
||||
if(rule.type == 'UTSJSON'){
|
||||
let vallen = val as UTSJSONObject;
|
||||
let mapkeys = vallen.toMap()
|
||||
let len = mapkeys.size
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max;
|
||||
}
|
||||
if(rule.type == 'date'&& (typeof val == 'string')){
|
||||
let vallen = val as string;
|
||||
let date = new Date(vallen)
|
||||
let times = date.getTime()
|
||||
if(isNaN(times)) return false;
|
||||
let len = times
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { FORM_ITEM } from "../x-form/interface.uts"
|
||||
import { FORM_RULE, FORM_SUBMIT_OBJECT, FORM_SUBMIT_RESULT } from "../../interface.uts"
|
||||
type FORMITEM_R = {
|
||||
key : string,
|
||||
value : any | null,
|
||||
}
|
||||
type FORMITEM_J = {
|
||||
value : any | null,
|
||||
item : FORM_ITEM
|
||||
}
|
||||
type labelDirType = 'vertical' | 'horizontal'
|
||||
type errorAlignType = 'left' | 'center'| 'right'
|
||||
type flatFunCall = (obj: UTSJSONObject, prefix: string) => UTSJSONObject
|
||||
/**
|
||||
* @name 表单 xForm
|
||||
* @description 从1.1.2开始允许xform可以嵌套view进行form-item布局了,但建议不要嵌套太深,影响性能.
|
||||
* @page /pages/index/form
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xForm"})
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 按钮提交表单时触发。
|
||||
* @param {FORM_SUBMIT_RESULT} result 表单验证结果
|
||||
*/
|
||||
'submit',
|
||||
/**
|
||||
* 校验事件,这个不是submit是用户在输入内容或者在校验阶段向外发出的事件
|
||||
* 里面包含了实时的校验信息,用于绑定外部的按钮提供实时的可提交状态。
|
||||
* 它对外输出,不能够双向绑定对内更改校验状态。
|
||||
*/
|
||||
'update:modelValid'
|
||||
])
|
||||
export type xFormPropsType = {
|
||||
/**
|
||||
* 标签宽(横向表单时有效)
|
||||
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
*/
|
||||
labelWidth:string,
|
||||
/**
|
||||
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
* vertical,horizontal
|
||||
*/
|
||||
labelDirection:labelDirType,
|
||||
/**
|
||||
* 标签的文本颜色,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
*/
|
||||
labelFontColor:string,
|
||||
/**
|
||||
* 出错时,是否滚动到表单的位置
|
||||
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
*/
|
||||
errorAutoPage:boolean,
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
modelValue:any,
|
||||
/**
|
||||
* 标签文本大小
|
||||
* 不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
*/
|
||||
labelFontSize:string,
|
||||
/**
|
||||
* 是否显示标题,不支持动态修改(uniappx 3.99不支持动态传递 inject),可在子组件上动态修改
|
||||
*/
|
||||
showLabel:boolean,
|
||||
/**
|
||||
* 错误标签的对齐方式,left,center,right
|
||||
*/
|
||||
errorAlign:errorAlignType,
|
||||
/**
|
||||
* rules这里提供和与formItem上提供的不冲突都可以校验
|
||||
* 如果两个名称相同会被校验两次。如果两有一边提供也会校验一次。
|
||||
*/
|
||||
rules:Map<string,FORM_RULE[]>,
|
||||
/**
|
||||
* 是否开启实时全部字段校验获取当前实时的校验状态,
|
||||
* 通过vmodel:valid对外输出当前实时的校验值,当字段值改变时会一直监测并监听所有字段并返回结果到对外
|
||||
* 如果你没有这个需求场景,请保持关闭状态,如果你确实外部需要实时改变并观察提交按钮状态可以打开,请在字段少的场景使用。
|
||||
*/
|
||||
watchValidStatus:boolean,
|
||||
/**
|
||||
* 当前的校验状态,请不要外部改变此值,此值只对外输出当前校验状态
|
||||
* 需要设置watchValidStatus为true才会实时监听并检测状态。请通过v-model:valid来得到状态值
|
||||
*/
|
||||
modelValid:boolean,
|
||||
}
|
||||
const list = ref<FORM_ITEM[]>([])
|
||||
const top = ref(0)
|
||||
const isOkTop = ref(false)
|
||||
const isdestry = ref(false)
|
||||
const oldFormData = ref<any>({})
|
||||
const id = ("xForm-" + getUid()) as string
|
||||
|
||||
const props = withDefaults(defineProps<xFormPropsType>(), {
|
||||
labelWidth:"100",
|
||||
labelDirection:"horizontal",
|
||||
labelFontColor:"#333333",
|
||||
errorAutoPage:true,
|
||||
modelValue:{} as UTSJSONObject,
|
||||
labelFontSize:"16",
|
||||
showLabel:true,
|
||||
errorAlign:'left',
|
||||
rules: ():Map<string,FORM_RULE[]> => new Map<string,FORM_RULE[]>(),
|
||||
watchValidStatus:false,
|
||||
modelValid:false
|
||||
})
|
||||
|
||||
const _modelValue = computed(():any =>props.modelValue)
|
||||
const _rules = computed(():Map<string,FORM_RULE[]> =>props.rules)
|
||||
let validTimeid = 12
|
||||
provide('XFORMITEM_TOP',computed(():number=>top.value))
|
||||
provide('XFORMITEM_LABEL_WIDTH',computed(():string=>props.labelWidth))
|
||||
provide('XFORMITEM_LABEL_Direction',computed(():string=>props.labelDirection))
|
||||
provide('labelFontColor',computed(():string=>props.labelFontColor))
|
||||
provide('XFORMITEM_LABEL_SCROLL',computed(():boolean=>props.errorAutoPage))
|
||||
provide('XFORMITEM_LABEL_FontSize',computed(():string=>props.labelFontSize))
|
||||
provide('XFORMITEM_SHOWLABEL',computed(():boolean=>props.showLabel))
|
||||
provide('XFORMITEM_ERROR_ALIGN',computed(():string=>props.errorAlign))
|
||||
|
||||
/**
|
||||
* 将嵌套对象拍平为点分隔的键值对
|
||||
* @param obj 要拍平的对象
|
||||
* @param prefix 键前缀
|
||||
* @returns 拍平后的对象
|
||||
*/
|
||||
let flattenObject:flatFunCall|null = null;
|
||||
|
||||
flattenObject = (obj: UTSJSONObject, prefix: string): UTSJSONObject => {
|
||||
let flattened: UTSJSONObject = {}
|
||||
const flatMap = obj.toMap()
|
||||
for (const aitem of flatMap) {
|
||||
let item = aitem as Array<any|null>
|
||||
let key = item[0]! as string;
|
||||
let value = item[1];
|
||||
if (flatMap.has(key)) {
|
||||
const newKey = prefix!='' ? `${prefix}/${key}` : key
|
||||
if (value !== null && value instanceof UTSJSONObject ) {
|
||||
// 递归处理嵌套对象
|
||||
let fun = flattenObject!
|
||||
flattened = {...flattened,...fun(value, newKey)}
|
||||
} else {
|
||||
flattened.set(newKey,value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattened
|
||||
}
|
||||
|
||||
function getNodeInfo() {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".xForm")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
top.value = nodeinfo.top!;
|
||||
isOkTop.value = true;
|
||||
})
|
||||
}
|
||||
function actionsRequired(templist : FORMITEM_R[]) {
|
||||
list.value.forEach((el : FORM_ITEM) => {
|
||||
let index = templist.findIndex((ele : FORMITEM_R) : boolean => ele.key == el.name)
|
||||
if (index > -1) {
|
||||
el.ele.vaildCompele(templist[index].value)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
//比较哪个字段有变化
|
||||
function diffFiled(newvals : any, olds : any) {
|
||||
if (isdestry.value) return;
|
||||
if (typeof newvals != 'object' || typeof olds != 'object') return;
|
||||
let newval = JSON.parseObject(JSON.stringify(newvals)) as UTSJSONObject;
|
||||
let old = JSON.parseObject(JSON.stringify(olds)) as UTSJSONObject;
|
||||
newval = flattenObject!(newval,'');
|
||||
old = flattenObject!(old,'');
|
||||
let fileds = [] as FORMITEM_R[]
|
||||
for (let key in old) {
|
||||
let val : any | null = newval.get(key)
|
||||
let oldval : any | null = old.get(key)
|
||||
if (Array.isArray(val) && Array.isArray(oldval)) {
|
||||
let temval = val.length
|
||||
let temoldval = oldval.length
|
||||
if (temval != temoldval) {
|
||||
fileds.push({
|
||||
key,
|
||||
value: val
|
||||
} as FORMITEM_R)
|
||||
}
|
||||
} else if (val != oldval) {
|
||||
fileds.push({
|
||||
key,
|
||||
value: val
|
||||
} as FORMITEM_R)
|
||||
}
|
||||
|
||||
}
|
||||
actionsRequired(fileds)
|
||||
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
|
||||
}
|
||||
|
||||
function _valid(keys : string[],isShowwStatusInTag:boolean = true) : FORM_SUBMIT_RESULT {
|
||||
let toptemp = 0
|
||||
let result = {
|
||||
valid: true,
|
||||
errorMessage: "",
|
||||
key: "",
|
||||
formData: [] as FORM_SUBMIT_OBJECT[]
|
||||
} as FORM_SUBMIT_RESULT
|
||||
let modevalue = JSON.parseObject(JSON.stringify(_modelValue.value)) as UTSJSONObject;
|
||||
modevalue = flattenObject!(modevalue,'');
|
||||
|
||||
for (let key in modevalue) {
|
||||
if (keys.length == 0 || keys.includes(key)) {
|
||||
|
||||
let valdata : any | null = modevalue.get(key)
|
||||
let index = list.value.findIndex((ele : FORM_ITEM) : boolean => ele.name == key)
|
||||
|
||||
if (index > -1) {
|
||||
let resultJg = null as FORM_SUBMIT_OBJECT|null
|
||||
if(isShowwStatusInTag){
|
||||
resultJg = list.value[index].ele.vaildCompele(valdata, false) as FORM_SUBMIT_OBJECT
|
||||
}else{
|
||||
resultJg = list.value[index].ele.getVaildStatus(valdata) as FORM_SUBMIT_OBJECT
|
||||
}
|
||||
if (!resultJg!.valid && result.valid) {
|
||||
result.valid = false;
|
||||
result.key = key;
|
||||
result.errorMessage = resultJg!.errorMessage;
|
||||
toptemp = list.value[index].top
|
||||
}
|
||||
result.formData.push(resultJg!)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(isShowwStatusInTag){
|
||||
/**
|
||||
* 按钮提交表单时触发。
|
||||
* @param {FORM_SUBMIT_RESULT} result 表单验证结果。
|
||||
*/
|
||||
emits('submit', result)
|
||||
if (props.errorAutoPage && !result.valid && toptemp > 0) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: toptemp,
|
||||
fail: (_) => {
|
||||
console.error("当前页面没有放置可滚动的列表,但使用了表单出错滚动功能,请关闭表单属性:error-auto-page")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 全局检测校验整体状态。
|
||||
function getStatus(){
|
||||
if(!props.watchValidStatus) return;
|
||||
clearTimeout(validTimeid)
|
||||
validTimeid = setTimeout(function() {
|
||||
const result = _valid([] as string[],false)
|
||||
console.log(result.valid)
|
||||
emits('update:modelValid',result.valid)
|
||||
}, 120);
|
||||
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
oldFormData.value = JSON.parse<any>(JSON.stringify(_modelValue.value))!;
|
||||
getNodeInfo();
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
clearTimeout(validTimeid)
|
||||
isdestry.value = true;
|
||||
list.value = [] as FORM_ITEM[]
|
||||
oldFormData.value = {} as UTSJSONObject
|
||||
})
|
||||
|
||||
|
||||
|
||||
watch(():any=>props.modelValue,(newValue : any)=>{
|
||||
diffFiled(newValue, oldFormData.value)
|
||||
if(props.watchValidStatus){
|
||||
getStatus()
|
||||
}
|
||||
},{deep:true})
|
||||
|
||||
|
||||
|
||||
defineExpose({
|
||||
pushAdd(item : FORM_ITEM) {
|
||||
|
||||
if (isdestry.value) return;
|
||||
let index = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == item.id);
|
||||
if (index > -1) {
|
||||
list.value.splice(index, 1, item)
|
||||
} else {
|
||||
list.value.push(item)
|
||||
}
|
||||
|
||||
},
|
||||
delItem(id : string) {
|
||||
if (isdestry.value) return;
|
||||
if (list.value.length == 0) return;
|
||||
let index : number = list.value.findIndex((el : FORM_ITEM) : boolean => el.id == id);
|
||||
if (index > -1) {
|
||||
list.value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
getRules(key:string):FORM_RULE[]{
|
||||
const _localRules = _rules.value.get(key)
|
||||
if(_localRules == null) return [] as FORM_RULE[]
|
||||
return _localRules!
|
||||
},
|
||||
/**
|
||||
* 用来首次同步检测:modelValid,一定要在onready生命期中执行,并且你赋值完表数据后再来个,nextTick中执行本方法,可以确保不会有遗漏。
|
||||
*/
|
||||
checkAsyncVaildStatus(){
|
||||
getStatus();
|
||||
},
|
||||
/**
|
||||
* 手动执行触发校验函数,如果提供空数组,表示校验所有,如果提供了指定值,则表示只校验提供的字段。
|
||||
* @public
|
||||
* @param {string[]} keys - 待校验的字段
|
||||
* @returns {FORM_SUBMIT_RESULT} 校验结果
|
||||
*/
|
||||
valid(keys : string[]) : FORM_SUBMIT_RESULT {
|
||||
return _valid(keys);
|
||||
},
|
||||
/**
|
||||
* 清除校验状态并回到初始状态。
|
||||
*/
|
||||
clearValid(){
|
||||
list.value.forEach((el : FORM_ITEM) => {
|
||||
el.ele.clearValid()
|
||||
})
|
||||
},
|
||||
submit() : FORM_SUBMIT_RESULT {
|
||||
|
||||
return _valid([] as string[]);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="xForm">
|
||||
<!--
|
||||
@slot 插槽内只能放置x-form-item子节点组件
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor, colorAddDeepen } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { XGRID_ITEM_INFO } from "../../interface.uts"
|
||||
|
||||
/**
|
||||
* @name 宫格子组件 xGridItem
|
||||
* @description 不可单独使用,请把放它在x-grid标签内。
|
||||
* @page /pages/index/grid
|
||||
* @category 导航组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({ name: "xGridItem" })
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 项目点击时触发
|
||||
*/
|
||||
'click'
|
||||
])
|
||||
|
||||
const isHover = ref(false)
|
||||
|
||||
type xGridItemPropsType = {
|
||||
/**
|
||||
* 背景,默认为空值,读取父xGrid组件统一设置的背景
|
||||
* 如果这里提供了,以子组件为准。
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 项目在列表中的索引,从0开始
|
||||
* 请务必在循环gridItem时提供order为循环的index
|
||||
*/
|
||||
order: number,
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* 文字
|
||||
*/
|
||||
text: string,
|
||||
/**
|
||||
* 图标颜色,空值取父xGrid的值
|
||||
*/
|
||||
iconColor: string,
|
||||
/**
|
||||
* 文字亮系,空值取父xGrid的值
|
||||
*/
|
||||
textColor: string,
|
||||
/**
|
||||
* 文字暗黑颜色,空值取父xGrid的值
|
||||
*/
|
||||
textDarkColor: string,
|
||||
/**
|
||||
* 文字大小,空值取父xGrid的值
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 图标大小,空值取父xGrid的值
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 是否开启链接hover效果
|
||||
*/
|
||||
isLink: boolean,
|
||||
/**
|
||||
* url链接地址,如果填写,点击会跳转
|
||||
*/
|
||||
url: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xGridItemPropsType>(), {
|
||||
bgColor: 'transparent',
|
||||
order: -1,
|
||||
icon: '',
|
||||
text: '',
|
||||
iconColor: '',
|
||||
textColor: '',
|
||||
textDarkColor: '',
|
||||
fontSize: '',
|
||||
iconSize: '',
|
||||
isLink: true,
|
||||
url: '',
|
||||
})
|
||||
|
||||
// gird共有几列
|
||||
const xGridCol = inject('xGridCol', computed(() : number => 1))
|
||||
const xGridHeight = inject('xGridHeight', computed(() : string => '0px'))
|
||||
const xGridItemBgColor = inject('xGridItemBgColor', computed(() : string => 'rgba(0,0,0,0)'))
|
||||
const xGridItemGlobalProptype = inject('xGridItemGlobalProptype', computed(() : XGRID_ITEM_INFO => {
|
||||
return {
|
||||
iconSize: '',
|
||||
fontSize: '',
|
||||
iconColor: '',
|
||||
fontColor: '',
|
||||
fontDarkColor: '',
|
||||
darkIconColor: ''
|
||||
} as XGRID_ITEM_INFO
|
||||
}))
|
||||
const borderColor = inject('borderColor', computed(() : string => 'rgba(0,0,0,0)'))
|
||||
const showBorder = inject('showBorder', computed(() : boolean => false))
|
||||
const _xGridCol = computed(():number => xGridCol.value)
|
||||
const _xGridColBl = computed(():string => `${100 / _xGridCol.value}%`)
|
||||
const _bgColor = computed(():string => {
|
||||
if (props.bgColor == '') return xGridItemBgColor.value
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _hoverbgColor = computed(():string => {
|
||||
if (!props.isLink && props.url == '') return _bgColor.value
|
||||
if (props.bgColor == '') return colorAddDeepen(xGridItemBgColor.value)
|
||||
return colorAddDeepen(props.bgColor)
|
||||
})
|
||||
const _text = computed(():string => props.text)
|
||||
// gird项目当前的位置索引
|
||||
const _order = computed(():number => props.order)
|
||||
const _icon = computed(():string => props.icon)
|
||||
const _iconColor = computed(():string => {
|
||||
if (props.iconColor == "") {
|
||||
if (xConfig.dark == 'dark') {
|
||||
return xGridItemGlobalProptype.value.darkIconColor
|
||||
}
|
||||
return xGridItemGlobalProptype.value.iconColor
|
||||
}
|
||||
return getDefaultColor(props.iconColor);
|
||||
})
|
||||
const _textColor = computed(():string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.textDarkColor == "") return xGridItemGlobalProptype.value.fontDarkColor == '' ? '#ffffff' : xGridItemGlobalProptype.value.fontDarkColor
|
||||
return props.textDarkColor
|
||||
}
|
||||
if (props.textColor == "") return xGridItemGlobalProptype.value.fontColor
|
||||
return getDefaultColor(props.textColor);
|
||||
})
|
||||
const _fontSize = computed(():string => {
|
||||
if (props.fontSize == "") return xGridItemGlobalProptype.value.fontSize
|
||||
return checkIsCssUnit(props.fontSize, 'px');
|
||||
})
|
||||
const _iconSize = computed(():string => {
|
||||
if (props.iconSize == "") return xGridItemGlobalProptype.value.iconSize
|
||||
return checkIsCssUnit(props.iconSize, 'px');
|
||||
})
|
||||
|
||||
|
||||
const borderMaps = computed(():Map<string,any> => {
|
||||
const borderMap = new Map<string, any>()
|
||||
|
||||
// 如果没有开启边线显示,返回空Map
|
||||
if (!showBorder.value) {
|
||||
return borderMap
|
||||
}
|
||||
// 计算当前项目在第几行第几列
|
||||
const currentCol = _order.value % _xGridCol.value // 当前列索引(从0开始)
|
||||
const currentRow = Math.floor(_order.value / _xGridCol.value) // 当前行索引(从0开始)
|
||||
|
||||
// 设置边线样式
|
||||
const borderStyle = `1px solid ${borderColor.value}`
|
||||
|
||||
// 上边线:只有非第一行才显示
|
||||
if (currentRow > 0) {
|
||||
borderMap.set('border-top', borderStyle)
|
||||
}
|
||||
// 右边线:只有非最右列才显示
|
||||
if (currentCol < _xGridCol.value - 1) {
|
||||
borderMap.set('border-right', borderStyle)
|
||||
}
|
||||
return borderMap
|
||||
})
|
||||
|
||||
const itemClick = () => {
|
||||
if (props.url != '') {
|
||||
uni.navigateTo({
|
||||
url: props.url
|
||||
})
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 项目点击时触发。
|
||||
*/
|
||||
emits('click')
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view @touchend="isHover = false" @touchcancel="isHover = false" @touchstart="isHover = true" @click="itemClick"
|
||||
class="xGridItem"
|
||||
:style="[{width:_xGridColBl,height:xGridHeight,backgroundColor:isHover?_hoverbgColor:_bgColor},borderMaps]">
|
||||
<!--
|
||||
@slot 默认插槽内容
|
||||
-->
|
||||
<slot>
|
||||
<x-icon v-if="_icon" :name="_icon" :font-size="_iconSize" :color="_iconColor"
|
||||
style="margin-bottom: 8px;"></x-icon>
|
||||
<text :style="{
|
||||
color:_textColor,
|
||||
fontSize:_fontSize,
|
||||
textAlign:'center'
|
||||
}">{{_text}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xGridItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
/* #ifndef APP */
|
||||
box-szing:border-box;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { XGRID_ITEM_INFO } from "../../interface.uts"
|
||||
|
||||
/**
|
||||
* @name 宫格 xGrid
|
||||
* @description 内部只可放置x-grid-item。
|
||||
* @page /pages/index/grid
|
||||
* @category 导航组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xGrid"})
|
||||
|
||||
type xGridPropsType = {
|
||||
/**
|
||||
* 显示几列
|
||||
*/
|
||||
col: number,
|
||||
/**
|
||||
* 项目高度
|
||||
*/
|
||||
itemHeight: string,
|
||||
/**
|
||||
* 统一设置子组件的背景
|
||||
*/
|
||||
itemBgColor: string,
|
||||
/**
|
||||
* 整体宫格的背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 整体宫格的背景暗黑,如果为空,读取全局sheetDark
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 整体宽度
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
iconColor: string,
|
||||
/**
|
||||
* 暗黑时图标颜色
|
||||
*/
|
||||
darkIconColor: string,
|
||||
/**
|
||||
* 文字颜色
|
||||
*/
|
||||
textColor: string,
|
||||
/**
|
||||
* 文字暗黑颜色
|
||||
*/
|
||||
textDarkColor: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 图标大小
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 是否显示边框。请务必为每个项目配置order
|
||||
*/
|
||||
showBorder: boolean,
|
||||
borderColor: string,
|
||||
borderDarkColor: string,
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xGridPropsType>(), {
|
||||
col: 3,
|
||||
itemHeight: '70',
|
||||
itemBgColor: 'white',
|
||||
bgColor: 'transparent',
|
||||
darkBgColor: 'transparent',
|
||||
width: 'auto',
|
||||
iconColor: '#333333',
|
||||
darkIconColor: '#FFFFFF',
|
||||
textColor: '#888888',
|
||||
textDarkColor: '',
|
||||
fontSize: '13',
|
||||
iconSize: '25',
|
||||
showBorder: true,
|
||||
borderColor: '#f5f5f5',
|
||||
borderDarkColor: '#333333',
|
||||
round: '0',
|
||||
})
|
||||
|
||||
const _col = computed(() : number => {
|
||||
return props.col
|
||||
})
|
||||
const _itemHeight = computed(() : string => {
|
||||
return checkIsCssUnit(props.itemHeight, xConfig.unit)
|
||||
})
|
||||
const _width = computed(() : string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
const _itemBgColor = computed(() : string => {
|
||||
return getDefaultColor(props.itemBgColor)
|
||||
})
|
||||
const _bgColor = computed(() : string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor != "") return getDefaultColor(props.darkBgColor)
|
||||
return getDefaultColor(xConfig.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _itemGloablStyle = computed(() : XGRID_ITEM_INFO =>{
|
||||
return {
|
||||
iconColor: getDefaultColor(props.iconColor),
|
||||
iconSize: checkIsCssUnit(props.iconSize, xConfig.unit),
|
||||
fontColor: getDefaultColor(props.textColor),
|
||||
fontDarkColor: getDefaultColor(props.textDarkColor),
|
||||
fontSize: checkIsCssUnit(props.fontSize, xConfig.unit),
|
||||
darkIconColor: getDefaultColor(props.darkIconColor),
|
||||
|
||||
} as XGRID_ITEM_INFO
|
||||
})
|
||||
const _round = computed(():string => checkIsCssUnit(props.round,xConfig.unit))
|
||||
provide("xGridCol",_col)
|
||||
provide("xGridHeight",_itemHeight)
|
||||
provide("xGridItemBgColor",_itemBgColor)
|
||||
provide("xGridItemGlobalProptype",_itemGloablStyle)
|
||||
const _borderColor = computed(():string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.borderDarkColor != "") return getDefaultColor(props.borderDarkColor)
|
||||
return getDefaultColor(xConfig.inputDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.borderColor)
|
||||
})
|
||||
const _showBorder = computed(():boolean => props.showBorder)
|
||||
provide("showBorder",_showBorder)
|
||||
provide("borderColor",_borderColor)
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="xGrid" :style="{
|
||||
width:_width,
|
||||
backgroundColor:_bgColor,
|
||||
border:`${_showBorder?('1px solid '+_borderColor):'none'}`,
|
||||
borderRadius:_round
|
||||
}">
|
||||
<!--
|
||||
@slot 插槽内只可放置x-grid-item
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xGrid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,342 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { getDefaultColor, colorAddDeepen, setBgColorLightByDark, setTextColorLightByDark, isBlackAndWhite } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { fillArrayCssValue, fillArrayCssValueBycolor, fillArrayCssValueByround, checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { xTween } from "../../core/util/xTween.uts"
|
||||
import { xTweenStatus, xTweenCallbackFunType, xTweenAnimate, xTweenEventCallFunType, xTweenEventCall } from "../../interface.uts"
|
||||
// #ifdef APP|| WEB
|
||||
import remixicon from "./remixicon.uts"
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* @name 图标 xIcon
|
||||
* @description 图标使用的是开源图标:[https://remixicon.com/](https://remixicon.com/),版本是:4.5.0 ,使用时,不用带ri-前缀。
|
||||
* @page /pages/index/icon
|
||||
* @category 常用组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xIcon"})
|
||||
const emits = defineEmits(['click'])
|
||||
|
||||
type xIconPropsType = {
|
||||
/**
|
||||
* 图标名称,不带ri-前缀
|
||||
* 也可以是本地或者远程图片
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* 图标大小,单位任意,比如"12",12px,12rpx
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 自定义图标字体,前提是
|
||||
* 你要在appuvue中已经安装好字体文件,
|
||||
* 否则无法使用。
|
||||
* 要让自定图标生效你需要配合code属性一起使用
|
||||
*/
|
||||
fontFamily: string,
|
||||
/**
|
||||
* 图标的16进制字符串,注意不含u
|
||||
* 比如:ea0c,ea14这种,
|
||||
* 如果你提供了code优化解析这,那么name将失效,如果你是纯字体图标使用
|
||||
* 你可以使用这个属性,可以提供性能和自己定义的图标会比较方便。
|
||||
*/
|
||||
code: string,
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 暗黑时的文本颜色,如果你不提供,将自动反转。
|
||||
* 自动反转是根据亮度反转,色相不变。
|
||||
*/
|
||||
darkColor: string,
|
||||
/**
|
||||
* 是否旋转动画
|
||||
*/
|
||||
spin: boolean,
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
rotation: number,
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: number,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xIconPropsType>(), {
|
||||
name: "home-3-fill",
|
||||
fontSize: "16",
|
||||
fontFamily: "remixicon",
|
||||
code: "",
|
||||
color: "black",
|
||||
darkColor: "",
|
||||
spin: false,
|
||||
rotation: 0,
|
||||
duration: 1500,
|
||||
})
|
||||
|
||||
// data -> refs
|
||||
const xIcon = ref<Element | null>(null)
|
||||
const refreshId = ref(1)
|
||||
const id = ref<string>("xIconspin" + getUid())
|
||||
const element = ref<UniElement | null>(null)
|
||||
const rotationDeg = ref<number>(0)
|
||||
const isLoad = ref<boolean>(false)
|
||||
const isdestory = ref<boolean>(false)
|
||||
const status = ref<string>('play')
|
||||
const tid = ref<number>(0)
|
||||
const xt = ref<xTween>(new xTween())
|
||||
const xIcons = ref<UniElement | null>(null)
|
||||
|
||||
// computed replacements
|
||||
const _iconName = computed((): string => {
|
||||
return props.name
|
||||
})
|
||||
|
||||
const _mpcode = computed((): string => {
|
||||
let cname = ''
|
||||
// #ifdef MP
|
||||
cname = props.code==''?('ri-'+props.name):'';
|
||||
// #endif
|
||||
return cname
|
||||
})
|
||||
|
||||
const _isFileImg = computed((): boolean => {
|
||||
if (props.name.lastIndexOf(".") > -1 ||
|
||||
props.name.indexOf("ftp:") > -1 ||
|
||||
props.name.indexOf("https:") > -1 ||
|
||||
props.name.indexOf("http:") > -1 ||
|
||||
props.name.indexOf("data:image") > -1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
|
||||
const iconName = computed((): string => {
|
||||
if (_isFileImg.value) return props.name
|
||||
let texts = ""
|
||||
try {
|
||||
// #ifdef APP||WEB
|
||||
let codestr = ''
|
||||
if(props.code==''){
|
||||
codestr = remixicon.getString(props.name)!
|
||||
}else{
|
||||
codestr = props.code
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
let codePoint = Integer.parseInt(codestr, 16);
|
||||
let charArray = Character.toChars(codePoint);
|
||||
texts = new String(charArray);
|
||||
// #endif
|
||||
// #ifdef APP-IOS|| WEB||APP-HARMONY
|
||||
texts = String.fromCharCode(parseInt(codestr, 16));
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
if(props.code!=''){
|
||||
texts = String.fromCharCode(parseInt(props.code, 16));
|
||||
}
|
||||
// #endif
|
||||
|
||||
} catch (e) {
|
||||
console.error("xicon解析失败。", e)
|
||||
}
|
||||
return texts
|
||||
})
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _color = computed((): string => {
|
||||
let color = props.color==""?'black':props.color;
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkColor != "") {
|
||||
color = props.darkColor!
|
||||
return getDefaultColor(color)
|
||||
}
|
||||
return setTextColorLightByDark(color)
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
})
|
||||
|
||||
const _spin = computed((): boolean => props.spin)
|
||||
const _rotation = computed((): number => props.rotation)
|
||||
|
||||
function setRadeg() {
|
||||
try {
|
||||
element.value = xIcons.value as UniElement
|
||||
element.value!.style.setProperty("transition-duration", props.duration.toString() + 'ms')
|
||||
element.value!.style.setProperty("transform", `rotate(${_rotation.value}deg)`)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function playSpin() {
|
||||
if (!_spin.value || isdestory.value) return;
|
||||
|
||||
let elementLocal = xIcons.value as UniElement
|
||||
|
||||
// #ifdef APP || WEB
|
||||
xt.value.pause()
|
||||
xt.value.destroy()
|
||||
xt.value.startRender()
|
||||
xt.value.addAnimate({
|
||||
loop:-1,
|
||||
duration:props.duration,
|
||||
complete:(item:xTweenEventCallFunType)=>{
|
||||
},
|
||||
enter:(item:xTweenEventCallFunType)=>{
|
||||
elementLocal!.style.setProperty("transition-duration", '0ms')
|
||||
elementLocal!.style.setProperty("transform", `rotate(${360 * item.progress}deg)`)
|
||||
},
|
||||
pause:(item:xTweenEventCallFunType)=>{},
|
||||
} as xTweenAnimate)
|
||||
xt.value.play()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function clickListen() {
|
||||
emits("click")
|
||||
}
|
||||
|
||||
|
||||
/** app加载的字体方式 */
|
||||
// https://cdn.tmui.design/public/static/remixicon.ttf
|
||||
function loadFontByX() {
|
||||
isLoad.value = true;
|
||||
|
||||
// uni.loadFontFace({
|
||||
// source: "url('static/remixicon.ttf')",
|
||||
// family: "remixicon",
|
||||
// success() {
|
||||
// this.isLoad = true;
|
||||
// uni.setStorageSync("loadedFontBytmx", "true")
|
||||
// },
|
||||
// fail() {
|
||||
// console.log('loadfontFail.---')
|
||||
// uni.setStorageSync("loadedFontBytmx", "")
|
||||
// },
|
||||
// complete() {
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
|
||||
// watchers
|
||||
watch(():boolean => props.spin, () => {
|
||||
if (_spin.value) {
|
||||
playSpin();
|
||||
}else{
|
||||
xt.value.pause()
|
||||
xt.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
watch(():number => props.rotation, () => {
|
||||
if (_spin.value) return;
|
||||
setRadeg();
|
||||
})
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isdestory.value = true;
|
||||
clearTimeout(tid.value)
|
||||
xt.value.destroy()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadFontByX();
|
||||
isdestory.value = false;
|
||||
if (_spin.value) {
|
||||
playSpin();
|
||||
} else {
|
||||
nextTick(() => {
|
||||
setRadeg();
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="faceBox">
|
||||
<!-- #endif -->
|
||||
<text
|
||||
v-if="!_isFileImg"
|
||||
@click="clickListen"
|
||||
:id="id"
|
||||
ref="xIcons"
|
||||
class="face"
|
||||
:class="[_spin?'faceSpinIcon':'',_mpcode]"
|
||||
:style="{
|
||||
'font-family': fontFamily,
|
||||
'font-size':_fontSize,
|
||||
'color':_color,
|
||||
'width':_fontSize,
|
||||
'height':_fontSize,
|
||||
'lineHeight':_fontSize
|
||||
}">
|
||||
{{iconName}}
|
||||
</text>
|
||||
<image @click="clickListen" :id="id" v-else ref="xIcons" :style="{width:_fontSize,height:_fontSize}" :src="iconName"></image>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<style scoped >
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.faceBox{
|
||||
display: inline-block;
|
||||
}
|
||||
/* #endif */
|
||||
.face {
|
||||
transition-property: transform;
|
||||
transition-duration: 0ms;
|
||||
transition-timing-function: linear;
|
||||
transform: rotate(0deg);
|
||||
|
||||
text-align: center;
|
||||
/* #ifdef WEB || MP-WEIXIN */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1 !important;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB || MP-WEIXIN */
|
||||
.faceSpinIcon{
|
||||
animation:xFontIconRotate 1s linear infinite ;
|
||||
}
|
||||
@keyframes xFontIconRotate {
|
||||
0%{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100%{
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 图集 xImageGroup
|
||||
* @description 主要是为了一些需要快速图片排版集的展示,比如评论图集,详情列表图集等,快速开发时使用。方便快捷。
|
||||
* @page /pages/index/image-group
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xImageGroup"})
|
||||
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 图片项目被点击
|
||||
* @param item - 项目
|
||||
*/
|
||||
click: [item: UTSJSONObject]
|
||||
}>()
|
||||
type xImageGroupPropsType = {
|
||||
/**
|
||||
* 图片列表
|
||||
* 只要包含有url字段即可。
|
||||
* label需要在图片显示的文本字段,如果没有不要出现此字段
|
||||
* 如果提供了temp缩略图,会优先展示temp字段,预览时采用url原图
|
||||
* {url:string,label?:string,temp?:string}
|
||||
*/
|
||||
list: UTSJSONObject[],
|
||||
/**
|
||||
* 显示的模式见:https://doc.dcloud.net.cn/uni-app-x/component/image.html
|
||||
*/
|
||||
model: string,
|
||||
/**
|
||||
* 图片高,不要使用auto,%,
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 图片宽,不要使用auto,可以%值
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 间隙
|
||||
*/
|
||||
gutter: string,
|
||||
/**
|
||||
* inset表示文本在图片上
|
||||
* ouuter表示文本在正文展示
|
||||
* 不要动态修改
|
||||
*/
|
||||
labelModel: 'inset' | 'outter',
|
||||
/**
|
||||
* 如果有文字显示文字的大小
|
||||
*/
|
||||
labelFontSize: string,
|
||||
/**
|
||||
* 如果有文字,显示文字的颜色
|
||||
*/
|
||||
labelFontColor: string,
|
||||
/**
|
||||
* 如果有文字,显示文字的颜色,暗黑时为空时取白
|
||||
*/
|
||||
darkLabelFontColor: string,
|
||||
/**
|
||||
* 是否预览图片
|
||||
*/
|
||||
preview: boolean,
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xImageGroupPropsType>(), {
|
||||
list: ():UTSJSONObject[] => [] as UTSJSONObject[],
|
||||
model: "scaleToFill",
|
||||
height: "100",
|
||||
width: "33.33%",
|
||||
gutter: "2",
|
||||
labelModel: "inset",
|
||||
labelFontSize: "14",
|
||||
labelFontColor: "white",
|
||||
darkLabelFontColor: "",
|
||||
preview: true,
|
||||
round: "0"
|
||||
})
|
||||
// 计算属性
|
||||
const _list = computed((): UTSJSONObject[] => {
|
||||
return props.list
|
||||
})
|
||||
|
||||
const _labelFontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkLabelFontColor != '') return getDefaultColor(props.darkLabelFontColor)
|
||||
return "#ffffff"
|
||||
}
|
||||
return getDefaultColor(props.labelFontColor)
|
||||
})
|
||||
|
||||
const _gutter = computed((): string => {
|
||||
return checkIsCssUnit(props.gutter, xConfig.unit)
|
||||
})
|
||||
|
||||
const _labelFontSize = computed((): string => {
|
||||
let fontSize = checkIsCssUnit(props.labelFontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _width = computed((): string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
|
||||
const _height = computed((): string => {
|
||||
return checkIsCssUnit(props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
// 方法
|
||||
function showUrlList(item: UTSJSONObject): string {
|
||||
let url = item.getString('url');
|
||||
url = url == null ? '' : url
|
||||
let temp = item.getString('temp');
|
||||
if (temp != null) return temp;
|
||||
return url;
|
||||
}
|
||||
|
||||
function showLabel(item: UTSJSONObject): string {
|
||||
let label = item.getString('label');
|
||||
label = label == null ? '' : label
|
||||
return label;
|
||||
}
|
||||
|
||||
function onClick(item: UTSJSONObject, index: number): void {
|
||||
let url = item.getString('url');
|
||||
url = url == null ? '' : url
|
||||
let listurl = _list.value.map((el: UTSJSONObject): string => {
|
||||
let temurl = el.getString('url');
|
||||
temurl = temurl == null ? '' : temurl
|
||||
return temurl;
|
||||
})
|
||||
if (props.preview) {
|
||||
uni.previewImage({
|
||||
current: url,
|
||||
urls: listurl as string[]
|
||||
})
|
||||
}
|
||||
|
||||
emits('click', item)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="xImageGroup">
|
||||
<view @click="onClick(item,index)" v-for="(item,index) in _list" :key="index" class="xImageGroupWrap"
|
||||
:style="{padding:_gutter,width:_width,height:_height,borderRadius:_round}">
|
||||
<view class="xImageGroupWrapBox">
|
||||
<x-image :round="_round" :ratio="1" width="100%" height="100%" style="height: 100%;width:100%" :model="props.model" :preview="false" :src="showUrlList(item)"></x-image>
|
||||
</view>
|
||||
<!-- margin:labelModel=='inset'?_gutter:'0px' -->
|
||||
<view v-if="showLabel(item)!=''" class="xImageGroupLabelBox" :style="{
|
||||
position:props.labelModel=='inset'?'absolute':'static',
|
||||
}">
|
||||
<view class="xImageGroupLabelBoxPadding" :style="{
|
||||
margin:props.labelModel=='inset'?_gutter:'0px',
|
||||
padding: props.labelModel=='inset'?'20rpx':'20rpx 0px',
|
||||
borderRadius:_round,
|
||||
'background-image': props.labelModel=='inset'?'linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,0.9))':''
|
||||
}">
|
||||
<text :style="{fontSize:_labelFontSize,color:_labelFontColor}"
|
||||
class="xImageGroupLabelLabel">{{showLabel(item)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xImageGroupLabelBox {
|
||||
z-index: 2;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
/* #ifdef MP || WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xImageGroupLabelBoxPadding {
|
||||
|
||||
/* background-image: linear-gradient(to bottom,rgba(0,0,0,0),rgba(0,0,0,0.9)); */
|
||||
}
|
||||
|
||||
.xImageGroupLabelLabel {
|
||||
|
||||
/* color:white; */
|
||||
lines: 1;
|
||||
/* #ifdef WEB */
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xImageGroupWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
/* #ifdef MP || WEB */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xImageGroupWrapBox {
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
|
||||
.xImageGroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,333 @@
|
||||
|
||||
export type xSIZE = {
|
||||
width : number,
|
||||
height : number,
|
||||
top : number,
|
||||
left : number
|
||||
}
|
||||
export type xImageInfo = {
|
||||
width : number,
|
||||
height : number,
|
||||
top : number,
|
||||
left : number,
|
||||
src : string,
|
||||
}
|
||||
export type xSIZEiMage = {
|
||||
width : number,
|
||||
height : number,
|
||||
top : number,
|
||||
left : number,
|
||||
scale : number
|
||||
}
|
||||
export type xPOINT = {
|
||||
x : number,
|
||||
y : number
|
||||
}
|
||||
export type xCalculatePinchZoomOpts = {
|
||||
initialPoints : xPOINT[],
|
||||
currentPoints : xPOINT[],
|
||||
originWidth : number, // 原始图片宽度
|
||||
originHeight : number, // 原始图片高度
|
||||
cropWidth : number, // 裁剪框宽度
|
||||
cropHeight : number,
|
||||
initialScale : number,
|
||||
width : number,
|
||||
height : number,
|
||||
top : number,
|
||||
left : number,
|
||||
scale : number
|
||||
}
|
||||
export function alignImageToFrame(imageParams : xSIZEiMage, frameParams : xSIZE) : xSIZE {
|
||||
const {
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
top: imgTop,
|
||||
left: imgLeft
|
||||
} = imageParams;
|
||||
|
||||
const {
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
top: frameTop,
|
||||
left: frameLeft
|
||||
} = frameParams;
|
||||
|
||||
// 图片是否完全覆盖框(修改为严格大于)
|
||||
const isWidthCovered = imgWidth > frameWidth;
|
||||
const isHeightCovered = imgHeight > frameHeight;
|
||||
|
||||
// 当图片宽高都大于框时,保持自由拖动
|
||||
// 同时图片确保覆盖住裁剪框.
|
||||
if (isWidthCovered
|
||||
&& isHeightCovered
|
||||
&& imgLeft < frameLeft
|
||||
&& imgTop < frameTop
|
||||
&& (imgLeft + imgWidth) > (frameLeft + frameWidth)
|
||||
&& (imgTop + imgHeight) > (frameTop + frameHeight)
|
||||
) {
|
||||
return {
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
top: imgTop,
|
||||
left: imgLeft
|
||||
};
|
||||
}
|
||||
|
||||
let newTop = imgTop;
|
||||
let newLeft = imgLeft;
|
||||
|
||||
// 图片的左侧是不是小于框的右侧
|
||||
let isMinLeft = imgLeft < frameLeft
|
||||
let isMaxRight = imgLeft + imgWidth > frameLeft + frameWidth
|
||||
let isMinTop = imgTop < frameTop
|
||||
let isMaxTop = imgTop + imgHeight > frameTop + frameHeight
|
||||
// 水平对齐逻辑(反向对齐),如果越过左边了要左对齐,
|
||||
if (imgLeft < frameLeft && !isMaxRight) {
|
||||
// 图片左侧越界,靠右对齐
|
||||
newLeft = frameLeft + frameWidth - imgWidth;
|
||||
} else if (imgLeft + imgWidth > frameLeft + frameWidth && !isMinLeft) {
|
||||
// 图片右侧越界,靠左对齐
|
||||
newLeft = frameLeft;
|
||||
}
|
||||
|
||||
// 垂直对齐逻辑(反向对齐)
|
||||
if (imgTop < frameTop && !isMaxTop) {
|
||||
// 图片顶部越界,靠底对齐
|
||||
newTop = frameTop + frameHeight - imgHeight;
|
||||
|
||||
} else if (imgTop + imgHeight > frameTop + frameHeight && !isMinTop) {
|
||||
// 图片底部越界,靠顶对齐
|
||||
newTop = frameTop;
|
||||
}
|
||||
|
||||
return {
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
top: newTop,
|
||||
left: newLeft
|
||||
};
|
||||
}
|
||||
function scaleImageToFitFrame(imageParams : xImageInfo, frameParams : xSIZE) : xSIZEiMage {
|
||||
// 解构图片和裁剪框的参数
|
||||
const {
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
top: imgTop = 0,
|
||||
left: imgLeft = 0
|
||||
} = imageParams;
|
||||
|
||||
const {
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
top: frameTop = 0,
|
||||
left: frameLeft = 0
|
||||
} = frameParams;
|
||||
|
||||
// 计算宽高比
|
||||
const imgRatio = imgWidth / imgHeight;
|
||||
const frameRatio = frameWidth / frameHeight;
|
||||
|
||||
let newWidth = 0
|
||||
let newHeight = 0
|
||||
let scale = 1
|
||||
|
||||
// 确保填充整个裁剪框
|
||||
if (imgRatio > frameRatio) {
|
||||
// 以高度为基准缩放
|
||||
newHeight = frameHeight;
|
||||
newWidth = newHeight * imgRatio;
|
||||
scale = newHeight / imgHeight;
|
||||
|
||||
// 如果宽度不够,再次调整
|
||||
if (newWidth < frameWidth) {
|
||||
newWidth = frameWidth;
|
||||
newHeight = newWidth / imgRatio;
|
||||
scale = newWidth / imgWidth;
|
||||
}
|
||||
} else {
|
||||
// 以宽度为基准缩放
|
||||
newWidth = frameWidth;
|
||||
newHeight = newWidth / imgRatio;
|
||||
scale = newWidth / imgWidth;
|
||||
|
||||
// 如果高度不够,再次调整
|
||||
if (newHeight < frameHeight) {
|
||||
newHeight = frameHeight;
|
||||
newWidth = newHeight * imgRatio;
|
||||
scale = newHeight / imgHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算最终定位(考虑裁剪框和图片的初始偏移)
|
||||
const top = frameTop + (frameHeight - newHeight) / 2 - imgTop * scale;
|
||||
const left = frameLeft + (frameWidth - newWidth) / 2 - imgLeft * scale;
|
||||
|
||||
return {
|
||||
width: newWidth, // 新的图片宽度
|
||||
height: newHeight, // 新的图片高度
|
||||
top, // 垂直方向偏移
|
||||
left, // 水平方向偏移
|
||||
scale // 缩放比例
|
||||
} as xSIZEiMage;
|
||||
}
|
||||
export function translateScalePosition(orimg : xImageInfo, img : xSIZEiMage, mask : xSIZE, parent : xSIZE) {
|
||||
let scaleinfo = scaleImageToFitFrame(orimg, mask)
|
||||
img.width = scaleinfo.width
|
||||
img.height = scaleinfo.height
|
||||
img.top = scaleinfo.top
|
||||
img.left = scaleinfo.left
|
||||
img.scale = scaleinfo.scale
|
||||
}
|
||||
|
||||
export function calculatePinchZoom(options : xCalculatePinchZoomOpts) : xSIZEiMage {
|
||||
const {
|
||||
initialPoints, // 初始双指坐标 [{x, y}, {x, y}]
|
||||
currentPoints, // 当前双指坐标 [{x, y}, {x, y}]
|
||||
originWidth, // 原始图片宽度
|
||||
originHeight, // 原始图片高度
|
||||
cropWidth, // 裁剪框宽度
|
||||
cropHeight, // 裁剪框高度
|
||||
width, // 当前图片宽度
|
||||
height, // 当前图片高度
|
||||
top, // 图片当前顶部位置
|
||||
left, // 图片当前左侧位置
|
||||
scale, // 当前缩放比例
|
||||
initialScale // 初始缩放比例
|
||||
} = options;
|
||||
|
||||
// 阻尼系数,0-1之间,越大越快,越小缩放越慢.
|
||||
const DAMPING_FACTOR = 0.45;
|
||||
|
||||
// 计算初始和当前双指距离
|
||||
const getDistance = (points : xPOINT[]) : number => {
|
||||
const p1 = points[0];
|
||||
const p2 = points[1];
|
||||
return Math.sqrt(
|
||||
Math.pow(p2.x - p1.x, 2) +
|
||||
Math.pow(p2.y - p1.y, 2)
|
||||
);
|
||||
};
|
||||
|
||||
// 计算双指中心点
|
||||
const getCenter = (points : xPOINT[]) : xPOINT => {
|
||||
const p1 = points[0];
|
||||
const p2 = points[1];
|
||||
return {
|
||||
x: (p1.x + p2.x) / 2,
|
||||
y: (p1.y + p2.y) / 2
|
||||
} as xPOINT;
|
||||
};
|
||||
|
||||
const initialDistance = getDistance(initialPoints);
|
||||
const currentDistance = getDistance(currentPoints);
|
||||
if (initialDistance == currentDistance) return {
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
scale
|
||||
} as xSIZEiMage;
|
||||
// 计算缩放比例(加入阻尼)
|
||||
const rawScaleChange = currentDistance / initialDistance;
|
||||
const dampedScaleChange = 1 + (rawScaleChange - 1) * DAMPING_FACTOR;
|
||||
let newScale = scale * dampedScaleChange;
|
||||
|
||||
// 计算最小宽高(确保至少覆盖裁剪框)
|
||||
const minWidth = Math.max(cropWidth, originWidth * initialScale);
|
||||
const minHeight = Math.max(cropHeight, originHeight * initialScale);
|
||||
|
||||
// 计算最大宽高
|
||||
const maxWidth = originWidth * 3;
|
||||
const maxHeight = originHeight * 3;
|
||||
|
||||
// 限制缩放范围
|
||||
const MIN_SCALE = initialScale;
|
||||
const MAX_SCALE = 3;
|
||||
|
||||
// 判断是否超过最大缩放限制
|
||||
if (newScale > MAX_SCALE) {
|
||||
return {
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
left,
|
||||
top,
|
||||
scale: MAX_SCALE
|
||||
} as xSIZEiMage;
|
||||
}
|
||||
if (newScale < MIN_SCALE) {
|
||||
return {
|
||||
width: minWidth,
|
||||
height: minHeight,
|
||||
left,
|
||||
top,
|
||||
scale: MIN_SCALE
|
||||
} as xSIZEiMage;
|
||||
}
|
||||
|
||||
// 计算新的图片宽高
|
||||
const newWidth = Math.max(minWidth, Math.min(originWidth * newScale, maxWidth));
|
||||
const newHeight = Math.max(minHeight, Math.min(originHeight * newScale, maxHeight));
|
||||
|
||||
// 计算缩放中心点
|
||||
const initialCenter = getCenter(initialPoints);
|
||||
const currentCenter = getCenter(currentPoints);
|
||||
|
||||
// 计算偏移量(同样应用阻尼)
|
||||
const rawOffsetX = (initialCenter.x - left) * (dampedScaleChange - 1);
|
||||
const rawOffsetY = (initialCenter.y - top) * (dampedScaleChange - 1);
|
||||
|
||||
// 计算新的位置
|
||||
const newLeft = left - rawOffsetX;
|
||||
const newTop = top - rawOffsetY;
|
||||
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
scale: newScale
|
||||
} as xSIZEiMage;
|
||||
}
|
||||
|
||||
// 根据裁剪框的缩放后的大小,与原始大小的缩放比,确定图片的当前大小的缩放比与位置
|
||||
export function maskRatioScaleToPhotoSizePosition (oldMaskSize:xSIZEiMage,newMaskSize:xSIZE,oldimg:xImageInfo,nowImgPosSize:xSIZEiMage,index:number){
|
||||
|
||||
|
||||
|
||||
|
||||
let difflen = oldMaskSize.width - newMaskSize.width
|
||||
let scale2 = difflen/oldMaskSize.width
|
||||
let nwdifflen = scale2*nowImgPosSize.width
|
||||
let scale = nwdifflen / oldimg.width
|
||||
if(nowImgPosSize.scale + scale>3){
|
||||
scale = 3-nowImgPosSize.scale
|
||||
}
|
||||
let diffx = oldimg.width*scale
|
||||
let diffy = oldimg.height*scale
|
||||
|
||||
|
||||
let newWidth = nowImgPosSize.width + diffx
|
||||
let newHeight = nowImgPosSize.height + diffy
|
||||
nowImgPosSize.width = newWidth
|
||||
nowImgPosSize.height = newHeight
|
||||
|
||||
// 左上角,应该是左上放大.
|
||||
if(index == 0){
|
||||
nowImgPosSize.left = nowImgPosSize.left - diffx
|
||||
nowImgPosSize.top = nowImgPosSize.top - diffy
|
||||
}
|
||||
if(index == 1){
|
||||
// nowImgPosSize.left = nowImgPosSize.left - diffx
|
||||
nowImgPosSize.top = nowImgPosSize.top - diffy
|
||||
}
|
||||
if(index == 3){
|
||||
nowImgPosSize.left = nowImgPosSize.left - diffx
|
||||
// nowImgPosSize.top = nowImgPosSize.top - diffy
|
||||
}
|
||||
|
||||
newMaskSize.width = oldMaskSize.width
|
||||
newMaskSize.height = oldMaskSize.height
|
||||
newMaskSize.left = oldMaskSize.left
|
||||
newMaskSize.top = oldMaskSize.top
|
||||
nowImgPosSize.scale = nowImgPosSize.scale + scale
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,501 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { checkIsCssUnit, getUid,getUnit } from "../../core/util/xCoreUtil.uts";
|
||||
import { xConfig,xProvitae } from "../../config/xConfig.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
type IMG_MODEL = "fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill";
|
||||
type IMG_SIZE_INFO = {
|
||||
width : number,
|
||||
height : number,
|
||||
}
|
||||
type IMG_SIZE_INFO_PLACE = {
|
||||
width : string,
|
||||
height : string,
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 图片 xImage
|
||||
* @description 宽高可以设置,支持百分比,px,rpx
|
||||
* @page /pages/index/image
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: ("xImage-" + getUid()) as string,
|
||||
idBox: ("xImage-" + getUid()) as string,
|
||||
/** 是否正在加载,默认为false */
|
||||
isLoading: true,
|
||||
/** 是否加载出错,默认为false */
|
||||
isError: false,
|
||||
reload: 0,
|
||||
imgrealWidth: 0,
|
||||
imgrealHeight: 0,
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
ratioWidth: 0,
|
||||
ratioHeight: 0,
|
||||
isLoaded:false,
|
||||
tid:0,
|
||||
resizeObserver: null as UniResizeObserver | null,
|
||||
androidAndWebUrl:"",
|
||||
dateTime: 0,
|
||||
_x: 0,
|
||||
_y: 0,
|
||||
boxLeft:0,
|
||||
boxTop:0,
|
||||
isVisibled:false,
|
||||
tid2:0,
|
||||
keyidsf:0
|
||||
}
|
||||
},
|
||||
emits:[
|
||||
/**
|
||||
* 图片被点击
|
||||
*/
|
||||
'click'],
|
||||
props: {
|
||||
/**
|
||||
* 宽度,默认100%
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 高度,auto,%,rpx,px,string number
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/** 图片源 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 预览的图片源,如果为空则与src同步
|
||||
*/
|
||||
previewSrc:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
/**
|
||||
* 模式
|
||||
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
|
||||
*
|
||||
*/
|
||||
model: {
|
||||
type: String as PropType<IMG_MODEL>,
|
||||
default: "fill"
|
||||
},
|
||||
/**
|
||||
* 点击后是否预览图片
|
||||
*/
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 预览占位比例
|
||||
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
|
||||
* 当图片加载成功后,使用正确的原图片比例设置。
|
||||
* 默认是5/4=1.25
|
||||
*/
|
||||
ratio: {
|
||||
type: Number,
|
||||
default: 1.25
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
/**
|
||||
* 加载和失败时的图标大小。
|
||||
*/
|
||||
iconSize:{
|
||||
type:String,
|
||||
default:"16"
|
||||
},
|
||||
/**
|
||||
* 占位背景色
|
||||
*/
|
||||
placeBgColor:{
|
||||
type:String,
|
||||
default:"#F5F5F5"
|
||||
},
|
||||
/**
|
||||
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
|
||||
*/
|
||||
placeDarkBgColor:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
/**
|
||||
* 是否在安卓上显示过渡动画
|
||||
*/
|
||||
fadeShow:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
},
|
||||
/**
|
||||
* 用于在scorllview根节点的页面进行懒加载,不可视范围内的不显示.请仅慎使用,不可在list-view中使用.
|
||||
*/
|
||||
lazy:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let t= this;
|
||||
// #ifdef APP
|
||||
this.imgLoad()
|
||||
// #endif
|
||||
let ele = uni.getElementById(this.idBox!);
|
||||
if(ele==null) return;
|
||||
// #ifdef APP || WEB
|
||||
if (this.resizeObserver == null) {
|
||||
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target == ele) {
|
||||
// #ifdef APP
|
||||
t.tid = setTimeout(function() {
|
||||
t.getNodes();
|
||||
}, 50);
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
t.getNodes();
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.resizeObserver!.observe(ele!)
|
||||
// #ifdef uniVersion >= 4.31 && APP-ANDROID
|
||||
t.tid = setTimeout(function() {
|
||||
t.getNodes();
|
||||
}, 50);
|
||||
// #endif
|
||||
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
this.getNodes();
|
||||
// #endif
|
||||
this.setIsVisibledShow()
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
clearTimeout(this.tid2)
|
||||
if(this.resizeObserver!=null){
|
||||
this.resizeObserver?.disconnect()
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.getNodes()
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
_model():string{
|
||||
return this.model;
|
||||
},
|
||||
_placeBgColor():string{
|
||||
let bgcolor = this.placeBgColor;
|
||||
if(xConfig.dark=='dark'){
|
||||
bgcolor = this.placeDarkBgColor
|
||||
if(this.placeDarkBgColor==''){
|
||||
bgcolor = xConfig.inputDarkColor
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
},
|
||||
_round() : string {
|
||||
return checkIsCssUnit(this.round, xConfig.unit)
|
||||
},
|
||||
_src() : string {
|
||||
return this.src;
|
||||
},
|
||||
_previewSrc() : string {
|
||||
if(this.previewSrc == '') return this.src;
|
||||
return this.previewSrc;
|
||||
},
|
||||
|
||||
_place_size() : IMG_SIZE_INFO_PLACE {
|
||||
return {
|
||||
width: checkIsCssUnit(this.width, xConfig.unit),
|
||||
height: checkIsCssUnit(this.height, xConfig.unit),
|
||||
} as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_scrollTop():number{
|
||||
return xProvitae.scrollTop;
|
||||
},
|
||||
_img_box_size() : IMG_SIZE_INFO_PLACE {
|
||||
|
||||
if(this.imgrealHeight>0){
|
||||
return this._img_size;
|
||||
}
|
||||
|
||||
let _w = this.width;
|
||||
let _h = this.height;
|
||||
let us_w = checkIsCssUnit(this.width, xConfig.unit)
|
||||
let us_h = checkIsCssUnit(this.height, xConfig.unit)
|
||||
if(this.width.lastIndexOf('%')>-1||this.width=='auto' ){
|
||||
us_w = '100%'
|
||||
_w = '100%'
|
||||
}
|
||||
|
||||
if(this.height.lastIndexOf('%')>-1||this.height=='auto' || this.isError){
|
||||
if(this.boxHeight>=5){
|
||||
_h = (this.boxHeight).toString()+'px'
|
||||
}else{
|
||||
if(this.width.lastIndexOf('%')>-1||this.width=='auto'){
|
||||
if(this.height=='100%'){
|
||||
_h = (this.ratio * this.boxHeight).toString()+'px'
|
||||
}else{
|
||||
_h = (this.ratio * this.boxWidth).toString()+'px'
|
||||
}
|
||||
}else{
|
||||
_h = (this.ratio * parseFloat(us_w)).toString()+getUnit(us_w)
|
||||
}
|
||||
}
|
||||
|
||||
return { width: _w, height: _h } as IMG_SIZE_INFO_PLACE
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_img_size() : IMG_SIZE_INFO_PLACE {
|
||||
|
||||
let us_w = checkIsCssUnit(this.width, xConfig.unit)
|
||||
let us_h = checkIsCssUnit(this.height, xConfig.unit)
|
||||
|
||||
|
||||
if (!this.isLoaded) {
|
||||
return { width: "300px", height: "300px" } as IMG_SIZE_INFO_PLACE
|
||||
}
|
||||
|
||||
if (this.boxWidth > 0) {
|
||||
let ratio = this.boxWidth / this.imgrealWidth;
|
||||
|
||||
|
||||
// 如果图片高为auto,表示自动高。
|
||||
if((this.height=='auto')){
|
||||
us_h = (ratio*this.imgrealHeight).toString()+'px'
|
||||
}
|
||||
|
||||
if((this.width=='auto')){
|
||||
us_w = this.boxWidth.toString()+'px'
|
||||
}
|
||||
|
||||
if((this.height.lastIndexOf('%')>-1)){
|
||||
us_h = (this.boxHeight).toString()+'px'
|
||||
}
|
||||
if((this.width.lastIndexOf('%')>-1)){
|
||||
us_w = (this.boxWidth).toString()+'px'
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_styleMap() : Map<string, string> {
|
||||
let styleMap = new Map<string, string>();
|
||||
styleMap.set("width", this._img_size.width)
|
||||
styleMap.set("height", this._img_size.height)
|
||||
styleMap.set("transform", this.isLoading ? 'scale(0.1)' : 'scale(1)')
|
||||
styleMap.set("visibility", this.isLoading ? 'visible' : (!this.isError ? 'visible' : 'hidden'))
|
||||
styleMap.set("opacity", this.isLoading ? '0' : '1')
|
||||
styleMap.set("border-raiuds", this._round)
|
||||
|
||||
return styleMap;
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
src(){
|
||||
// #ifdef APP-ANDROID || WEB
|
||||
this.imgLoad()
|
||||
// #endif
|
||||
},
|
||||
_scrollTop(){
|
||||
if(!this.lazy) return;
|
||||
clearTimeout(this.tid2)
|
||||
let t = this;
|
||||
this.tid2 = setTimeout(function() {
|
||||
t.setIsVisibledShow()
|
||||
}, 10);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setIsVisibledShow(){
|
||||
|
||||
let win = uni.getWindowInfo()
|
||||
|
||||
let t = this;
|
||||
// let ele = this?.$parent?.$el as UniElement||null;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
let winHeight = win.windowHeight;
|
||||
// let winWidth = win.windowWidth;
|
||||
let eleTop = nodeinfo.top!
|
||||
let eleBotom = nodeinfo.bottom!
|
||||
t.isVisibled = eleBotom < 0 || eleTop > winHeight ?false:true;
|
||||
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
prevImage() {
|
||||
/**
|
||||
* 图片被点击
|
||||
*/
|
||||
this.$emit('click')
|
||||
if (this.preview) {
|
||||
uni.previewImage({
|
||||
current: this._previewSrc,
|
||||
urls: [this._previewSrc]
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
imgLoad2(evt : ImageLoadEvent) {
|
||||
|
||||
this.imgrealWidth = evt.detail.width;
|
||||
this.imgrealHeight = evt.detail.height;
|
||||
this.isLoading = false;
|
||||
this.androidAndWebUrl = this._src
|
||||
this.isError = false;
|
||||
this.isLoaded = true;
|
||||
},
|
||||
imgLoad() {
|
||||
let t = this;
|
||||
uni.getImageInfo({
|
||||
src:this._src,
|
||||
fail(error:IMediaError){
|
||||
console.log('error',error.errMsg)
|
||||
t.isError = true;
|
||||
t.isLoading = false
|
||||
},
|
||||
success(result:GetImageInfoSuccess){
|
||||
|
||||
t.isLoading = false;
|
||||
t.isError = false;
|
||||
t.isLoaded = true;
|
||||
if( result.path!=t.androidAndWebUrl){
|
||||
t.imgrealWidth = result.width
|
||||
t.imgrealHeight = result.height
|
||||
t.androidAndWebUrl = result.path;
|
||||
t.keyidsf+=1;
|
||||
}
|
||||
}
|
||||
} as GetImageInfoOptions)
|
||||
|
||||
},
|
||||
// evt:ImageErrorEvent
|
||||
imgError() {
|
||||
this.isError = true;
|
||||
this.isLoading = false
|
||||
},
|
||||
|
||||
getNodes() {
|
||||
let t = this;
|
||||
// let ele = this?.$parent?.$el as UniElement||null;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
t.boxWidth = nodeinfo.width!
|
||||
t.boxHeight = nodeinfo.height!
|
||||
})
|
||||
},
|
||||
resize(){
|
||||
this.isLoaded = false
|
||||
let t = this;
|
||||
// let ele = this?.$parent?.$el as UniElement||null;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
t.boxWidth = nodeinfo.width!
|
||||
t.boxHeight = nodeinfo.height!
|
||||
t.isLoaded = true
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view
|
||||
:key="keyidsf"
|
||||
@click="prevImage"
|
||||
class="xImage" ref="xImage" :id="idBox" :style="{width:_place_size.width,height:_place_size.height}">
|
||||
<view class="xImageBox"
|
||||
:style="{width:_img_box_size.width,height:_img_box_size.height,borderRadius:_round,pointerEvent:'none'}">
|
||||
<!--
|
||||
@slot 加载中的插槽,插槽内自行给你布局的view宽和高写100%
|
||||
-->
|
||||
<slot name="loading" v-if="isLoading">
|
||||
<view class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
|
||||
<x-icon :font-size="iconSize" v-if="isLoading" name="loader-2-line" color="primary" :spin="true"></x-icon>
|
||||
</view>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 加载失败时的插槽,插槽内自行给你布局的view宽和高写100%
|
||||
-->
|
||||
<slot name="error" v-if="isError">
|
||||
<view class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
|
||||
<x-icon :font-size="iconSize" v-if="isError" color="error" name="landscape-line"></x-icon>
|
||||
</view>
|
||||
</slot>
|
||||
<!-- @error="imgError" -->
|
||||
<!-- #ifdef APP -->
|
||||
<image :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
|
||||
:mode="_model" :style="[_styleMap,{visibility:isVisibled||!lazy?'visible':'hidden'}]" :src="_src" >
|
||||
</image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP -->
|
||||
<image @load="imgLoad2" @error="imgError" :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
|
||||
:mode="_model" :style="[_styleMap,{visibility:isVisibled||!lazy?'visible':'hidden'}]" :src="_src" >
|
||||
</image>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
.xImage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.xImageBox{
|
||||
pointer-events: none;
|
||||
}
|
||||
.xImagePlace {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.xImageImgAbs {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,437 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { checkIsCssUnit, getUid,getUnit } from "../../core/util/xCoreUtil.uts";
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
type IMG_MODEL = "fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill";
|
||||
type IMG_SIZE_INFO = {
|
||||
width : number,
|
||||
height : number,
|
||||
}
|
||||
type IMG_SIZE_INFO_PLACE = {
|
||||
width : string,
|
||||
height : string,
|
||||
}
|
||||
|
||||
/**
|
||||
* @name 图片 xImage
|
||||
* @description 宽高可以设置,支持百分比,px,rpx
|
||||
* @page /pages/index/image
|
||||
* @category 展示组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: ("xImage-" + getUid()) as string,
|
||||
idBox: ("xImage-" + getUid()) as string,
|
||||
/** 是否正在加载,默认为false */
|
||||
isLoading: true,
|
||||
/** 是否加载出错,默认为false */
|
||||
isError: false,
|
||||
reload: 0,
|
||||
imgrealWidth: 0,
|
||||
imgrealHeight: 0,
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
ratioWidth: 0,
|
||||
ratioHeight: 0,
|
||||
isLoaded:false,
|
||||
tid:0,
|
||||
resizeObserver: null as UniResizeObserver | null,
|
||||
androidAndWebUrl:"",
|
||||
dateTime: 0,
|
||||
_x: 0,
|
||||
_y: 0,
|
||||
}
|
||||
},
|
||||
emits:[
|
||||
/**
|
||||
* 图片被点击
|
||||
*/
|
||||
'click'],
|
||||
props: {
|
||||
/**
|
||||
* 宽度,默认100%
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 高度,auto,%,rpx,px,string number
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/** 图片源 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 模式
|
||||
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
|
||||
*
|
||||
*/
|
||||
model: {
|
||||
type: String as PropType<IMG_MODEL>,
|
||||
default: "fill"
|
||||
},
|
||||
/**
|
||||
* 点击后是否预览图片
|
||||
*/
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 预览占位比例
|
||||
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
|
||||
* 当图片加载成功后,使用正确的原图片比例设置。
|
||||
* 默认是5/4=1.25
|
||||
*/
|
||||
ratio: {
|
||||
type: Number,
|
||||
default: 1.25
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
/**
|
||||
* 加载和失败时的图标大小。
|
||||
*/
|
||||
iconSize:{
|
||||
type:String,
|
||||
default:"16"
|
||||
},
|
||||
/**
|
||||
* 占位背景色
|
||||
*/
|
||||
placeBgColor:{
|
||||
type:String,
|
||||
default:"#F5F5F5"
|
||||
},
|
||||
/**
|
||||
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
|
||||
*/
|
||||
placeDarkBgColor:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
fadeShow:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let t= this;
|
||||
this.imgLoad()
|
||||
let ele = uni.getElementById(this.idBox!);
|
||||
if(ele==null) return;
|
||||
if (this.resizeObserver == null) {
|
||||
this.resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.target == ele) {
|
||||
|
||||
// #ifdef APP
|
||||
t.tid = setTimeout(function() {
|
||||
t.getNodes();
|
||||
}, 50);
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
t.getNodes();
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.resizeObserver!.observe(ele!)
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
this.resizeObserver?.disconnect()
|
||||
// uni.$off('onResize', this.resize)
|
||||
},
|
||||
computed: {
|
||||
_model():string{
|
||||
return this.model;
|
||||
},
|
||||
_placeBgColor():string{
|
||||
let bgcolor = this.placeBgColor;
|
||||
if(xConfig.dark=='dark'){
|
||||
bgcolor = this.placeDarkBgColor
|
||||
if(this.placeDarkBgColor==''){
|
||||
bgcolor = xConfig.inputDarkColor
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
},
|
||||
_round() : string {
|
||||
return checkIsCssUnit(this.round, xConfig.unit)
|
||||
},
|
||||
_src() : string {
|
||||
return this.src;
|
||||
},
|
||||
_place_size() : IMG_SIZE_INFO_PLACE {
|
||||
return {
|
||||
width: checkIsCssUnit(this.width, xConfig.unit),
|
||||
height: checkIsCssUnit(this.height, xConfig.unit),
|
||||
} as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_img_box_size() : IMG_SIZE_INFO_PLACE {
|
||||
|
||||
if(this.imgrealHeight>0){
|
||||
return this._img_size;
|
||||
}
|
||||
|
||||
let _w = this.width;
|
||||
let _h = this.height;
|
||||
let us_w = checkIsCssUnit(this.width, xConfig.unit)
|
||||
let us_h = checkIsCssUnit(this.height, xConfig.unit)
|
||||
if(this.width.lastIndexOf('%')>-1||this.width=='auto' ){
|
||||
us_w = '100%'
|
||||
_w = '100%'
|
||||
}
|
||||
|
||||
if(this.height.lastIndexOf('%')>-1||this.height=='auto' || this.isError){
|
||||
if(this.boxHeight>=5){
|
||||
_h = (this.boxHeight).toString()+'px'
|
||||
}else{
|
||||
if(this.width.lastIndexOf('%')>-1||this.width=='auto'){
|
||||
if(this.height=='100%'){
|
||||
_h = (this.ratio * this.boxHeight).toString()+'px'
|
||||
}else{
|
||||
_h = (this.ratio * this.boxWidth).toString()+'px'
|
||||
}
|
||||
}else{
|
||||
_h = (this.ratio * parseFloat(us_w)).toString()+getUnit(us_w)
|
||||
}
|
||||
}
|
||||
|
||||
return { width: _w, height: _h } as IMG_SIZE_INFO_PLACE
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_img_size() : IMG_SIZE_INFO_PLACE {
|
||||
|
||||
let us_w = checkIsCssUnit(this.width, xConfig.unit)
|
||||
let us_h = checkIsCssUnit(this.height, xConfig.unit)
|
||||
|
||||
|
||||
if (!this.isLoaded) {
|
||||
return { width: "300px", height: "300px" } as IMG_SIZE_INFO_PLACE
|
||||
}
|
||||
|
||||
if (this.boxWidth > 0) {
|
||||
let ratio = this.boxWidth / this.imgrealWidth;
|
||||
|
||||
|
||||
// 如果图片高为auto,表示自动高。
|
||||
if((this.height=='auto')){
|
||||
us_h = (ratio*this.imgrealHeight).toString()+'px'
|
||||
}
|
||||
|
||||
if((this.width=='auto')){
|
||||
us_w = this.boxWidth.toString()+'px'
|
||||
}
|
||||
|
||||
if((this.height.lastIndexOf('%')>-1)){
|
||||
us_h = (this.boxHeight).toString()+'px'
|
||||
}
|
||||
if((this.width.lastIndexOf('%')>-1)){
|
||||
us_w = (this.boxWidth).toString()+'px'
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h } as IMG_SIZE_INFO_PLACE
|
||||
},
|
||||
_styleMap() : Map<string, string> {
|
||||
let styleMap = new Map<string, string>();
|
||||
styleMap.set("width", this._img_size.width)
|
||||
styleMap.set("height", this._img_size.height)
|
||||
styleMap.set("transform", this.isLoading ? 'scale(0.1)' : 'scale(1)')
|
||||
styleMap.set("visibility", this.isLoading ? 'visible' : (!this.isError ? 'visible' : 'hidden'))
|
||||
styleMap.set("opacity", this.isLoading ? '0' : '1')
|
||||
styleMap.set("border-raiuds", this._round)
|
||||
|
||||
return styleMap;
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
src(){
|
||||
// #ifdef APP-ANDROID || WEB
|
||||
this.imgLoad()
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
prevImage() {
|
||||
/**
|
||||
* 图片被点击
|
||||
*/
|
||||
this.$emit('click')
|
||||
if (this.preview) {
|
||||
uni.previewImage({
|
||||
current: this.src,
|
||||
urls: [this.src]
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
imgLoad2(evt : ImageLoadEvent) {
|
||||
|
||||
this.imgrealWidth = evt.detail.width;
|
||||
this.imgrealHeight = evt.detail.height;
|
||||
this.isLoading = false;
|
||||
this.isError = false;
|
||||
this.isLoaded = true;
|
||||
},
|
||||
imgLoad() {
|
||||
let t = this;
|
||||
uni.getImageInfo({
|
||||
src:this._src,
|
||||
fail(_:IMediaError){
|
||||
console.log('error')
|
||||
t.isError = true;
|
||||
t.isLoading = false
|
||||
},
|
||||
success(result:GetImageInfoSuccess){
|
||||
console.log('success')
|
||||
t.isLoading = false;
|
||||
t.isError = false;
|
||||
t.isLoaded = true;
|
||||
if( result.path!=t.androidAndWebUrl){
|
||||
t.imgrealWidth = result.width
|
||||
t.imgrealHeight = result.height
|
||||
t.androidAndWebUrl = result.path;
|
||||
|
||||
}
|
||||
}
|
||||
} as GetImageInfoOptions)
|
||||
|
||||
},
|
||||
// evt:ImageErrorEvent
|
||||
imgError() {
|
||||
this.isError = true;
|
||||
this.isLoading = false
|
||||
},
|
||||
mStart(evt:UniTouchEvent){
|
||||
this.dateTime = Date.now()
|
||||
this._x = evt.changedTouches[0].clientX
|
||||
this._y = evt.changedTouches[0].clientY
|
||||
},
|
||||
mEnd(evt:UniTouchEvent){
|
||||
let diffdate = Date.now() - this.dateTime
|
||||
let diffx = Math.abs(evt.changedTouches[0].clientX - this._x)
|
||||
let diffy = Math.abs(evt.changedTouches[0].clientY - this._y)
|
||||
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
|
||||
this.prevImage()
|
||||
}
|
||||
|
||||
},
|
||||
// #ifdef WEB
|
||||
mmStart(evt:UniMouseEvent){
|
||||
this.dateTime = Date.now()
|
||||
this._x = evt.clientX
|
||||
this._y = evt.clientY
|
||||
},
|
||||
mmEnd(evt:UniMouseEvent){
|
||||
let diffdate = Date.now() - this.dateTime
|
||||
let diffx = Math.abs(evt.clientX - this._x)
|
||||
let diffy = Math.abs(evt.clientY - this._y)
|
||||
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
|
||||
this.prevImage()
|
||||
}
|
||||
|
||||
},
|
||||
// #endif
|
||||
getNodes() {
|
||||
let t = this;
|
||||
// let ele = this?.$parent?.$el as UniElement||null;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
t.boxWidth = nodeinfo.width!
|
||||
t.boxHeight = nodeinfo.height!
|
||||
})
|
||||
},
|
||||
resize(){
|
||||
this.isLoaded = false
|
||||
let t = this;
|
||||
// let ele = this?.$parent?.$el as UniElement||null;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo;
|
||||
t.boxWidth = nodeinfo.width!
|
||||
t.boxHeight = nodeinfo.height!
|
||||
t.isLoaded = true
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view
|
||||
|
||||
@touchstart="mStart"
|
||||
@touchend="mEnd"
|
||||
<!-- #ifdef WEB -->
|
||||
@mousedown="mmStart"
|
||||
@mouseup="mmEnd"
|
||||
<!-- #endif -->
|
||||
class="xImage" :id="idBox" :style="{width:_place_size.width,height:_place_size.height}">
|
||||
<view class="xImageBox"
|
||||
:style="{width:_img_box_size.width,height:_img_box_size.height,borderRadius:_round,pointerEvent:'none'}">
|
||||
<view v-if="isLoading||isError" class="xImagePlace" :style="{backgroundColor:_placeBgColor}">
|
||||
<x-icon :font-size="iconSize" v-if="isError" color="error" name="landscape-line"></x-icon>
|
||||
<x-icon :font-size="iconSize" v-if="isLoading" name="loader-2-line" color="primary" :spin="true"></x-icon>
|
||||
</view>
|
||||
<image :fade-show="fadeShow" v-if="!isError" class="xImageImg" :class="[isLoading?'xImageImgAbs':'']"
|
||||
:mode="_model" :style="_styleMap" :src="_src" @error="imgError">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
.xImage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.xImageBox{
|
||||
pointer-events: none;
|
||||
}
|
||||
.xImagePlace {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.xImageImgAbs {
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
export type INDEXBAR_ITEM = {
|
||||
id : string,
|
||||
ele : XIndexbarItemComponentPublicInstance,
|
||||
top:number,
|
||||
name:string,
|
||||
height:number
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
<script lang="ts">
|
||||
import { SlotsType, PropType } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
type indexbarTYPE = {
|
||||
index : number,
|
||||
title : string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 索引 xIndexbar
|
||||
* @description 特别提醒:本组件在1.0.9重构不向下兼容,并且删除了子组件:x-indexbar-item,不再需要子组件。请使用对应的动态插槽来渲染
|
||||
* 数据,demo是526条数据的测试依赖右边索引滑动跟手流畅。
|
||||
* 虚拟列表有个缺点会在滚动时分页读取数据并复用布局,因此如果你想做带图片的索引,
|
||||
* 建议进入应用后启用后台缓存已有的头像或者图片数据类似微信那样缓存图片,这样虚拟加载的时候闪烁感就少了
|
||||
* 重要:插槽内不要使用任何自定组件布局,也不要增加任何额外的节点,能少就少。
|
||||
* @page /pages/index/indexbar
|
||||
* @category 导航组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
listIndexBar: [] as indexbarTYPE[],
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
boxTop: 0,
|
||||
boxLeft: 0,
|
||||
nowIndex: 0,
|
||||
currentIndexBYbar: 0,
|
||||
// 视图内显示的条数。
|
||||
viewConunt: 0,
|
||||
windowTop: 0,
|
||||
scrollTop: 0,
|
||||
realScrollY:0,
|
||||
headerDomRectHeight:0,
|
||||
_y: 0,
|
||||
tid: 0,
|
||||
isMoveing: false,
|
||||
olsShowView: [] as UTSJSONObject[],
|
||||
isShowOldViewData: false,
|
||||
dotParentOffsetTop: -1,
|
||||
dotRight: 0,
|
||||
tid2: 0,
|
||||
showDot: false,
|
||||
sliderDotSize:16,
|
||||
// #ifdef MP-WEIXIN
|
||||
mpBgBounds:null as any|null,
|
||||
sliderBgBounds:null as any|null
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
slots: Object as SlotsType<{
|
||||
header : { title : string, index : number },
|
||||
default : {
|
||||
current : UTSJSONObject,
|
||||
currentIndex : number,
|
||||
index : number
|
||||
}
|
||||
}>,
|
||||
props: {
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 高,%,rpx,px单位均可。
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 侧边指示激活时的文字颜色
|
||||
* 空值是取全局主题值
|
||||
*/
|
||||
dotActiveColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 侧边指示未激活时的文字颜色
|
||||
*/
|
||||
dotColor: {
|
||||
type: String,
|
||||
default: "#c0c0c0"
|
||||
},
|
||||
/**
|
||||
* 侧边指示的背景颜色
|
||||
*/
|
||||
dotBgColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<UTSJSONObject[]>,
|
||||
default: () : UTSJSONObject[] => [] as UTSJSONObject[]
|
||||
},
|
||||
/**
|
||||
* 项目的高
|
||||
* 只能是数字,或者带rpx,px单位
|
||||
*/
|
||||
cellHeight: {
|
||||
type: String,
|
||||
default: '50'
|
||||
},
|
||||
/**
|
||||
* 项目的标题高
|
||||
* 只能是数字,或者带rpx,px单位
|
||||
*/
|
||||
titleHeight: {
|
||||
type: String,
|
||||
default: '32'
|
||||
},
|
||||
customSliderBar:{
|
||||
type:Array as PropType<Array<string>>,
|
||||
default:():string[] => [] as string[]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_customBarLen():number{
|
||||
return this.customSliderBar.length;
|
||||
},
|
||||
_height() : string {
|
||||
return checkIsCssUnit(this.height, xConfig.unit)
|
||||
},
|
||||
_width() : string {
|
||||
return checkIsCssUnit(this.width, xConfig.unit)
|
||||
},
|
||||
_cellHeight() : number {
|
||||
let height = checkIsCssUnit(this.cellHeight, xConfig.unit)
|
||||
let unit = getUnit(height)
|
||||
let realheight = parseInt(height)
|
||||
if (unit == 'rpx') {
|
||||
realheight = rpx2px(realheight)
|
||||
}
|
||||
|
||||
return realheight;
|
||||
},
|
||||
_titleHeight() : number {
|
||||
let height = checkIsCssUnit(this.titleHeight, xConfig.unit)
|
||||
let unit = getUnit(height)
|
||||
let realheight = parseInt(height)
|
||||
if (unit == 'rpx') {
|
||||
realheight = rpx2px(realheight)
|
||||
}
|
||||
|
||||
return realheight;
|
||||
},
|
||||
_totalHeight() : number {
|
||||
return this.list.length * this._cellHeight;
|
||||
},
|
||||
_viewConunt() : number {
|
||||
if (this.boxHeight == 0 || this.list.length == 0) return 0;
|
||||
let itemcount = this.boxHeight / this._cellHeight;
|
||||
|
||||
return Math.ceil(itemcount + 1)
|
||||
},
|
||||
_dotActiveColor() : string {
|
||||
if (this.dotActiveColor == "") return getDefaultColor(xConfig.color);
|
||||
return getDefaultColor(this.dotActiveColor)
|
||||
},
|
||||
_dotColor() : string {
|
||||
return getDefaultColor(this.dotColor)
|
||||
},
|
||||
_dotBgColor() : string {
|
||||
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
|
||||
return getDefaultColor(this.dotBgColor)
|
||||
},
|
||||
|
||||
_viewList() : UTSJSONObject[] {
|
||||
if (this.isShowOldViewData) return this.olsShowView
|
||||
let start = this.nowIndex - this._viewConunt;
|
||||
let end = this.nowIndex + this._viewConunt;
|
||||
start = Math.max(0, start)
|
||||
// end = Math.min(this.list.length-1,end)
|
||||
let oldIndexList = this.list.slice(start, end);
|
||||
let list = oldIndexList.map((el : UTSJSONObject, index : number) : UTSJSONObject => {
|
||||
el.set("oldIndex", index + start)
|
||||
return el;
|
||||
})
|
||||
this.olsShowView = list
|
||||
return list
|
||||
},
|
||||
_list() : UTSJSONObject[] {
|
||||
return this.list;
|
||||
},
|
||||
_listTotal() : UTSJSONObject[] {
|
||||
|
||||
let customlist = this.customSliderBar.map((el:string,index:number):UTSJSONObject => {
|
||||
return {
|
||||
"id": index,
|
||||
"name": el,
|
||||
"logo": "",
|
||||
"index": el
|
||||
} as UTSJSONObject
|
||||
})
|
||||
return [...customlist,...this.list];
|
||||
}
|
||||
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.scrollTop = 0
|
||||
this.nowIndex = 0
|
||||
this.listIndexBar = [] as indexbarTYPE[]
|
||||
this.olsShowView = [] as UTSJSONObject[]
|
||||
this.currentIndexBYbar = 0
|
||||
|
||||
this.olsShowView = this._viewList
|
||||
this.setInitData();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.windowTop = uni.getWindowInfo().windowTop
|
||||
this.getNodeInfo();
|
||||
this.olsShowView = this._viewList
|
||||
this.setInitData();
|
||||
// #ifdef MP-WEIXIN
|
||||
this.mpweixinGetBoundec()
|
||||
// #endif
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.tid)
|
||||
},
|
||||
|
||||
methods: {
|
||||
// #ifdef MP-WEIXIN
|
||||
async mpweixinGetBoundec(){
|
||||
let _this = this;
|
||||
setTimeout(async function() {
|
||||
let ele = _this.$refs['xIndexBarRightSliderBg'] as UniElement;
|
||||
_this.mpBgBounds= await ele.getBoundingClientRectAsync()
|
||||
let parentele = _this.$refs['xIndexBarRightSlider'] as UniElement;
|
||||
_this.sliderBgBounds = await parentele.getBoundingClientRectAsync()
|
||||
}, 100);
|
||||
|
||||
},
|
||||
// #endif
|
||||
getHeaderDom(){
|
||||
let t = this;
|
||||
const ele = this.$refs['headerRef']! as UniElement;
|
||||
ele.getBoundingClientRectAsync()
|
||||
?.then((rect:DOMRect)=>{
|
||||
t.headerDomRectHeight = rect.height
|
||||
})
|
||||
.catch(()=>{})
|
||||
},
|
||||
setInitData() {
|
||||
let lst = Date.now()
|
||||
let list = this._list as UTSJSONObject[]
|
||||
if (list.length == 0) return
|
||||
let listbar = [] as indexbarTYPE[]
|
||||
let listbarTitle = [] as string[];
|
||||
for (let i = 0; i < this._list.length; i++) {
|
||||
let current = this._list[i]
|
||||
let keya = current.getString("index")!;
|
||||
if (!listbarTitle.includes(keya!)) {
|
||||
listbarTitle.push(keya)
|
||||
listbar.push({ index: i+this._customBarLen, title: keya } as indexbarTYPE)
|
||||
}
|
||||
}
|
||||
|
||||
let customlist = this.customSliderBar.map((el:string,index:number):indexbarTYPE => {
|
||||
return {
|
||||
index,
|
||||
title:el
|
||||
} as indexbarTYPE
|
||||
})
|
||||
this.listIndexBar = [...customlist,...listbar];
|
||||
|
||||
console.warn("索引数据处理时间(毫秒):", Date.now() - lst, "如果此值过大,表示sdk有问题,正常在10ms内,安卓dev会在60ms内")
|
||||
|
||||
},
|
||||
sliderOnclik(evt : UniPointerEvent, item : indexbarTYPE, index : number) {
|
||||
|
||||
// let customIndex = this.customSliderBar.length + index -1;
|
||||
this.nowIndex = this.listIndexBar[index].index;
|
||||
// this.nowIndex = index;
|
||||
if(index <=this.customSliderBar.length-1){
|
||||
this.scrollTop = 0
|
||||
}else{
|
||||
this.scrollTop = this._cellHeight * this.nowIndex + this.headerDomRectHeight
|
||||
}
|
||||
this.currentIndexBYbar = index;
|
||||
this.transxyByDotTop(evt.clientY)
|
||||
},
|
||||
onScrollEvent(evt : UniScrollEvent) {
|
||||
if (this.isMoveing) return;
|
||||
this.realScrollY = evt.detail.scrollTop ;
|
||||
this.transxyend(evt.detail.scrollTop - this._titleHeight - this.headerDomRectHeight)
|
||||
this.getHeaderDom()
|
||||
},
|
||||
ONscrollend(evt : UniScrollEvent) {
|
||||
// #ifdef APP-IOS||APP-ANDROID
|
||||
this.scrollTop = evt.detail.scrollTop;
|
||||
// #endif
|
||||
},
|
||||
transxyend(top : number) {
|
||||
|
||||
|
||||
let index = Math.ceil((top) / this._cellHeight)
|
||||
index = Math.max(0, index)
|
||||
index = Math.min(this._list.length - 1, index)
|
||||
|
||||
if (index == this.nowIndex) return;
|
||||
|
||||
this.nowIndex = index;
|
||||
let title = this._list[index].getString("index")!
|
||||
if (title == this.listIndexBar[this.currentIndexBYbar].title) return;
|
||||
let barindexItem = this.listIndexBar.findIndex((el : indexbarTYPE) : boolean => el.title == title)
|
||||
if (barindexItem == this.currentIndexBYbar) return;
|
||||
if (barindexItem > -1) {
|
||||
this.currentIndexBYbar = barindexItem;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
getNodeInfo() {
|
||||
let t = this;
|
||||
uni.createSelectorQuery().in(this)
|
||||
.select(".xIndexBarBox")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as NodeInfo
|
||||
t.boxTop = nodeinfo.top!;
|
||||
t.boxWidth = nodeinfo.width!;
|
||||
t.boxHeight = nodeinfo.height!;
|
||||
t.boxLeft = nodeinfo.left!;
|
||||
})
|
||||
this.getHeaderDom();
|
||||
},
|
||||
|
||||
hideDot() {
|
||||
let t = this;
|
||||
clearTimeout(this.tid2)
|
||||
this.tid2 = setTimeout(function () {
|
||||
t.showDot = false
|
||||
}, 700);
|
||||
},
|
||||
itemMove(evt : UniTouchEvent) {
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
let t = this;
|
||||
let y = evt.changedTouches[0].clientY
|
||||
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
|
||||
let boundsTop = 0
|
||||
|
||||
// #ifdef APP || WEB
|
||||
boundsTop = ele.getBoundingClientRect()!.top
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
boundsTop = this.mpBgBounds!.top
|
||||
// #endif
|
||||
|
||||
|
||||
let diff = y - boundsTop;
|
||||
|
||||
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * t.sliderDotSize))
|
||||
|
||||
let barIndex = Math.floor(diff / t.sliderDotSize)
|
||||
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
|
||||
|
||||
if (barIndex != this.currentIndexBYbar) {
|
||||
this.currentIndexBYbar = barIndex;
|
||||
}
|
||||
let index = this.listIndexBar[barIndex].index;
|
||||
if (index == this.nowIndex) return
|
||||
this.nowIndex = index;
|
||||
clearTimeout(this.tid)
|
||||
this.isShowOldViewData = true
|
||||
t.transxyByDotTop(y)
|
||||
// 减少处理次数
|
||||
this.tid = setTimeout(function () {
|
||||
if(index<=t._customBarLen-1){
|
||||
t.scrollTop = 0
|
||||
}else{
|
||||
t.scrollTop = index * t._cellHeight + t.headerDomRectHeight
|
||||
}
|
||||
t.isShowOldViewData = false;
|
||||
t.realScrollY = t.scrollTop ;
|
||||
}, 10);
|
||||
|
||||
},
|
||||
itemStart(evt : UniTouchEvent) {
|
||||
this.isMoveing = true
|
||||
this._y = evt.changedTouches[0].clientY;
|
||||
|
||||
|
||||
},
|
||||
transxyByDotTop(top : number) {
|
||||
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
|
||||
let boundsTop = 0
|
||||
|
||||
// #ifdef APP || WEB
|
||||
boundsTop = ele.getBoundingClientRect()!.top
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
boundsTop = this.mpBgBounds!.top
|
||||
// #endif
|
||||
let diff = top - boundsTop!
|
||||
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * this.sliderDotSize))
|
||||
|
||||
let barIndex = Math.floor(diff / this.sliderDotSize)
|
||||
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
|
||||
|
||||
let parentele = this.$refs['xIndexBarRightSlider'] as UniElement;
|
||||
let boundsTop2 = 0
|
||||
|
||||
// #ifdef APP || WEB
|
||||
boundsTop2 = parentele.getBoundingClientRect()!.top
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
boundsTop2 = this.sliderBgBounds!.top
|
||||
// #endif
|
||||
|
||||
|
||||
let topdiff = barIndex * this.sliderDotSize + boundsTop - boundsTop2 - (50 - this.sliderDotSize) / 2;
|
||||
// #ifdef APP-ANDROID
|
||||
topdiff = -25 + (boundsTop - boundsTop2) + barIndex * this.sliderDotSize + this.sliderDotSize / 2
|
||||
|
||||
// #endif
|
||||
|
||||
let dotele = this.$refs['xIndexBarDot'] as UniElement;
|
||||
if (topdiff == this.dotParentOffsetTop) return;
|
||||
this.dotParentOffsetTop = topdiff
|
||||
dotele.style.setProperty('transform', `translateY(${topdiff}px) rotate(-45deg)`)
|
||||
this.showDot = true
|
||||
this.hideDot();
|
||||
},
|
||||
itemEnd() {
|
||||
this.isMoveing = false
|
||||
this.dotParentOffsetTop = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<view class="xIndexBar" :style="{height:_height,width:_width}">
|
||||
<view :style="{height:_titleHeight+'px',top:realScrollY>=headerDomRectHeight?'0px':(headerDomRectHeight-realScrollY)+'px'}">
|
||||
|
||||
<!--
|
||||
@slot 悬浮的菜单头
|
||||
@props {title} 当前选中的菜单头
|
||||
@props {index} 当前选中的菜单头索引
|
||||
-->
|
||||
<slot name="header" :title="listIndexBar.length>0?listIndexBar[currentIndexBYbar].title:''"
|
||||
:index="listIndexBar.length>0?listIndexBar[currentIndexBYbar+(Math.max(customSliderBar.length-1,0))].index:0">
|
||||
<view style="display: flex;flex-direction: row;align-items: center;height: 100%;">
|
||||
<x-text v-if="listIndexBar.length>0">{{listIndexBar[currentIndexBYbar<= (_customBarLen-1)?_customBarLen:currentIndexBYbar].title}}</x-text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="xIndexBarBox" >
|
||||
<scroll-view @scrollend="ONscrollend" :show-scrollbar="false" @scroll="onScrollEvent" :scroll-top="scrollTop"
|
||||
:bounces="false" :enable-back-to-top="true" class="xIndexBar" :style="{height:boxHeight+'px'}">
|
||||
|
||||
<view ref="headerRef">
|
||||
<view style="flex:1">
|
||||
<!--
|
||||
@slot 顶部布局,可以自由布局
|
||||
-->
|
||||
<slot name="top"></slot>
|
||||
</view>
|
||||
<view :style="{height:_titleHeight+'px'}"></view>
|
||||
</view>
|
||||
<view :style="{height:_totalHeight+'px',position:'relative'}" v-if="_viewList.length>0">
|
||||
|
||||
<template v-for="(item,index) in _viewList" :key="index">
|
||||
<view class="xIndexBarBoxItem" :style="{
|
||||
height:_cellHeight+'px',
|
||||
transform:`translateY(${(_cellHeight*(item.getNumber('oldIndex')!))}px)`
|
||||
}">
|
||||
<!--
|
||||
@slot 动态插槽
|
||||
@prop {UTSJSONObject} current - 当前的项目信息,可通过此布局当前数据
|
||||
@prop {number} currentIndex - 右边导航当前的索引
|
||||
@prop {number} index - 当前索引
|
||||
-->
|
||||
<slot :index="(item.getNumber('oldIndex')!)" :currentIndex="nowIndex" :current="item"></slot>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<view class="xIndexBarRightSlider" ref="xIndexBarRightSlider">
|
||||
<view @touchmove="itemMove" @touchstart="itemStart" @touchend="itemEnd" ref="xIndexBarRightSliderBg"
|
||||
class="xIndexBarRightSliderBg" :style="{background:_dotBgColor}">
|
||||
<view @click.stop="sliderOnclik($event as UniPointerEvent,item,index)"
|
||||
v-for="(item,index) in listIndexBar" :key="index" class="xIndexBarRightSliderItem"
|
||||
:class="[currentIndexBYbar==index?'xIndexBarRightSliderItemHover':'']" :hover-start-time="50"
|
||||
:hover-stay-time="100" hover-class="xIndexBarRightSliderItemHover" :style="{
|
||||
backgroundColor:currentIndexBYbar==index?_dotActiveColor:_dotBgColor,
|
||||
}">
|
||||
<text :style="{color:currentIndexBYbar==index?'white':_dotColor}" class="xIndexBarRightSliderItemText">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view android-layer-type="software" ref="xIndexBarDot" class="xIndexBarDot"
|
||||
:style="{opacity:!showDot?'0':'1',backgroundColor:_dotActiveColor}">
|
||||
<text v-if="(_list.length>0)" class="xIndexBarDotText">{{_listTotal[nowIndex]['index']}}</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.xIndexBar{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.xIndexBarBox {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.xIndexBarBoxItem {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.xIndexBarDot {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50px 50px 6px 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* transform: rotate(-45deg); */
|
||||
/* transition-duration: 100ms;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: linear; */
|
||||
right: 47px;
|
||||
top: 0;
|
||||
/* #ifdef WEB */
|
||||
box-szing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xIndexBarDotText {
|
||||
transform: rotate(45deg);
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.xIndexBarRightSliderItemHover {
|
||||
/* background-color: rgba(0, 0, 0, 0.05); */
|
||||
/* background-color:red; */
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.xIndexBarHeaderTitleBox {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xIndexBarHeaderTitle {
|
||||
height: 32px;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
/* #ifdef H5 */
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
font-size: bold;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xIndexBarRightSlider {
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
right: 5px;
|
||||
top:0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xIndexBarRightSliderItem {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.xIndexBarRightSliderItemText {
|
||||
font-size: 10px;
|
||||
line-height: 11px;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
|
||||
.xIndexBarRightSliderBg {
|
||||
border-radius: 21px;
|
||||
/* box-shadow: 0 0px 10px rgba(0, 0, 0, 0.06); */
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,872 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
/**
|
||||
*
|
||||
* @name 输入框 xInputNumber
|
||||
* @description 表单数字输入框,样式可定制化强,允许整数,小数限制,注意配合属性type和inputmodel来实现业务功能体验
|
||||
* @page /pages/index/input-number
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xInputNumber"})
|
||||
|
||||
// 事件定义
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 清空时触发
|
||||
*/
|
||||
'clear',
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {number} value - 已输入的数字,如果是空值返回的是NaN
|
||||
*/
|
||||
'rightClick',
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {number} value - 已输入的字符串,如果是空值返回的是NaN
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {number} value - 当前已输入的字符串,如果是空值返回的是NaN
|
||||
*/
|
||||
'input',
|
||||
/**
|
||||
* 获得焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'focus',
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'blur',
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
|
||||
*/
|
||||
'keyboardheightchange',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
export type xInputPropsType = {
|
||||
/**
|
||||
* 自定义style
|
||||
* 标签请写_style,不是-style,插件文档转换问题
|
||||
*/
|
||||
_style: string,
|
||||
/**
|
||||
* 输入框统一的聚集样式
|
||||
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
|
||||
* ['2px','solid','','']
|
||||
* 全局的配置名称是:inputFocusBorder,可以全局设置.
|
||||
*/
|
||||
focusBorder: string[],
|
||||
/**
|
||||
* 占位的样式
|
||||
*/
|
||||
placeholderStyle: string,
|
||||
/**
|
||||
* 自定class
|
||||
* 标签请写_class,不是-class,插件文档转换问题
|
||||
*/
|
||||
_class: string,
|
||||
/**
|
||||
* 输入框圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 是否显示清除图标
|
||||
*/
|
||||
showClear: boolean,
|
||||
/**
|
||||
* 右侧文本
|
||||
*/
|
||||
rightText: string,
|
||||
/**
|
||||
* 左侧文本
|
||||
*/
|
||||
leftText: string,
|
||||
/**
|
||||
* 双向绑定的输入值,如果是空值返回的是NaN
|
||||
*/
|
||||
modelValue: number,
|
||||
/**
|
||||
* 修饰符同vmodel.xxx='',比如v-model.trim=''
|
||||
*/
|
||||
// modelModifiers: UTSJSONObject,
|
||||
/**
|
||||
* 输入框提示语
|
||||
*/
|
||||
placeholder: string,
|
||||
/**
|
||||
* 左图标的颜色
|
||||
* 默认空值取全局的主题色。
|
||||
*/
|
||||
iconColor: string,
|
||||
/**
|
||||
* 清除图标的颜色
|
||||
*/
|
||||
clearColor: string,
|
||||
/**
|
||||
* 输入框背景
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 输入框暗黑背景,空值取全局的配置
|
||||
* 提供会覆盖全局的配色。默认是透明
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 输入框的字体颜色
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 如果你提供,就会覆盖自动的反转配色。
|
||||
* 默认是fontColor的反转颜色。
|
||||
*/
|
||||
darkFontColor: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
leftIcon: string,
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 输入类型,数字仅限整数,小数或者整数
|
||||
*/
|
||||
type: "number" | "digit",
|
||||
/**
|
||||
* numeric:整数,配合type=number时,输入框只允许输入整数,手机会自动切换为整数数字键盘(不带小数点符号)
|
||||
* decimal:小数,配合type=digit时,输入框允许输入小数或者整数,在手机键盘会自动切换为带小数点的键盘
|
||||
*/
|
||||
inputmode: "decimal" | "numeric",
|
||||
/**
|
||||
* 当type=digit时,可以控制小数点长度,默认1
|
||||
*/
|
||||
decimalLen:number,
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max:number,
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min:number,
|
||||
/**
|
||||
* 是否是密码类型
|
||||
*/
|
||||
password: boolean,
|
||||
/**
|
||||
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
|
||||
*/
|
||||
maxlength: number,
|
||||
cursorSpacing: number,
|
||||
cursorColor: string,
|
||||
autoFocus: boolean,
|
||||
focus: boolean,
|
||||
confirmType: "send" | "search" | "next" | "go" | "done",
|
||||
confirmHold: boolean,
|
||||
cursor: number,
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
adjustPosition: boolean,
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 高
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 自动删除首尾空格?
|
||||
* 只会在失去焦点时删除.
|
||||
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
|
||||
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
|
||||
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
|
||||
*/
|
||||
trim: boolean,
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
align: 'left' | 'right' | 'center',
|
||||
/**
|
||||
* type=textarea时生效
|
||||
*/
|
||||
autoHeight: boolean,
|
||||
/**
|
||||
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
|
||||
*/
|
||||
fixed: boolean,
|
||||
/**
|
||||
* 显示底部的注释说明及出错信息。
|
||||
*/
|
||||
showFooter: boolean,
|
||||
/**
|
||||
* 是否显示字符统计。
|
||||
*/
|
||||
showChartCount: boolean,
|
||||
/**
|
||||
* 格式就是正常的css格式
|
||||
* 比如:8rpx 8rpx 0rpx 0rpx
|
||||
*/
|
||||
inputPadding: string,
|
||||
|
||||
/**
|
||||
* focus时,点击页面的时候不收起键盘
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
|
||||
*/
|
||||
holdKeyboard: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xInputPropsType>(), {
|
||||
_style: "",
|
||||
focusBorder: ():string[] => [] as string[],
|
||||
placeholderStyle: "",
|
||||
_class: "",
|
||||
round: "",
|
||||
showClear: false,
|
||||
rightText: "",
|
||||
leftText: "",
|
||||
modelValue: NaN,
|
||||
placeholder: "",//请输入
|
||||
iconColor: "",
|
||||
clearColor: "#bfbfbf",
|
||||
color: "",
|
||||
darkBgColor: "transparent",
|
||||
fontColor: "#333333",
|
||||
darkFontColor: "",
|
||||
fontSize: "16",
|
||||
leftIcon: "",
|
||||
name: "",
|
||||
disabled: false,
|
||||
type: "number",
|
||||
password: false,
|
||||
maxlength: -1,
|
||||
cursorSpacing: 0,
|
||||
cursorColor: "",
|
||||
autoFocus: false,
|
||||
focus: false,
|
||||
confirmType: "next",
|
||||
confirmHold: false,
|
||||
cursor: 0,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1,
|
||||
adjustPosition: true,
|
||||
width: "auto",
|
||||
height: "44",
|
||||
trim: true,
|
||||
align: "left",
|
||||
autoHeight: false,
|
||||
fixed: false,
|
||||
showFooter: false,
|
||||
showChartCount: false,
|
||||
inputPadding: "8px 12px",
|
||||
inputmode: 'numeric',
|
||||
decimalLen:1,
|
||||
max:9999999999,
|
||||
min:-999999999,
|
||||
holdKeyboard: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const nowValue = ref("")
|
||||
const seePass = ref(false)
|
||||
const isFocus = ref(false)
|
||||
|
||||
// 获取当前实例
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
|
||||
// 计算属性
|
||||
const _focusBorder = computed(():string[] => {
|
||||
let style = props.focusBorder.slice(0);
|
||||
if(props.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
|
||||
style = xConfig.inputFocusBorder.slice(0);
|
||||
}
|
||||
if(style.length <4){
|
||||
return ['0px','solid','transparent'] as string[];
|
||||
}
|
||||
let oldcolor = style[2]
|
||||
let hoverColor = getDefaultColor(style[3])
|
||||
if(oldcolor==""){
|
||||
oldcolor = props.color
|
||||
}
|
||||
if(hoverColor==""){
|
||||
hoverColor = getDefaultColor(xConfig.color)
|
||||
}
|
||||
return [style[0],style[1],isFocus.value?hoverColor:oldcolor];
|
||||
})
|
||||
|
||||
const _inputLen = computed(() : number => {
|
||||
return nowValue.value.split("").length
|
||||
})
|
||||
|
||||
const _maxlength = computed(() : number => {
|
||||
return props.maxlength
|
||||
})
|
||||
|
||||
const _showFooter = computed(() : boolean => {
|
||||
return props.showFooter
|
||||
})
|
||||
|
||||
const _holdKeyboard = computed(():boolean => {
|
||||
return props.holdKeyboard
|
||||
})
|
||||
|
||||
const _autoHeight = computed(() : boolean => {
|
||||
return props.autoHeight
|
||||
})
|
||||
|
||||
const _showChartCount = computed(():boolean => {
|
||||
return props.showChartCount
|
||||
})
|
||||
|
||||
const _fixed = computed(() : boolean => {
|
||||
return props.fixed
|
||||
})
|
||||
|
||||
const _width = computed(() : string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
|
||||
const _height = computed(() : string => {
|
||||
return checkIsCssUnit(props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
const _cstyle = computed(() : string => {
|
||||
return props._style
|
||||
})
|
||||
|
||||
const _placeholderStyle = computed(() : string => {
|
||||
return props.placeholderStyle==''?xConfig.placeholderStyle:props.placeholderStyle
|
||||
})
|
||||
|
||||
const _cclass = computed(() : string => {
|
||||
return props._class
|
||||
})
|
||||
|
||||
const _round = computed(() : string => {
|
||||
if(props.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _fontSizeUnScale = computed(() : string => {
|
||||
return props.fontSize
|
||||
})
|
||||
|
||||
const _showClear = computed(() : boolean => {
|
||||
return props.showClear
|
||||
})
|
||||
|
||||
const _rightText = computed(() : string => {
|
||||
return props.rightText
|
||||
})
|
||||
|
||||
const _leftText = computed(() : string => {
|
||||
return props.leftText
|
||||
})
|
||||
|
||||
const _confirmType = computed(() : string => {
|
||||
return props.confirmType
|
||||
})
|
||||
|
||||
const _placeholder = computed(() : string => {
|
||||
if(props.placeholder=='') return i18n.t("tmui4x.input.placeholder")
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
|
||||
const _iconColor = computed(() : string => {
|
||||
if(props.iconColor==""){
|
||||
return getDefaultColor(xConfig.color)
|
||||
}
|
||||
return getDefaultColor(props.iconColor)
|
||||
})
|
||||
|
||||
const _color = computed(() : string => {
|
||||
let color = getDefaultColor(props.color==''?xConfig.inputBgColor:props.color)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor == "") {
|
||||
color = xConfig.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(props.darkBgColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _clearColor = computed(():string => {
|
||||
if(props.clearColor=='') return _iconColor.value
|
||||
return getDefaultColor(props.clearColor)
|
||||
})
|
||||
|
||||
const _fontColor = computed(() : string => {
|
||||
let color = getDefaultColor(props.fontColor)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkFontColor == "") {
|
||||
color = "#ffffff"
|
||||
} else {
|
||||
color = getDefaultColor(props.darkFontColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _cursorColor = computed(():string => {
|
||||
let color = props.cursorColor
|
||||
if(props.cursorColor==''){
|
||||
color = xConfig.color
|
||||
}
|
||||
return getDefaultColor(color)
|
||||
})
|
||||
|
||||
const _leftIcon = computed(() : string => {
|
||||
return props.leftIcon
|
||||
})
|
||||
|
||||
const _disabled = computed(() : boolean => {
|
||||
return props.disabled
|
||||
})
|
||||
|
||||
const _password = computed(() : boolean => {
|
||||
return props.password
|
||||
})
|
||||
|
||||
const _autoFocus = computed(() : boolean => {
|
||||
return props.autoFocus
|
||||
})
|
||||
|
||||
const _focus = computed(() : boolean => {
|
||||
return props.focus
|
||||
})
|
||||
|
||||
const _adjustPosition = computed(() : boolean => {
|
||||
return props.adjustPosition
|
||||
})
|
||||
|
||||
const _selectionEnd = computed(() : number => {
|
||||
return props.selectionEnd
|
||||
})
|
||||
|
||||
const _selectionStart = computed(() : number => {
|
||||
return props.selectionStart
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 方法函数
|
||||
function getTrimAfterValue(value:string):string{
|
||||
if(props.trim&& typeof value == 'string'){
|
||||
|
||||
return value.trim()
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function clampValue(value : number) : number {
|
||||
return Math.min(Math.max(value, props.min), props.max);
|
||||
}
|
||||
function getDien(str:string):string{
|
||||
let v1 = str.split(".")
|
||||
let result = v1[0]
|
||||
if(v1.length==0){
|
||||
result = '0.'
|
||||
for(let i =0;i<props.decimalLen;i++){
|
||||
result+='0'
|
||||
}
|
||||
}else if(v1.length==1){
|
||||
result = result + "."
|
||||
for(let i =0;i<props.decimalLen;i++){
|
||||
result+='0'
|
||||
}
|
||||
|
||||
}else{
|
||||
result = result + "." + v1[1].substring(0,props.decimalLen)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function translaterNumberToModel(val:string|number,isTranVal:boolean = true):number{
|
||||
if(val==''||val==null) return 0;
|
||||
let realVal = 0
|
||||
if(typeof val == 'string'){
|
||||
realVal = parseFloat(val)
|
||||
}else if(typeof val == 'number'){
|
||||
realVal = val
|
||||
}
|
||||
|
||||
if(isNaN(realVal)) return 0;
|
||||
if(isTranVal){
|
||||
if(props.type == 'number'){
|
||||
realVal = Math.floor(realVal)
|
||||
}
|
||||
}
|
||||
|
||||
return realVal;
|
||||
}
|
||||
|
||||
//处理值
|
||||
function translaterNumberToInputVal(val:string|number,isTranVal:boolean = true):string{
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
if(val===''||val===null) return '';
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(val==''||val==null) return '';
|
||||
// #endif
|
||||
|
||||
let realVal = ''
|
||||
if(typeof val == 'string'){
|
||||
realVal = val
|
||||
}else if(typeof val == 'number'){
|
||||
if(!isNaN(val)){
|
||||
if(props.type == 'number'){
|
||||
realVal = val.toString()
|
||||
}else if(props.type == 'digit'){
|
||||
realVal = val.toFixed(props.decimalLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const tval = translaterNumberToModel(realVal,isTranVal)
|
||||
// realVal = tval.toString()
|
||||
return realVal;
|
||||
}
|
||||
|
||||
function inputHndler(evt : UniInputEvent) {
|
||||
const val = getTrimAfterValue(getDien(evt.detail.value))
|
||||
let realVal = translaterNumberToInputVal(val)
|
||||
let modelsnycn = translaterNumberToModel(realVal)
|
||||
modelsnycn = clampValue(modelsnycn)
|
||||
realVal = translaterNumberToInputVal(modelsnycn)
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {string} value 当前已输入的字符串
|
||||
*/
|
||||
emits('input', modelsnycn)
|
||||
// emits('update:modelValue', modelsnycn)
|
||||
}
|
||||
function confirm() {
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
emits('confirm', translaterNumberToModel(nowValue.value))
|
||||
}
|
||||
function raightCellClick() {
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
emits('rightClick', translaterNumberToModel(nowValue.value))
|
||||
}
|
||||
|
||||
function clearHandler() {
|
||||
nowValue.value = "";
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', NaN)
|
||||
emits('clear', NaN)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onFocus(evt : UniInputFocusEvent) {
|
||||
/**
|
||||
* 获取焦点时
|
||||
* @param {UniInputFocusEvent} evt
|
||||
*/
|
||||
emits('focus',evt)
|
||||
isFocus.value = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt
|
||||
*/
|
||||
emits('keyboardheightchange', evt)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onClick() {
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
emits('click')
|
||||
}
|
||||
|
||||
|
||||
|
||||
type FindParentCall = (parent:VueComponent|null)=> VueComponent|null
|
||||
let findParent:FindParentCall|null = null;
|
||||
findParent = (parent:VueComponent|null):VueComponent|null=>{
|
||||
if(parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS|| MP-WEIXIN
|
||||
// @ts-ignore
|
||||
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parent.$parent?.$options?.name?.indexOf('xFormItem')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
|
||||
let parents = findParent!(parent.$parent)
|
||||
|
||||
// #ifdef WEB||APP-IOS || MP-WEIXIN
|
||||
// @ts-ignore
|
||||
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-HARMONY
|
||||
if(parents?.$options?.name?.indexOf('xFormItem')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
if(parents instanceof XFormItemComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
function valid(){
|
||||
let pelement = findParent!(proxy);
|
||||
|
||||
if (pelement == null) return;
|
||||
// @ts-ignore
|
||||
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
|
||||
// #ifndef APP-ANDROID
|
||||
if (typeof parent?.validByblur != 'function') return
|
||||
// #endif
|
||||
|
||||
parent.validByblur(nowValue.value)
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch(():number => props.modelValue, (newValue : number) => {
|
||||
const val = translaterNumberToInputVal(isNaN(newValue)?'':newValue)
|
||||
if (val == nowValue.value) return;
|
||||
nowValue.value = val;
|
||||
})
|
||||
|
||||
function onBlur(evt : UniInputBlurEvent) {
|
||||
// 对内容进行首尾清空
|
||||
const val = getTrimAfterValue(getDien(evt.detail.value))
|
||||
let realVal = translaterNumberToInputVal(val)
|
||||
let modelsnycn = translaterNumberToModel(realVal)
|
||||
modelsnycn = clampValue(modelsnycn)
|
||||
|
||||
realVal = translaterNumberToInputVal(modelsnycn)
|
||||
|
||||
nowValue.value = realVal
|
||||
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', realVal==''?NaN:modelsnycn)
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {InputBlurEvent} evt
|
||||
*/
|
||||
emits('blur',evt)
|
||||
isFocus.value = false;
|
||||
valid();
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = translaterNumberToInputVal(isNaN(props.modelValue)?'':props.modelValue,false);
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="onClick" class="xInput"
|
||||
:style="{width:_width}">
|
||||
<view class="xInputLeft">
|
||||
<!--
|
||||
@slot 左插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
|
||||
style="padding-right: 12px;">{{_leftText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view :class="[_cclass]" class="xInputCenter"
|
||||
:style="[
|
||||
{
|
||||
borderRadius:_round,
|
||||
backgroundColor:_color,
|
||||
borderWidth:_focusBorder[0],
|
||||
borderStyle:_focusBorder[1],
|
||||
borderColor:_focusBorder[2]
|
||||
},_cstyle]">
|
||||
<!--
|
||||
@slot 输入框内的左插槽
|
||||
-->
|
||||
<slot name="inputLeft"></slot>
|
||||
|
||||
<view v-if="_leftIcon" style="margin-left:12px;">
|
||||
<x-icon :color="_iconColor" :name="_leftIcon"
|
||||
:font-size="_fontSizeUnScale"></x-icon>
|
||||
</view>
|
||||
<input :inputmode="props.inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
|
||||
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
|
||||
@input="inputHndler" @confirm="confirm" @blur="onBlur"
|
||||
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
|
||||
v-model="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="props.type"
|
||||
:disabled="_disabled" :password="!seePass&&_password" :maxlength="props.maxlength"
|
||||
:cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
|
||||
:confirmType="props.confirmType" :confirmHold="props.confirmHold" :cursor="props.cursor"
|
||||
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
|
||||
:fixed="_fixed" />
|
||||
|
||||
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
|
||||
style="padding: 0 12px;">
|
||||
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
|
||||
</view>
|
||||
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
|
||||
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
|
||||
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
|
||||
</view>
|
||||
<!--
|
||||
@slot 输入框内右插槽
|
||||
-->
|
||||
<slot name="inputRight"></slot>
|
||||
</view>
|
||||
<view class="xInputRight">
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right">
|
||||
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
|
||||
class="xInputRightText">{{_rightText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
|
||||
<view>
|
||||
<!--
|
||||
@slot 底部提示插槽
|
||||
-->
|
||||
<slot v-if="_showFooter" name="footer"></slot>
|
||||
</view>
|
||||
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
{{_inputLen}}/{{_maxlength}}
|
||||
</text>
|
||||
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
字符数:{{_inputLen}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.xInputFooter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.xInputMaxLen {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.xInputCenterInput {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
/* padding: 16rpx 24rpx; */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputCenterInputArea {
|
||||
line-height: 1.6;
|
||||
/* #ifdef WEB || MP-WEIXIN */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xInput {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xInputCenter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xInputLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRightText {
|
||||
padding-left: 12px;
|
||||
font-size: 16px;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,366 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
type POSITIONTYPE = "out" | "in"
|
||||
/**
|
||||
* @name 标签输入框 xInputTag
|
||||
* @description 可通过键盘或者按钮,输入框输入字段回车保存标签词
|
||||
* @page /pages/index/input-tag
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xInputTag"})
|
||||
|
||||
const emits = defineEmits<{
|
||||
/**
|
||||
* 标签变化时触发
|
||||
* @param value - 当前的标签数组
|
||||
*/
|
||||
change: [value: string[]],
|
||||
/**
|
||||
* 等同v-model
|
||||
* @param value - 当前的标签数组
|
||||
*/
|
||||
"update:modelValue": [value: string[]]
|
||||
}>()
|
||||
type xInputTagPropsType = {
|
||||
/**
|
||||
* 输入框背景及标签背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 输入框的暗黑背景色
|
||||
* 空值读取全局的Input暗黑背景色
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 右边按钮主题色,空取全局主题色
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 文本大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 文本颜色,暗黑时取白
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 高
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 输入提示词,默认:请输入并回车
|
||||
*/
|
||||
placeholder: string,
|
||||
/**
|
||||
* 双向绑定
|
||||
*/
|
||||
modelValue: string[],
|
||||
/**
|
||||
* 标签在内还是在外
|
||||
*/
|
||||
postion: POSITIONTYPE,
|
||||
/**
|
||||
* postion为in时,可以控制隐藏按钮.
|
||||
*/
|
||||
showBtn: boolean,
|
||||
/**
|
||||
* 添加按钮的文本,默认:添加标签
|
||||
*/
|
||||
btnText: string,
|
||||
/**
|
||||
* 设置键盘右下角按钮的文字,仅在 type为text 时生效。
|
||||
*/
|
||||
confirmType: string,
|
||||
/**
|
||||
* 最佳输入标签数量,只有用户主动输入才会触发此限制
|
||||
* 你代码赋值不会限制.-1表示不限制
|
||||
*/
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xInputTagPropsType>(), {
|
||||
bgColor: "#f5f5f5",
|
||||
darkBgColor: "",
|
||||
btnColor: "",
|
||||
fontSize: "16",
|
||||
fontColor: "#1d1d1f",
|
||||
width: "auto",
|
||||
height: "40",
|
||||
round: "",
|
||||
placeholder: "",
|
||||
modelValue: (): string[] => [] as string[],
|
||||
postion: "out",
|
||||
showBtn: true,
|
||||
btnText: "",
|
||||
confirmType: "done",
|
||||
maxCount: -1
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const nowValue = ref<string[]>([])
|
||||
const keyword = ref<string>("")
|
||||
|
||||
// 计算属性
|
||||
const _postion = computed((): POSITIONTYPE => {
|
||||
return props.postion
|
||||
})
|
||||
|
||||
const _maxCount = computed((): number => {
|
||||
return props.maxCount
|
||||
})
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
let color = getDefaultColor(props.bgColor)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor == "") {
|
||||
color = xConfig.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(props.darkBgColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _intagBg = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return 'rgb(44,44,46)'
|
||||
return "#fff"
|
||||
})
|
||||
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return '#f5f5f7'
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
|
||||
const _btnColor = computed((): string => {
|
||||
if (props.btnColor == "") return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
|
||||
const _round = computed((): string => {
|
||||
if (props.round == "") return checkIsCssUnit(xConfig.buttonRadius, xConfig.unit)
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _width = computed((): string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
|
||||
const _height = computed((): string => {
|
||||
return checkIsCssUnit(props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
const _placeholder = computed((): string => {
|
||||
if (props.placeholder == '') return xConfig.i18n.t("tmui4x.inputTag.placeholder")
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
const _btnText = computed((): string => {
|
||||
if (props.btnText == '') return xConfig.i18n.t("tmui4x.inputTag.btnText")
|
||||
return props.btnText
|
||||
})
|
||||
|
||||
// 方法
|
||||
function okConfirm(): void {
|
||||
let word = keyword.value.trim()
|
||||
|
||||
if (word == "") {
|
||||
// 不能为空
|
||||
uni.showToast({ title: xConfig.i18n.t("tmui4x.inputTag.tips"), icon: 'none' })
|
||||
return;
|
||||
}
|
||||
if (_maxCount.value > -1 && nowValue.value.length >= _maxCount.value) {
|
||||
// 超过限制最大数
|
||||
uni.showToast({ title: xConfig.i18n.t("tmui4x.inputTag.tips2", _maxCount.value), icon: 'none' })
|
||||
return;
|
||||
}
|
||||
let isKey = nowValue.value.findIndex((el: string): boolean => el == word)
|
||||
if (isKey == -1) {
|
||||
if (_postion.value == 'out') {
|
||||
nowValue.value.unshift(word)
|
||||
} else {
|
||||
nowValue.value.push(word)
|
||||
}
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value)
|
||||
|
||||
/**
|
||||
* 标签变化时触发
|
||||
* @param value 当前的标签数组
|
||||
*/
|
||||
emits('change', nowValue.value)
|
||||
}
|
||||
keyword.value = ""
|
||||
}
|
||||
|
||||
function del(index: number): void {
|
||||
nowValue.value.splice(index, 1)
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value)
|
||||
|
||||
/**
|
||||
* 标签变化时触发
|
||||
* @param value 当前的标签数组
|
||||
*/
|
||||
emits('change', nowValue.value)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted((): void => {
|
||||
nowValue.value = props.modelValue;
|
||||
})
|
||||
|
||||
// 监听器
|
||||
watch((): string[] => props.modelValue, (newValue: string[]) => {
|
||||
if (newValue.join("") == nowValue.value.join("")) return;
|
||||
nowValue.value = props.modelValue;
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view v-if="_postion == 'out'">
|
||||
<view class="xInputTag" :style="{width:_width,height:_height,borderRadius:_round,backgroundColor:_bgColor,}">
|
||||
<input :confirm-type="props.confirmType" v-model="keyword" @confirm="okConfirm" :placeholder="_placeholder" class="xInputTagWrap"
|
||||
:style="{color:_fontColor,fontSize:_fontSize,height:_height}" type="text" />
|
||||
<x-button v-if="props.showBtn" @click="okConfirm" :round="_round" :font-size="props.fontSize" :color="_btnColor" width="30%"
|
||||
style="min-width:100px;max-width:190px" :height="_height">{{_btnText}}</x-button>
|
||||
</view>
|
||||
<view class="xInputTagGroup">
|
||||
<!--
|
||||
@slot 标签插槽,如果对标签样式不喜欢可通过此修改。
|
||||
@prop {string[]} tags - 当前标签组件。
|
||||
-->
|
||||
<slot name="tag" :tags="nowValue">
|
||||
<view v-for="(item,index) in nowValue" :key="index" class="xInputTagTag"
|
||||
:style="{backgroundColor:_bgColor,borderRadius:_round}">
|
||||
<view style="flex: 1;">
|
||||
<x-text :font-size="props.fontSize" class="xInputTagText">{{item}}</x-text>
|
||||
</view>
|
||||
<view @click="del(index)" class="xInputTagClose">
|
||||
<x-icon color="#d0d0d0" font-size="16" name="close-circle-fill"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="_postion == 'in'">
|
||||
<view class="xInputTag xInputTagIntag" :style="{width:_width,minHeight:_height,borderRadius:_round,backgroundColor:_bgColor,}">
|
||||
<!--
|
||||
@slot 标签插槽,如果对标签样式不喜欢可通过此修改。
|
||||
@prop {string[]} tags - 当前标签组件。
|
||||
-->
|
||||
<slot name="tag" :tags="nowValue">
|
||||
<view v-for="(item,index) in nowValue" :key="index" class="xInputTagTag xInputTagTagItemIn"
|
||||
:style="{backgroundColor:_intagBg,borderRadius:_round}">
|
||||
<view style="flex: 1;">
|
||||
<x-text :font-size="props.fontSize" class="xInputTagText">{{item}}</x-text>
|
||||
</view>
|
||||
<view @click="del(index)" class="xInputTagClose">
|
||||
<x-icon color="#d0d0d0" font-size="16" name="close-circle-fill"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<input :confirm-type="props.confirmType" :confirm-hold="true" v-model="keyword" @confirm="okConfirm" :placeholder="_placeholder" class="xInputTagWrap xInputTagWrapIn"
|
||||
:style="{color:_fontColor,fontSize:_fontSize,height:_postion == 'in'?'32px':'24px',marginBottom:'8px'}" type="text" />
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.xInputTagIntag{
|
||||
flex-wrap: wrap;
|
||||
padding-top: 8px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* justify-items: flex-start; */
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
}
|
||||
.xInputTagClose {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.xInputTagText {
|
||||
/* font-size: 28rpx; */
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.xInputTagGroupBox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xInputTagTag {
|
||||
margin-bottom: 12px;
|
||||
margin-right: 6px;
|
||||
padding: 3px 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
.xInputTagTagItemIn{
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.xInputTagGroup {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.xInputTag {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.xInputTagWrap {
|
||||
padding: 0 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.xInputTagWrapIn{
|
||||
min-width: 120px;
|
||||
padding: 0px 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,848 @@
|
||||
<script lang="ts">
|
||||
import { type PropType } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
*
|
||||
* @name 输入框 xInput
|
||||
* @description 表单输入框,样式可定制化强
|
||||
* @page /pages/index/input
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.44+ | 1.1.9 |
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
nowValue: "",
|
||||
seePass: false,
|
||||
isFocus:false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 清空时触发
|
||||
*/
|
||||
'clear',
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {string} value - 已输入的字符串
|
||||
*/
|
||||
'rightClick',
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {string} value - 已输入的字符串
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {string} value - 当前已输入的字符串
|
||||
*/
|
||||
'input',
|
||||
/**
|
||||
* 获得焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'focus',
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'blur',
|
||||
/**
|
||||
* 行高变化时,type=textarea时生效
|
||||
* @param {UniTextareaLineChangeEvent} evt - 事件对象
|
||||
*/
|
||||
'linechange',
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
|
||||
*/
|
||||
'keyboardheightchange', 'update:modelValue'],
|
||||
props: {
|
||||
/**
|
||||
* 自定义style
|
||||
* 标签请写_style,不是-style,插件文档转换问题
|
||||
*/
|
||||
_style: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输入框统一的聚集样式
|
||||
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
|
||||
* ['2px','solid','','']
|
||||
* 全局的配置名称是:inputFocusBorder,可以全局设置.
|
||||
*/
|
||||
focusBorder:{
|
||||
type:Array as PropType<string[]>,
|
||||
default:():string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 占位的样式
|
||||
*/
|
||||
placeholderStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 自定class
|
||||
* 标签请写_class,不是-class,插件文档转换问题
|
||||
*/
|
||||
_class: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输入框圆角
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示清除图标
|
||||
*/
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 右侧文本
|
||||
*/
|
||||
rightText: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左侧文本
|
||||
*/
|
||||
leftText: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 双向绑定的输入值
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 修饰符同vmodel.xxx='',比如v-model.trim=''
|
||||
*/
|
||||
// modelModifiers:{
|
||||
// type: Object as PropType<UTSJSONObject>,
|
||||
// default: ():UTSJSONObject => ({})
|
||||
// },
|
||||
/**
|
||||
* 输入框提示语
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入",
|
||||
},
|
||||
/**
|
||||
* 左图标的颜色
|
||||
* 默认空值取全局的主题色。
|
||||
*/
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 清除图标的颜色
|
||||
*/
|
||||
clearColor: {
|
||||
type: String,
|
||||
default: "#bfbfbf",
|
||||
},
|
||||
/**
|
||||
* 输入框背景
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 输入框暗黑背景,空值取全局的配置
|
||||
* 提供会覆盖全局的配色。默认是透明
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: "transparent",
|
||||
},
|
||||
/**
|
||||
* 输入框的字体颜色
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "#333333",
|
||||
},
|
||||
/**
|
||||
* 如果你提供,就会覆盖自动的反转配色。
|
||||
* 默认是fontColor的反转颜色。
|
||||
*/
|
||||
darkFontColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "16",
|
||||
},
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 类型
|
||||
* "text"|"number"|"digit"|"tel"
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
* 不管你是number,还是digit或者tel是可以让用户按规范填写的只能是数字
|
||||
* 但你定义modelValue类型时,只能是string,请特别注意。
|
||||
*/
|
||||
type: {
|
||||
type: String as PropType<"text" | "number" | "digit" | "tel" | "textarea">,
|
||||
default: "text",
|
||||
},
|
||||
/**
|
||||
* 是否是密码类型
|
||||
*/
|
||||
password: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
|
||||
*/
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
cursorSpacing: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
cursorColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
autoFocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmType: {
|
||||
type: String as PropType<"send" | "search" | "next" | "go" | "done">,
|
||||
default: "next",
|
||||
},
|
||||
confirmHold: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
cursor: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
selectionStart: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
selectionEnd: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
adjustPosition: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 高
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "44"
|
||||
},
|
||||
/**
|
||||
* 自动删除首尾空格?
|
||||
* 只会在失去焦点时删除.
|
||||
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
|
||||
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
|
||||
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
|
||||
*/
|
||||
trim: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
align: {
|
||||
type: String as PropType<'left' | 'right' | 'center'>,
|
||||
default: "left"
|
||||
},
|
||||
/**
|
||||
* type=textarea时生效
|
||||
*/
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
|
||||
*/
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 显示底部的注释说明及出错信息。
|
||||
*/
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否显示字符统计。
|
||||
*/
|
||||
showChartCount:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 格式就是正常的css格式
|
||||
* 比如:8rpx 8rpx 0rpx 0rpx
|
||||
*/
|
||||
inputPadding: {
|
||||
type: String,
|
||||
default: "8px 12px"
|
||||
},
|
||||
inputmode:{
|
||||
type:String,
|
||||
default:'text'
|
||||
},
|
||||
holdKeyboard:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_focusBorder():string[]{
|
||||
let style = this.focusBorder.slice(0);
|
||||
if(this.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
|
||||
style = xConfig.inputFocusBorder.slice(0);
|
||||
}
|
||||
if(style.length <4){
|
||||
|
||||
return ['0px','solid','transparent'] as string[];
|
||||
}
|
||||
let oldcolor = style[2]
|
||||
let hoverColor = getDefaultColor(style[3])
|
||||
if(oldcolor==""){
|
||||
oldcolor = this._color
|
||||
}
|
||||
if(hoverColor==""){
|
||||
hoverColor = getDefaultColor(xConfig.color)
|
||||
}
|
||||
return [style[0],style[1],this.isFocus?hoverColor:oldcolor];
|
||||
},
|
||||
_inputLen() : number {
|
||||
return this.nowValue.split("").length
|
||||
},
|
||||
_maxlength() : number {
|
||||
return this.maxlength
|
||||
},
|
||||
_showFooter() : boolean {
|
||||
return this.showFooter
|
||||
},
|
||||
_holdKeyboard():boolean{
|
||||
return this.holdKeyboard
|
||||
},
|
||||
_autoHeight() : boolean {
|
||||
return this.autoHeight
|
||||
},
|
||||
_showChartCount():boolean{
|
||||
return this.showChartCount
|
||||
},
|
||||
_fixed() : boolean {
|
||||
return this.fixed
|
||||
},
|
||||
_width() : string {
|
||||
return checkIsCssUnit(this.width, xConfig.unit)
|
||||
},
|
||||
_height() : string {
|
||||
if (this.autoHeight&&this.type == 'textarea') return "auto"
|
||||
return checkIsCssUnit(this.height, xConfig.unit)
|
||||
},
|
||||
_cstyle() : string {
|
||||
return this._style
|
||||
},
|
||||
_placeholderStyle() : string {
|
||||
return this.placeholderStyle==''?xConfig.placeholderStyle:this.placeholderStyle
|
||||
},
|
||||
|
||||
_cclass() : string {
|
||||
return this._class
|
||||
},
|
||||
_round() : string {
|
||||
if(this.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
|
||||
return checkIsCssUnit(this.round, xConfig.unit)
|
||||
},
|
||||
_fontSize() : string {
|
||||
let fontSize = checkIsCssUnit(this.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
},
|
||||
_fontSizeUnScale() : string {
|
||||
return this.fontSize
|
||||
},
|
||||
_showClear() : boolean {
|
||||
return this.showClear
|
||||
},
|
||||
_rightText() : string {
|
||||
return this.rightText
|
||||
},
|
||||
_leftText() : string {
|
||||
return this.leftText
|
||||
},
|
||||
_confirmType() : string {
|
||||
return this.confirmType
|
||||
},
|
||||
_placeholder() : string {
|
||||
return this.placeholder
|
||||
},
|
||||
_iconColor() : string {
|
||||
if(this.iconColor==""){
|
||||
return getDefaultColor(xConfig.color)
|
||||
}
|
||||
return getDefaultColor(this.iconColor)
|
||||
},
|
||||
_color() : string {
|
||||
let color = getDefaultColor(this.color==''?xConfig.inputBgColor:this.color)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkBgColor == "") {
|
||||
color = xConfig.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(this.darkBgColor)
|
||||
}
|
||||
}
|
||||
|
||||
return color
|
||||
},
|
||||
_clearColor():string{
|
||||
if(this.clearColor=='') return this._iconColor
|
||||
return getDefaultColor(this.clearColor)
|
||||
},
|
||||
_fontColor() : string {
|
||||
let color = getDefaultColor(this.fontColor)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (this.darkFontColor == "") {
|
||||
color = "#ffffff"
|
||||
} else {
|
||||
color = getDefaultColor(this.darkFontColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
},
|
||||
_cursorColor():string{
|
||||
let color = this.cursorColor
|
||||
if(this.cursorColor==''){
|
||||
color = xConfig.color
|
||||
}
|
||||
return getDefaultColor(color)
|
||||
},
|
||||
_leftIcon() : string {
|
||||
return this.leftIcon
|
||||
},
|
||||
_disabled() : boolean {
|
||||
return this.disabled
|
||||
},
|
||||
_password() : boolean {
|
||||
return this.password
|
||||
},
|
||||
_autoFocus() : boolean {
|
||||
return this.autoFocus
|
||||
},
|
||||
_focus() : boolean {
|
||||
return this.focus
|
||||
},
|
||||
_adjustPosition() : boolean {
|
||||
return this.adjustPosition
|
||||
},
|
||||
_selectionEnd() : number {
|
||||
return this.selectionEnd
|
||||
},
|
||||
_selectionStart() : number {
|
||||
return this.selectionStart
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue : string) {
|
||||
if (newValue == this.nowValue) return;
|
||||
this.nowValue = newValue;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.nowValue = this.modelValue;
|
||||
},
|
||||
methods: {
|
||||
getTrimAfterValue(value:string):string{
|
||||
if(this.trim) return value.trim()
|
||||
return value;
|
||||
},
|
||||
confirm() {
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
this.$emit('confirm', this.getTrimAfterValue(this.nowValue))
|
||||
},
|
||||
inputHndler(evt : UniInputEvent) {
|
||||
this.nowValue = this.getTrimAfterValue(evt.detail.value)
|
||||
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {string} value 当前已输入的字符串
|
||||
*/
|
||||
this.$emit('input', this.nowValue)
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
this.$emit('update:modelValue', this.nowValue)
|
||||
this.$forceUpdate()
|
||||
},
|
||||
|
||||
raightCellClick() {
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
this.$emit('rightClick', this.nowValue)
|
||||
},
|
||||
clearHandler() {
|
||||
this.nowValue = "";
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
this.$emit('update:modelValue', "")
|
||||
this.$emit('clear', "")
|
||||
},
|
||||
onBlur(evt : UniInputBlurEvent) {
|
||||
|
||||
// 对内容进行首尾清空
|
||||
let newVal = this.getTrimAfterValue(this.nowValue)
|
||||
if(newVal != this.nowValue){
|
||||
this.nowValue = newVal
|
||||
this.$emit('update:modelValue', this.nowValue)
|
||||
|
||||
}
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {InputBlurEvent} evt
|
||||
*/
|
||||
this.$emit('blur',evt)
|
||||
this.isFocus =false;
|
||||
this.valid();
|
||||
},
|
||||
onFocus(evt : UniInputFocusEvent) {
|
||||
/**
|
||||
* 获取焦点时
|
||||
* @param {UniInputFocusEvent} evt
|
||||
*/
|
||||
this.$emit('focus',evt)
|
||||
this.isFocus = true;
|
||||
},
|
||||
onAreaBlur(evt : UniTextareaBlurEvent) {
|
||||
let newVal = this.getTrimAfterValue(this.nowValue)
|
||||
if(newVal != this.nowValue){
|
||||
this.nowValue = newVal
|
||||
this.$emit('update:modelValue', this.nowValue)
|
||||
|
||||
}
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {InputBlurEvent} evt
|
||||
*/
|
||||
this.$emit('blur',evt)
|
||||
this.isFocus =false;
|
||||
this.valid();
|
||||
},
|
||||
onAreaFocus(evt : UniTextareaFocusEvent) {
|
||||
/**
|
||||
* 获取焦点时
|
||||
* @param {UniInputFocusEvent} evt
|
||||
*/
|
||||
this.$emit('focus',evt)
|
||||
this.isFocus = true;
|
||||
},
|
||||
onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt
|
||||
*/
|
||||
this.$emit('keyboardheightchange', evt)
|
||||
},
|
||||
onLinechange(evt : UniTextareaLineChangeEvent) {
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniTextareaLineChangeEvent} evt
|
||||
*/
|
||||
this.$emit('linechange', evt)
|
||||
},
|
||||
onClick() {
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
this.$emit('click')
|
||||
},
|
||||
// #ifdef MP-WEIXIN
|
||||
inpuUnfocusChange(evt){
|
||||
let value = this.getTrimAfterValue(evt.detail.value);
|
||||
if(value!=this.modelValue&&value!=this.nowValue){
|
||||
this.nowValue = value;
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
valid(){
|
||||
let pelement = this.findParent(this);
|
||||
|
||||
if (pelement == null) return;
|
||||
// @ts-ignore
|
||||
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
|
||||
// #ifndef APP-ANDROID
|
||||
if (typeof parent?.validByblur != 'function') return
|
||||
// #endif
|
||||
|
||||
parent.validByblur(this.nowValue)
|
||||
},
|
||||
findParent(parent:VueComponent|null):VueComponent|null{
|
||||
|
||||
if(parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS|| MP-WEIXIN
|
||||
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
|
||||
let parents = this.findParent(parent.$parent)
|
||||
|
||||
// #ifdef WEB||APP-IOS || MP-WEIXIN
|
||||
|
||||
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
if(parents instanceof XFormItemComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="onClick" class="xInput"
|
||||
:style="{width:_width}">
|
||||
<view class="xInputLeft">
|
||||
<!--
|
||||
@slot 左插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
|
||||
style="padding-right: 12px;">{{_leftText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view :class="[_cclass]" class="xInputCenter"
|
||||
:style="[
|
||||
{
|
||||
borderRadius:_round,
|
||||
backgroundColor:_color,
|
||||
borderWidth:_focusBorder[0],
|
||||
borderStyle:_focusBorder[1],
|
||||
borderColor:_focusBorder[2]
|
||||
},_cstyle]">
|
||||
<!--
|
||||
@slot 输入框内的左插槽
|
||||
-->
|
||||
<slot name="inputLeft"></slot>
|
||||
|
||||
<view v-if="_leftIcon" style="margin-left:12px;">
|
||||
<x-icon :color="_iconColor" :name="_leftIcon"
|
||||
:font-size="_fontSizeUnScale"></x-icon>
|
||||
</view>
|
||||
<input v-if="type!='textarea'" :inputmode="inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@change="inpuUnfocusChange"
|
||||
<!-- #endif -->
|
||||
:style="{color:_fontColor,fontSize:_fontSize,textAlign:align,padding:inputPadding,height:_height}"
|
||||
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onBlur"
|
||||
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
|
||||
:value="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="type"
|
||||
:disabled="_disabled" :password="!seePass&&_password" :maxlength="maxlength"
|
||||
:cursorSpacing="cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
|
||||
:confirmType="confirmType" :confirmHold="confirmHold" :cursor="cursor"
|
||||
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
|
||||
:fixed="_fixed" />
|
||||
<textarea v-if="type=='textarea'" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
|
||||
:style="{color:_fontColor,fontSize:_fontSize,textAlign:align,padding:inputPadding,height:_height}"
|
||||
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onAreaBlur"
|
||||
@keyboardheightchange="onkeyboardheightchange" @focus="onAreaFocus" :value="nowValue"
|
||||
:placeholder="_placeholder" class="xInputCenterInput xInputCenterInputArea" :disabled="_disabled"
|
||||
:maxlength="maxlength" :cursorSpacing="cursorSpacing" :cursor-color="_cursorColor"
|
||||
:autoFocus="_autoFocus" :focus="_focus" :confirmHold="confirmHold" :cursor="cursor"
|
||||
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
|
||||
:fixed="_fixed" :autoHeight="_autoHeight"></textarea>
|
||||
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
|
||||
style="padding: 0 12px;">
|
||||
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
|
||||
</view>
|
||||
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
|
||||
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
|
||||
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
|
||||
</view>
|
||||
<!--
|
||||
@slot 输入框内右插槽
|
||||
-->
|
||||
<slot name="inputRight"></slot>
|
||||
</view>
|
||||
<view class="xInputRight">
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right">
|
||||
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
|
||||
class="xInputRightText">{{_rightText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
|
||||
<view>
|
||||
<!--
|
||||
@slot 底部提示插槽
|
||||
-->
|
||||
<slot v-if="_showFooter" name="footer"></slot>
|
||||
</view>
|
||||
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
{{_inputLen}}/{{_maxlength}}
|
||||
</text>
|
||||
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
字符数:{{_inputLen}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.xInputFooter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.xInputMaxLen {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.xInputCenterInput {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
/* padding: 16rpx 24rpx; */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputCenterInputArea {
|
||||
line-height: 1.6;
|
||||
/* #ifdef WEB || MP-WEIXIN */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xInput {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xInputCenter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xInputLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRightText {
|
||||
padding-left: 12px;
|
||||
font-size: 16px;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,847 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue"
|
||||
import { getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { getDefaultColor,rgbToHex,hexToRgb } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
const i18n = xConfig.i18n;
|
||||
/**
|
||||
*
|
||||
* @name 输入框 xInput
|
||||
* @description 表单输入框,样式可定制化强
|
||||
* @page /pages/index/input
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xInput"})
|
||||
|
||||
// 事件定义
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 清空时触发
|
||||
*/
|
||||
'clear',
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {string} value - 已输入的字符串
|
||||
*/
|
||||
'rightClick',
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {string} value - 已输入的字符串
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {string} value - 当前已输入的字符串
|
||||
*/
|
||||
'input',
|
||||
/**
|
||||
* 获得焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'focus',
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {UniInputBlurEvent} evt - 事件对象
|
||||
*/
|
||||
'blur',
|
||||
/**
|
||||
* 行高变化时,type=textarea时生效
|
||||
* @param {UniTextareaLineChangeEvent} evt - 事件对象
|
||||
*/
|
||||
'linechange',
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt - 事件对象
|
||||
*/
|
||||
'keyboardheightchange', 'update:modelValue'])
|
||||
|
||||
export type xInputPropsType = {
|
||||
/**
|
||||
* 自定义style
|
||||
* 标签请写_style,不是-style,插件文档转换问题
|
||||
*/
|
||||
_style: string,
|
||||
/**
|
||||
* 输入框统一的聚集样式
|
||||
* 第3表示默认的边颜色(如果为空表示默认边颜色不生效.),第4表示聚焦时的颜色(空表示取全局color,transparent为不生效就是没有聚集样式)
|
||||
* ['2px','solid','','']
|
||||
* 全局的配置名称是:inputFocusBorder,可以全局设置.
|
||||
*/
|
||||
focusBorder: string[],
|
||||
/**
|
||||
* 占位的样式
|
||||
*/
|
||||
placeholderStyle: string,
|
||||
/**
|
||||
* 自定class
|
||||
* 标签请写_class,不是-class,插件文档转换问题
|
||||
*/
|
||||
_class: string,
|
||||
/**
|
||||
* 输入框圆角
|
||||
*/
|
||||
round: string,
|
||||
/**
|
||||
* 是否显示清除图标
|
||||
*/
|
||||
showClear: boolean,
|
||||
/**
|
||||
* 右侧文本
|
||||
*/
|
||||
rightText: string,
|
||||
/**
|
||||
* 左侧文本
|
||||
*/
|
||||
leftText: string,
|
||||
/**
|
||||
* 双向绑定的输入值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 修饰符同vmodel.xxx='',比如v-model.trim=''
|
||||
*/
|
||||
// modelModifiers: UTSJSONObject,
|
||||
/**
|
||||
* 输入框提示语
|
||||
*/
|
||||
placeholder: string,
|
||||
/**
|
||||
* 左图标的颜色
|
||||
* 默认空值取全局的主题色。
|
||||
*/
|
||||
iconColor: string,
|
||||
/**
|
||||
* 清除图标的颜色
|
||||
*/
|
||||
clearColor: string,
|
||||
/**
|
||||
* 输入框背景
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 输入框暗黑背景,空值取全局的配置
|
||||
* 提供会覆盖全局的配色。默认是透明
|
||||
*/
|
||||
darkBgColor: string,
|
||||
/**
|
||||
* 输入框的字体颜色
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 如果你提供,就会覆盖自动的反转配色。
|
||||
* 默认是fontColor的反转颜色。
|
||||
*/
|
||||
darkFontColor: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: string,
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
leftIcon: string,
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
*/
|
||||
disabled: boolean,
|
||||
/**
|
||||
* 类型
|
||||
* "text"|"number"|"digit"|"tel"
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html
|
||||
* 不管你是number,还是digit或者tel是可以让用户按规范填写的只能是数字
|
||||
* 但你定义modelValue类型时,只能是string,请特别注意。
|
||||
*/
|
||||
type: "text" | "number" | "idcard" | "digit" | "tel" | "safe-password" | "nickname" | "textarea",
|
||||
/**
|
||||
* 是否是密码类型
|
||||
*/
|
||||
password: boolean,
|
||||
/**
|
||||
* 最大字符数量,如果要显示统计字符,请设置showChartCount为ture
|
||||
*/
|
||||
maxlength: number,
|
||||
cursorSpacing: number,
|
||||
cursorColor: string,
|
||||
autoFocus: boolean,
|
||||
focus: boolean,
|
||||
confirmType: "send" | "search" | "next" | "go" | "done",
|
||||
confirmHold: boolean,
|
||||
cursor: number,
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
adjustPosition: boolean,
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: string,
|
||||
/**
|
||||
* 高
|
||||
*/
|
||||
height: string,
|
||||
/**
|
||||
* 自动删除首尾空格?
|
||||
* 只会在失去焦点时删除.
|
||||
* 这里需要个解释:由于用户输入过快或者允许用户自由的输入,组件本身不会去干涉用户输入
|
||||
* 因为一旦干涉就在会在低端机上会出现字符闪烁的情况(特别是微信小程序上的安桌机),看似简单的功能后面隐藏着非常大的风险
|
||||
* 因此你在事件中收到的字符绝对是经过处理的字符串,但用户的输入框可能还是有空格.
|
||||
*/
|
||||
trim: boolean,
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
align: 'left' | 'right' | 'center',
|
||||
/**
|
||||
* type=textarea时生效
|
||||
*/
|
||||
autoHeight: boolean,
|
||||
/**
|
||||
* 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
|
||||
*/
|
||||
fixed: boolean,
|
||||
/**
|
||||
* 显示底部的注释说明及出错信息。
|
||||
*/
|
||||
showFooter: boolean,
|
||||
/**
|
||||
* 是否显示字符统计。
|
||||
*/
|
||||
showChartCount: boolean,
|
||||
/**
|
||||
* 格式就是正常的css格式
|
||||
* 比如:8rpx 8rpx 0rpx 0rpx
|
||||
*/
|
||||
inputPadding: string,
|
||||
/**
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
|
||||
*/
|
||||
inputmode: "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url",
|
||||
/**
|
||||
* focus时,点击页面的时候不收起键盘
|
||||
* 见官方文档:https://doc.dcloud.net.cn/uni-app-x/component/input.html#%E5%B1%9E%E6%80%A7
|
||||
*/
|
||||
holdKeyboard: boolean,
|
||||
/**
|
||||
* 是否作为可点击指示。会在右边显示图标指示可以点击的链接状态
|
||||
*/
|
||||
isLink:boolean;
|
||||
rightIcon: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xInputPropsType>(), {
|
||||
_style: "",
|
||||
focusBorder: ():string[] => [] as string[],
|
||||
placeholderStyle: "",
|
||||
_class: "",
|
||||
round: "",
|
||||
showClear: false,
|
||||
rightText: "",
|
||||
leftText: "",
|
||||
modelValue: "",
|
||||
placeholder: "",//请输入
|
||||
iconColor: "",
|
||||
clearColor: "#bfbfbf",
|
||||
color: "",
|
||||
darkBgColor: "transparent",
|
||||
fontColor: "#333333",
|
||||
darkFontColor: "",
|
||||
fontSize: "16",
|
||||
leftIcon: "",
|
||||
name: "",
|
||||
disabled: false,
|
||||
type: "text",
|
||||
password: false,
|
||||
maxlength: -1,
|
||||
cursorSpacing: 0,
|
||||
cursorColor: "",
|
||||
autoFocus: false,
|
||||
focus: false,
|
||||
confirmType: "next",
|
||||
confirmHold: false,
|
||||
cursor: 0,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1,
|
||||
adjustPosition: true,
|
||||
width: "auto",
|
||||
height: "44",
|
||||
trim: true,
|
||||
align: "left",
|
||||
autoHeight: false,
|
||||
fixed: false,
|
||||
showFooter: false,
|
||||
showChartCount: false,
|
||||
inputPadding: "8px 12px",
|
||||
inputmode: 'text',
|
||||
holdKeyboard: false,
|
||||
isLink:false,
|
||||
rightIcon:""
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const nowValue = ref("")
|
||||
const seePass = ref(false)
|
||||
const isFocus = ref(false)
|
||||
|
||||
// 获取当前实例
|
||||
const proxy = getCurrentInstance()?.proxy??null;
|
||||
|
||||
// 计算属性
|
||||
const _focusBorder = computed(():string[] => {
|
||||
let style = props.focusBorder.slice(0);
|
||||
if(props.focusBorder.length<4&&xConfig.inputFocusBorder.length==4){
|
||||
style = xConfig.inputFocusBorder.slice(0);
|
||||
}
|
||||
if(style.length <4){
|
||||
return ['0px','solid','transparent'] as string[];
|
||||
}
|
||||
let oldcolor = style[2]
|
||||
let hoverColor = getDefaultColor(style[3])
|
||||
if(oldcolor==""){
|
||||
oldcolor = props.color
|
||||
}
|
||||
if(hoverColor==""){
|
||||
hoverColor = getDefaultColor(xConfig.color)
|
||||
}
|
||||
return [style[0],style[1],isFocus.value?hoverColor:oldcolor];
|
||||
})
|
||||
|
||||
const _inputLen = computed(() : number => {
|
||||
return nowValue.value.split("").length
|
||||
})
|
||||
|
||||
const _maxlength = computed(() : number => {
|
||||
return props.maxlength
|
||||
})
|
||||
|
||||
const _showFooter = computed(() : boolean => {
|
||||
return props.showFooter
|
||||
})
|
||||
|
||||
const _holdKeyboard = computed(():boolean => {
|
||||
return props.holdKeyboard
|
||||
})
|
||||
|
||||
const _autoHeight = computed(() : boolean => {
|
||||
return props.autoHeight
|
||||
})
|
||||
const _isLink = computed(() : boolean => {
|
||||
return props.isLink
|
||||
})
|
||||
const _rightIcon = computed(() : string => {
|
||||
return props.rightIcon
|
||||
})
|
||||
|
||||
const _showChartCount = computed(():boolean => {
|
||||
return props.showChartCount
|
||||
})
|
||||
|
||||
const _fixed = computed(() : boolean => {
|
||||
return props.fixed
|
||||
})
|
||||
|
||||
const _width = computed(() : string => {
|
||||
return checkIsCssUnit(props.width, xConfig.unit)
|
||||
})
|
||||
|
||||
const _height = computed(() : string => {
|
||||
if (props.autoHeight&&props.type == 'textarea') return "auto"
|
||||
return checkIsCssUnit(props.height, xConfig.unit)
|
||||
})
|
||||
|
||||
const _cstyle = computed(() : string => {
|
||||
return props._style
|
||||
})
|
||||
|
||||
const _placeholderStyle = computed(() : string => {
|
||||
return props.placeholderStyle==''?xConfig.placeholderStyle:props.placeholderStyle
|
||||
})
|
||||
|
||||
const _cclass = computed(() : string => {
|
||||
return props._class
|
||||
})
|
||||
|
||||
const _round = computed(() : string => {
|
||||
if(props.round=="") return checkIsCssUnit(xConfig.inputRadius, xConfig.unit)
|
||||
return checkIsCssUnit(props.round, xConfig.unit)
|
||||
})
|
||||
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _fontSizeUnScale = computed(() : string => {
|
||||
return props.fontSize
|
||||
})
|
||||
|
||||
const _showClear = computed(() : boolean => {
|
||||
return props.showClear
|
||||
})
|
||||
|
||||
const _rightText = computed(() : string => {
|
||||
return props.rightText
|
||||
})
|
||||
|
||||
const _leftText = computed(() : string => {
|
||||
return props.leftText
|
||||
})
|
||||
|
||||
const _confirmType = computed(() : string => {
|
||||
return props.confirmType
|
||||
})
|
||||
|
||||
const _placeholder = computed(() : string => {
|
||||
if(props.placeholder=='') return i18n!.t("tmui4x.input.placeholder")
|
||||
return props.placeholder
|
||||
})
|
||||
|
||||
const _iconColor = computed(() : string => {
|
||||
if(props.iconColor==""){
|
||||
return getDefaultColor(xConfig.color)
|
||||
}
|
||||
return getDefaultColor(props.iconColor)
|
||||
})
|
||||
|
||||
const _color = computed(() : string => {
|
||||
let color = getDefaultColor(props.color==''?xConfig.inputBgColor:props.color)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkBgColor == "") {
|
||||
color = xConfig.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(props.darkBgColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _clearColor = computed(():string => {
|
||||
if(props.clearColor=='') return _iconColor.value
|
||||
return getDefaultColor(props.clearColor)
|
||||
})
|
||||
|
||||
const _fontColor = computed(() : string => {
|
||||
let color = getDefaultColor(props.fontColor)
|
||||
if (xConfig.dark == 'dark') {
|
||||
if (props.darkFontColor == "") {
|
||||
color = "#ffffff"
|
||||
} else {
|
||||
color = getDefaultColor(props.darkFontColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _cursorColor = computed(():string => {
|
||||
let color = props.cursorColor
|
||||
if(props.cursorColor==''){
|
||||
color = xConfig.color
|
||||
}
|
||||
return getDefaultColor(color)
|
||||
})
|
||||
|
||||
const _leftIcon = computed(() : string => {
|
||||
return props.leftIcon
|
||||
})
|
||||
|
||||
const _disabled = computed(() : boolean => {
|
||||
return props.disabled
|
||||
})
|
||||
|
||||
const _password = computed(() : boolean => {
|
||||
return props.password
|
||||
})
|
||||
|
||||
const _autoFocus = computed(() : boolean => {
|
||||
return props.autoFocus
|
||||
})
|
||||
|
||||
const _focus = computed(() : boolean => {
|
||||
return props.focus
|
||||
})
|
||||
|
||||
const _adjustPosition = computed(() : boolean => {
|
||||
return props.adjustPosition
|
||||
})
|
||||
|
||||
const _selectionEnd = computed(() : number => {
|
||||
return props.selectionEnd
|
||||
})
|
||||
|
||||
const _selectionStart = computed(() : number => {
|
||||
return props.selectionStart
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 方法函数
|
||||
function getTrimAfterValue(value:string):string{
|
||||
if(props.trim) return value.trim()
|
||||
return value;
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
/**
|
||||
* 输入法点了确认搜索按钮时触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
emits('confirm', getTrimAfterValue(nowValue.value))
|
||||
}
|
||||
|
||||
function inputHndler(evt : UniInputEvent) {
|
||||
nowValue.value = getTrimAfterValue(evt.detail.value)
|
||||
|
||||
/**
|
||||
* 输入时触发
|
||||
* @param {string} value 当前已输入的字符串
|
||||
*/
|
||||
emits('input', nowValue.value)
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value)
|
||||
}
|
||||
|
||||
function raightCellClick() {
|
||||
/**
|
||||
* 点击右侧文本时触发,如果你使用了插槽替换了,此事件不会触发
|
||||
* @param {string} value 已输入的字符串
|
||||
*/
|
||||
emits('rightClick', nowValue.value)
|
||||
}
|
||||
|
||||
type FindParentCall = (parent:VueComponent|null)=> VueComponent|null
|
||||
let findParent:FindParentCall|null = null;
|
||||
|
||||
findParent = (parent:VueComponent|null):VueComponent|null =>{
|
||||
if(parent == null) return null;
|
||||
// #ifdef WEB||APP-IOS|| MP-WEIXIN
|
||||
// @ts-ignore
|
||||
if(parent.$parent?.id?.indexOf('xFormItem')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parent.$parent?.$options?.name?.indexOf('xFormItem')>-1) return parent.$parent;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
if(parent.$parent instanceof XFormItemComponentPublicInstance) return parent.$parent;
|
||||
// #endif
|
||||
|
||||
let parents = findParent!(parent.$parent)
|
||||
|
||||
// #ifdef WEB||APP-IOS || MP-WEIXIN
|
||||
// @ts-ignore
|
||||
if(parents?.id?.indexOf('xFormItem')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
if(parents?.$options?.name?.indexOf('xFormItem')>-1) return parents;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
// @ts-ignore
|
||||
if(parents instanceof XFormItemComponentPublicInstance) return parents;
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
function valid(){
|
||||
let pelement = findParent!(proxy);
|
||||
|
||||
if (pelement == null) return;
|
||||
// @ts-ignore
|
||||
let parent : XFormItemComponentPublicInstance = pelement as XFormItemComponentPublicInstance;
|
||||
// #ifndef APP-ANDROID
|
||||
|
||||
if (typeof parent?.validByblur != 'function') return
|
||||
// #endif
|
||||
|
||||
parent.validByblur(nowValue.value)
|
||||
}
|
||||
|
||||
function clearHandler() {
|
||||
nowValue.value = "";
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', "")
|
||||
emits('clear', "")
|
||||
}
|
||||
|
||||
function onBlur(evt : UniInputBlurEvent) {
|
||||
// 对内容进行首尾清空
|
||||
let newVal = getTrimAfterValue(nowValue.value)
|
||||
if(newVal != nowValue.value){
|
||||
nowValue.value = newVal
|
||||
emits('update:modelValue', nowValue.value)
|
||||
}
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {InputBlurEvent} evt
|
||||
*/
|
||||
emits('blur',evt)
|
||||
isFocus.value = false;
|
||||
valid();
|
||||
}
|
||||
|
||||
function onFocus(evt : UniInputFocusEvent) {
|
||||
/**
|
||||
* 获取焦点时
|
||||
* @param {UniInputFocusEvent} evt
|
||||
*/
|
||||
emits('focus',evt)
|
||||
isFocus.value = true;
|
||||
}
|
||||
|
||||
function onAreaBlur(evt : UniTextareaBlurEvent) {
|
||||
let newVal = getTrimAfterValue(nowValue.value)
|
||||
if(newVal != nowValue.value){
|
||||
nowValue.value = newVal
|
||||
emits('update:modelValue', nowValue.value)
|
||||
}
|
||||
/**
|
||||
* 失去焦点时
|
||||
* @param {InputBlurEvent} evt
|
||||
*/
|
||||
emits('blur',evt)
|
||||
isFocus.value = false;
|
||||
valid();
|
||||
}
|
||||
|
||||
function onAreaFocus(evt : UniTextareaFocusEvent) {
|
||||
/**
|
||||
* 获取焦点时
|
||||
* @param {UniInputFocusEvent} evt
|
||||
*/
|
||||
emits('focus',evt)
|
||||
isFocus.value = true;
|
||||
}
|
||||
|
||||
function onkeyboardheightchange(evt : UniInputKeyboardHeightChangeEvent) {
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniInputKeyboardHeightChangeEvent} evt
|
||||
*/
|
||||
emits('keyboardheightchange', evt)
|
||||
}
|
||||
|
||||
function onLinechange(evt : UniTextareaLineChangeEvent) {
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @param {UniTextareaLineChangeEvent} evt
|
||||
*/
|
||||
emits('linechange', evt)
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
/**
|
||||
* 点击整个输入框触发
|
||||
*/
|
||||
emits('click')
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function inpuUnfocusChange(evt){
|
||||
let value = getTrimAfterValue(evt.detail.value);
|
||||
if(value!=props.modelValue&&value!=nowValue.value){
|
||||
nowValue.value = value;
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch(():string => props.modelValue, (newValue : string) => {
|
||||
if (newValue == nowValue.value) return;
|
||||
nowValue.value = newValue;
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue;
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="onClick" class="xInput"
|
||||
:style="{width:_width}">
|
||||
<view class="xInputLeft">
|
||||
<!--
|
||||
@slot 左插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<x-text v-if="_leftText!=''" :font-size="_fontSizeUnScale"
|
||||
style="padding-right: 12px;">{{_leftText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view :class="[_cclass]" class="xInputCenter"
|
||||
:style="[
|
||||
{
|
||||
borderRadius:_round,
|
||||
backgroundColor:_color,
|
||||
borderWidth:_focusBorder[0],
|
||||
borderStyle:_focusBorder[1],
|
||||
borderColor:_focusBorder[2]
|
||||
},_cstyle]">
|
||||
<!--
|
||||
@slot 输入框内的左插槽
|
||||
-->
|
||||
<slot name="inputLeft"></slot>
|
||||
|
||||
<view v-if="_leftIcon" style="margin-left:12px;">
|
||||
<x-icon :color="_iconColor" :name="_leftIcon"
|
||||
:font-size="_fontSizeUnScale"></x-icon>
|
||||
</view>
|
||||
<input v-if="props.type!='textarea'" :inputmode="props.inputmode" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
@change="inpuUnfocusChange"
|
||||
<!-- #endif -->
|
||||
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
|
||||
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onBlur"
|
||||
@keyboardheightchange="onkeyboardheightchange" @focus="onFocus" confirm-type="search"
|
||||
:value="nowValue" :placeholder="_placeholder" class="xInputCenterInput" :type="props.type"
|
||||
:disabled="_disabled" :password="!seePass&&_password" :maxlength="props.maxlength"
|
||||
:cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor" :autoFocus="_autoFocus" :focus="_focus"
|
||||
:confirmType="props.confirmType" :confirmHold="props.confirmHold" :cursor="props.cursor"
|
||||
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
|
||||
:fixed="_fixed" />
|
||||
<textarea v-if="props.type=='textarea'" :holdKeyboard="_holdKeyboard" :placeholder-style="_placeholderStyle"
|
||||
:style="{color:_fontColor,fontSize:_fontSize,textAlign:props.align,padding:props.inputPadding,height:_height}"
|
||||
@input="inputHndler" @confirm="confirm" @linechange="onLinechange" @blur="onAreaBlur"
|
||||
@keyboardheightchange="onkeyboardheightchange" @focus="onAreaFocus" :value="nowValue"
|
||||
:placeholder="_placeholder" class="xInputCenterInput xInputCenterInputArea" :disabled="_disabled"
|
||||
:maxlength="props.maxlength" :cursorSpacing="props.cursorSpacing" :cursor-color="_cursorColor"
|
||||
:autoFocus="_autoFocus" :focus="_focus" :confirmHold="props.confirmHold" :cursor="props.cursor"
|
||||
:selectionStart="_selectionStart" :selectionEnd="_selectionEnd" :adjustPosition="_adjustPosition"
|
||||
:fixed="_fixed" :autoHeight="_autoHeight"></textarea>
|
||||
<view @click="clearHandler" v-if="_showClear&&nowValue.length>0" class="xInputclear"
|
||||
style="padding: 0 12px;">
|
||||
<x-icon :color="_clearColor" name="close-circle-fill"></x-icon>
|
||||
</view>
|
||||
<view @click="seePass=!seePass" v-if="_password" class="xInputclear" style="padding: 0 12px;">
|
||||
<x-icon v-if="!seePass" :color="_iconColor" name="eye-off-line"></x-icon>
|
||||
<x-icon v-else :color="_iconColor" name="eye-fill"></x-icon>
|
||||
</view>
|
||||
<!--
|
||||
@slot 输入框内右插槽
|
||||
-->
|
||||
<slot name="inputRight"></slot>
|
||||
<view v-if="_isLink||_rightIcon!=''" style="padding: 0 12px;">
|
||||
<x-icon :color="_iconColor" name="arrow-right-s-line"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xInputRight">
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right">
|
||||
<x-text v-if="_rightText!=''" @click="raightCellClick" :font-size="_fontSizeUnScale"
|
||||
class="xInputRightText">{{_rightText}}</x-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xInputFooter" v-if="_showFooter||_maxlength>-1">
|
||||
<view>
|
||||
<!--
|
||||
@slot 底部提示插槽
|
||||
-->
|
||||
<slot v-if="_showFooter" name="footer"></slot>
|
||||
</view>
|
||||
<text v-if="_maxlength>-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
{{_inputLen}}/{{_maxlength}}
|
||||
</text>
|
||||
<text v-if="_maxlength==-1&&_showChartCount" style="margin-left: 20px;" class="xInputMaxLen">
|
||||
字符数:{{_inputLen}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.xInputFooter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
.xInputMaxLen {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.xInputCenterInput {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
/* padding: 16rpx 24rpx; */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputCenterInputArea {
|
||||
line-height: 1.6;
|
||||
/* #ifdef WEB || MP-WEIXIN */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.xInput {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xInputCenter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xInputLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xInputRightText {
|
||||
padding-left: 12px;
|
||||
font-size: 16px;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,411 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
|
||||
/**
|
||||
* @name 车牌键盘 xKeyboardCar
|
||||
* @description 功能齐全的车牌快速输入键盘。
|
||||
* @page /pages/index/keyboard
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xKeyboardCar"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
|
||||
defineSlots<{
|
||||
default(props:{show:boolean}): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 确认时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'cancel',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
type xKeyboardCarPropsType = {
|
||||
/**
|
||||
* 当前输入的值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
maxLen: number,
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: boolean,
|
||||
/**
|
||||
* 顶部标题,默认:安全键盘请放心输入
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 主按钮色,空值取全局主题
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 按钮背景
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 键盘背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 文字颜色
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 点击确认是否保持键盘不收起
|
||||
*/
|
||||
hold: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xKeyboardCarPropsType>(), {
|
||||
modelValue: "",
|
||||
maxLen: 8,
|
||||
modelShow: false,
|
||||
title: "",
|
||||
color: "",
|
||||
btnColor: 'white',
|
||||
bgColor: 'info',
|
||||
fontColor: '#3b3b3b',
|
||||
hold: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const show = ref(false)
|
||||
const nowValue = ref("")
|
||||
const model = ref<'省份' | 'abc'>('省份')
|
||||
const abcList = ref([
|
||||
['京', '沪', '津', '渝', '鲁', '冀', '晋','蒙','辽','吉' ],
|
||||
['黑', '苏', '浙', '皖', '闽','赣','豫', '湘', '鄂','粤'],
|
||||
['桂', '琼', '川', '贵','云', '藏', '陕', '甘', '青', '宁'],
|
||||
['新', '港', '澳', '台', '警', '使', '学', '教'],
|
||||
['abc', 'del', '确认'],
|
||||
])
|
||||
const numList = ref([
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K'],
|
||||
['L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V'],
|
||||
['省份','W', 'X', 'Y', 'Z', 'del', '确认'],
|
||||
])
|
||||
const isShift = ref(false)
|
||||
const tid = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const _hold = computed((): boolean => props.hold)
|
||||
const _color = computed((): string => {
|
||||
if (props.color == '') return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _btnColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _btnBorderColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
|
||||
return "#f5f5f5"
|
||||
})
|
||||
const _bgColor = computed((): string => {
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return "#ffffff"
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
const _title = computed((): string => {
|
||||
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
|
||||
return props.title
|
||||
})
|
||||
const _carvalue = computed((): string[] => {
|
||||
if(nowValue.value.length<=2){
|
||||
return [nowValue.value.substring(0,2),'']
|
||||
}
|
||||
return [nowValue.value.substring(0,2),nowValue.value.substring(2)]
|
||||
})
|
||||
|
||||
// 方法
|
||||
function getFontSize(k: string): string {
|
||||
return checkIsCssUnit(k, xConfig.unit)
|
||||
}
|
||||
|
||||
function openShow(): void {
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onCancel(): void {
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
*/
|
||||
emits('cancel', nowValue.value);
|
||||
}
|
||||
|
||||
function onClose(): void {
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
|
||||
function ok(): void {
|
||||
/**
|
||||
* 点击确认时触发
|
||||
*/
|
||||
emits('confirm', nowValue.value);
|
||||
|
||||
if(!_hold.value){
|
||||
show.value = false;
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(): void {
|
||||
if (nowValue.value.split('').length == 0) return;
|
||||
let stp = nowValue.value.split('');
|
||||
stp = stp.slice(0, stp.length - 1)
|
||||
nowValue.value = stp.join("")
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
if(nowValue.value.length==0){
|
||||
model.value = '省份'
|
||||
}
|
||||
}
|
||||
|
||||
function getFontColor(value: string): string {
|
||||
if (value == "确认") return 'white'
|
||||
if (value == 'del') return _color.value
|
||||
if (value == 'shift' && isShift.value) return _color.value
|
||||
return _fontColor.value
|
||||
}
|
||||
|
||||
function itemClick(value: string): void {
|
||||
let value_convaer = value
|
||||
if (value == 'abc') {
|
||||
model.value = 'abc'
|
||||
return;
|
||||
} else if (value == '省份') {
|
||||
model.value = '省份'
|
||||
return;
|
||||
} else if (value == 'shift') {
|
||||
isShift.value = !isShift.value
|
||||
return;
|
||||
} else if (value == 'del') {
|
||||
del()
|
||||
return;
|
||||
} else if (value == '确认') {
|
||||
ok()
|
||||
return;
|
||||
} else if (value == '空格') {
|
||||
value_convaer = " "
|
||||
}
|
||||
if (isShift.value) {
|
||||
value_convaer = value_convaer.toLocaleUpperCase()
|
||||
}
|
||||
|
||||
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
|
||||
if (isMaxvalu) {
|
||||
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '个字符', icon: 'error' })
|
||||
return;
|
||||
}
|
||||
|
||||
nowValue.value = nowValue.value + value_convaer;
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
if(model.value=='省份'){
|
||||
model.value = 'abc'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch((): string => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == nowValue.value) return;
|
||||
nowValue.value = newvalue
|
||||
})
|
||||
|
||||
watch((): boolean => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue
|
||||
if (props.modelShow) {
|
||||
tid.value = setTimeout(function () {
|
||||
show.value = props.modelShow
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@prop {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot :show="show"></slot>
|
||||
</view>
|
||||
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
|
||||
@cancel="onCancel" v-model:show="show" :show-close="true">
|
||||
<template v-slot:title>
|
||||
|
||||
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<text v-if="nowValue.split('').length==0" :style="{fontSize:getFontSize('12'),color:_fontColor}">{{_title}}</text>
|
||||
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor}">{{_carvalue[0]}}</text>
|
||||
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor,margin: '0 8px'}">·</text>
|
||||
<text v-if="nowValue.split('').length>0" :style="{fontSize:getFontSize('16'),color:_fontColor}">{{_carvalue[1]}}</text>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<view v-if="model=='省份'" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in abcList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)"
|
||||
v-for="(item2,index2) in item"
|
||||
:key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover"
|
||||
:style="{
|
||||
backgroundColor:item2=='确认'?_color:_btnColor,
|
||||
flex:item2=='空格'?'2':'1',
|
||||
border:`1px solid ${_btnBorderColor}`
|
||||
}">
|
||||
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
|
||||
class="xKeyboardText">
|
||||
|
||||
{{item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):(isShift?item2.toLocaleUpperCase():item2)}}
|
||||
</text>
|
||||
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
|
||||
font-size="19"></x-icon>
|
||||
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
|
||||
font-size="19" color="white"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="model=='abc'" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in numList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{
|
||||
backgroundColor:item2=='确认'?_color:_btnColor,
|
||||
flex:item2=='确认'||item2=='abc'?'2':'1',
|
||||
border:`1px solid ${_btnBorderColor}`
|
||||
}">
|
||||
|
||||
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
|
||||
class="xKeyboardText">
|
||||
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):item2)}}
|
||||
</text>
|
||||
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
|
||||
font-size="19"></x-icon>
|
||||
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
|
||||
font-size="19" color="white"></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:12px"></view>
|
||||
</template>
|
||||
|
||||
</x-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xKeyboardText {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.xKeyboardHover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.xKeyboardLeftLine {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardNumber {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xKeyboardItem {
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemNoright {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.xKeyboardRight {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
export function validateIdCard(idCard: string):boolean {
|
||||
let vcity = new Map<number,string>([
|
||||
[11, "北京"], [12, "天津"], [13, "河北"], [14, "山西"], [15, "内蒙古"],
|
||||
[21, "辽宁"], [22, "吉林"], [23, "黑龙江"], [31, "上海"], [32, "江苏"],
|
||||
[33, "浙江"], [34, "安徽"], [35, "福建"], [36, "江西"], [37, "山东"], [41, "河南"],
|
||||
[42, "湖北"], [43, "湖南"], [44, "广东"], [45, "广西"], [46, "海南"], [50, "重庆"],
|
||||
[51, "四川"], [52, "贵州"], [53, "云南"], [54, "西藏"], [61, "陕西"], [62, "甘肃"],
|
||||
[63, "青海"], [64, "宁夏"], [65, "新疆"], [71, "台湾"], [81, "香港"], [82, "澳门"], [91, "国外"]
|
||||
]);;
|
||||
//是否为空
|
||||
if (idCard === '') {
|
||||
return false;
|
||||
}
|
||||
//校验长度,类型
|
||||
if (isCardNo(idCard) === false) {
|
||||
return false;
|
||||
}
|
||||
//检查省份
|
||||
if (checkProvince(idCard, vcity) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//校验生日
|
||||
if (checkBirthday(idCard) === false) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//检验位的检测
|
||||
if (checkParity(idCard) === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function isCardNo(card:string):boolean {
|
||||
//身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X
|
||||
let reg = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/;
|
||||
if (reg.test(card) === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function checkProvince(card:string, vcity:Map<number,string>):boolean {
|
||||
let province = card.substring(0,2)
|
||||
|
||||
if (!vcity.has(parseInt(province))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
function checkBirthday(card:string):boolean {
|
||||
let len = card.length;
|
||||
//身份证15位时,次序为省(3位)市(3位)年(2位)月(2位)日(2位)校验位(3位),皆为数字
|
||||
if (len == 15) {
|
||||
let re_fifteen = /^(\d{6})(\d{2})(\d{2})(\d{2})(\d{3})$/;
|
||||
let arr_data = card.match(re_fifteen);
|
||||
if(arr_data==null) return false
|
||||
let year = arr_data[2];
|
||||
let month = arr_data[3];
|
||||
let day = arr_data[4];
|
||||
if(year==null||month==null||day==null) return false
|
||||
let birthday = new Date('19' + year + '/' + month + '/' + day);
|
||||
return verifyBirthday(parseInt('19' + year), parseInt(month), parseInt(day), birthday);
|
||||
}
|
||||
//身份证18位时,次序为省(3位)市(3位)年(4位)月(2位)日(2位)校验位(4位),校验位末尾可能为X
|
||||
if (len == 18) {
|
||||
let re_eighteen = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X|x)$/;
|
||||
let arr_data = card.match(re_eighteen);
|
||||
if(arr_data==null) return false
|
||||
let year = arr_data[2];
|
||||
let month = arr_data[3];
|
||||
let day = arr_data[4];
|
||||
if(year==null||month==null||day==null) return false
|
||||
let birthday = new Date(year + '/' + month + '/' + day);
|
||||
return verifyBirthday(parseInt(year), parseInt(month), parseInt(day), birthday);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
function verifyBirthday(year:number, month:number, day:number, birthday:Date):boolean {
|
||||
let now = new Date();
|
||||
let now_year = now.getFullYear();
|
||||
//年月日是否合理
|
||||
if (birthday.getFullYear() == year && (birthday.getMonth() + 1) == month && birthday.getDate() == day) {
|
||||
//判断年份的范围(0岁到100岁之间)
|
||||
let time = now_year - year;
|
||||
if (time >= 0 && time <= 100) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function checkParity(card:string):boolean {
|
||||
//15位转18位
|
||||
card = changeFivteenToEighteen(card);
|
||||
let len = card.length;
|
||||
if (len == 18) {
|
||||
let arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
|
||||
let arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
|
||||
let cardTemp = 0
|
||||
let i =0;
|
||||
let valnum = '';
|
||||
|
||||
for (i = 0; i < 17; i++) {
|
||||
cardTemp += parseInt(card.charAt(i)) * arrInt[i];
|
||||
}
|
||||
|
||||
valnum = arrCh[cardTemp % 11];
|
||||
if (valnum == card.substring(17).toLocaleUpperCase()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function changeFivteenToEighteen(card:string):string {
|
||||
if (card.length == 15) {
|
||||
let arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
|
||||
let arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
|
||||
let cardTemp = 0
|
||||
let i =0;
|
||||
card = card.substring(0, 6) + '19' + card.substring(6, card.length - 6);
|
||||
for (i = 0; i < 17; i++) {
|
||||
cardTemp += parseInt(card.substring(i, 1)) * arrInt[i];
|
||||
}
|
||||
card += arrCh[cardTemp % 11];
|
||||
return card;
|
||||
}
|
||||
return card;
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { validateIdCard } from "./idcard.uts"
|
||||
|
||||
/**
|
||||
* @name 身份证键盘 xKeyboardIdcard
|
||||
* @description 自动解析第二代身份证号,验证用户输入的是否正确,事件中能得到当前输入值及校验值。
|
||||
* @page /pages/index/keyboard
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xKeyboardIdcard"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
|
||||
defineSlots<{
|
||||
default(props:{show:boolean}): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 确认时触发
|
||||
* @param {string} value - 当前值
|
||||
* @param {boolean} pass - 是否验证通过
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'cancel',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
type xKeyboardIdcardPropsType = {
|
||||
/**
|
||||
* 当前输入的值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
maxLen: number,
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: boolean,
|
||||
/**
|
||||
* 顶部标题,默认:安全键盘请放心输入
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 主按钮色,空值取全局主题
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 按钮背景,暗黑时会取二级inputDarkbg灰
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 键盘背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 文字颜色,暗黑是会取白。
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 点击确认是否保持键盘不收起
|
||||
*/
|
||||
hold: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xKeyboardIdcardPropsType>(), {
|
||||
modelValue: "",
|
||||
maxLen: 18,
|
||||
modelShow: false,
|
||||
title: "",
|
||||
color: "",
|
||||
btnColor: 'white',
|
||||
bgColor: 'info',
|
||||
fontColor: '#3b3b3b',
|
||||
hold: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const show = ref(false)
|
||||
const nowValue = ref("")
|
||||
const numbList = ref([
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
["00", "0", "x"]
|
||||
])
|
||||
const tid = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const _hold = computed((): boolean => props.hold)
|
||||
const _color = computed((): string => {
|
||||
if (props.color == '') return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _btnColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _bgColor = computed((): string => {
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return "#ffffff"
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
const _title = computed((): string => {
|
||||
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
|
||||
return props.title
|
||||
})
|
||||
const _isPass = computed((): boolean => {
|
||||
return validateIdCard(nowValue.value)
|
||||
})
|
||||
|
||||
// 方法
|
||||
function getFontSize(k: string): string {
|
||||
return checkIsCssUnit(k, xConfig.unit)
|
||||
}
|
||||
|
||||
function openShow(): void {
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onCancel(): void {
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
*/
|
||||
emits('cancel', nowValue.value);
|
||||
}
|
||||
|
||||
function onClose(): void {
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
|
||||
function ok(): void {
|
||||
/**
|
||||
* 点击确认时触发
|
||||
*/
|
||||
emits('confirm', nowValue.value, validateIdCard(nowValue.value));
|
||||
if(!_hold.value){
|
||||
show.value = false;
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(): void {
|
||||
if (nowValue.value.split('').length == 0) return;
|
||||
let stp = nowValue.value.split('');
|
||||
stp = stp.slice(0, stp.length - 1)
|
||||
nowValue.value = stp.join("")
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
function itemClick(value: string): void {
|
||||
let isDem = nowValue.value.lastIndexOf('.') > -1;
|
||||
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
|
||||
if (isMaxvalu) {
|
||||
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '位数', icon: 'error' })
|
||||
return;
|
||||
}
|
||||
if (isDem && value == 'x') return;
|
||||
|
||||
if ((nowValue.value.split('').length == 0 && value == '00') || (nowValue.value.split('').length == 0 && value == 'x')) return
|
||||
|
||||
|
||||
nowValue.value = nowValue.value + value;
|
||||
|
||||
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch((): string => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == nowValue.value) return;
|
||||
nowValue.value = newvalue
|
||||
})
|
||||
|
||||
watch((): boolean => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue
|
||||
if (props.modelShow) {
|
||||
tid.value = setTimeout(function () {
|
||||
show.value = props.modelShow
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@prop {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot :show="show"></slot>
|
||||
</view>
|
||||
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
|
||||
@cancel="onCancel" v-model:show="show" :show-close="true">
|
||||
<template v-slot:title >
|
||||
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<x-icon style="margin-right: 5px;" v-if="_isPass" name="checkbox-circle-fill" color="success"></x-icon>
|
||||
<x-icon style="margin-right: 5px;" v-if="!_isPass&&nowValue.length>0" color="warn" name="error-warning-fill"></x-icon>
|
||||
<text :style="{fontWeight:'bold',fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_isPass?'rgb(9, 204, 84)':_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-slot:default>
|
||||
|
||||
<view class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in numbList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
|
||||
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xKeyboardRight">
|
||||
<view :style="{backgroundColor:_btnColor,height:'50px'}" @click="del" class="xKeyboardItemDel xKeyboardItemNoright"
|
||||
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
|
||||
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
|
||||
</view>
|
||||
<view @click="ok" :style="{backgroundColor:_color}" class="xKeyboardItem xKeyboardItemNoright"
|
||||
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
|
||||
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
|
||||
<text style="color: white;font-size: 16px;">
|
||||
<!-- 确认 -->
|
||||
{{i18n.t('tmui4x.keyboard.confirm')}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view style="height:12px"></view>
|
||||
</template>
|
||||
|
||||
</x-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.xKeyboardText {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.xKeyboardHover {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.xKeyboardLeftLine {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardNumber {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xKeyboardItem {
|
||||
flex: 1;
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemDel {
|
||||
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemNoright {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.xKeyboardRight {
|
||||
width: 140rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,412 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
|
||||
/**
|
||||
* @name 数字键盘 xKeyboardNumber
|
||||
* @description 数字键盘,如果你要密码键盘见x-keyboard
|
||||
* @page /pages/index/keyboard
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xKeyboardNumber"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
|
||||
defineSlots<{
|
||||
default(props:{show:boolean}): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 确认时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'cancel',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
type xKeyboardNumberPropsType = {
|
||||
/**
|
||||
* 当前输入的值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
maxLen: number,
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: boolean,
|
||||
/**
|
||||
* 顶部标题,默认:安全键盘请放心输入
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 主按钮色,空值取全局主题
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 按钮背景,暗黑时会取二级inputDarkbg灰
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 键盘背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 文字颜色,暗黑是会取白。
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 输入的最大值
|
||||
* 默认0表示不限制。
|
||||
*/
|
||||
max: number,
|
||||
/**
|
||||
* 是否显示小数及00填充
|
||||
*/
|
||||
digit: boolean,
|
||||
/**
|
||||
* 模式
|
||||
* number 数字键盘
|
||||
* password 密码键盘,作为密码用,那就不会限制数字模式就当字符串使用。
|
||||
*/
|
||||
mode: string,
|
||||
/**
|
||||
* 点击确认是否保持键盘不收起
|
||||
*/
|
||||
hold: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xKeyboardNumberPropsType>(), {
|
||||
modelValue: "",
|
||||
maxLen: 9,
|
||||
modelShow: false,
|
||||
title: "",
|
||||
color: "",
|
||||
btnColor: 'transparent',
|
||||
bgColor: 'info',
|
||||
fontColor: '#3b3b3b',
|
||||
max: 0,
|
||||
digit: true,
|
||||
mode: "number",
|
||||
hold: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const show = ref(false)
|
||||
const nowValue = ref("")
|
||||
const numbList = ref([
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
["00", "0", "."]
|
||||
])
|
||||
const numbList2 = ref([
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9']
|
||||
])
|
||||
const tid = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const _hold = computed((): boolean => props.hold)
|
||||
const _color = computed((): string => {
|
||||
if (props.color == '') return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _btnColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _bgColor = computed((): string => {
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return "#ffffff"
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
const _title = computed((): string => {
|
||||
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
|
||||
return props.title
|
||||
})
|
||||
const _max = computed((): number => {
|
||||
return props.max;
|
||||
})
|
||||
|
||||
// 方法
|
||||
function getFontSize(k: string): string {
|
||||
return checkIsCssUnit(k, xConfig.unit)
|
||||
}
|
||||
|
||||
function openShow(): void {
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onCancel(): void {
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
*/
|
||||
emits('cancel', nowValue.value);
|
||||
}
|
||||
|
||||
function onClose(): void {
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
|
||||
function ok(): void {
|
||||
/**
|
||||
* 点击确认时触发
|
||||
*/
|
||||
emits('confirm', nowValue.value);
|
||||
if(!_hold.value){
|
||||
show.value = false;
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(): void {
|
||||
if (nowValue.value.split('').length == 0) return;
|
||||
let stp = nowValue.value.split('');
|
||||
stp = stp.slice(0, stp.length - 1)
|
||||
nowValue.value = stp.join("")
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
function itemClick(value: string): void {
|
||||
let isDem = nowValue.value.lastIndexOf('.') > -1;
|
||||
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
|
||||
if (isMaxvalu) {
|
||||
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '位数', icon: 'error' })
|
||||
return;
|
||||
}
|
||||
if(props.mode=='number'){
|
||||
if (isDem && value == '.') return;
|
||||
|
||||
if ((nowValue.value.split('').length == 0 && value == '00') || (nowValue.value.split('').length == 0 && value == '.')) return
|
||||
if (nowValue.value.substring(0, 1) == '0' && nowValue.value.split('').length == 1 && value != '.') return;
|
||||
let totalvalue = parseFloat(nowValue.value + value);
|
||||
if(totalvalue>_max.value&&_max.value!=0){
|
||||
uni.showToast({ title: '应该小于' + _max.value.toString(), icon: 'error' })
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nowValue.value = nowValue.value + value;
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 监听器
|
||||
watch((): string => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == nowValue.value) return;
|
||||
nowValue.value = newvalue
|
||||
})
|
||||
|
||||
watch((): boolean => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue
|
||||
if (props.modelShow) {
|
||||
tid.value = setTimeout(function () {
|
||||
show.value = props.modelShow
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@prop {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot :show="show"></slot>
|
||||
</view>
|
||||
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
|
||||
@cancel="onCancel" v-model:show="show" :show-close="true">
|
||||
<template v-slot:title>
|
||||
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<text
|
||||
:style="{fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<view v-if="digit" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in numbList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
|
||||
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xKeyboardRight">
|
||||
<view :style="{backgroundColor:_btnColor,height:'50px'}" @click="del" class="xKeyboardItemDel xKeyboardItemNoright"
|
||||
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
|
||||
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
|
||||
</view>
|
||||
<view @click="ok" :style="{backgroundColor:_color}" class="xKeyboardItem xKeyboardItemNoright"
|
||||
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
|
||||
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
|
||||
<text style="color: white;font-size: 16px;">
|
||||
<!-- 确认 -->
|
||||
{{i18n.t('tmui4x.keyboard.confirm')}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view v-if="!digit" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in numbList2" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
|
||||
<text :style="{color:_fontColor}" class="xKeyboardText">{{item2}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="xKeyboardLeftLine">
|
||||
<view @click="itemClick('0')"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
|
||||
<text :style="{color:_fontColor}" class="xKeyboardText">0</text>
|
||||
</view>
|
||||
<view @click="del"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{backgroundColor:_btnColor}">
|
||||
<x-icon :color="_fontColor" name="delete-back-2-line" font-size="24"></x-icon>
|
||||
</view>
|
||||
|
||||
<view @click="ok" :style="{backgroundColor:_color,'margin-right': '5px'}" class="xKeyboardItem xKeyboardItemNoright"
|
||||
hover-class="xKeyboardHover" :hover-start-time="10" :hover-stay-time="250">
|
||||
<!-- <x-icon name="check-line" font-size="38" color="white"></x-icon> -->
|
||||
<text style="color: white;font-size: 16px;">
|
||||
<!-- 确认 -->
|
||||
{{i18n.t('tmui4x.keyboard.confirm')}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view style="height:12px"></view>
|
||||
</template>
|
||||
|
||||
</x-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xKeyboardText {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.xKeyboardHover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.xKeyboardLeftLine {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardNumber {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xKeyboardItem {
|
||||
flex: 1;
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemDel {
|
||||
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemNoright {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.xKeyboardRight {
|
||||
width: 140rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,406 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
import { checkIsCssUnit, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
|
||||
/**
|
||||
* @name 密码键盘 xKeyboard
|
||||
* @description 密码键盘,如果你只是要单纯的数字键盘见x-keyboard-number
|
||||
* @page /pages/index/keyboard
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xKeyboard"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
|
||||
defineSlots<{
|
||||
default(props:{show:boolean}): any
|
||||
}>()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 确认时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
* @param {string} value - 当前值
|
||||
*/
|
||||
'cancel',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
type xKeyboardPropsType = {
|
||||
/**
|
||||
* 当前输入的值
|
||||
*/
|
||||
modelValue: string,
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
maxLen: number,
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: boolean,
|
||||
/**
|
||||
* 顶部标题,默认:安全键盘请放心输入
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* 主按钮色,空值取全局主题
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 按钮背景
|
||||
*/
|
||||
btnColor: string,
|
||||
/**
|
||||
* 键盘背景
|
||||
*/
|
||||
bgColor: string,
|
||||
/**
|
||||
* 文字颜色
|
||||
*/
|
||||
fontColor: string,
|
||||
/**
|
||||
* 点击确认是否保持键盘不收起
|
||||
*/
|
||||
hold: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xKeyboardPropsType>(), {
|
||||
modelValue: "",
|
||||
maxLen: 9,
|
||||
modelShow: false,
|
||||
title: "",
|
||||
color: "",
|
||||
btnColor: 'white',
|
||||
bgColor: 'info',
|
||||
fontColor: '#3b3b3b',
|
||||
hold: false
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const show = ref(false)
|
||||
const nowValue = ref("")
|
||||
const model = ref<'number' | 'abc'>('abc')
|
||||
const abcList = ref([
|
||||
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
|
||||
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
|
||||
['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'del'],
|
||||
['123', '空格', '确认'],
|
||||
])
|
||||
const numList = ref([
|
||||
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],
|
||||
['#', '/', ':', ';', '(', ')', '^', '*', '+'],
|
||||
['-', '=', '\\', '|', '~', '$', '&', '.', ',', 'del'],
|
||||
['abc', '%', '?', '!', '{', '}', '确认'],
|
||||
])
|
||||
const isShift = ref(false)
|
||||
const tid = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const _hold = computed((): boolean => props.hold)
|
||||
const _color = computed((): string => {
|
||||
if (props.color == '') return getDefaultColor(xConfig.color)
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _btnColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
|
||||
return getDefaultColor(props.btnColor)
|
||||
})
|
||||
const _btnBorderColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return xConfig.borderDarkColor
|
||||
return "#f5f5f5"
|
||||
})
|
||||
const _bgColor = computed((): string => {
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
const _fontColor = computed((): string => {
|
||||
if (xConfig.dark == 'dark') return "#ffffff"
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
const _title = computed((): string => {
|
||||
if(props.title=='') return i18n.t('tmui4x.keyboard.placeholder')
|
||||
return props.title
|
||||
})
|
||||
|
||||
// 方法
|
||||
function getFontSize(k: string): string {
|
||||
return checkIsCssUnit(k, xConfig.unit)
|
||||
}
|
||||
|
||||
function openShow(): void {
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emits('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onCancel(): void {
|
||||
/**
|
||||
* 关闭取消时触发
|
||||
*/
|
||||
emits('cancel', nowValue.value);
|
||||
}
|
||||
|
||||
function onClose(): void {
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
function ok(): void {
|
||||
/**
|
||||
* 点击确认时触发
|
||||
*/
|
||||
emits('confirm', nowValue.value);
|
||||
if(!_hold.value){
|
||||
show.value = false;
|
||||
emits('update:modelShow', false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(): void {
|
||||
if (nowValue.value.split('').length == 0) return;
|
||||
let stp = nowValue.value.split('');
|
||||
stp = stp.slice(0, stp.length - 1)
|
||||
nowValue.value = stp.join("")
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
function itemClick(value: string): void {
|
||||
let value_convaer = value
|
||||
if (value == 'abc') {
|
||||
model.value = 'abc'
|
||||
return;
|
||||
} else if (value == '123') {
|
||||
model.value = 'number'
|
||||
return;
|
||||
} else if (value == 'shift') {
|
||||
isShift.value = !isShift.value
|
||||
return;
|
||||
} else if (value == 'del') {
|
||||
del()
|
||||
return;
|
||||
} else if (value == '确认') {
|
||||
ok()
|
||||
return;
|
||||
} else if (value == '空格') {
|
||||
value_convaer = " "
|
||||
}
|
||||
if (isShift.value) {
|
||||
value_convaer = value_convaer.toLocaleUpperCase()
|
||||
}
|
||||
|
||||
let isMaxvalu = nowValue.value.split('').length >= props.maxLen;
|
||||
if (isMaxvalu) {
|
||||
uni.showToast({ title: '最多输入' + props.maxLen.toString() + '个字符', icon: 'error' })
|
||||
return;
|
||||
}
|
||||
|
||||
nowValue.value = nowValue.value + value_convaer;
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 值变化时触发
|
||||
* @paramt {string} value
|
||||
*/
|
||||
emits('change', nowValue.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function getFontColor(value: string): string {
|
||||
if (value == "确认") return 'white'
|
||||
if (value == 'del') return _color.value
|
||||
if (value == 'shift' && isShift.value) return _color.value
|
||||
return _fontColor.value
|
||||
}
|
||||
|
||||
// 监听器
|
||||
watch((): string => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == nowValue.value) return;
|
||||
nowValue.value = newvalue
|
||||
})
|
||||
|
||||
watch((): boolean => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue
|
||||
if (props.modelShow) {
|
||||
tid.value = setTimeout(function () {
|
||||
show.value = props.modelShow
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@prop {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot :show="show"></slot>
|
||||
</view>
|
||||
<x-drawer @close="onClose" :widthCoverCenter="true" :disabled-scroll="true" :bgColor="_bgColor" size="auto" overflayBgColor="rgba(0,0,0,0)" :title="title"
|
||||
@cancel="onCancel" v-model:show="show" :show-close="true">
|
||||
<template v-slot:title>
|
||||
<view style="height: 44px;display: flex;justify-content: center;align-items: center;flex-direction: row;">
|
||||
<text :style="{fontSize:nowValue.split('').length>0?getFontSize('16'):getFontSize('12'),color:_fontColor}">{{nowValue.split('').length>0?nowValue:_title}}</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<view v-if="model=='abc'" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in abcList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)"
|
||||
v-for="(item2,index2) in item"
|
||||
:key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover"
|
||||
:style="{
|
||||
backgroundColor:item2=='确认'?_color:_btnColor,
|
||||
flex:item2=='空格'?'2':'1',
|
||||
border:`1px solid ${_btnBorderColor}`
|
||||
}">
|
||||
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
|
||||
class="xKeyboardText">
|
||||
|
||||
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):'')}}
|
||||
{{(item2!='确认'&&item2!='空格'?(isShift?item2.toLocaleUpperCase():item2):'')}}
|
||||
{{(item2=='空格'?i18n.t('tmui4x.keyboard.space'):'')}}
|
||||
|
||||
</text>
|
||||
|
||||
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
|
||||
font-size="19"></x-icon>
|
||||
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
|
||||
font-size="19" ></x-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="model=='number'" class="xKeyboardNumber">
|
||||
<view class="xKeyboardLeft">
|
||||
<view v-for="(item,index) in numList" :key="index" class="xKeyboardLeftLine">
|
||||
<view @click="itemClick(item2)" v-for="(item2,index2) in item" :key="index2"
|
||||
class="xKeyboardItem" :hover-start-time="20" :hover-stay-time="250"
|
||||
hover-class="xKeyboardHover" :style="{
|
||||
backgroundColor:item2=='确认'?_color:_btnColor,
|
||||
flex:item2=='确认'||item2=='abc'?'2':'1',
|
||||
border:`1px solid ${_btnBorderColor}`
|
||||
}">
|
||||
|
||||
<text v-if="item2!='shift'&&item2!=='del'" :style="{color:getFontColor(item2)}"
|
||||
class="xKeyboardText">
|
||||
<!-- #ifdef WEB -->
|
||||
{{(item2=='\\'?'\\\\':'')}}
|
||||
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):'')}}
|
||||
{{(item2!='确认'&&item2!='\\'?item2:'')}}
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef APP || MP-WEIXIN -->
|
||||
{{(item2=='确认'?i18n.t('tmui4x.keyboard.confirm'):item2)}}
|
||||
<!-- #endif -->
|
||||
</text>
|
||||
<x-icon v-if="item2=='del'" :color="_color" name="delete-back-2-line"
|
||||
font-size="19"></x-icon>
|
||||
<x-icon v-if="item2=='shift'" :color="isShift?_color:_fontColor" name="upload-fill"
|
||||
font-size="19" ></x-icon>
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:12px"></view>
|
||||
</template>
|
||||
|
||||
</x-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xKeyboardText {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.xKeyboardHover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.xKeyboardLeftLine {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardNumber {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.xKeyboardLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xKeyboardItem {
|
||||
height: 50px;
|
||||
/* background-color: white; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.xKeyboardItemNoright {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.xKeyboardRight {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts" setup>
|
||||
import { getDefaultColor,setBgColorLightByDark,isBlackAndWhite } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 占位排版 xLayout
|
||||
* @description 这是一个占位排版布局组件,需要配合x-layout-item组件实现占位排版
|
||||
* 具体表现为:当组件在可视区域外时或者你指定延迟渲染或者你指定不渲染时,会以一个空的view来占位,内容不渲染.以此来达到排版不塌陷,同时又提高渲染速度.
|
||||
* 场景:非常适配个人中心/首页,或者需要占位,渲染较多的情况下使用
|
||||
* @page /pages/index/row
|
||||
* @category 其它组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xLayout"})
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 默认是false自动判断要不要显示内容
|
||||
* 如果你强制设置为true那内部不再判断,直接显示内容
|
||||
* 并且不可动态更新,只有初始设置有效.
|
||||
*/
|
||||
show:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示时,是否需要过渡.
|
||||
*/
|
||||
fade:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
customStyle:{
|
||||
type:Object as PropType<UTSJSONObject>,
|
||||
default:()=>{
|
||||
return {} as UTSJSONObject
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 占位时的背景
|
||||
*/
|
||||
color:{
|
||||
type:String,
|
||||
default:"transparent"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的占位背景.
|
||||
*/
|
||||
darkColor:{
|
||||
type:String,
|
||||
default:"transparent"
|
||||
}
|
||||
})
|
||||
const xLayoutRef = ref<UniElement|null>(null)
|
||||
const show = ref<boolean>(props.show)
|
||||
const _customStyle = computed(():any=>props.customStyle)
|
||||
const _isshowInit = ref(false)
|
||||
let domTop = 0 as number|null
|
||||
let winHeight = 0
|
||||
let scrollTop = 0
|
||||
const _bgColor = computed(():string=>{
|
||||
let color = getDefaultColor(props.color)
|
||||
if (xConfig.dark == 'dark' && props.darkColor != '') {
|
||||
color = getDefaultColor(props.darkColor)
|
||||
}
|
||||
if (xConfig.dark == 'dark' && props.darkColor == '') {
|
||||
if (isBlackAndWhite(color)) {
|
||||
color = xConfig.sheetDarkColor;
|
||||
} else {
|
||||
color = setBgColorLightByDark(color)
|
||||
}
|
||||
}
|
||||
return color;
|
||||
})
|
||||
|
||||
|
||||
const setDomShow = ()=>{
|
||||
if(xLayoutRef.value==null||winHeight==0||domTop==null||props.show||show.value) return;
|
||||
let dtop = domTop!;
|
||||
if(dtop>0&&dtop<winHeight){
|
||||
show.value = true
|
||||
}
|
||||
}
|
||||
const onInitDom = ()=>{
|
||||
if(xLayoutRef.value!=null){
|
||||
let ele = xLayoutRef.value!;
|
||||
ele.getBoundingClientRectAsync()
|
||||
?.then((rect:DOMRect)=>{
|
||||
domTop= rect.top;
|
||||
_isshowInit.value = true;
|
||||
setDomShow();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// onResize(()=>{
|
||||
// winHeight = uni.getWindowInfo().windowHeight
|
||||
// onInitDom();
|
||||
// })
|
||||
// onPageScroll((evt)=>{
|
||||
// if(!_isshowInit.value) return;
|
||||
// scrollTop = evt.scrollTop;
|
||||
// onInitDom()
|
||||
// })
|
||||
|
||||
uni.$once("onReady",()=>{
|
||||
winHeight = uni.getWindowInfo().windowHeight
|
||||
// onInitDom();
|
||||
// show.value = true;
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view ref="xLayoutRef" v-if="show"
|
||||
:style="[{backgroundColor:_bgColor}]"
|
||||
>
|
||||
<view :style="_customStyle">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<view @click="onclick" class="xLink"
|
||||
:style="{color:_color,fontSize:_fontSize,'text-decoration-line':props.line?'underline':'none'}">
|
||||
<text v-if="_prefix!=''" :style="{
|
||||
'font-family': 'remixicon',
|
||||
'font-size':_fontSize,
|
||||
'color':_color,
|
||||
paddingRight:'5px'
|
||||
}">{{_prefix}}</text>
|
||||
<text
|
||||
:style="{color:_color,fontSize:_fontSize}"
|
||||
>
|
||||
<!--
|
||||
@slot 默认插槽,仅可放置文本
|
||||
-->
|
||||
<slot></slot>
|
||||
</text>
|
||||
<text v-if="_suffix!=''" :style="{
|
||||
'font-family': 'remixicon',
|
||||
'font-size':_fontSize,
|
||||
'color':_color,
|
||||
paddingLeft:'5px'
|
||||
}">{{_suffix}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
|
||||
import { PropType } from "vue"
|
||||
import { xConfig } from "../../config/xConfig.uts"
|
||||
import { NAVIGATE_TYPE } from "../../interface.uts"
|
||||
import remixicon from "../x-icon/remixicon.uts"
|
||||
import {openWeb} from "@/uni_modules/x-openweb"
|
||||
/**
|
||||
* @name 链接 xLink
|
||||
* @page /pages/index/link
|
||||
* @category 展示组件
|
||||
* @description 链接可以打开指定的页面,也可以打开外链(打开外链依赖于x-openweb插件,加密用户请联系发你源码自行源码编译)
|
||||
* 微信小程序无法打开外链,微信小程序正式版本pc版本可以打开外链,真机手机仅可打开应用内页面.
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xLink"})
|
||||
|
||||
const emits = defineEmits(
|
||||
[
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click'
|
||||
])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 需要打开的链接,可以是页面地址也可以是网页链接地址.
|
||||
*/
|
||||
href: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 空值时取全局主题
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 字号,rpx,px,单位均可
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "15"
|
||||
},
|
||||
/**
|
||||
* 是否需要下划线
|
||||
*/
|
||||
line: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 打开方式,如果是网页链接将启动新的窗口打开.
|
||||
*/
|
||||
openType: {
|
||||
type: String as PropType<NAVIGATE_TYPE>,
|
||||
default: "navigate"
|
||||
},
|
||||
/**
|
||||
* 前缀图标名称
|
||||
*/
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "links-line"
|
||||
},
|
||||
/**
|
||||
* 后缀图标名称
|
||||
*/
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
const _color = computed(() : string => {
|
||||
if (props.color == "") {
|
||||
return getDefaultColor(xConfig.color)
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const getIcon = (icon : string) : string => {
|
||||
let texts = ""
|
||||
try {
|
||||
let code = ''
|
||||
// #ifdef APP-ANDROID
|
||||
code = remixicon[icon] as string;
|
||||
let codePoint = Integer.parseInt(code, 16);
|
||||
let charArray = Character.toChars(codePoint);
|
||||
texts = new String(charArray);
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
|
||||
code = remixicon[icon] as string;
|
||||
texts = String.fromCharCode(parseInt(code, 16));
|
||||
|
||||
// #endif
|
||||
|
||||
} catch (e) {
|
||||
|
||||
console.error("xicon解析失败。", e)
|
||||
}
|
||||
|
||||
return texts
|
||||
}
|
||||
const _fontSize = computed(() : string => {
|
||||
let fontSize = checkIsCssUnit(props.fontSize, xConfig.unit);
|
||||
if (xConfig.fontScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber * xConfig.fontScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _prefix = computed(() : string => {
|
||||
if (props.prefix == '') return ''
|
||||
return getIcon(props.prefix)
|
||||
})
|
||||
const _suffix = computed(() : string => {
|
||||
if (props.suffix == '') return ''
|
||||
return getIcon(props.suffix)
|
||||
})
|
||||
|
||||
const onclick = () => {
|
||||
console.log('click')
|
||||
emits('click')
|
||||
if (props.href == '') return;
|
||||
let isHttpRef = props.href.indexOf(':')
|
||||
let href = props.href;
|
||||
if (isHttpRef > -1) {
|
||||
openWeb(href)
|
||||
return;
|
||||
}
|
||||
switch (props.openType) {
|
||||
case 'navigateBack':
|
||||
uni.navigateBack({})
|
||||
break;
|
||||
case 'navigate':
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
})
|
||||
break;
|
||||
case 'reLaunch':
|
||||
uni.reLaunch({
|
||||
url: href
|
||||
})
|
||||
break;
|
||||
case 'redirect':
|
||||
uni.redirectTo({
|
||||
url: href
|
||||
})
|
||||
break;
|
||||
case 'switchTab':
|
||||
uni.switchTab({
|
||||
url: href
|
||||
})
|
||||
break;
|
||||
default: {
|
||||
|
||||
uni.navigateTo({
|
||||
url: href
|
||||
} as NavigateToOptions)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.xLink {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* #ifdef WEB */
|
||||
cursor: pointer;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
/* #ifdef WEB */
|
||||
.xLink:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||||
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
|
||||
import { xConfig, xProvitae } from "../../config/xConfig.uts"
|
||||
|
||||
/**
|
||||
* @name 加载中 xLoading
|
||||
* @description 加载中占位符,场景用到页面加载前。请在标签内写上你的文本, 如果不写,默认显示"加载中..."
|
||||
* @page /pages/index/loading
|
||||
* @category 反馈组件
|
||||
* @constant 平台兼容
|
||||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||||
*/
|
||||
defineOptions({name:"xLoading"})
|
||||
|
||||
const i18n = xConfig.i18n
|
||||
|
||||
defineSlots<{
|
||||
default(): any
|
||||
}>()
|
||||
|
||||
type xLoadingPropsType = {
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
color: string,
|
||||
/**
|
||||
* 文字颜色
|
||||
*/
|
||||
textColor: string,
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
textSize: string,
|
||||
/**
|
||||
* 图标大小
|
||||
*/
|
||||
iconSize: string,
|
||||
/**
|
||||
* 是否垂直,默认是水平
|
||||
*/
|
||||
vertical: boolean,
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
icon: string,
|
||||
/**
|
||||
* 隐藏加载文本插槽
|
||||
*/
|
||||
hideText: boolean,
|
||||
/**
|
||||
* 加载中的文本,加载中...
|
||||
*/
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<xLoadingPropsType>(), {
|
||||
color: "#8b8b8b",
|
||||
textColor: "#8b8b8b",
|
||||
textSize: "12",
|
||||
iconSize: "21",
|
||||
vertical: true,
|
||||
icon: "loader-line",
|
||||
hideText: false,
|
||||
label: ""
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const _label = computed((): string => {
|
||||
if(props.label == '') return i18n.t("tmui4x.xloading.label");
|
||||
return props.label;
|
||||
})
|
||||
const _icon = computed((): string => {
|
||||
return props.icon
|
||||
})
|
||||
const _hideText = computed((): boolean => {
|
||||
return props.hideText
|
||||
})
|
||||
const _color = computed((): string => {
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _textColor = computed((): string => {
|
||||
return getDefaultColor(props.textColor)
|
||||
})
|
||||
const _textSize = computed((): string => {
|
||||
return checkIsCssUnit(props.textSize, xConfig.unit)
|
||||
})
|
||||
const _iconSize = computed((): string => {
|
||||
return checkIsCssUnit(props.iconSize, xConfig.unit)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<view class="xLoading" :style="{'flex-direction':vertical?'column':'row'}">
|
||||
<x-icon :font-size="_iconSize" :color="_color" :name="_icon" :spin="true"></x-icon>
|
||||
<text v-if="!_hideText" :style="{'font-size': _textSize,'color': _textColor,marginLeft:vertical?'0px':'5px',marginTop:vertical?'8px':'0px',lineHeight:'1.1'}">
|
||||
<!--
|
||||
@slot 请在插槽内写上你的文本, 如果不写,默认显示"加载中..."
|
||||
-->
|
||||
<slot>{{_label}}</slot>
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xLoading {
|
||||
display: flex;
|
||||
/* flex-direction: column; */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
Theme: Default
|
||||
Description: Original highlight.js style
|
||||
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||
Maintainer: @highlightjs/core-team
|
||||
Website: https://highlightjs.org/
|
||||
License: see project LICENSE
|
||||
Touched: 2021
|
||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
||||
import * as l from "./katex.js"
|
||||
const g = /([A-Z])/g, o = function(r) {
|
||||
return r.replace(g, "-$1").toLowerCase();
|
||||
}, p = {
|
||||
"&": "&",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
'"': """,
|
||||
"'": "'"
|
||||
}, d = /[&><"']/g;
|
||||
function h(r) {
|
||||
return String(r).replace(d, (t) => p[t]);
|
||||
}
|
||||
const y = (r) => "data:image/svg+xml," + encodeURIComponent(r.replace(/\s+/g, " ")), x = (r, t, e) => {
|
||||
let n = !1;
|
||||
t.classes && t.classes.length > 0 && (n = !0);
|
||||
const i = h(b(t.classes));
|
||||
let a = "";
|
||||
r === "text" && t.italic > 0 && (a += "margin-right:" + t.italic + "em;");
|
||||
for (const s in t.style)
|
||||
t.style.hasOwnProperty(s) && (a += `${o(s)}:${t.style[s]};`);
|
||||
a && (n = !0);
|
||||
for (const s in t.attributes)
|
||||
t.attributes.hasOwnProperty(s) && h(t.attributes[s]);
|
||||
if (r === "span")
|
||||
return {
|
||||
name: "span",
|
||||
attrs: {
|
||||
class: i + " katex-span",
|
||||
style: a
|
||||
},
|
||||
children: e
|
||||
};
|
||||
if (r === "img")
|
||||
return {
|
||||
name: "img",
|
||||
attrs: {
|
||||
class: i + " katex-img",
|
||||
style: a
|
||||
},
|
||||
children: e
|
||||
};
|
||||
if (r === "text") {
|
||||
const s = h(t.text);
|
||||
return n ? {
|
||||
name: "span",
|
||||
attrs: {
|
||||
class: i,
|
||||
style: a
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
text: s
|
||||
}
|
||||
]
|
||||
} : {
|
||||
type: "text",
|
||||
text: s
|
||||
};
|
||||
}
|
||||
if (r === "svg") {
|
||||
const s = t.toMarkup();
|
||||
return {
|
||||
name: "img",
|
||||
attrs: {
|
||||
src: y(s),
|
||||
class: "katex-svg"
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, b = function(r) {
|
||||
return (r == null ? void 0 : r.filter((t) => t).join(" ")) ?? "";
|
||||
}, u = (r, t) => r.map((e) => {
|
||||
var s;
|
||||
let n = t;
|
||||
(s = e == null ? void 0 : e.style) != null && s.color && (n = e.style.color);
|
||||
let i;
|
||||
e instanceof l.__domTree.Span && (i = "span"), e instanceof l.__domTree.Anchor && (i = "anchor"), e instanceof l.__domTree.LineNode && (i = "line"), e instanceof l.__domTree.PathNode && (i = "path"), e instanceof l.__domTree.SvgNode && (i = "svg", n && (e.attributes.fill = n)), e instanceof l.__domTree.SymbolNode && (i = "text");
|
||||
const a = e.children && e.children.length > 0 ? u(e.children, n) : [];
|
||||
return i ? x(i, e, a) : a;
|
||||
}).reduce((e, n) => (Array.isArray(n) ? e.push(...n) : e.push(n), e), []).filter((e) => !!e), _ = (r, t = {}) => {
|
||||
const { throwError: e, ...n } = t || {};
|
||||
try {
|
||||
const i = l.__renderToDomTree(r, {
|
||||
...n,
|
||||
output: "html"
|
||||
});
|
||||
return u([i]);
|
||||
} catch (i) {
|
||||
if (e) throw i;
|
||||
return [
|
||||
{
|
||||
name: "span",
|
||||
attrs: {
|
||||
style: "color:red;"
|
||||
},
|
||||
children: [{ type: "text", text: i.message }]
|
||||
}
|
||||
];
|
||||
}
|
||||
}, w = function(r, t, e) {
|
||||
let n = e, i = 0;
|
||||
const a = r.length;
|
||||
for (; n < t.length; ) {
|
||||
const s = t[n];
|
||||
if (i <= 0 && t.slice(n, n + a) === r)
|
||||
return n;
|
||||
s === "\\" ? n++ : s === "{" ? i++ : s === "}" && i--, n++;
|
||||
}
|
||||
return -1;
|
||||
}, k = function(r) {
|
||||
return r.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||
}, E = /^\\begin{/, T = function(r, t) {
|
||||
let e;
|
||||
const n = [], i = new RegExp(
|
||||
"(" + t.map((a) => k(a.left)).join("|") + ")"
|
||||
);
|
||||
for (; e = r.search(i), e !== -1; ) {
|
||||
e > 0 && (n.push({
|
||||
type: "text",
|
||||
data: r.slice(0, e)
|
||||
}), r = r.slice(e));
|
||||
const a = t.findIndex((c) => r.startsWith(c.left));
|
||||
if (e = w(t[a].right, r, t[a].left.length), e === -1)
|
||||
break;
|
||||
const s = r.slice(0, e + t[a].right.length), f = E.test(s) ? s : r.slice(t[a].left.length, e);
|
||||
n.push({
|
||||
type: "math",
|
||||
data: f,
|
||||
rawData: s,
|
||||
display: t[a].display
|
||||
}), r = r.slice(e + t[a].right.length);
|
||||
}
|
||||
return r !== "" && n.push({
|
||||
type: "text",
|
||||
data: r
|
||||
}), n;
|
||||
}, P = function(r, t) {
|
||||
var i;
|
||||
const e = T(
|
||||
r,
|
||||
(t == null ? void 0 : t.delimiters) ?? [
|
||||
{ left: "$$", right: "$$", display: !0 },
|
||||
{ left: "\\(", right: "\\)", display: !1 },
|
||||
{ left: "\\begin{equation}", right: "\\end{equation}", display: !0 },
|
||||
{ left: "\\begin{align}", right: "\\end{align}", display: !0 },
|
||||
{ left: "\\begin{alignat}", right: "\\end{alignat}", display: !0 },
|
||||
{ left: "\\begin{gather}", right: "\\end{gather}", display: !0 },
|
||||
{ left: "\\begin{CD}", right: "\\end{CD}", display: !0 },
|
||||
{ left: "\\[", right: "\\]", display: !0 }
|
||||
]
|
||||
);
|
||||
if (e.length === 1 && e[0].type === "text")
|
||||
return e[0].data;
|
||||
const n = [];
|
||||
for (let a = 0; a < e.length; a++)
|
||||
if (e[a].type === "text")
|
||||
n.push({
|
||||
type: "node",
|
||||
name: "span",
|
||||
attrs: {
|
||||
style: "white-space: pre-wrap;"
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
text: e[a].data
|
||||
}
|
||||
]
|
||||
});
|
||||
else {
|
||||
const s = {
|
||||
...t
|
||||
};
|
||||
let f = e[a].data;
|
||||
s.displayMode = e[a].display;
|
||||
try {
|
||||
s.preProcess && (f = s.preProcess(f));
|
||||
const c = _(f, s);
|
||||
n.push(...c);
|
||||
} catch (c) {
|
||||
if (!(c instanceof l.ParseError))
|
||||
throw c;
|
||||
(i = t.errorCallback) == null || i.call(
|
||||
t,
|
||||
"KaTeX auto-render: Failed to parse `" + e[a].data + "` with ",
|
||||
c
|
||||
), n.push(e[a].rawData);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
};
|
||||
export {
|
||||
b as createClass,
|
||||
_ as default,
|
||||
_ as parseLatex,
|
||||
P as renderMathInText
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user