111
This commit is contained in:
commit
7d450caf77
146 files changed
+39809
No files matched your search
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<view v-if="showAlert" @click="onclickbar" class="tmAlert" :style="_styleMap.bgStyle">
|
||||
<!--
|
||||
@slot 左边图标插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<view class="tmAlertLeft">
|
||||
<tm-icon :size="iconSize" :color="(_styleMap.textStyle['color'] as string)"
|
||||
:dark-color="(_styleMap.textStyle['color'] as string)" :name="_iconName"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="tmAlertContent">
|
||||
<text :style="_styleMap.textStyle">
|
||||
<slot></slot>
|
||||
</text>
|
||||
</view>
|
||||
<view @click="closeAlert" v-if="_showClose" class="tmAlertRight">
|
||||
<tm-icon :size="iconSize" :color="(_styleMap.textStyle['color'] as string)"
|
||||
:dark-color="(_styleMap.textStyle['color'] as string)" :name="closeIcon"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed, ref, onMounted } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber,arrayNumberValidByStyleBorderColor, linearValid ,getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
|
||||
/**
|
||||
* @displayName 警告
|
||||
* @exportName tm-alert
|
||||
* @category 展示组件
|
||||
* @description 样式丰富常用警告提醒
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmAlert' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type styleMapType = {
|
||||
textStyle : Record<string, string>,
|
||||
bgStyle : Record<string, string>
|
||||
}
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 关闭时触发
|
||||
*/
|
||||
"close",
|
||||
/**
|
||||
* 组件被点击时触发
|
||||
*/
|
||||
"click"
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 类型
|
||||
* warn:警告
|
||||
* success:成功
|
||||
* error:错误
|
||||
* info:信息
|
||||
* primary:正常主题
|
||||
*/
|
||||
status: {
|
||||
type: String,
|
||||
default: "primary"
|
||||
},
|
||||
/**
|
||||
* 警告图标,不填写取status默认图标
|
||||
* 填写以填写为准
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 警告图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
closeIcon:{
|
||||
type: String,
|
||||
default: "close-line"
|
||||
},
|
||||
/**
|
||||
* 显示还是隐藏关闭按钮
|
||||
*/
|
||||
showClose:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
/**
|
||||
* 主题色,如果不填写以status为准
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 文字颜色,如果不填写以status为准
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},/**
|
||||
* 暗黑主题颜色,如果不填写自动计算
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 暗黑文字颜色,如果不填写自动计算
|
||||
*/
|
||||
fontDarkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 它是建立在你没有提供color时才有效。
|
||||
* 如果提供了color是以你color为背景最终色。
|
||||
* thin浅色模式,
|
||||
* normal标准背景色
|
||||
*/
|
||||
skin: {
|
||||
type: String,
|
||||
default: "thin"
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [顶左,顶右,底右,底左]
|
||||
* [顶左,底右]
|
||||
* [顶左,顶右,底右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"round": {
|
||||
type: [String,Number,Array<string|number>],
|
||||
default: 16
|
||||
},
|
||||
/**
|
||||
* 边线
|
||||
* 数组数字时
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [左,上,右,下]
|
||||
* [左右,上下]
|
||||
* [左,上,右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"border": {
|
||||
type: [String,Number,Array<string|number>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 边框颜色
|
||||
* 格式同border边线。
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"borderColor": {
|
||||
type: [String,Array<string>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 如果不填写,自动计算
|
||||
*/
|
||||
darkBorderColor: {
|
||||
type: [String,Array<string>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 边线类型,默认solid,可以为none
|
||||
*/
|
||||
"borderStyle": {
|
||||
type: String,
|
||||
default: 'solid'
|
||||
},
|
||||
/**
|
||||
* 间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"margin": {
|
||||
type:[String,Number,Array<string|number>],
|
||||
default: () : string[] => ['24', '0', '24', '24'] as string[]
|
||||
},
|
||||
/**
|
||||
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"padding": {
|
||||
type:[String,Number,Array<string|number>],
|
||||
default: () : string[] => ['28', '20'] as string[]
|
||||
},
|
||||
/**
|
||||
* 自定义高度,可以是数字,单位或者百分比,auto
|
||||
*/
|
||||
"height": {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 宽,单位合法即可数字,字符串带单位,百分比,auto
|
||||
*/
|
||||
"width": {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
})
|
||||
|
||||
const showAlert = ref(true)
|
||||
const _color = computed(() : string => {
|
||||
|
||||
let tcolor = attrs.color;
|
||||
if (tcolor == '') {
|
||||
tcolor = attrs.status;
|
||||
}
|
||||
if (config.mode == 'dark' && attrs.darkColor != '') {
|
||||
tcolor = attrs.darkColor
|
||||
}
|
||||
let color = getDefaultColor(tcolor)
|
||||
|
||||
return color;
|
||||
})
|
||||
const _fontColor = computed(() : string => {
|
||||
let tcolor = attrs.fontColor;
|
||||
if (config.mode == 'dark' && attrs.fontDarkColor != '') {
|
||||
tcolor = attrs.fontDarkColor
|
||||
}
|
||||
return tcolor;
|
||||
})
|
||||
const _isDark = computed(() : boolean => config.mode == 'dark')
|
||||
const _showClose= computed(() : boolean => attrs.showClose)
|
||||
const _fontSize = computed(() : string => covetUniNumber(attrs.fontSize))
|
||||
const _margin = computed(() : string => arrayNumberValidByStyleMP(attrs.margin).join(' '))
|
||||
const _padding = computed(() : string => arrayNumberValidByStyleMP(attrs.padding).join(' '))
|
||||
const _round = computed(() : string => arrayNumberValidByStyleMP(attrs.round).join(' '))
|
||||
|
||||
const _border = computed(() : string => arrayNumberValidByStyleMP(attrs.border).join(' '))
|
||||
const _borderColor = computed(() : string => arrayNumberValidByStyleBorderColor(attrs.borderColor).join(' '))
|
||||
|
||||
const _iconName = computed(():string=>{
|
||||
const iconsmap = new Map<string, string>([
|
||||
['warn','alert-line'],
|
||||
['success','check-double-line'],
|
||||
['error','close-circle-line'],
|
||||
['info','information-line'],
|
||||
['primary','notification-line'],
|
||||
])
|
||||
let dicon = iconsmap.get(attrs.status)
|
||||
return attrs.icon==''?(dicon==null?'':dicon!):attrs.icon
|
||||
})
|
||||
|
||||
const _styleMap = computed(() : styleMapType => {
|
||||
let bgStylemap:any = {}
|
||||
let textStylemap:any = {}
|
||||
let colorObj = getDefaultColorObj(_color.value, _color.value)
|
||||
|
||||
if (attrs.skin == 'thin') {
|
||||
colorObj = getThinColorObj(_color.value, _color.value, _isDark.value)
|
||||
}
|
||||
let defaultObj = colorObj.default
|
||||
bgStylemap["backgroundColor"] = defaultObj.background
|
||||
bgStylemap["margin"] = _margin.value
|
||||
bgStylemap["padding"] = _padding.value
|
||||
bgStylemap["border-radius"] = _round.value
|
||||
bgStylemap["border-width"] = _border.value
|
||||
bgStylemap["height"] = covetUniNumber(attrs.height)
|
||||
bgStylemap["width"] = covetUniNumber(attrs.width)
|
||||
bgStylemap["border-style"] = attrs.borderStyle
|
||||
if(attrs.borderColor.length>0){
|
||||
bgStylemap["border-color"] = _borderColor.value
|
||||
}else{
|
||||
bgStylemap["border-color"] = getDefaultColor(_color.value)
|
||||
}
|
||||
|
||||
|
||||
if (_fontColor.value == '') {
|
||||
textStylemap["color"] = defaultObj.fontColor
|
||||
} else {
|
||||
textStylemap["color"] = _fontColor.value
|
||||
}
|
||||
textStylemap["font-size"] = _fontSize.value
|
||||
return {
|
||||
bgStyle: bgStylemap,
|
||||
textStyle: textStylemap
|
||||
} as styleMapType;
|
||||
})
|
||||
|
||||
const closeAlert = ()=>{
|
||||
emits('close')
|
||||
showAlert.value = false
|
||||
}
|
||||
const onclickbar = ()=>{
|
||||
emits('click')
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tmAlert {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tmAlertContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmAlertLeft {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.tmAlertLeft {
|
||||
flex-shrink: 0;
|
||||
padding-right: 12rpx;
|
||||
}
|
||||
|
||||
.tmAlertRight {
|
||||
flex-shrink: 0;
|
||||
padding-left: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, PropType } from 'vue'
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
/**
|
||||
* @displayName 角标
|
||||
* @exportName tm-badge
|
||||
* @category 展示组件
|
||||
* @description 角标,位置丰富。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmBadge' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const attrs = defineProps({
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: "20"
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "error"
|
||||
},
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: 99
|
||||
},
|
||||
/**
|
||||
* 如果是boolean就显示角标小红点
|
||||
* 如果是string显示文本标签
|
||||
* 如果是数字显示数字并启用maxCount
|
||||
*/
|
||||
label: {
|
||||
type: [String, Number,Boolean],
|
||||
default: ""
|
||||
},
|
||||
position: {
|
||||
type: String as PropType<"right" | "left" | "bottomLeft" | "bottomRight" | 'top' | 'bottom'>,
|
||||
default: "right"
|
||||
},
|
||||
offset: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: (): number[] => [0, 0] as number[],
|
||||
validator(val: number[]): boolean {
|
||||
if (val.length == 0 || val.length == 2) return true
|
||||
console.error("x:必须是长度为2的数字数组参数或者空数组")
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const _offset = computed(() => attrs.offset)
|
||||
const _isDot = computed(() => {
|
||||
if(typeof attrs.label === 'boolean') return attrs.label
|
||||
return false;
|
||||
})
|
||||
|
||||
const _cStyles = computed(() => {
|
||||
let trs = ''
|
||||
if (attrs.position == 'right') {
|
||||
trs = 'translate(50%, -50%)'
|
||||
} else if (attrs.position == 'left') {
|
||||
trs = 'translate(-50%, -50%)'
|
||||
} else if (attrs.position == 'bottomLeft') {
|
||||
trs = 'translate(-50%, 50%)'
|
||||
} else if (attrs.position == 'bottomRight') {
|
||||
trs = 'translate(50%, 50%)'
|
||||
} else if (attrs.position == 'top') {
|
||||
trs = 'translate(0%, -50%)'
|
||||
} else if (attrs.position == 'bottom') {
|
||||
trs = 'translate(0%, 50%)'
|
||||
}
|
||||
|
||||
let top = ''
|
||||
let bottom = ''
|
||||
let left = ''
|
||||
let right = ''
|
||||
|
||||
if (attrs.position == 'top') {
|
||||
top = '0px'
|
||||
left = 'auto'
|
||||
right = 'auto'
|
||||
} else if (attrs.position == 'bottom') {
|
||||
bottom = '0px'
|
||||
left = 'auto'
|
||||
right = 'auto'
|
||||
} else if (attrs.position == 'right') {
|
||||
top = _offset.value[1].toString() + 'px'
|
||||
right = _offset.value[0].toString() + 'px'
|
||||
} else if (attrs.position == 'left') {
|
||||
top = '0px'
|
||||
left = '0px'
|
||||
} else if (attrs.position == 'bottomLeft') {
|
||||
bottom = '0px'
|
||||
left = '0px'
|
||||
} else if (attrs.position == 'bottomRight') {
|
||||
bottom = '0px'
|
||||
right = '0px'
|
||||
}
|
||||
|
||||
let dotMapCs:{[key:string]:any} = {}
|
||||
dotMapCs['background'] = getDefaultColor(attrs.bgColor)
|
||||
dotMapCs['left'] = left
|
||||
dotMapCs['right'] = right
|
||||
dotMapCs['top'] = top
|
||||
dotMapCs['bottom'] = bottom
|
||||
dotMapCs['transform'] = trs
|
||||
|
||||
let labelMapCs:{[key:string]:any} = {}
|
||||
labelMapCs['background'] = getDefaultColor(attrs.bgColor)
|
||||
labelMapCs['left'] = left
|
||||
labelMapCs['right'] = right
|
||||
labelMapCs['top'] = top
|
||||
labelMapCs['bottom'] = bottom
|
||||
labelMapCs['transform'] = trs
|
||||
labelMapCs['visibility'] = _label.value == "" ? "hidden" : "visible"
|
||||
|
||||
return [dotMapCs, labelMapCs]
|
||||
|
||||
})
|
||||
|
||||
const _fontColor = computed(() => getDefaultColor(attrs.fontColor))
|
||||
const _fontSize = computed(() => covetUniNumber(attrs.fontSize, config.unit))
|
||||
const _label = computed((): string => {
|
||||
if(typeof attrs.label === 'boolean') return ''
|
||||
if(typeof attrs.label === 'string') return attrs.label
|
||||
let count = attrs.label
|
||||
let maxCount = typeof attrs.maxCount == "number" ? attrs.maxCount : parseInt(attrs.maxCount)
|
||||
if (count > 0 && count <= maxCount) return count.toString();
|
||||
if (count <= 0) return ""
|
||||
return maxCount.toString() + "+"
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmBadge">
|
||||
<view class="tmBadgeWrap">
|
||||
<text :style='_cStyles[0]' class="tmBadge-dot" :class="[_isDot ? 'noneShow' : 'nonex']"></text>
|
||||
<view id="tmBadge-countAndLabel" class="tmBadge-countAndLabel" :class="[_isDot ? 'nonex' : 'noneShow']"
|
||||
:style='_cStyles[1]'>
|
||||
<text class="tmBadge-countAndLabelText" :style='{ color: _fontColor, fontSize: _fontSize }'>{{ _label}}</text>
|
||||
</view>
|
||||
<!--
|
||||
@slot 默认内容插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tmBadge {
|
||||
overflow: visible;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.tmBadge-countAndLabel {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
border-radius: 100px;
|
||||
min-width: 0px;
|
||||
padding: 0rpx 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tmBadge-countAndLabelText {
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tmBadge-dot {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 18px;
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.noneShow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nonex {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tmBadgeWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,516 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, type PropType } from 'vue';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { tmDate, type tmDateTypeTime, createDate } from '../../libs/tmDate';
|
||||
import { getDefaultColor } from "../../libs/colors";
|
||||
|
||||
type PICKER_ITEM_INFO = Record<string, any>
|
||||
|
||||
/**
|
||||
* @displayName 时间区间选择
|
||||
* @exportName tm-between-time
|
||||
* @category 表单组件
|
||||
* @description 快速的时间区间选择器,方便时间选择自动判断前后时间大小并校正。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmBetweenTime' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type coverValue = {
|
||||
value: string[],
|
||||
str: string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Array<string>],
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
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及输入框展示有效
|
||||
* 因此它可能不是一个标准时间,比如YY SS ,所以不能作为modelValue使用
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD"
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => ['年', '月', '日', '时', '分', '秒'] as string[]
|
||||
},
|
||||
/**
|
||||
* 快速时间区间选择,如果直接填写数字字符,会以你提供的数字最近多少来天来算。
|
||||
* d:本日
|
||||
* w:本周
|
||||
* m:本月
|
||||
* y:本年
|
||||
* q:本季度
|
||||
* 7:最近7天,后面的依此类推,数字的就是最近xx天。
|
||||
*/
|
||||
quickDate: {
|
||||
type: [Array<string>],
|
||||
default: (): string[] => ['d', 'w', 'm', 'y', 'q'] as string[]
|
||||
},
|
||||
/**
|
||||
* 是否懒加载内部内容。
|
||||
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
|
||||
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
|
||||
*/
|
||||
lazyContent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 如果你的快捷选择较多可能会让高度不足,需要自行设置下高。
|
||||
*/
|
||||
drawerSize: {
|
||||
type: String,
|
||||
default: '1020'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认触发
|
||||
* @param {string} date 当前选中时间
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
let startValue = new tmDate()
|
||||
let endValue = new tmDate()
|
||||
startValue.subtraction(1, 'y')
|
||||
|
||||
const show = ref(false)
|
||||
const nowValue = ref<string[]>(['', ''])
|
||||
const nowModelValue = ref<string[]>(['', ''])
|
||||
const startDate = ref(startValue)
|
||||
const endDate = ref(endValue)
|
||||
const changeIndex = ref(0)
|
||||
const yanchiDuration = ref(false)
|
||||
const quicklist = ref<coverValue[]>([])
|
||||
|
||||
const _lazyContent = computed(() => props.lazyContent)
|
||||
|
||||
const _start_date = computed(() => props.start === "" ? startDate.value : new tmDate(props.start))
|
||||
const _end_date = computed(() => props.end === "" ? endDate.value : new tmDate(props.end))
|
||||
const _start_date_str = computed(() => _start_date.value.format())
|
||||
const _end_date_str = computed(() => _end_date.value.format())
|
||||
const _start_date_str_format = computed(() => nowValue.value[0] === '' ? '开始时间' : (new tmDate(nowValue.value[0])).format(props.format))
|
||||
const _end_date_str_format = computed(() => nowValue.value[1] === '' ? '结束时间' : (new tmDate(nowValue.value[1])).format(props.format))
|
||||
|
||||
const _backgroundColor = computed(() => config.mode === 'dark' ? getDefaultColor(config.inputDarkColor) : getDefaultColor('#f5f5f5'))
|
||||
const _borderColor = computed(() => config.mode === 'dark' ? getDefaultColor(config.borderDarkColor) : getDefaultColor('#eaeaea'))
|
||||
const _activeBorderColor = computed(() => getDefaultColor(config.color))
|
||||
const _placeStyle = computed(() => config.mode === 'dark' ? "color:#c7c7c7;" : "color:#838383;")
|
||||
const _fontColor = computed(() => config.mode === 'dark' ? "#efefef" : "#333")
|
||||
const _isDark = computed(() => config.mode === 'dark')
|
||||
|
||||
watch(() => 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()
|
||||
})
|
||||
|
||||
watch(() => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue === show.value) return
|
||||
show.value = newValue
|
||||
})
|
||||
|
||||
watch(() => props.quickDate, (newvalue: string[]) => {
|
||||
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()
|
||||
})
|
||||
|
||||
function clearDate() {
|
||||
let dst = ['', ''] as string[]
|
||||
nowValue.value = dst
|
||||
}
|
||||
|
||||
function tongbuModelStr() {
|
||||
let str = coverStrVal()
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', str === '~' ? '' : str)
|
||||
}
|
||||
|
||||
function dateChangeView(datestr: string) {
|
||||
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 tmDate(strStart).isBetweenOf(new tmDate(strEnd), '>', types)) {
|
||||
strEnd = strStart
|
||||
}
|
||||
} else if (changeIndex.value === 1) {
|
||||
if (new tmDate(strEnd).isBetweenOf(new tmDate(strStart), '<', types)) {
|
||||
strStart = strEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
|
||||
function tagsClick(item: coverValue) {
|
||||
nowValue.value = item.value
|
||||
}
|
||||
|
||||
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())
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
} else if (changeIndex.value === 1) {
|
||||
if (strEnd === '') {
|
||||
strEnd = strStart !== '' ? strStart : (endDate.value.format())
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getQuickDateType(): coverValue[] {
|
||||
let typelist: coverValue[] = []
|
||||
if (props.quickDate.length === 0) return typelist
|
||||
let list = props.quickDate
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let item: string = list[i]
|
||||
if (!isNaN(parseInt(item))) {
|
||||
let dshi = parseInt(item)! as number
|
||||
let date = new tmDate()
|
||||
let start = date.format()
|
||||
date.subtraction(dshi, 'd')
|
||||
let end = date.format()
|
||||
|
||||
typelist.push({ value: [end, start], str: '最近' + item + '天' } as coverValue)
|
||||
} else {
|
||||
let date = new tmDate()
|
||||
let startFDate = "YYYY/DD/MM 00:00:00"
|
||||
let endFDate = "YYYY/DD/MM 23:59:59"
|
||||
let start = date.format(startFDate)
|
||||
let end = date.format(endFDate)
|
||||
let desc = "本日"
|
||||
if (item === 'w') {
|
||||
start = date.getDateStartOf('w').format(startFDate)
|
||||
end = date.getDateEndOf('w').format(endFDate)
|
||||
desc = "本周"
|
||||
} else if (item === 'm') {
|
||||
start = date.getDateStartOf('m').format(startFDate)
|
||||
end = date.getDateEndOf('m').format(endFDate)
|
||||
desc = "本月"
|
||||
} else if (item === 'y') {
|
||||
start = date.getDateStartOf('y').format(startFDate)
|
||||
end = date.getDateEndOf('y').format(endFDate)
|
||||
desc = "本年"
|
||||
} else if (item === 'q') {
|
||||
let nowq = date.getQuarter('')
|
||||
let itemqatar = nowq[0]
|
||||
start = itemqatar.start
|
||||
end = itemqatar.end
|
||||
desc = "本季度"
|
||||
}
|
||||
|
||||
typelist.push({ value: [start, end], str: desc } as coverValue)
|
||||
}
|
||||
}
|
||||
return typelist
|
||||
}
|
||||
|
||||
// 获取精确单位
|
||||
function getTypes(): tmDateTypeTime {
|
||||
if (props.type === 'year') {
|
||||
return 'y' as tmDateTypeTime
|
||||
} else if (props.type === 'month') {
|
||||
return 'm' as tmDateTypeTime
|
||||
} else if (props.type === 'day') {
|
||||
return 'd' as tmDateTypeTime
|
||||
} else if (props.type === 'hour') {
|
||||
return 'h' as tmDateTypeTime
|
||||
} else if (props.type === 'minute') {
|
||||
return 'M' as tmDateTypeTime
|
||||
} else if (props.type === 'second') {
|
||||
return 's' as tmDateTypeTime
|
||||
}
|
||||
return 's' as tmDateTypeTime
|
||||
}
|
||||
|
||||
//验证提供的值是否合法,并纠正为合法值。
|
||||
function validTimeDate(val: string[]): string[] {
|
||||
let str = ['', '']
|
||||
if (val.length >= 1) {
|
||||
str[0] = val[0]!
|
||||
}
|
||||
if (val.length >= 2) {
|
||||
str[1] = val[1]!
|
||||
}
|
||||
|
||||
return sorDateVaild(str)
|
||||
}
|
||||
|
||||
function sorDateVaild(str: string[]): string[] {
|
||||
let types = getTypes()
|
||||
str.sort((a: string, b: string): number => {
|
||||
return new tmDate(a).getTime(types) - new tmDate(b).getTime(types)
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function openShow() {
|
||||
show.value = true
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit('update:modelShow', true)
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit('update:modelShow', false)
|
||||
cancelResetDataCol()
|
||||
if (_lazyContent.value) {
|
||||
yanchiDuration.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
yanchiDuration.value = true
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit('cancel')
|
||||
cancelResetDataCol()
|
||||
}
|
||||
|
||||
function cancelResetDataCol() {
|
||||
nowValue.value = nowModelValue.value.slice(0)
|
||||
}
|
||||
|
||||
function coverStrVal(): string {
|
||||
let estrt = nowValue.value[0]
|
||||
let eend = nowValue.value[1]
|
||||
let start = estrt === '' ? '' : (new tmDate(estrt)).format(props.format)
|
||||
let ebd = eend === '' ? '' : (new tmDate(eend)).format(props.format)
|
||||
return start + '~' + ebd
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
let str = coverStrVal()
|
||||
let tmdate: string[] = []
|
||||
if (nowValue.value[0] !== '' && nowValue.value[1] !== '') {
|
||||
tmdate = nowValue.value.slice(0)
|
||||
nowModelValue.value = tmdate
|
||||
}
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emit('update:modelValue', tmdate)
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', str === '~' ? '' : str)
|
||||
|
||||
emit('confirm', tmdate)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<tm-drawer @open="onOpen" :widthCoverCenter="true" :disabledScroll="true" @close="onClose" @confirm="onConfirm"
|
||||
@cancel="onCancel" :showFooter="true" v-model:show="show" :size="drawerSize" :showClose="false">
|
||||
<template v-slot:title>
|
||||
<view
|
||||
style="padding: 0 20px;flex:1;display: flex;flex-direction: row;justify-content: space-between;align-items: center;height: 100%;">
|
||||
<tm-text>{{ title }}</tm-text>
|
||||
<tm-text @click="clearDate" style="opacity: 0.5;">清空</tm-text>
|
||||
</view>
|
||||
</template>
|
||||
<view v-if="yanchiDuration" class="tmPickerDateWrap">
|
||||
<view v-if="quicklist.length > 0" class="tmPickerDateWrapQuickTags">
|
||||
<tm-tag font-size="26" :font-color="(_isDark ? 'white' : '')" @click="tagsClick(item)" skin='thin'
|
||||
size="m" style="margin-right:10px;margin-bottom: 5px;" v-for="(item, index) in quicklist"
|
||||
:key="index">{{
|
||||
item.str }}</tm-tag>
|
||||
</view>
|
||||
<view v-if="quicklist.length > 0" style="height: 8px;"></view>
|
||||
<view class="tmPickerDateWrapQuickInput">
|
||||
<view @click="inputClick(0)" style="flex:1;height: 40px;">
|
||||
<input :placeholder-style="_placeStyle"
|
||||
:style="{ color: changeIndex == 0 ? _activeBorderColor : _fontColor, border: `2px solid ${changeIndex == 0 ? _activeBorderColor : _borderColor}`, backgroundColor: _backgroundColor }"
|
||||
class="tmPickerInput" :disabled="true" :value="_start_date_str_format" />
|
||||
</view>
|
||||
<tm-text style="width: 64px;text-align: center;">至</tm-text>
|
||||
<view @click="inputClick(1)" style="flex:1;height: 40px;">
|
||||
<input :placeholder-style="_placeStyle"
|
||||
:style="{ color: changeIndex == 1 ? _activeBorderColor : _fontColor, border: `2px solid ${changeIndex == 1 ? _activeBorderColor : _borderColor}`, backgroundColor: _backgroundColor }"
|
||||
class="tmPickerInput" :disabled="true" :value="_end_date_str_format" />
|
||||
</view>
|
||||
</view>
|
||||
<tm-date-view v-if="show" @change="dateChangeView" :format="format" :type="type" :cell-units="cellUnits"
|
||||
:start="_start_date_str" :end="_end_date_str" :model-value='nowValue[changeIndex]'></tm-date-view>
|
||||
</view>
|
||||
<tm-icon v-if="!yanchiDuration" size="42" spin name="loader-line"></tm-icon>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmPickerDateText {
|
||||
text-align: left;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: linear;
|
||||
transition-property: transform, opacity;
|
||||
transform: translateY(100%) scale(0);
|
||||
opacity: 0;
|
||||
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickInputPlackeTips {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tmPickerInput {
|
||||
border-radius: 80rpx;
|
||||
height: 80rpx;
|
||||
|
||||
padding: 0 20rpx;
|
||||
font-size: 30rpx;
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickInput {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickTags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,541 @@
|
||||
<template>
|
||||
<view class="tmButton" :class="[_attrs.loading || _attrs.disabled ? 'tmButtonLoading' : '']"
|
||||
@touchstart="touchStart" @touchcancel="touchEnd" @touchend="touchEnd" @mousedown="touchStart"
|
||||
@mouseup="touchEnd" @mouseleave="touchEnd" :style="[
|
||||
{
|
||||
minWidth: _width,
|
||||
width: _attrs.block ? '100%' : 'auto',
|
||||
height: _attrs.btnIcon ? _width : _height,
|
||||
fontSize: _fontSize,
|
||||
},
|
||||
buttonStyle,
|
||||
]">
|
||||
<tm-icon v-if="_attrs.loading" name="loader-line" spin :color="buttonStyle.color"></tm-icon>
|
||||
<!--
|
||||
@slot icon图标
|
||||
-->
|
||||
<slot name="icon">
|
||||
<tm-icon v-if="!_attrs.loading && _attrs.icon" :name="_attrs.icon" :size="_attrs.iconSize || _fontSize"
|
||||
:color="buttonStyle.color"></tm-icon>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot>
|
||||
<text v-if="!_attrs.btnIcon"></text>
|
||||
</slot>
|
||||
<button @getphonenumber="getphonenumber" @getuserinfo="getuserinfo" @error="error" @opensetting="opensetting"
|
||||
@launchapp="launchapp" @contact="contact" @chooseavatar="chooseavatar"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization" @addgroupapp="addgroupapp"
|
||||
@chooseaddress="chooseaddress" @chooseinvoicetitle="chooseinvoicetitle" @subscribe="subscribe"
|
||||
@login="login" @im="im" :appParameter="_attrs.appParameter" :lang="_attrs.lang"
|
||||
:sessionFrom="_attrs.sessionFrom" :sendMessageTitle="_attrs.sendMessageTitle"
|
||||
:sendMessagePath="_attrs.sendMessagePath" :sendMessageImg="_attrs.sendMessageImg"
|
||||
:showMessageCard="_attrs.showMessageCard" :groupId="_attrs.groupId" :guildId="_attrs.groupId"
|
||||
:publicId="_attrs.publicId" :dataImId="_attrs.dataImId" :dataImType="_attrs.dataImType"
|
||||
:dataGoodsId="_attrs.dataGoodsId" :dataOrderId="_attrs.dataOrderId" :dataBizLine="_attrs.dataBizLine"
|
||||
@click="onClick" :open-type="_attrs.openType" class="tmButtonBlock"></button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, type PropType, computed, ref, getCurrentInstance } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import TmForm from "../tm-form/tm-form.vue";
|
||||
|
||||
/**
|
||||
* @displayName 按钮
|
||||
* @exportName tm-button
|
||||
* @category 常用组件
|
||||
* @description 圆角,主题可通过配置统一设置或者动态全局设置,使设计风格统一并保持一致性。让你的风格独一无二。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmButton' });
|
||||
const { config } = useTmConfig()
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @property {object} evt 事件参数
|
||||
*/
|
||||
'click',
|
||||
'getphonenumber',
|
||||
'getuserinfo',
|
||||
'error',
|
||||
'opensetting',
|
||||
'launchapp',
|
||||
'contact',
|
||||
'chooseavatar',
|
||||
'agreeprivacyauthorization',
|
||||
'addgroupapp',
|
||||
'chooseaddress',
|
||||
'chooseinvoicetitle',
|
||||
'subscribe',
|
||||
'login',
|
||||
'im'
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 尺寸大小,可选值:s/m/n/g/xs
|
||||
*/
|
||||
size: {
|
||||
type: String as PropType<"s" | "g" | "xs" | "m" | "n">,
|
||||
default: "n"
|
||||
},
|
||||
/**
|
||||
* 样式
|
||||
* 可选值:thin/outlined/text/normal/dashed
|
||||
*/
|
||||
skin: {
|
||||
type: String as PropType<"thin" | "outlined" | "text" | "normal" | "dashed">,
|
||||
default: "normal"
|
||||
},
|
||||
/**
|
||||
* 遵循规则:
|
||||
* string或者[x]:全部边线
|
||||
* [x,x]左右边线,上下边线
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
borderWidth: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
* 左上,右上,右下,左下
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 50%圆角
|
||||
*/
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**宽度 */
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**高度 */
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**开放能力 */
|
||||
openType: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**是否占据一行 */
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**是否禁用 */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**图标名称 */
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**图标大小 */
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否开启仅图标按钮模式,宽和高相等
|
||||
*/
|
||||
btnIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**是否加载中 */
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义主题颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义背景 */
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑背景 */
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义字体颜色 */
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑字体颜色 */
|
||||
darkFontColorColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 渐变,前面的color,bgColor,darkBgColor失效 */
|
||||
linear: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
validator: (val: string[]) => {
|
||||
if (val.length == 0) return true;
|
||||
let pass = val.length == 3;
|
||||
if (!pass) {
|
||||
console.error("tmui提醒:linear 参数格式错误,请检查,必须长度为3");
|
||||
}
|
||||
return pass
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数字或者字符串时自动计算投影,它会投影大小
|
||||
* 数组时(必须为4),第一个x,第二为y,第三个为大小 ,第四个为投影颜色
|
||||
* 空字符串时,取全局配置,如果不想投影设置为none即可.
|
||||
*/
|
||||
shadow: {
|
||||
type: [String, Number, Array<string>],
|
||||
default: '',
|
||||
validator: (val: string | number | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length != 4) {
|
||||
console.error("tmui提醒:shadow 第一个x,第二为y,第三个为大小 ,第四个为投影颜色");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
formType: {
|
||||
type: String as PropType<"submit" | "reset" | "form">,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 跳转的地址链接,出现时点击click会跳转以/pages开头。navigator模式跳转
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
appParameter: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: "en"
|
||||
},
|
||||
sessionFrom: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessageTitle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessagePath: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessageImg: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
showMessageCard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
guildId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
publicId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataImId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataImType: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataGoodsId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataOrderId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataBizLine: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
type PropsKeyType = keyof typeof attrs;
|
||||
|
||||
const covetButtonSize = (ops: PropsKeyType, s: TM.BUTTON_SIZE, size?: string | number) => {
|
||||
const widths = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '88'],
|
||||
['s', '96'],
|
||||
['m', '120'],
|
||||
['n', '220'],
|
||||
['g', '300']
|
||||
|
||||
])
|
||||
const heights = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '44'],
|
||||
['s', '60'],
|
||||
['m', '72'],
|
||||
['n', '90'],
|
||||
['g', '100']
|
||||
|
||||
])
|
||||
const fontSizes = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '22'],
|
||||
['s', '24'],
|
||||
['m', '28'],
|
||||
['n', '32'],
|
||||
['g', '34']
|
||||
])
|
||||
const rounds = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '8'],
|
||||
['s', '10'],
|
||||
['m', '12'],
|
||||
['n', '16'],
|
||||
['g', '20']
|
||||
])
|
||||
if (ops === 'width') {
|
||||
return covetUniNumber(widths.get(s)!)
|
||||
}
|
||||
if (ops === 'height') {
|
||||
return covetUniNumber(heights.get(s)!)
|
||||
}
|
||||
if (ops === 'round') {
|
||||
if (size && s == 'n') {
|
||||
return covetUniNumber(size!)
|
||||
}
|
||||
return covetUniNumber(rounds.get(s)!)
|
||||
}
|
||||
return covetUniNumber(fontSizes.get(s)!);
|
||||
}
|
||||
const _attrs = computed(() => attrs);
|
||||
const _width = computed(() => {
|
||||
if (attrs.block) {
|
||||
return 'auto'
|
||||
}
|
||||
return attrs.width === '' ? covetButtonSize('width', attrs.size) : covetUniNumber(attrs.width)
|
||||
})
|
||||
const _height = computed(() => attrs.height === '' ? covetButtonSize('height', attrs.size) : covetUniNumber(attrs.height))
|
||||
const _fontSize = computed(() => attrs.fontSize === '' ? covetButtonSize('fontSize', attrs.size) : covetUniNumber(attrs.fontSize))
|
||||
|
||||
const isHover = ref(false);
|
||||
|
||||
const touchStart = () => {
|
||||
isHover.value = true;
|
||||
}
|
||||
const touchEnd = () => {
|
||||
isHover.value = false;
|
||||
}
|
||||
const buttonStyle = computed(() => {
|
||||
let style = {
|
||||
borderColor: ``,
|
||||
borderWidth: ``,
|
||||
borderStyle: ``,
|
||||
background: ``,
|
||||
color: ``,
|
||||
borderRadius: ``,
|
||||
boxShadow: `none`
|
||||
}
|
||||
let isDark = config.mode == 'dark';
|
||||
let color = attrs.color || config.color;
|
||||
let round = attrs.round === '' ? covetButtonSize('round', attrs.size, config.buttonRadius) : attrs.round;
|
||||
let linear = linearValid(attrs.linear)
|
||||
let background = linear || (attrs.bgColor ? getDefaultColor(attrs.bgColor) : '')
|
||||
|
||||
let fontColor = attrs.fontColor ? getDefaultColor(attrs.fontColor) : ''
|
||||
let borderStyle = attrs.skin == 'dashed' ? 'dashed' : 'solid';
|
||||
|
||||
if (isDark) {
|
||||
if (attrs.darkBgColor) {
|
||||
background = getDefaultColor(attrs.darkBgColor)
|
||||
}
|
||||
if (attrs.darkFontColorColor) {
|
||||
fontColor = getDefaultColor(attrs.darkFontColorColor)
|
||||
}
|
||||
}
|
||||
|
||||
let obj = getDefaultColorObj(color, color)
|
||||
if (attrs.skin == 'text') {
|
||||
obj = getTextColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'thin') {
|
||||
obj = getThinColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'outlined' || attrs.skin == 'dashed') {
|
||||
obj = getOutlineColorObj(color, color)
|
||||
}
|
||||
|
||||
if (obj) {
|
||||
if (isHover.value && !attrs.disabled && !attrs.loading) {
|
||||
style.color = linear ? '#ffffff' : fontColor || obj.active.fontColor;
|
||||
style.background = background || obj.active.background;
|
||||
style.borderStyle = borderStyle;
|
||||
style.borderColor = background || obj.active.borderColor;
|
||||
style.borderWidth = linear ? '0px' : arrayNumberValidByStyleMP(attrs.borderWidth, '0px').join(" ")
|
||||
style.borderRadius = attrs.rounded ? '50%' : arrayNumberValid(round, '0px').join(" ")
|
||||
} else {
|
||||
style.color = linear ? '#ffffff' : fontColor || obj.default.fontColor;
|
||||
style.background = background || obj.default.background;
|
||||
style.borderStyle = borderStyle;
|
||||
style.borderColor = background || obj.default.borderColor;
|
||||
style.borderWidth = linear ? '0px' : arrayNumberValidByStyleMP(attrs.borderWidth, '0px').join(" ")
|
||||
style.borderRadius = attrs.rounded ? '50%' : arrayNumberValid(round, '0px').join(" ")
|
||||
let shadow = 'none'
|
||||
if (Array.isArray(attrs.shadow)) {
|
||||
shadow = attrs.shadow.join(' ')
|
||||
} else if (attrs.shadow !== '' && attrs.shadow != 'none') {
|
||||
shadow = `0 2px ${covetUniNumber(attrs.shadow)} ${obj.default.shadow}`
|
||||
} else if (attrs.shadow === '') {
|
||||
if (Array.isArray(config.buttonShadow)) {
|
||||
shadow = config.buttonShadow.join(' ')
|
||||
} else if (config.buttonShadow && config.buttonShadow != 'none') {
|
||||
shadow = `0 2px ${covetUniNumber(config.buttonShadow)} ${obj.default.shadow}`
|
||||
}
|
||||
}
|
||||
|
||||
style.boxShadow = shadow
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
const onClick = (evt: any) => {
|
||||
emits('click', evt);
|
||||
if (_attrs.value.disabled || _attrs.value.loading) return;
|
||||
if (_attrs.value.url) {
|
||||
uni.navigateTo({
|
||||
url: _attrs.value.url,
|
||||
fail(error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (attrs.formType == 'submit' || attrs.formType == 'reset') {
|
||||
let parent = findParent(proxy)
|
||||
if (parent != null) {
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
if (attrs.formType == 'submit') {
|
||||
ele.submit()
|
||||
} else if (attrs.formType == 'reset') {
|
||||
ele.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const getphonenumber = (evt: any) => emits('getphonenumber', evt)
|
||||
const getuserinfo = (evt: any) => emits('getuserinfo', evt)
|
||||
const error = (evt: any) => emits('error', evt)
|
||||
const opensetting = (evt: any) => emits('opensetting', evt)
|
||||
const launchapp = (evt: any) => emits('launchapp', evt)
|
||||
const contact = (evt: any) => emits('contact', evt)
|
||||
const chooseavatar = (evt: any) => emits('chooseavatar', evt)
|
||||
const agreeprivacyauthorization = (evt: any) => emits('agreeprivacyauthorization', evt)
|
||||
const addgroupapp = (evt: any) => emits('addgroupapp', evt)
|
||||
const chooseaddress = (evt: any) => emits('chooseaddress', evt)
|
||||
const chooseinvoicetitle = (evt: any) => emits('chooseinvoicetitle', evt)
|
||||
const subscribe = (evt: any) => emits('subscribe', evt)
|
||||
const login = (evt: any) => emits('login', evt)
|
||||
const im = (evt: any) => emits('im', evt)
|
||||
|
||||
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmForm") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmForm") return parents;
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmButton {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&.tmButtonLoading {
|
||||
opacity: 0.5;
|
||||
|
||||
.tmButtonBlock,
|
||||
& {
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
|
||||
.tmButtonBlock {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
border: 0px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
|
||||
&::after {
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<!-- @vue-ignore -->
|
||||
<view @click="onclick" :hover-start-time="_isLinksHover ? 50 : 0" :hover-stay-time="_isLinksHover ? 100 : 0"
|
||||
:hover-class="_isLinksHover ? 'cellHover' : ''" class="tmCell "
|
||||
:class="[_allAttr.card == true ? 'cellCard' : 'cardInset']" :style="{
|
||||
backgroundColor: _color,
|
||||
borderRadius: _cardRadius,
|
||||
minHeight: _minHeight,
|
||||
borderBottom: _allAttr.bottom && !_allAttr.card && !bottomBorderInsert ? `1px solid ${_bottomBorderColor}` : 'none',
|
||||
padding: `0px ${_lrPadding}`
|
||||
}">
|
||||
|
||||
<view v-if="_icon != ''" class="tmCellAvatar"
|
||||
:style="{ width: _leftSize, height: _leftSize, borderRadius: _avatarRound }">
|
||||
<!--
|
||||
@slot 头像图标
|
||||
@binding {string} icon 图标名称
|
||||
-->
|
||||
<slot name="avatar" :icon="_icon">
|
||||
<tm-icon :color="_allAttr.iconColor" :size="_iconSize" :name="_icon"></tm-icon>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="tmCellWrap" :style="{
|
||||
padding: `${_tbPadding} 0px `,
|
||||
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 简介
|
||||
@binding {string} desc 简介
|
||||
-->
|
||||
<slot name="desc" :desc="_allAttr.desc">
|
||||
<tm-text v-if="_allAttr.desc != ''" size="28" color='#bfbfbf' dark-color='#bfbfbf' class="desc">{{
|
||||
_allAttr.desc }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="tmCellRight">
|
||||
<!--
|
||||
@slot 右边文字
|
||||
@binding {string} label 标签内容
|
||||
-->
|
||||
<slot name="label" :label="_allAttr.label">
|
||||
<text v-if="_allAttr.label != ''" :style="{ color: _allAttr.labelColor, fontSize: _rightLableSize }"
|
||||
class="rightLabel">{{ _allAttr.label }}</text>
|
||||
|
||||
</slot>
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right"></slot>
|
||||
<tm-icon v-if="_allAttr.url != '' || _allAttr.link" color="#bfbfbf" size="36" :name="attrs.linkIcon"
|
||||
_style="paddingLeft:12rpx"></tm-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted, PropType } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
/**
|
||||
* @displayName 列表
|
||||
* @exportName tm-cell
|
||||
* @category 展示组件
|
||||
* @description 样式丰富常用警告提醒
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCell' });
|
||||
const { config } = useTmConfig()
|
||||
type tmCellItemType = {
|
||||
icon: string,
|
||||
title: string,
|
||||
desc: string,
|
||||
label: string,
|
||||
bottom: boolean,
|
||||
link: boolean,
|
||||
url: string,
|
||||
iconColor: string,
|
||||
labelColor: string,
|
||||
card: boolean
|
||||
}
|
||||
const emits = defineEmits(
|
||||
[
|
||||
/**
|
||||
* 项目点击
|
||||
*/
|
||||
"click"
|
||||
]
|
||||
)
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左侧图标、头像圆角。默认为8
|
||||
*/
|
||||
avatarRound: {
|
||||
type: String,
|
||||
default: "8"
|
||||
},
|
||||
/**
|
||||
* 背景的主题色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
/**
|
||||
* 暗黑背景的主题色,空值时取sheetDarkColor
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 图标色,空值时取全局主题值。
|
||||
*/
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题"
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: "black"
|
||||
},
|
||||
/**
|
||||
* 暗黑标题颜色,如果不填写取白
|
||||
*/
|
||||
darkTitleColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleSize: {
|
||||
type: [String, Number],
|
||||
default: "32"
|
||||
},
|
||||
/**
|
||||
* 左边图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "42"
|
||||
},
|
||||
/**
|
||||
* 右边文本
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 右边文本颜色
|
||||
*/
|
||||
labelColor: {
|
||||
type: String,
|
||||
default: "#bfbfbf"
|
||||
},
|
||||
/**
|
||||
* 右侧label文字大小
|
||||
*/
|
||||
labelSize: {
|
||||
type: [String, Number],
|
||||
default: "24"
|
||||
},
|
||||
/**
|
||||
* 标题正文的简介文本
|
||||
*/
|
||||
desc: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示下边线
|
||||
*/
|
||||
showBottomBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否让下边线显示居右,不贯穿到左边。
|
||||
*/
|
||||
bottomBorderInsert: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 下边线的颜色。如果你设定了的话。
|
||||
* 暗黑的边颜色失效,采用你自定的颜色。
|
||||
*/
|
||||
bottomBorderColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示链接状态,有点按效果。包括出现右边跳转指示。
|
||||
* 关闭的话,事件反应和跳转会更快。
|
||||
* 如果true右侧箭头图标会显示
|
||||
*/
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 需要跳转的页面地址。
|
||||
* 如果填写了右侧箭头图标会显示
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
openType: {
|
||||
type: String as PropType<"navigate" | "redirect" | "switchTab" | "reLaunch" | "navigateBack" | "exit">,
|
||||
default: "navigate"
|
||||
},
|
||||
/**
|
||||
* 是否是卡片模式
|
||||
*/
|
||||
card: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 卡片模式圆角,不填写采用全局的cardRadius属性值.
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左边图标区域宽和高的大小。
|
||||
*/
|
||||
leftSize: {
|
||||
type: [String, Number],
|
||||
default: '60'
|
||||
},
|
||||
/**
|
||||
* 最小高度,主要是用来统一风格高度不至于让点击范围过小
|
||||
* 如果你需要紧凑型可以设置为auto
|
||||
*/
|
||||
minHeight: {
|
||||
type: [String, Number],
|
||||
default: "88"
|
||||
},
|
||||
/**
|
||||
* 左右的间隙。
|
||||
*/
|
||||
lrPadding: {
|
||||
type: [String, Number],
|
||||
default: "32"
|
||||
},
|
||||
linkIcon: {
|
||||
type: String,
|
||||
default: "arrow-right-s-line"
|
||||
},
|
||||
/**
|
||||
* 上下间隙
|
||||
*/
|
||||
tbPadding: {
|
||||
type: [String, Number],
|
||||
default: "32"
|
||||
}
|
||||
})
|
||||
|
||||
const _color = computed(() => {
|
||||
if (config.mode === 'dark') {
|
||||
if (attrs.darkColor !== '') return getDefaultColor(attrs.darkColor)
|
||||
return getDefaultColor(config.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(attrs.color)
|
||||
})
|
||||
|
||||
const _titleColor = computed(() => {
|
||||
if (config.mode === 'dark') {
|
||||
if (attrs.darkTitleColor !== '') return getDefaultColor(attrs.darkTitleColor)
|
||||
return '#ffffff'
|
||||
}
|
||||
return getDefaultColor(attrs.titleColor)
|
||||
})
|
||||
|
||||
const _leftSize = computed(() => covetUniNumber(attrs.leftSize))
|
||||
const _lrPadding = computed(() => covetUniNumber(attrs.lrPadding))
|
||||
const _tbPadding = computed(() => covetUniNumber(attrs.tbPadding))
|
||||
|
||||
const _avatarRound = computed(() => covetUniNumber(attrs.avatarRound))
|
||||
|
||||
const _minHeight = computed(() => covetUniNumber(attrs.minHeight))
|
||||
|
||||
const _bottomBorderColor = computed(() => {
|
||||
if (attrs.bottomBorderColor !== '') return getDefaultColor(attrs.bottomBorderColor)
|
||||
if (config.mode === 'dark') return config.borderDarkColor
|
||||
return "#f5f5f5"
|
||||
})
|
||||
|
||||
const icon = ref('')
|
||||
|
||||
const _allAttr = computed(() => {
|
||||
let iconColor = attrs.iconColor;
|
||||
if (iconColor === '') {
|
||||
iconColor = config.color;
|
||||
}
|
||||
let p = {
|
||||
icon: icon.value,
|
||||
title: attrs.title,
|
||||
desc: attrs.desc,
|
||||
label: attrs.label,
|
||||
bottom: attrs.showBottomBorder,
|
||||
link: attrs.link,
|
||||
url: attrs.url,
|
||||
iconColor: getDefaultColor(iconColor),
|
||||
labelColor: getDefaultColor(attrs.labelColor),
|
||||
card: attrs.card
|
||||
} as tmCellItemType
|
||||
return p
|
||||
})
|
||||
|
||||
const _cardRadius = computed(() => {
|
||||
if (attrs.round === '') return arrayNumberValidByStyleMP(config.cellRadius)
|
||||
return arrayNumberValidByStyleMP(attrs.round, config.unit)
|
||||
})
|
||||
|
||||
const _titleSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.titleSize, config.unit);
|
||||
if (config.fontSizeScale === 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _iconSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.iconSize, config.unit);
|
||||
if (config.fontSizeScale === 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 17
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _rightLableSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.labelSize, config.unit);
|
||||
|
||||
if (config.fontSizeScale === 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 13
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _isLinksHover = computed(() => attrs.link)
|
||||
const _icon = computed(() => attrs.icon)
|
||||
|
||||
const onclick = () => {
|
||||
/**
|
||||
* 整个列表被点击
|
||||
*/
|
||||
emits("click");
|
||||
if (attrs.url != "") {
|
||||
if (attrs.openType == 'navigate') {
|
||||
uni.navigateTo({
|
||||
url: attrs.url
|
||||
})
|
||||
} else if (attrs.openType == 'navigateBack') {
|
||||
uni.navigateBack({})
|
||||
} else if (attrs.openType == 'reLaunch') {
|
||||
uni.reLaunch({
|
||||
url: attrs.url
|
||||
})
|
||||
} else if (attrs.openType == 'redirect') {
|
||||
uni.redirectTo({
|
||||
url: attrs.url
|
||||
})
|
||||
} else if (attrs.openType == 'switchTab') {
|
||||
uni.switchTab({
|
||||
url: attrs.url
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.cellHover {
|
||||
filter: contrast(90%);
|
||||
}
|
||||
|
||||
.tmCell {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cardInset {
|
||||
// padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.cellCard {
|
||||
// padding: 0 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
margin-left: 24rpx;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.tmCellWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
// padding: 32rpx 0rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.title {
|
||||
line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.tmCellAvatar {
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmCellRight {
|
||||
padding-left: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rightLabel {
|
||||
line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 24rpx;
|
||||
text-align: right;
|
||||
max-width: 200rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, provide } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmCheckbox from '../tm-checkbox/tm-checkbox.vue';
|
||||
|
||||
/**
|
||||
* @displayName 多选框组
|
||||
* @exportName tm-checkbox-group
|
||||
* @page tm-checkbox
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCheckboxGroup' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Array<string | number | boolean>],
|
||||
default: () => [] as string[],
|
||||
},
|
||||
direction: {
|
||||
type: String as PropType<"row" | "column">,
|
||||
default: "row",
|
||||
},
|
||||
align: {
|
||||
type: String as PropType<"flex-start" | "center" | "flex-end">,
|
||||
default: "left"
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
/**
|
||||
* 排列时之间的间隙,如果是数组第一项是列间隙,第二项是行间隙
|
||||
*/
|
||||
gap: {
|
||||
type: [Number, String, Array<string | number>],
|
||||
default: 20,
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue']);
|
||||
|
||||
const checkvaluelist = ref<Array<string | number | boolean>>([]);
|
||||
const isDestroy = ref(false);
|
||||
const id = "tmCheckboxGroup-" + getUid();
|
||||
|
||||
const _max = computed(() => props.max);
|
||||
const _gap = computed(() => arrayNumber(props.gap).join(" "))
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
checkvaluelist.value = newValue;
|
||||
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
checkvaluelist.value = props.modelValue;
|
||||
isDestroy.value = false;
|
||||
});
|
||||
|
||||
function addItem(item: string | number | boolean, ischange: boolean) {
|
||||
let index = checkvaluelist.value.findIndex((el) => el === item);
|
||||
if (index == -1) {
|
||||
if (checkvaluelist.value.length >= _max.value && _max.value > -1 && ischange && checkvaluelist.value.length > 0) {
|
||||
// uni.showToast({ title: "已是最大选择数量", icon: 'none', mask: true });
|
||||
checkvaluelist.value.splice(checkvaluelist.value.length - 1, 1);
|
||||
}
|
||||
checkvaluelist.value.push(item)
|
||||
} else {
|
||||
checkvaluelist.value.splice(index, 1);
|
||||
}
|
||||
|
||||
if (ischange) {
|
||||
emits("update:modelValue", checkvaluelist.value.slice(0));
|
||||
emits("change", checkvaluelist.value.slice(0));
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(item: string | number | boolean) {
|
||||
let index = checkvaluelist.value.findIndex((el) => el === item);
|
||||
if (index > -1) {
|
||||
checkvaluelist.value.splice(index, 1);
|
||||
emits("update:modelValue", checkvaluelist.value.slice(0));
|
||||
emits("change", checkvaluelist.value.slice(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
provide("tmCheckboxGroupValue", computed(() => checkvaluelist.value.slice(0)))
|
||||
|
||||
defineExpose({ addItem, removeItem })
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmCheckboxGroup"
|
||||
:style="{ 'flex-direction': direction, 'align-items': direction == 'row' ? 'flex-start' : undefined, 'justify-content': align, gap: _gap }">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tmCheckboxGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,377 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmCheckboxGroup from '../tm-checkbox-group/tm-checkbox-group.vue';
|
||||
import tmCheckbox from './tm-checkbox.vue';
|
||||
const proxy = getCurrentInstance()?.proxy as InstanceType<typeof tmCheckbox> | null;
|
||||
|
||||
/**
|
||||
* @displayName 多选框
|
||||
* @exportName tm-checkbox
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCheckbox' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param {Boolean} check - 当前是否选中
|
||||
* @param {Number} value - 当前选中的值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前主题色,空值时取全局
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
/**
|
||||
* 当前未选中时主题色,空值时取全局
|
||||
*/
|
||||
unCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前未选中时的暗黑主题色
|
||||
*/
|
||||
darkUnCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前双向绑定值
|
||||
*/
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 选中的值,
|
||||
*/
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 未选中的值
|
||||
*/
|
||||
unCheckValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 选中的图标名称。
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "check-line"
|
||||
},
|
||||
/**
|
||||
* 右侧文字。
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否隐藏选中框。然后利用默认插槽自定义选中所有样式和状态。
|
||||
*/
|
||||
hiddenCheckbox: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 半选中
|
||||
*/
|
||||
indeterminate: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: "42"
|
||||
},
|
||||
/**
|
||||
* 中间小图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "40"
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 如果不要显示label时设置为false可以隐藏label并把label间隙删除,
|
||||
*/
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const nowValue = ref<string | number | boolean>('');
|
||||
const boxId = ref("tmCheckbox-" + getUid());
|
||||
const tid = ref(0);
|
||||
const isDestroy = ref(false);
|
||||
const parentVal = inject("tmCheckboxGroupValue", computed((): Array<string | number | boolean> => []))
|
||||
const _color = computed(() => {
|
||||
if (props.color == "") return getDefaultColor(config.color);
|
||||
return getDefaultColor(props.color);
|
||||
});
|
||||
const _showLabel = computed(() => props.showLabel)
|
||||
|
||||
const _unCheckColor = computed(() => {
|
||||
if (config.mode == 'dark' && props.darkUnCheckColor != '') {
|
||||
return getDefaultColor(props.darkUnCheckColor);
|
||||
}
|
||||
if (props.unCheckColor == "") return getDefaultColor(config.unRadioAndCheckBoxColor);
|
||||
return getDefaultColor(props.unCheckColor);
|
||||
});
|
||||
|
||||
const _isCheck = computed(() => {
|
||||
return nowValue.value === props.value;
|
||||
});
|
||||
|
||||
const _disabled = computed(() => {
|
||||
return props.disabled;
|
||||
});
|
||||
|
||||
const _label = computed(() => {
|
||||
return props.label;
|
||||
});
|
||||
|
||||
const _indeterminate = computed(() => {
|
||||
return props.indeterminate;
|
||||
});
|
||||
|
||||
const _size = computed(() => {
|
||||
let size = covetUniNumber(props.size, config.unit);
|
||||
if (config.fontSizeScale == 1) return size;
|
||||
let sizeNumber = parseInt(size);
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 24;
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(size);
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue === nowValue.value) return;
|
||||
nowValue.value = newValue;
|
||||
// this.pushDataToParent();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
let t = this;
|
||||
isDestroy.value = false;
|
||||
nextTick(() => {
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) {
|
||||
nowValue.value = props.modelValue;
|
||||
}
|
||||
// if (nowValue.value !== props.unCheckValue) {
|
||||
// pushDataToParent(false);
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
function boxClick() {
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
emit('click');
|
||||
if (_disabled.value) return;
|
||||
/**
|
||||
* 要点提示:当本组件属性半选中状态时,应该继续保持选中状态,
|
||||
* 并触发change。
|
||||
*/
|
||||
if (_isCheck.value && !_indeterminate.value) {
|
||||
nowValue.value = props.unCheckValue;
|
||||
} else {
|
||||
nowValue.value = props.value;
|
||||
}
|
||||
/**
|
||||
* 当前选中的值,等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param check {boolean} 当前是否选中
|
||||
* @param value {number} 当前选中的值
|
||||
*/
|
||||
emit('change', _isCheck.value, nowValue.value);
|
||||
|
||||
if (_isCheck.value) {
|
||||
pushDataToParent(true);
|
||||
|
||||
} else {
|
||||
removeValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
} else {
|
||||
nowValue.value = props.unCheckValue;
|
||||
}
|
||||
}
|
||||
|
||||
function pushDataToParent(isChange: boolean) {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement == null) return;
|
||||
let parent: InstanceType<typeof tmCheckboxGroup> = pelement!;
|
||||
if (typeof parent?.addItem != 'function') return;
|
||||
parent.addItem(nowValue.value, isChange);
|
||||
}
|
||||
function removeValue() {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement == null) return;
|
||||
let parent: InstanceType<typeof tmCheckboxGroup> = pelement!;
|
||||
if (typeof parent?.removeItem != 'function') return;
|
||||
parent.removeItem(props.value);
|
||||
}
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmCheckboxGroup") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmCheckboxGroup") return parents;
|
||||
return null;
|
||||
}
|
||||
|
||||
watch(parentVal, (newvalue: Array<string | number | boolean>) => {
|
||||
setSelected(newvalue)
|
||||
}, { deep: true })
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</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}`,
|
||||
width: _size,
|
||||
height: _size
|
||||
}">
|
||||
<view :class="['checkboxBoxIcon', 'checkboxBoxIcon_' + (nowValue ? 'on' : 'off')]">
|
||||
<tm-icon color="white" :name="_indeterminate ? 'subtract-line' : icon" :size="iconSize"></tm-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view v-if="_showLabel" class="checkboxLabelBox" :class="[!hiddenCheckbox ? 'checkboxLabelBoxLeftSpace' : '']">
|
||||
<!--
|
||||
默认文本插槽
|
||||
@binding {boolean} checked - 是否选中
|
||||
-->
|
||||
<slot name="label" :checked="_isCheck">
|
||||
<tm-text :font-size="labelFontSize" class="checkboxLabel">{{ _label }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transition-property: background-color;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_on {
|
||||
opacity: 1;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_off {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.checkboxBox {
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkboxLabelBoxLeftSpace {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
font-size: 28rpx;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view @click="onClick" class="tmCol" :style="{ width: _span }">
|
||||
<!-- @slot 默认插槽 -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ComputedRef, inject } from 'vue';
|
||||
/**
|
||||
* @displayName 布局Col
|
||||
* @exportName tm-col
|
||||
* @page tm-row
|
||||
* @category 常用组件
|
||||
* @description 快速排版布局利器,内只能放置在父组件tmRow中
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({
|
||||
name: "TmCol", options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
}
|
||||
});
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 列宽,它是根据row定义的总列宽计算具体的宽度值。
|
||||
*/
|
||||
span: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
})
|
||||
const tmRowColWidth = inject<ComputedRef<number>>('tmRowColWidth', computed((): number => 5));
|
||||
const tmRowColGutter = inject<ComputedRef<string[]>>('tmRowColGutter', computed((): string[] => ['0px', '0px']));
|
||||
const _span = computed(() => {
|
||||
let col = typeof attrs.span == 'string' ? parseInt(attrs.span) : attrs.span
|
||||
let gutter = tmRowColGutter.value[1];
|
||||
if (gutter == '0rpx') {
|
||||
gutter = '0px'
|
||||
}
|
||||
if (col == tmRowColWidth.value) {
|
||||
return `100%`
|
||||
}
|
||||
return `calc(${col / tmRowColWidth.value * 100}% - ${gutter} + ${gutter} / ${tmRowColWidth.value})`;
|
||||
})
|
||||
const onClick = ()=>{
|
||||
emits('click');
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tmCol {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, getCurrentInstance, PropType, computed, inject, onMounted } from 'vue'
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, setTextColorLightByDark, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import tmCollapse from '../tm-collapse/tm-collapse.vue';
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
|
||||
/**
|
||||
* @displayName 折叠面板子组件
|
||||
* @exportName tm-collapse-item
|
||||
* @page tm-collapse
|
||||
* @category 展示组件
|
||||
* @description 可单,可多开,可放置在tm-collapse内
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCollapse' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 唯一标识
|
||||
*/
|
||||
name: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部边线
|
||||
*/
|
||||
showBottomLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleFontSize: {
|
||||
type: [String, Number],
|
||||
default: '30'
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
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: '100'
|
||||
},
|
||||
/**
|
||||
* 标题最多显示几行出现省略号
|
||||
*/
|
||||
titleLines: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 下边线颜色.默认等同全局边线颜色.
|
||||
*/
|
||||
bottomColor: {
|
||||
type: String,
|
||||
default: '#f5f5f5'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const tmCollapseDefaultName = inject('tmCollapseDefaultName', computed((): Array<string | number> => []))
|
||||
const _disabled = computed(() => props.disabled)
|
||||
const _titleFontSize = computed(() => {
|
||||
let fontSize = covetUniNumber(props.titleFontSize, config.unit)
|
||||
if (config.fontSizeScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _titleHeight = computed(() => covetUniNumber(props.titleHeight, config.unit))
|
||||
const _titleColor = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkTitleColor != '') return getDefaultColor(props.darkTitleColor)
|
||||
return '#ffffff'
|
||||
}
|
||||
return getDefaultColor(props.titleColor)
|
||||
})
|
||||
const _activeColor = computed(() => {
|
||||
if (props.activeColor == "") return getDefaultColor(config.color)
|
||||
return getDefaultColor(props.activeColor)
|
||||
})
|
||||
const _color = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkColor != '') return getDefaultColor(props.darkColor)
|
||||
return config.sheetDarkColor
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _bottomColor = computed(()=>{
|
||||
if(props.bottomColor != '') return getDefaultColor(props.bottomColor)
|
||||
if(config.mode == 'dark'){
|
||||
return getDefaultColor(config.borderDarkColor)
|
||||
}
|
||||
return getDefaultColor('#f5f5f5')
|
||||
})
|
||||
const _leftIcon = computed(() => props.leftIcon)
|
||||
const _title = computed(() => props.title)
|
||||
const _isActive = computed(() => tmCollapseDefaultName.value.includes(props.name))
|
||||
const _textMap = computed(() => {
|
||||
let styleMap = {} as Record<string, string>
|
||||
styleMap["fontSize"] = _titleFontSize.value
|
||||
styleMap["color"] = _isActive.value ? _activeColor.value : _titleColor.value
|
||||
styleMap["line-clamp"] = props.titleLines.toString()
|
||||
return styleMap
|
||||
})
|
||||
|
||||
function itemClick() {
|
||||
/**
|
||||
* 项目被点击时触发。
|
||||
* @param name {string} 当前name值
|
||||
*/
|
||||
emit('click', props.name)
|
||||
if(_disabled.value) return;
|
||||
let parent: InstanceType<typeof tmCollapse> | null = findParent(proxy)
|
||||
if (parent == null) return
|
||||
parent.toggleActivate(props.name, true)
|
||||
}
|
||||
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmCollapse") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmCollapse") return parents;
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmCollapseItemBox" :style="{ background: _color }">
|
||||
<view @click="itemClick" class="tmCollapseItem" :style="{ opacity: _disabled ? 0.5 : 1 }">
|
||||
<view class="tmCollapseItemBoxLeft">
|
||||
<tm-icon :size="_titleFontSize" v-if="_leftIcon" :name="_leftIcon"
|
||||
:color="_isActive ? _activeColor : _titleColor" _style="margin-right: 20rpx;"></tm-icon>
|
||||
<!--
|
||||
@slot 左边插槽
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="left" :status="_isActive"></slot>
|
||||
|
||||
<view class="tmCollapseItemBoxTextBox" :style="{ height: _titleHeight }">
|
||||
<view style="flex:1">
|
||||
<!--
|
||||
@slot 标题插槽,如果你要完全自定标题样式请在此插槽内布局
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="title" :status="_isActive">
|
||||
<text class="tmCollapseItemBoxText" :style="_textMap">
|
||||
{{ _title }}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="tmCollapseItemBoxRight">
|
||||
<!--
|
||||
@slot 右边插槽
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="right" :status="_isActive"></slot>
|
||||
<tm-icon :color="_isActive ? _activeColor : '#bfbfbf'" _style="margin-left: 20rpx;"
|
||||
:name="_isActive ? 'arrow-down-s-line' : 'arrow-right-s-line'"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmCollapseItemWrap" :class="[!_isActive ? 'tmCollapseItemWrapOff' : 'tmCollapseItemWrapOn']">
|
||||
<view class="tmCollapseItemContent">
|
||||
<!--
|
||||
@slot 默认内容插槽。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{height:_isActive?'20rpx':'0px'}"</view>
|
||||
<view v-if="showBottomLine" :style="{height:'1px',background:_bottomColor}"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmCollapseItemBoxTextBox {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxText {
|
||||
text-overflow: ellipsis;
|
||||
/* #ifndef APP */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.tmCollapseItemContent {
|
||||
/* padding: 12px 0rpx; */
|
||||
box-sizing: border-box;
|
||||
min-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmCollapseItemBox {
|
||||
/* background-color: white; */
|
||||
padding: 0px 12px;
|
||||
}
|
||||
|
||||
.tmCollapseItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrap {
|
||||
/* display: flex;
|
||||
flex-direction: column; */
|
||||
transition-property: all;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrapOff {
|
||||
grid-template-rows: 0fr;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrapOn {
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, onBeforeMount, PropType, computed } from 'vue'
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, setTextColorLightByDark, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 折叠面板
|
||||
* @exportName tm-collapse
|
||||
* @category 展示组件
|
||||
* @description 可单,可多开,内部可放置tm-collapse-item子节点组件,动画采用了新css,兼容性请看这里:[点我](https://caniuse.com/?search=grid-template-rows)
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCollapse' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前子项值。可v-model,类型兼容性高,因此不管你提供的是不是数组,最终同步时会被设置成数组
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Array<string | number>, String, Number],
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 是否允许打开多个。
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 变换时触发
|
||||
* @param {String[]} value - 当前打开的值
|
||||
*/
|
||||
'change',
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
|
||||
const activeName = ref<Array<string | number>>([])
|
||||
|
||||
onBeforeMount(() => {
|
||||
toggleActivate(props.modelValue)
|
||||
})
|
||||
|
||||
provide('tmCollapseDefaultName', computed((): Array<string | number> => activeName.value))
|
||||
|
||||
watch(() => props.modelValue, (newValue: Array<string | number> | string | number) => {
|
||||
if(newValue == activeName.value.join('')) return;
|
||||
if(Array.isArray(newValue)){
|
||||
if(newValue.join('') == activeName.value.join('')) return;
|
||||
}
|
||||
toggleActivate(newValue)
|
||||
})
|
||||
/**
|
||||
* 切换选中
|
||||
*/
|
||||
const toggleActivate = (name: Array<string | number> | string | number, isEmits = false) => {
|
||||
let ar = [] as Array<string | number>
|
||||
if (Array.isArray(name)) {
|
||||
ar = name
|
||||
} else if(name !==''&&name!=undefined&&name!=null) {
|
||||
ar = [name]
|
||||
}
|
||||
|
||||
if (ar.length == 0) {
|
||||
activeName.value = []
|
||||
} else {
|
||||
if (props.multiple) {
|
||||
let diff = activeName.value.filter((item: string | number) => {
|
||||
return ar.indexOf(item) == -1
|
||||
})
|
||||
let diffname = ar.filter((item: string | number) => {
|
||||
return activeName.value.indexOf(item) == -1
|
||||
})
|
||||
activeName.value = [...diff, ...diffname].slice(0)
|
||||
} else {
|
||||
if (activeName.value.indexOf(ar[0]) != -1) {
|
||||
activeName.value = []
|
||||
} else {
|
||||
activeName.value = [ar[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmits) {
|
||||
let covervalue:string|number|Array<string|number> = activeName.value.slice(0)
|
||||
emit('update:modelValue', covervalue)
|
||||
emit('change', covervalue)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ toggleActivate })
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<!--
|
||||
@slot 默认插槽,可放置tm-collapse-item子节点
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,470 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, type PropType } from 'vue';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { tmDate, type tmDateTypeTime, createDate } from '../../libs/tmDate';
|
||||
import pickerItem from './../tm-picker-view/picker-item.vue';
|
||||
type PICKER_ITEM_INFO = Record<string, any>
|
||||
|
||||
/**
|
||||
* @displayName 日期选择容器
|
||||
* @exportName tm-date-view
|
||||
* @page tm-picker-date
|
||||
* @category 表单组件
|
||||
* @description 如果需要弹层式用tm-picker-date组件,这个是展示型.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmDateView' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type coverValue = {
|
||||
value: string[],
|
||||
str: string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
/**
|
||||
* @name 日期选择器 tmPickerDate
|
||||
* @description 日期选择,可以控制显示精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
|
||||
* 使用时,建议不要显示过多年份以防卡太多数据。
|
||||
* @page /pages/index/picker-date
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
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有效
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD"
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => ['年', '月', '日', '时', '分', '秒']
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const nowValue = ref<string[]>([]);
|
||||
const _modelValueIndex = ref<number[]>([])
|
||||
const nowValueStr = ref('');
|
||||
const startDate = ref(new tmDate().subtraction(1, 'y'));
|
||||
const endDate = ref(new tmDate());
|
||||
const dateList = ref<PICKER_ITEM_INFO[][]>([]);
|
||||
const _start_date = computed(() => props.start ? new tmDate(props.start) : startDate.value);
|
||||
const _end_date = computed(() => props.end ? new tmDate(props.end) : endDate.value);
|
||||
const _getDateType = computed((): tmDateTypeTime => {
|
||||
let isType: tmDateTypeTime = 's';
|
||||
if (props.type == 'year') isType = 'y';
|
||||
if (props.type == 'month') isType = 'm';
|
||||
if (props.type == 'day') isType = 'd';
|
||||
if (props.type == 'hour') isType = 'h';
|
||||
if (props.type == 'minute') isType = 'M';
|
||||
return isType;
|
||||
});
|
||||
|
||||
const defaultModelvalue = (newvalue: string, showStr: boolean) => {
|
||||
let isType = _getDateType.value;
|
||||
let selfnowValue = new tmDate(newvalue);
|
||||
if (selfnowValue.isBetweenOf(_start_date.value, '<=', isType)) {
|
||||
selfnowValue = _start_date.value;
|
||||
}
|
||||
if (selfnowValue.isBetweenOf(_end_date.value, '>=', isType)) {
|
||||
|
||||
selfnowValue = _end_date.value;
|
||||
}
|
||||
|
||||
let stp = getRangByDateTime(selfnowValue);
|
||||
nowValue.value = stp.value;
|
||||
nowValueStr.value = stp.str;
|
||||
dateList.value = getTimeTreeByStartAndEnd(_start_date.value, _end_date.value);
|
||||
_modelValueIndex.value = getIndexsByids(stp.value)
|
||||
|
||||
if (showStr) {
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
}
|
||||
}
|
||||
|
||||
const getIndexsByids = (ids: string[]) => {
|
||||
let indexs = [] as number[];
|
||||
for (let i = 0; i < dateList.value.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
let index = item.findIndex(el => parseInt(el.id) == parseInt(ids[i]))
|
||||
indexs.push(Math.max(0, index))
|
||||
}
|
||||
return indexs
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const getTimeTreeByStartAndEnd = (start: tmDate, end: tmDate): PICKER_ITEM_INFO[][] => {
|
||||
let startCopy = start.getClone();
|
||||
let nowDate = new tmDate(nowValueStr.value)
|
||||
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: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
|
||||
function getD(type: string, s: number, n: number) {
|
||||
if (type == 'm') {
|
||||
|
||||
for (let i = s; i <= n; i++) {
|
||||
months.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i + 1)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'd') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
days.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'h') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
hours.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'M') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
minutes.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 's') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
seconds.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getDnumber(type: tmDateTypeTime, target: tmDateTypeTime): 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 = 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, getNowTypeLenIndex()) as PICKER_ITEM_INFO[][];
|
||||
}
|
||||
const indexToHex = (i: number) => {
|
||||
let n = i.toString()
|
||||
if (n.length == 1) return '0' + n;
|
||||
return n;
|
||||
}
|
||||
const getRangByDateTime = (d: tmDate) => {
|
||||
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;
|
||||
}
|
||||
|
||||
const getNowTypeLenIndex = () => {
|
||||
let index = 6
|
||||
if (props.type == 'year') {
|
||||
index = 1;
|
||||
} else if (props.type == 'month') {
|
||||
index = 2;
|
||||
} else if (props.type == 'day') {
|
||||
index = 3;
|
||||
} else if (props.type == 'hour') {
|
||||
index = 4;
|
||||
} else if (props.type == 'minute') {
|
||||
index = 5;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
const fillNumber = (n: string) => {
|
||||
if (parseInt(n) > 9) return n;
|
||||
return "0" + n
|
||||
}
|
||||
const stringArValuCoverToString = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
|
||||
let newsday = new tmDate(nowValue.value[0] + "-" + (parseInt(nowValue.value[1]) + 1).toString() + "-1")
|
||||
let days = parseInt(nowValue.value[2])
|
||||
days = days >= newsday.getMonthCountDay() ? newsday.getMonthCountDay() : days
|
||||
nowValue.value.splice(2, 1, days.toString())
|
||||
|
||||
return fillNumber(nowValue.value[0]) + "-" + fillNumber((parseInt(nowValue.value[1]) + 1).toString())
|
||||
+ "-" + fillNumber(nowValue.value[2])
|
||||
+ " " + fillNumber(nowValue.value[3]) + ":"
|
||||
+ fillNumber(nowValue.value[4]) + ":"
|
||||
+ fillNumber(nowValue.value[5])
|
||||
}
|
||||
|
||||
|
||||
const change = (ixs: number[]) => {
|
||||
let nowvalstrs = [] as string[]
|
||||
for (let i = 0; i < ixs.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
nowvalstrs.push(item[ixs[i]].title)
|
||||
}
|
||||
|
||||
let str = nowvalstrs.join('/');
|
||||
let stp = getRangByDateTime(new tmDate(str));
|
||||
|
||||
defaultModelvalue(stp.str, true)
|
||||
|
||||
onConfirm()
|
||||
}
|
||||
|
||||
|
||||
const formatTimeDate = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
let sp = props.format;
|
||||
sp = sp.replace(/YYYY/g, fillNumber(nowValue.value[0]))
|
||||
sp = sp.replace(/MM/g, fillNumber((parseInt(nowValue.value[1]) + 1).toString()))
|
||||
sp = sp.replace(/DD/g, fillNumber(nowValue.value[2]))
|
||||
sp = sp.replace(/hh/g, fillNumber(nowValue.value[3]))
|
||||
sp = sp.replace(/mm/g, fillNumber(nowValue.value[4]))
|
||||
sp = sp.replace(/ss/g, fillNumber(nowValue.value[5]))
|
||||
return sp;
|
||||
}
|
||||
const onConfirm = () => {
|
||||
nowValueStr.value = stringArValuCoverToString();
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValueStr.value);
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
|
||||
emit('change', nowValueStr.value)
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == '') return;
|
||||
let isType = _getDateType.value;
|
||||
if (new tmDate(newvalue).isBetweenOf(new tmDate(nowValueStr.value), '=', isType)) return;
|
||||
defaultModelvalue(new tmDate(newvalue).format('YYYY/MM/DD HH:mm:ss'), true);
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
let nowValue = new tmDate(props.modelValue);
|
||||
defaultModelvalue(nowValue.format('YYYY/MM/DD HH:mm:ss'), props.modelValue != '');
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<picker-item @change-deep="change" :selected-index="_modelValueIndex" :list="dateList"
|
||||
:cell-units="cellUnits.slice(0, dateList.length)"></picker-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,757 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, onUpdated, nextTick } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
|
||||
/**
|
||||
* @displayName 抽屉
|
||||
* @exportName tm-drawer
|
||||
* @category 反馈组件
|
||||
* @description 提供四个方向的弹出。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmDrawer' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题"
|
||||
},
|
||||
/**
|
||||
* 显示底部操作栏
|
||||
*/
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否显示标题
|
||||
*/
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 取消按钮的文本
|
||||
*/
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消"
|
||||
},
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
*/
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "确认"
|
||||
},
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
/**
|
||||
* 打开方向。
|
||||
*/
|
||||
position: {
|
||||
type: String as PropType<"top" | "bottom" | "left" | "right">,
|
||||
default: "bottom"
|
||||
},
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左右时为内容宽,
|
||||
* 上下时为内容高
|
||||
* 百分比,数字字符或者带单位,或者为auto(根据内容自动高度或者宽高)
|
||||
*/
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: "50%"
|
||||
},
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
/**
|
||||
* 暗黑背景颜色,如果不提供默认读取全局的sheet配置
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否禁用内部的scroll标签
|
||||
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
|
||||
* 但如果没有指定高,内容自动的话,高是自动的。
|
||||
*/
|
||||
disabledScroll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 内容区域左右和下的边距。
|
||||
*/
|
||||
contentMargin: {
|
||||
type: String,
|
||||
default: '24'
|
||||
},
|
||||
/**
|
||||
* 宽屏时是否让内容剧中显示
|
||||
* 并限制其它宽为屏幕宽,只展示中间内容以适应宽屏。
|
||||
* 注意只有top,bottom才会生效。
|
||||
*/
|
||||
widthCoverCenter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 距离顶部的偏移量
|
||||
*/
|
||||
offsetTop: {
|
||||
type: [String, Number],
|
||||
default: '0'
|
||||
},
|
||||
/**
|
||||
* 弹层的层级
|
||||
*/
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 1100
|
||||
},
|
||||
/**
|
||||
* 懒加载
|
||||
* 为了解决业务布局节点超多时,你可能需要内容延迟加载以免阻塞动画流畅度.
|
||||
* 如果你启用了lazy,每天次打开时,动画执行后才会显示内容.这样动画就流畅,不会因为节点过多造成的卡.
|
||||
*/
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 关闭前执行,可以是异步函数,只有当用户点击confirm时才会触发。如果返回false将阻止关闭。true允许关闭。
|
||||
*/
|
||||
beforeClose: {
|
||||
type: [Function, Boolean] as PropType<() => Promise<boolean>>,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show',
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
'confirm'
|
||||
])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const actinon = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("tmDrawer" + getUid())
|
||||
const wrapId = ref("tmDrawerWrap" + getUid())
|
||||
const first = ref(true)
|
||||
const tid = ref(10)
|
||||
const tid2 = ref(0)
|
||||
const windtop = ref(0)
|
||||
const windtopReal = ref(0)
|
||||
const tantiaoTrue = ref(false)
|
||||
const safeFooterHeight = ref(0)
|
||||
const lezyShowModal = ref(props.lazy ? false : true)
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (newVal) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
})
|
||||
|
||||
const _lazy = computed(() => props.lazy)
|
||||
const _customStyle = computed(() => props.customStyle)
|
||||
const _show = computed(() => props.show)
|
||||
const _widthCoverCenter = computed(() => props.widthCoverCenter)
|
||||
const _showClose = computed(() => props.showClose)
|
||||
const _duration = computed(() => props.duration)
|
||||
const _position = computed(() => props.position)
|
||||
const _title = computed(() => props.title)
|
||||
const _showTitle = computed(() => props.showTitle)
|
||||
const _round = computed(() => {
|
||||
let round = props.round
|
||||
if (round == "") {
|
||||
round = config.drawerRadius
|
||||
}
|
||||
let radius = covetUniNumber(round, config.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(() => {
|
||||
let offset = covetUniNumber(props.offsetTop, config.unit)
|
||||
let x = parseFloat(offset)
|
||||
let unit = getUnit(offset)
|
||||
if (unit == 'rpx') {
|
||||
x = uni.upx2px(x)
|
||||
}
|
||||
return x
|
||||
})
|
||||
const _size = computed(() => covetUniNumber(props.size, config.unit))
|
||||
const _contentMargin = computed(() => covetUniNumber(props.contentMargin, config.unit))
|
||||
const _showFooter = computed(() => props.showFooter)
|
||||
const _maxHeight = computed(() => {
|
||||
if (props.maxHeight == "") return ""
|
||||
if (props.position == 'left' || props.position == 'right') return ""
|
||||
|
||||
return covetUniNumber(props.maxHeight, config.unit)
|
||||
})
|
||||
const _showCancel = computed(() => props.showCancel)
|
||||
const _cancelText = computed(() => props.cancelText)
|
||||
const _confirmText = computed(() => props.confirmText)
|
||||
const _animationFun = computed(() => config.animation)
|
||||
const _bgColor = computed(() => {
|
||||
let bgcolor = props.bgColor
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkBgColor != '') {
|
||||
bgcolor = props.darkBgColor
|
||||
} else {
|
||||
bgcolor = config.sheetDarkColor
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
})
|
||||
const __height = computed(() => {
|
||||
let h = '100%'
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtopReal.value}px)`
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
if (_offset.value > 0) {
|
||||
h = _height.value + 'px'
|
||||
}
|
||||
// #endif
|
||||
return h
|
||||
})
|
||||
const _titleFontSize = computed(() => (config.fontSizeScale * 16).toString() + 'px')
|
||||
const _isDark = computed(() => config.mode == 'dark')
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
lezyShowModal.value = props.lazy ? false : true
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifdef WEB
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight
|
||||
|
||||
windtop.value = sys.windowTop + (_position.value == 'bottom' ? 0 : _offset.value)
|
||||
windtopReal.value = _position.value == 'bottom' ? (sys.windowTop + _offset.value) : sys.windowTop
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight
|
||||
windtop.value = _offset.value
|
||||
// #endif
|
||||
safeFooterHeight.value = sys.safeAreaInsets.bottom == 0 ? 16 : sys.safeAreaInsets.bottom
|
||||
if (props.show) {
|
||||
showAlert()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
clearTimeout(tid2.value)
|
||||
})
|
||||
|
||||
const cancelEvt = () => {
|
||||
emit('cancel')
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const confirmEvt = async () => {
|
||||
emit('confirm')
|
||||
let isPass = true;
|
||||
if (typeof props.beforeClose == 'function') {
|
||||
isLoading.value = true;
|
||||
// @ts-ignore
|
||||
isPass = await props.beforeClose();
|
||||
}
|
||||
isLoading.value = false;
|
||||
if (!isPass) {
|
||||
return;
|
||||
}
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
|
||||
const onClickOverflowy = (evt: Event) => {
|
||||
evt.stopPropagation()
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
emit("click")
|
||||
if (isLoading.value) return
|
||||
if (!props.overlayClick) {
|
||||
clearTimeout(tid2.value)
|
||||
tantiaoTrue.value = true
|
||||
tid2.value = setTimeout(() => {
|
||||
tantiaoTrue.value = false
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
closeAlert()
|
||||
|
||||
}
|
||||
|
||||
const closeAlert = () => {
|
||||
if (actinon.value || isLoading.value) return
|
||||
if (status.value == 'close') return
|
||||
actinon.value = true
|
||||
status.value = 'close'
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emit('beforeClose')
|
||||
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}
|
||||
|
||||
const showAlert = () => {
|
||||
if (actinon.value) return
|
||||
if (status.value == 'open') return
|
||||
showOverflay.value = true
|
||||
actinon.value = true
|
||||
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emit('beforeOpen')
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
status.value = 'open'
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
showAlert();
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
actinon.value = false;
|
||||
|
||||
if (status.value === 'close') {
|
||||
showOverflay.value = false;
|
||||
emit('close');
|
||||
emit('update:show', false);
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = false;
|
||||
}
|
||||
} else {
|
||||
emit('open');
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function maskerMove(evt: TouchEvent) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer">
|
||||
<!--
|
||||
标签触发显示遮罩,免于使用变量控制
|
||||
@slot {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="show"></slot>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport to="uni-app">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
<view @click="onClickOverflowy" @touchmove="maskerMove" v-if="showOverflay" :id="id" ref="tmDrawerWrap"
|
||||
class="tmDrawerWrap" :class="[
|
||||
status == 'open' ? 'tmModalWrap_on' : 'tmModalWrap_off',
|
||||
(_position == 'top' || _position == 'bottom') && _widthCoverCenter ? 'tmDrawerWrapContentMinwidthWrapDir' : '',
|
||||
'tmDrawerWrap_' + _position
|
||||
]" :style="[{
|
||||
width: '100%',
|
||||
top: (windtop + 'px'),
|
||||
height: __height,
|
||||
zIndex: zIndex,
|
||||
'transition-timing-function': _animationFun
|
||||
}, _customStyle]">
|
||||
|
||||
<view @click.stop="" ref="tmDrawerWrapContent" class="tmDrawerWrapContent" :class="[
|
||||
(_position == 'top' || _position == 'bottom') && _widthCoverCenter ? 'tmDrawerWrapContentMinwidth' : '',
|
||||
'tmDrawerWrapContent_' + _position + (status == 'open' ? '_on' : ''),
|
||||
tantiaoTrue ? 'tmModalWrapContent_Tantiao' : ''
|
||||
]" :id="wrapId" :style="{
|
||||
width: _position == 'left' || _position == 'right' ? _size : '100%',
|
||||
height: _position == 'left' || _position == 'right' ? '100%' : _size,
|
||||
borderRadius: _round,
|
||||
maxHeight: _maxHeight != '' ? _maxHeight : '100%',
|
||||
minHeight: _size == 'auto' ? '350rpx' : '0px',
|
||||
'transition-timing-function': _animationFun,
|
||||
backgroundColor: _bgColor
|
||||
}">
|
||||
|
||||
<view v-if="_showClose" class="tmDrawerXclose">
|
||||
<tm-icon @click="closeAlert" color="#dcdcdc" size="52" name="close-circle-fill"></tm-icon>
|
||||
</view>
|
||||
<view>
|
||||
<view v-if="_showTitle" class="tmDrawerTitleBox">
|
||||
<!--
|
||||
标题插槽
|
||||
@slot {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="show">
|
||||
<text
|
||||
:style="{ fontSize: _titleFontSize, color: _isDark ? 'white' : 'black', opacity: '0.64' }"
|
||||
class="tmDrawertitleBox">{{ _title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!disabledScroll && _size != 'auto'" style="flex: 1;" class="tmModalWrapBoxContent">
|
||||
<scroll-view style="height:100%;position:absolute;width:100%;" :scroll-y="true"
|
||||
:rebound="false">
|
||||
<view :style="{ padding: `0px ${_contentMargin} 0px ${_contentMargin}` }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view v-else
|
||||
:style="{ height: '100%', padding: `0px ${_contentMargin} 0px ${_contentMargin}` }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
|
||||
|
||||
<view v-if="showFooter && lezyShowModal" class="tmDrawerFooter"
|
||||
:style="{ backgroundColor: _bgColor }">
|
||||
<view :style="{ height: _contentMargin }"></view>
|
||||
|
||||
<!--
|
||||
@slot 底部操作栏
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view
|
||||
style="flex-direction: row;align-items: center;justify-content: center;display:flex;">
|
||||
<tm-button :loading="isLoading" width="0px" @click="cancelEvt" v-if="_showCancel"
|
||||
skin="thin" style="margin-right: 16px;flex:1">{{ _cancelText }}</tm-button>
|
||||
<tm-button :loading="isLoading" width="0px" @click="confirmEvt" style="flex:1">{{
|
||||
_confirmText
|
||||
}}</tm-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 lang="scss" scoped>
|
||||
.tmDrawerFooter {
|
||||
width: 100%;
|
||||
padding: 0 24rpx 0px 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmDrawerXclose {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 18rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tmDrawerTitleBox {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tmDrawertitleBox {
|
||||
max-width: 175px;
|
||||
overflow: hidden;
|
||||
line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
}
|
||||
|
||||
.tmDrawerWrap_bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContentMinwidth {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContentMinwidthWrapDir {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_bottom {
|
||||
transform: translate(0%, 100%);
|
||||
}
|
||||
|
||||
@keyframes bottomAnimate3d {
|
||||
|
||||
0% {
|
||||
transform: translate(0%, 100%) translate3d(0%, 20px, 0px) rotateX(45deg)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0%, 0%) translate3d(0, 0, 0px) rotateX(0deg)
|
||||
}
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_bottom_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_top {
|
||||
transform: translate(0%, -100%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_top_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_left {
|
||||
transform: translate(-100%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_left_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_right {
|
||||
transform: translate(100%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_right_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrap {
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
transition-duration: 350ms;
|
||||
transition-property: background-color;
|
||||
perspective: 1000px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.tmModalWrap_on {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.tmModalWrap_off {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.tmModalWrapBoxContent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes tantiao {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.tmModalWrapContent_Tantiao {
|
||||
animation: tantiao 0.15s linear;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
// @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 => {
|
||||
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,12 @@
|
||||
import { use } from 'echarts/lib/extension.js';
|
||||
export * from 'echarts/lib/export/core.js';
|
||||
import { install as CanvasRenderer } from 'echarts/lib/renderer/installCanvasRenderer.js';
|
||||
import { install as LineChart } from 'echarts/lib/chart/line/install.js';
|
||||
import { install as BarChart } from 'echarts/lib/chart/bar/install.js';
|
||||
import { install as PieChart } from 'echarts/lib/chart/pie/install.js';
|
||||
import { install as GridSimpleComponent } from 'echarts/lib/component/grid/installSimple.js';
|
||||
import { install as AriaComponent } from 'echarts/lib/component/aria/install.js';
|
||||
import { install as DatasetComponent } from 'echarts/lib/component/dataset/install.js';
|
||||
use([CanvasRenderer]);
|
||||
use([LineChart, BarChart, PieChart]);
|
||||
use([GridSimpleComponent, AriaComponent, DatasetComponent]);
|
||||
@@ -0,0 +1,397 @@
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { computed, ref, onMounted, watch, PropType, getCurrentInstance, nextTick } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import WxCanvas from './canvasinit';
|
||||
// @ts-ignore
|
||||
import * as echarts from "./simple.js";
|
||||
import { ECharts } from "echarts";
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
let canvasId = ref("canvasId")
|
||||
// #ifdef H5 || APP-PLUS || APP-VUE
|
||||
canvasId.value = "tm" + new Date().getTime().toString();
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* @displayName 百度图表
|
||||
* @exportName tm-echart
|
||||
* @category 展示组件
|
||||
* @description 可以通过onInt或者ref函数getChart取得ECharts对象自行处理和设置数据,这个组件使用会超过530kb+大小,请小心你的包大小限制,超过大小哦.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmF2' });
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 初始成功触发
|
||||
* @param {Cart} chart对象
|
||||
*/
|
||||
'onInit',
|
||||
/**
|
||||
* 图表元素被点击时触发
|
||||
* @param {Object} chart的事件对象参数
|
||||
*/
|
||||
'click'
|
||||
])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 宽度css合法值,不要用auto
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
},
|
||||
/**
|
||||
* 高度,css合法值,不要用auto,需要一个值,单位随意,不带单位默认是rpx
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '300px'
|
||||
}
|
||||
})
|
||||
|
||||
const _width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
const _height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
let ctx: UniApp.CanvasContext;
|
||||
let ctxNode: any;
|
||||
let chart: echarts.ECharts | null = null
|
||||
const pixelRatio = uni.getSystemInfoSync().pixelRatio
|
||||
const is2d = ref(false)
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-QQ
|
||||
is2d.value = true
|
||||
// #endif
|
||||
const isPc = ref(false)
|
||||
|
||||
isPc.value = uni.getSystemInfoSync().deviceType == 'pc' ? true : false;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (is2d.value) {
|
||||
setTimeout(() => MpWeix_init(), 100)
|
||||
} else {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
setTimeout(() => appvueH5Other(), 50)
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
//appvue,h5,和其它平台。
|
||||
async function appvueH5Other(fun: Function) {
|
||||
const { width, height } = await getDomSize()
|
||||
echarts.registerPreprocessor((option: any) => {
|
||||
if (option && option.series) {
|
||||
if (option.series.length > 0) {
|
||||
option.series.forEach((series: echarts.SeriesOption) => {
|
||||
series.progressive = 0;
|
||||
});
|
||||
} else if (typeof option.series === 'object') {
|
||||
option.series.progressive = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
ctx = uni.createCanvasContext(canvasId.value, proxy);
|
||||
|
||||
if (!isPc.value) {
|
||||
const canvas: any = new WxCanvas(ctx, canvasId.value, false, false)
|
||||
echarts.setPlatformAPI({ createCanvas: () => canvas });
|
||||
chart = echarts.init(canvas, "", {
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
} else {
|
||||
const canvasNode: HTMLCanvasElement | undefined = document.querySelector('#' + canvasId.value)?.getElementsByTagName("canvas")[0];
|
||||
document.querySelector('#' + canvasId.value)?.removeChild(document.querySelector('#' + canvasId.value)?.getElementsByTagName("div")[0])
|
||||
ctx = canvasNode?.getContext("2d")
|
||||
const canvas: any = new WxCanvas(ctx, canvasId.value, false, false)
|
||||
chart = echarts.init(canvasNode);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
}
|
||||
|
||||
emits("onInit", chart)
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
}
|
||||
//支付宝和微信,QQ 支持2d渲染。
|
||||
async function MpWeix_init(fun: Function) {
|
||||
const { width, height } = await getDomSize()
|
||||
|
||||
echarts.registerPreprocessor((option: any) => {
|
||||
if (option && option.series) {
|
||||
if (option.series.length > 0) {
|
||||
option.series.forEach((series: echarts.SeriesOption) => {
|
||||
series.progressive = 0;
|
||||
});
|
||||
} else if (typeof option.series === 'object') {
|
||||
option.series.progressive = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const query = uni.createSelectorQuery().in(proxy)
|
||||
// #ifdef MP-ALIPAY
|
||||
query.select('#canvasId').node().exec((res2) => {
|
||||
const canvasNode = res2[0].node;
|
||||
|
||||
let ctxvb: UniApp.CanvasContext = canvas.getContext('2d');
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctx = ctxvb;
|
||||
ctxNode = canvasNode
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-QQ
|
||||
query.select('#canvasId')
|
||||
.fields({
|
||||
node: true,
|
||||
size: true,
|
||||
context: true
|
||||
})
|
||||
.exec((res) => {
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const canvasNode = res[0].node
|
||||
const ctxvb = canvasNode.getContext('2d')
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctxvb.scale(pixelRatio, pixelRatio)
|
||||
ctx = ctxvb;
|
||||
ctxNode = canvasNode
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
|
||||
const canvasNode = {}
|
||||
const ctxvb = res[0].context
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctxvb.scale(pixelRatio, pixelRatio)
|
||||
ctx = ctxvb;
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
// #endif
|
||||
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
function getChart(): Promise<ECharts> {
|
||||
return new Promise((res, rej) => {
|
||||
if (is2d.value) {
|
||||
setTimeout(() => MpWeix_init(chart => res(chart)), 100)
|
||||
} else {
|
||||
setTimeout(() => appvueH5Other(chart => res(chart)), 50)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getImg() {
|
||||
return new Promise((res, rej) => {
|
||||
if (!ctx) {
|
||||
console.error('tmChart没有初始化')
|
||||
rej('没有初始化')
|
||||
return
|
||||
}
|
||||
uni.canvasToTempFilePath({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: _width.value,
|
||||
height: _height.value,
|
||||
canvasId: canvasId.value,
|
||||
canvas: ctxNode,
|
||||
success: function (respone) {
|
||||
// 在H5平台下,tempFilePath 为 base64
|
||||
res(respone.tempFilePath);
|
||||
},
|
||||
fail: (respone) => {
|
||||
console.error(respone)
|
||||
rej(res);
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getDomSize = (): Promise<{ width: number, height: number }> => {
|
||||
return new Promise((res, rej) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.canvas')
|
||||
.boundingClientRect()
|
||||
.exec((result) => {
|
||||
let node = result[0]
|
||||
res({
|
||||
width: node.width,
|
||||
height: node.height
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
defineExpose({ getChart, getImg })
|
||||
|
||||
|
||||
function touchStart(e: TouchEvent) {
|
||||
if (chart && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mousedown', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'start');
|
||||
// emits('touchStart', e)
|
||||
}
|
||||
}
|
||||
|
||||
function touchMove(e: TouchEvent) {
|
||||
if (chart && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'change');
|
||||
// emits('touchMove', e)
|
||||
}
|
||||
}
|
||||
|
||||
function touchEnd(e: TouchEvent) {
|
||||
if (chart) {
|
||||
const touch = e.changedTouches ? e.changedTouches[0] : {};
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mouseup', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('click', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'end');
|
||||
// emits('touchEnd', e)
|
||||
}
|
||||
}
|
||||
|
||||
function wrapTouch(event: TouchEvent) {
|
||||
for (let i = 0; i < event.touches.length; ++i) {
|
||||
const touch = event.touches[i];
|
||||
touch.offsetX = touch.x;
|
||||
touch.offsetY = touch.y;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped></style>
|
||||
<template>
|
||||
|
||||
<!-- #ifndef MP-WEIXIN || MP-ALIPAY || MP-QQ || APP-NVUE -->
|
||||
<canvas v-if="!isPc" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" :id="canvasId"
|
||||
:canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
|
||||
<canvas v-if="isPc" :id="canvasId" :canvas-id="canvasId" class="canvas"
|
||||
:style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<canvas @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" type="2d" id="canvasId"
|
||||
canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<canvas @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" type="2d" :id="canvasId"
|
||||
:canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance, inject, onBeforeUnmount, watch, type PropType, onMounted } from 'vue';
|
||||
import { defaultValidator } from "./../tm-form/validator"
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import TmForm from '../tm-form/tm-form.vue';
|
||||
import { covetUniNumber } from '../../libs/tool';
|
||||
import { getDefaultColor } from '../../libs/colors';
|
||||
|
||||
/**
|
||||
* @displayName 表单子组件
|
||||
* @exportName tm-form-item
|
||||
* @category 表单组件
|
||||
* @page tm-form
|
||||
* @description 表单组件的校验规则现在统一放到了form组件上,form-item上不再配置校验,主要是方便统一管理校验模块,并且校验函数作了升级处理。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmFormItem" });
|
||||
const { config } = useTmConfig();
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 表单字段唯一标识。
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 必款时的符号,如果为false,或者空则不显示
|
||||
*/
|
||||
required: {
|
||||
type: [String, Boolean],
|
||||
default: "*"
|
||||
},
|
||||
/**
|
||||
* 表单标题
|
||||
*/
|
||||
label: {
|
||||
type: [String],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 方向,这里会覆盖form父上的方向值。
|
||||
*/
|
||||
direction: {
|
||||
type: String as PropType<"horizontal" | "vertical">,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 方向,里会覆盖form父上的值。
|
||||
*/
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 标题字号大小,会覆盖父form值
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标题颜色,会覆盖父form值
|
||||
*/
|
||||
labelFontColor: {
|
||||
type: [String],
|
||||
default: ""
|
||||
},
|
||||
})
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
let tid = 0;
|
||||
const isPass = ref<boolean | null>(null)
|
||||
const errorMessage = ref<string | null>(null)
|
||||
|
||||
const tmFormModelValue = inject("tmFormModelValue", computed((): Record<string, any> => {
|
||||
return {}
|
||||
}))
|
||||
const tmFormVaildType = inject("tmFormVaildType", computed(() => ''))
|
||||
const tmFormDirection = inject("tmFormDirection", computed(() => ''))
|
||||
const tmFormLabelwidth = inject("tmFormLabelwidth", computed(() => ''))
|
||||
const tmFormGap = inject("tmFormGap", computed(() => '0'))
|
||||
const tmFormLabelFontSize = inject("tmFormLabelFontSize", computed(() => '30'))
|
||||
const tmFormLabelFontColor = inject("tmFormLabelFontColor", computed(() => '#333333'))
|
||||
const _tmFormGap = computed(() => covetUniNumber(tmFormGap.value))
|
||||
const _tmFormDirection = computed(() => {
|
||||
if (props.direction) return props.direction
|
||||
if (tmFormDirection.value) return tmFormDirection.value
|
||||
return "horizontal"
|
||||
})
|
||||
const _labelWidth = computed(() => {
|
||||
if (props.labelWidth) return covetUniNumber(props.labelWidth)
|
||||
if (tmFormLabelwidth.value) return covetUniNumber(tmFormLabelwidth.value)
|
||||
return "190rpx"
|
||||
})
|
||||
const _tmFormLabelFontSize = computed(() => {
|
||||
if (props.labelFontSize) return covetUniNumber(props.labelFontSize)
|
||||
if (tmFormLabelFontSize.value) return covetUniNumber(tmFormLabelFontSize.value)
|
||||
return "30rpx"
|
||||
})
|
||||
const _tmFormLabelFontColor = computed(() => {
|
||||
if (props.labelFontColor) return getDefaultColor(props.labelFontColor)
|
||||
if (tmFormLabelFontColor.value) return getDefaultColor(tmFormLabelFontColor.value)
|
||||
return "#333333"
|
||||
})
|
||||
|
||||
const _required = computed(() => props.required)
|
||||
const _label = computed(() => props.label)
|
||||
const _showLabel = computed(() => props.showLabel)
|
||||
const _showBottom = computed(() => props.showBottom)
|
||||
const isFirstVaild = ref(true)
|
||||
watch(() => tmFormModelValue.value[props.name], (val: any) => {
|
||||
if (tmFormVaildType.value != 'valid') return
|
||||
if (isFirstVaild.value) {
|
||||
isFirstVaild.value = false;
|
||||
return
|
||||
}
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(() => {
|
||||
_validFun(val)
|
||||
}, 200);
|
||||
}, { deep: true })
|
||||
|
||||
watch(tmFormVaildType, (val: string) => {
|
||||
if (val == 'reset' || val == '') {
|
||||
isPass.value = null;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
if (val == 'valid') {
|
||||
let value = tmFormModelValue.value[props.name]
|
||||
if (typeof value !== 'undefined') {
|
||||
_validFun(value)
|
||||
}
|
||||
isFirstVaild.value = false;
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const _validFun = (val: any) => {
|
||||
if (!proxy) return;
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) return;
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
let valid = ele._validate(props.name, val)
|
||||
if (typeof valid == 'boolean') {
|
||||
if (valid === true) {
|
||||
isPass.value = true;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!valid.validate) {
|
||||
isPass.value = false;
|
||||
errorMessage.value = valid.message;
|
||||
} else {
|
||||
isPass.value = true;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const setVisibled = (isvisible = true) => {
|
||||
if (!proxy) return;
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) return;
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
ele._setMarker(props.name, isvisible)
|
||||
}
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmForm") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmForm") return parents;
|
||||
return null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setVisibled(true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
setVisibled(false)
|
||||
})
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmFormItem" :style="{ marginBottom: _tmFormGap }">
|
||||
<view class="tmFormItemWrap"
|
||||
:class="[_tmFormDirection == 'horizontal' ? 'tmFormItemWrapRow' : 'tmFormItemWrapCol']">
|
||||
|
||||
<view v-if="_showLabel" class="tmFormItemLabel"
|
||||
:style="{ width: _tmFormDirection == 'horizontal' ? _labelWidth : 'auto', fontSize: _tmFormLabelFontSize, color: _tmFormLabelFontColor }">
|
||||
<text class="tmFormRequired" v-if="_required">{{ _required }}</text>
|
||||
<slot name="label">
|
||||
{{ _label }}
|
||||
</slot>
|
||||
</view>
|
||||
<view class="tmFormItemWrapContent">
|
||||
<!--
|
||||
@slot 表单默认插槽,可以不是直接form-item直接子节点,可以根据自己需要随意布局,只要form-item在里面即可.
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmFormError" v-if="errorMessage && !isPass">
|
||||
{{ errorMessage }}
|
||||
</view>
|
||||
<view v-if="_showBottom" :style="{ marginTop: _tmFormGap, height: '1px', backgroundColor: '#f5f5f5' }"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmFormItemWrapRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmFormItemLabel {
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.tmFormItemWrapContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmFormRequired {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tmFormError {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import { type PropType, computed, nextTick, provide, ref } from 'vue';
|
||||
import { defaultValidator } from "./validator"
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { deepClone } from '../../libs/tool';
|
||||
|
||||
/**
|
||||
* @displayName 表单
|
||||
* @exportName tm-form
|
||||
* @category 表单组件
|
||||
* @description 表单组件的校验规则现在统一放到了form组件上,form-item上不再配置校验,主要是方便统一管理校验模块,并且校验函数作了升级处理。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmForm" });
|
||||
const { config } = useTmConfig();
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 提交表单时触发.
|
||||
* @param {TM.FORM_SUBMIT_RESULT} result 校验结果
|
||||
*/
|
||||
"submit",
|
||||
/**
|
||||
* 表单数据时一定要vmodel绑定,当重置时会触发此事件。
|
||||
*/
|
||||
"update:modelValue"
|
||||
])
|
||||
const props = defineProps({
|
||||
// 表单数据
|
||||
modelValue: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 表单校验规则
|
||||
* 经过验证后,觉得把form-item的rules属性去掉,直接使用form上统一到一起rules属性,这样就可以不用写form-item了.
|
||||
*/
|
||||
rules: {
|
||||
type: Object as PropType<Record<string, Array<TM.FORM_RULE> | TM.FORM_RULE>>,
|
||||
default: (): Record<string, Array<TM.FORM_RULE>> => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 方向,子组件的方向会覆盖此处。
|
||||
*/
|
||||
direction: {
|
||||
type: String as PropType<"horizontal" | "vertical">,
|
||||
default: "horizontal"
|
||||
},
|
||||
/**
|
||||
* 方向,子组件的覆盖此处。
|
||||
*/
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: "160"
|
||||
},
|
||||
/**
|
||||
* 标题字号大小
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
labelFontColor: {
|
||||
type: [String],
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 间隙,项目之间的间距。
|
||||
*/
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: "10"
|
||||
}
|
||||
})
|
||||
const ruleCover = (rule: TM.FORM_RULE): TM.FORM_RULE_TYPE => {
|
||||
return {
|
||||
required: rule?.required ?? false,
|
||||
message: rule?.message ?? "请正确认选择/填写",
|
||||
validator: rule?.validator ?? null,
|
||||
min: rule?.min ?? 1,
|
||||
max: rule?.max ?? -1,
|
||||
type: rule?.type ?? 'auto',
|
||||
rule: rule?.rule ?? null
|
||||
}
|
||||
}
|
||||
const _rules = computed((): Record<string, Array<TM.FORM_RULE_TYPE>> => {
|
||||
let rules: Record<string, Array<TM.FORM_RULE_TYPE>> = {};
|
||||
let keys = Object.keys(props.modelValue)
|
||||
for (let key in props.rules) {
|
||||
if (keys.includes(key)) {
|
||||
let item = props.rules[key];
|
||||
if (Array.isArray(item)) {
|
||||
rules[key] = item.map(item => ruleCover(item))
|
||||
} else if (typeof props.rules[key] == 'object' && !Array.isArray(props.rules[key])) {
|
||||
rules[key] = [ruleCover(item)]
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
})
|
||||
const vaildTypeStr = ref('valid')
|
||||
provide("tmFormModelValue", computed(() => props.modelValue))
|
||||
provide("tmFormVaildType", computed(() => vaildTypeStr.value))
|
||||
provide("tmFormDirection", computed(() => props.direction))
|
||||
provide("tmFormLabelwidth", computed(() => props.labelWidth))
|
||||
provide("tmFormGap", computed(() => props.gap))
|
||||
provide("tmFormLabelFontSize", computed(() => props.labelFontSize))
|
||||
provide("tmFormLabelFontColor", computed(() => props.labelFontColor))
|
||||
const oldbackValue = deepClone(props.modelValue)
|
||||
// 被标记的字段
|
||||
const markedKeys: string[] = []
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const submit = () => {
|
||||
vaildTypeStr.value = ''
|
||||
nextTick(() => {
|
||||
vaildTypeStr.value = 'valid'
|
||||
})
|
||||
emit("submit", validate())
|
||||
}
|
||||
/**
|
||||
* 获取校验结果,不会校验页面.
|
||||
* 如果想触发submit事件,并校验页面提示,请使用ref函数submit()而不是此函数方法.
|
||||
*/
|
||||
const validate = (): TM.FORM_SUBMIT_RESULT => {
|
||||
let result: TM.FORM_SUBMIT_RESULT = {
|
||||
//整体是否校验通过
|
||||
isPass: true,
|
||||
//第一个校验不通过的字段
|
||||
firstValid: null,
|
||||
//校验结果
|
||||
result: []
|
||||
}
|
||||
for (let key of markedKeys) {
|
||||
let rules = _rules.value[key]
|
||||
if (!rules) continue
|
||||
for (let rule of rules) {
|
||||
if (!defaultValidator(props.modelValue[key], rule)) {
|
||||
result.isPass = false;
|
||||
if (result.firstValid == null) {
|
||||
result.firstValid = { key, isPass: false, message: rule.message, data: props.modelValue[key] }
|
||||
}
|
||||
result.result.push({ key, isPass: false, message: rule.message, data: props.modelValue[key] })
|
||||
} else {
|
||||
result.result.push({ key, isPass: true, message: '校验通过', data: props.modelValue[key] })
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 重置表单,数据恢复至最开始的数据.
|
||||
*/
|
||||
const reset = () => {
|
||||
vaildTypeStr.value = 'reset'
|
||||
emit('update:modelValue', deepClone(oldbackValue))
|
||||
}
|
||||
/**
|
||||
* 单个校验,子组件用。(外部请不要使用.)
|
||||
*/
|
||||
const _validate = (name: string, value: any): { validate: boolean, message: string } | boolean => {
|
||||
let rules = _rules.value[name]
|
||||
if (!rules) return true
|
||||
for (let rule of rules) {
|
||||
if (!defaultValidator(value, rule)) {
|
||||
return { validate: false, message: rule.message }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const _setMarker = (name: string, isvisibl: boolean) => {
|
||||
let index = markedKeys.findIndex((el) => el == name);
|
||||
if (isvisibl) {
|
||||
if (index == -1) {
|
||||
markedKeys.push(name)
|
||||
}
|
||||
} else {
|
||||
if (index > -1) {
|
||||
markedKeys.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
submit,
|
||||
validate,
|
||||
reset,
|
||||
_validate,
|
||||
_setMarker
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmForm">
|
||||
<!--
|
||||
@slot 表单默认插槽,可以不是直接form-item直接子节点,可以根据自己需要随意布局,只要form-item在里面即可.
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,82 @@
|
||||
//指定了类型校验
|
||||
const defaultValidatorByType = (value: any, rule: TM.FORM_RULE_TYPE) => {
|
||||
|
||||
//但也有可能是string
|
||||
if (rule.type == 'number') {
|
||||
let val = typeof value == 'string' ? parseFloat(value) : value
|
||||
if (isNaN(value)) return false;
|
||||
if (rule.max == -1) return value >= rule.min
|
||||
return value >= rule.min && value <= rule.max
|
||||
}
|
||||
if (typeof value == 'undefined' || typeof value == null) {
|
||||
return false
|
||||
}
|
||||
if (rule.type == 'string') {
|
||||
let val = value.trim()
|
||||
if (val === '') return false;
|
||||
let len = value.split('').length
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max
|
||||
}
|
||||
if (Array.isArray(value) && rule.type == 'array') {
|
||||
if (value.length == 0) return false;
|
||||
if (rule.max == -1) return value.length >= rule.min
|
||||
return value.length >= rule.min && value.length <= rule.max
|
||||
}
|
||||
|
||||
if (rule.type == 'date') {
|
||||
if (typeof value == 'string') {
|
||||
let date = new Date(value.replace(/-/g, '/')).getTime()
|
||||
return !isNaN(date)
|
||||
} else if (typeof value == 'number') {
|
||||
let date = new Date(value).getTime()
|
||||
return !isNaN(date)
|
||||
}
|
||||
}
|
||||
if (rule.type == 'phone') {
|
||||
let val = typeof value == 'string' ? value : value.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 val.match(reg);
|
||||
}
|
||||
if (rule.type == 'email' && typeof value == 'string') {
|
||||
let reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
export const defaultValidator = (value: any, rule: TM.FORM_RULE_TYPE) => {
|
||||
if (!rule.required) return true;
|
||||
if(rule.validator) return rule.validator(value);
|
||||
if(rule.rule) {
|
||||
return rule.rule.test(value);
|
||||
}
|
||||
if (rule.type != 'auto') return defaultValidatorByType(value, rule)
|
||||
if (typeof value == 'number') {
|
||||
if (isNaN(value)) return false;
|
||||
if (rule.max == -1) return value >= rule.min
|
||||
return value >= rule.min && value <= rule.max
|
||||
}
|
||||
if (typeof value == 'undefined' || typeof value == null) {
|
||||
return false
|
||||
}
|
||||
if (typeof value == 'string') {
|
||||
let val = value.trim()
|
||||
if (val === '') return false;
|
||||
let len = value.split('').length
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length == 0) return false;
|
||||
if (rule.max == -1) return value.length >= rule.min
|
||||
return value.length >= rule.min && value.length <= rule.max
|
||||
}
|
||||
|
||||
if (typeof value == 'object') {
|
||||
let keys = Object.keys(value)
|
||||
return keys.length > 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
<template>
|
||||
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
|
||||
<block v-for="(n, i) in childs" v-bind:key="i">
|
||||
<!-- 图片 -->
|
||||
<!-- 占位图 -->
|
||||
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)&&typeof (ctrl[i]<0?opts[2]:opts[1]) == 'string'" class="_img" :style="n.attrs.style"
|
||||
:src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
|
||||
<!-- 显示图片 -->
|
||||
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
|
||||
<img v-if="n.name==='img'&&typeof (n.attrs.src||(ctrl.load?n.attrs['data-src']:'')) == 'string'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
|
||||
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
|
||||
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t"
|
||||
:nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i"
|
||||
@tap.stop="imgTap(n.attrs.sr)" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style"
|
||||
:src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]"
|
||||
:webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore"
|
||||
:image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src)" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]"
|
||||
:data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap(n.attrs.src)"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS && VUE3 -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')"
|
||||
:mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- 文本 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
|
||||
<text v-else-if="n.text" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<text v-else-if="n.name==='br'">\n</text>
|
||||
<!-- 链接 -->
|
||||
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class"
|
||||
hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
|
||||
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
|
||||
</view>
|
||||
<!-- 视频 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style"
|
||||
v-html="n.html" @vplay.stop="play" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted"
|
||||
:object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i"
|
||||
@play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 || APP-PLUS -->
|
||||
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen"
|
||||
:frameborder="n.attrs.frameborder" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
|
||||
<!-- 音频 -->
|
||||
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name"
|
||||
:poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id"
|
||||
:class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
|
||||
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
|
||||
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x"
|
||||
:class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
|
||||
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
|
||||
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
|
||||
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class"
|
||||
:style="tr.attrs.style">
|
||||
<node :childs="tr.children" :opts="opts" />
|
||||
</view>
|
||||
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
|
||||
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class"
|
||||
:style="td.attrs.style">
|
||||
<node :childs="td.children" :opts="opts" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 富文本 -->
|
||||
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f"
|
||||
:user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false"
|
||||
:selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- 继续递归 -->
|
||||
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class"
|
||||
:style="n.f+';'+n.attrs.style">
|
||||
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs"
|
||||
:childs="n2.children" :opts="opts" />
|
||||
</view>
|
||||
<node :imgList="imgs" @imgtap="onimgTop" v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs">
|
||||
// 行内标签列表
|
||||
var inlineTags = {
|
||||
abbr: true,
|
||||
b: true,
|
||||
big: true,
|
||||
code: true,
|
||||
del: true,
|
||||
em: true,
|
||||
i: true,
|
||||
ins: true,
|
||||
label: true,
|
||||
q: true,
|
||||
small: true,
|
||||
span: true,
|
||||
strong: true,
|
||||
sub: true,
|
||||
sup: true
|
||||
}
|
||||
/**
|
||||
* @description 判断是否为行内标签
|
||||
*/
|
||||
module.exports = {
|
||||
isInline: function(tagName, style) {
|
||||
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
import node from './mp-node.vue'
|
||||
export default {
|
||||
name: 'node',
|
||||
options: {
|
||||
// #ifdef MP-WEIXIN
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
addGlobalClass: false
|
||||
// #endif
|
||||
},
|
||||
emits:['imgtap'],
|
||||
data() {
|
||||
return {
|
||||
ctrl: {},
|
||||
imgs:[],
|
||||
// #ifdef MP-WEIXIN
|
||||
isiOS: uni.getSystemInfoSync().system.includes('iOS')
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
attrs: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
childs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
opts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
imgList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
watch:{
|
||||
imgs:{
|
||||
handler(val){
|
||||
this.imgs = val;
|
||||
},
|
||||
deep:true,
|
||||
immediate:true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
||||
// #ifndef (H5 || APP-PLUS) && VUE3
|
||||
node
|
||||
// #endif
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// #ifdef MP-WEIXIN
|
||||
toJSON() {
|
||||
return this
|
||||
},
|
||||
// #endif
|
||||
/**
|
||||
* @description 播放视频事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
play(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
this.root.$emit('play', {
|
||||
source: node.name,
|
||||
attrs: {
|
||||
...node.attrs,
|
||||
src: node.src[this.ctrl[i] || 0]
|
||||
}
|
||||
})
|
||||
// #ifndef APP-PLUS
|
||||
if (this.root.pauseVideo) {
|
||||
let flag = false
|
||||
const id = e.target.id
|
||||
for (let i = this.root._videos.length; i--;) {
|
||||
if (this.root._videos[i].id === id) {
|
||||
flag = true
|
||||
} else {
|
||||
this.root._videos[i].pause() // 自动暂停其他视频
|
||||
}
|
||||
}
|
||||
// 将自己加入列表
|
||||
if (!flag) {
|
||||
const ctx = uni.createVideoContext(id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
)
|
||||
ctx.id = id
|
||||
if (this.root.playbackRate) {
|
||||
ctx.playbackRate(this.root.playbackRate)
|
||||
}
|
||||
this.root._videos.push(ctx)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgTap(e) {
|
||||
|
||||
this.$emit('imgtap', e)
|
||||
// 自动预览图片
|
||||
if (this.opts[5]) {
|
||||
uni.previewImage({
|
||||
// #ifdef MP-WEIXIN
|
||||
showmenu: this.opts[3],
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
enablesavephoto: this.opts[3],
|
||||
enableShowPhotoDownload:this.opts[3],
|
||||
// #endif
|
||||
current:0,
|
||||
urls: [e]
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onimgTop(src){
|
||||
this.$emit('imgtap', src)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片长按
|
||||
*/
|
||||
imgLongTap(e) {
|
||||
// #ifdef APP-PLUS
|
||||
const attrs = this.childs[e.currentTarget.dataset.i].attrs
|
||||
if (this.opts[3] && !attrs.ignore) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['保存图片'],
|
||||
success: () => {
|
||||
const save = path => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success() {
|
||||
uni.showToast({
|
||||
title: '保存成功'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.root.imgList[attrs.i].startsWith('http')) {
|
||||
uni.downloadFile({
|
||||
url: this.root.imgList[attrs.i],
|
||||
success: res => save(res.tempFilePath)
|
||||
})
|
||||
} else {
|
||||
save(this.root.imgList[attrs.i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片加载完成事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgLoad(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
/* #ifndef H5 || (APP-PLUS && VUE2) */
|
||||
if (!this.childs[i].w) {
|
||||
// 设置原宽度
|
||||
this.$set(this.ctrl, i, e.detail.width)
|
||||
} else /* #endif */
|
||||
if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
|
||||
// 加载完毕,取消加载中占位图
|
||||
this.$set(this.ctrl, i, 1)
|
||||
}
|
||||
this.checkReady()
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 检查是否所有图片加载完毕
|
||||
*/
|
||||
checkReady() {
|
||||
if (this.root && !this.root.lazyLoad) {
|
||||
this.root._unloadimgs -= 1
|
||||
if (!this.root._unloadimgs) {
|
||||
setTimeout(() => {
|
||||
this.root.getRect().then(rect => {
|
||||
this.root.$emit('ready', rect)
|
||||
}).catch(() => {
|
||||
this.root.$emit('ready', {})
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 链接点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
linkTap(e) {
|
||||
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
|
||||
const attrs = node.attrs || e
|
||||
const href = attrs.href
|
||||
this.root.$emit('linktap', Object.assign({
|
||||
innerText: this.root.getText(node.children || []) // 链接内的文本内容
|
||||
}, attrs))
|
||||
if (href) {
|
||||
if (href[0] === '#') {
|
||||
// 跳转锚点
|
||||
this.root.navigateTo(href.substring(1)).catch(() => {})
|
||||
} else if (href.split('?')[0].includes('://')) {
|
||||
// 复制外部链接
|
||||
if (this.root.copyLink) {
|
||||
// #ifdef H5
|
||||
window.open(href)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.setClipboardData({
|
||||
data: href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(href)
|
||||
// #endif
|
||||
}
|
||||
} else {
|
||||
// 跳转页面
|
||||
uni.navigateTo({
|
||||
url: href,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: href,
|
||||
fail() {}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 错误事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
mediaError(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
// 加载其他源
|
||||
if (node.name === 'video' || node.name === 'audio') {
|
||||
let index = (this.ctrl[i] || 0) + 1
|
||||
if (index > node.src.length) {
|
||||
index = 0
|
||||
}
|
||||
if (index < node.src.length) {
|
||||
this.$set(this.ctrl, i, index)
|
||||
return
|
||||
}
|
||||
} else if (node.name === 'img') {
|
||||
// #ifdef H5 && VUE3
|
||||
if (this.opts[0] && !this.ctrl.load) return
|
||||
// #endif
|
||||
// 显示错误占位图
|
||||
if (this.opts[2]) {
|
||||
this.$set(this.ctrl, i, -1)
|
||||
}
|
||||
this.checkReady()
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.$emit('error', {
|
||||
source: node.name,
|
||||
attrs: node.attrs,
|
||||
// #ifndef H5 && VUE3
|
||||
errMsg: e.detail.errMsg
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* a 标签默认效果 */
|
||||
._a {
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* a 标签点击态效果 */
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 图片默认效果 */
|
||||
._img {
|
||||
max-width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* 内部样式 */
|
||||
|
||||
._block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
._ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
._ol,
|
||||
._ul {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
._thead,
|
||||
._tbody,
|
||||
._tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
._tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
._td,
|
||||
._th {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
._th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
._ul ._ul {
|
||||
margin: 0;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
._ul ._ul ._ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
._video {
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<view id="_root" :class="(selectable?'_select ':'')+'_root'">
|
||||
<slot v-if="!nodes[0]" />
|
||||
<mp-node :imgList="imgList" v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable,previewImg]" name="span"></mp-node>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted, PropType, onBeforeUnmount, getCurrentInstance, watch, nextTick } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import Parser from "./pare";
|
||||
import mpNode from "./mp-node.vue"
|
||||
|
||||
/**
|
||||
* @displayName HTML
|
||||
* @exportName tm-html
|
||||
* @category 展示组件
|
||||
* @description html渲染组件,这是精简后的不是所有插件都带上容易让组件变大,如果想要全部的,可以去用作者的,它是引用第三方实现见:https://github.com/jin-yufeng/mp-html
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmHtml' });
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits(['load', 'ready', 'imgtap', 'linktap', 'play', 'error'])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 窗口宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 窗口高
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
selectable: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
lazyLoad: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
loadingImg: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
errorImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showImgMenu: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
|
||||
previewImg: {
|
||||
type: [Boolean, Number],
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
const _width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
const _height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
const nodes = ref([])
|
||||
const _videos = ref([])
|
||||
const imgList = ref([])
|
||||
const _in = ref<Record<string, any>>({})
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
|
||||
const inthis = (page, selector, scrollTop) => {
|
||||
if (page && selector && scrollTop) {
|
||||
_in.value = {
|
||||
page,
|
||||
selector,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const navigateTo = (id, offset) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
offset = offset || 0
|
||||
let deep = ' '
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
deep = '>>>'
|
||||
// #endif
|
||||
|
||||
const selector = uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(_in.value ? _in.value.page : proxy)
|
||||
// #endif
|
||||
.select((_in.value ? _in.value.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
|
||||
if (_in.value) {
|
||||
selector.select(_in.value.selector).scrollOffset()
|
||||
.select(_in.value.selector).boundingClientRect()
|
||||
} else {
|
||||
// 获取 scroll-view 的位置和滚动距离
|
||||
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
|
||||
}
|
||||
selector.exec(res => {
|
||||
if (!res[0]) {
|
||||
reject(Error('Label not found'))
|
||||
return
|
||||
}
|
||||
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
|
||||
if (_in.value) {
|
||||
// scroll-view 跳转
|
||||
_in.value.page[_in.value.scrollTop] = scrollTop
|
||||
} else {
|
||||
// 页面跳转
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getText = (nodesa) => {
|
||||
let text = '';
|
||||
(function traversal(nodesa) {
|
||||
for (let i = 0; i < nodesa.length; i++) {
|
||||
const node = nodesa[i]
|
||||
if (node.type === 'text') {
|
||||
text += node.text.replace(/&/g, '&')
|
||||
} else if (node.name === 'br') {
|
||||
text += '\n'
|
||||
} else {
|
||||
// 块级标签前后加换行
|
||||
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
|
||||
if (isBlock && text && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
}
|
||||
// 递归获取子节点的文本
|
||||
if (node.children) {
|
||||
traversal(node.children)
|
||||
}
|
||||
if (isBlock && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
} else if (node.name === 'td' || node.name === 'th') {
|
||||
text += '\t'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(nodesa || nodes.value)
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
const getRect = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(proxy)
|
||||
// #endif
|
||||
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
|
||||
})
|
||||
}
|
||||
|
||||
const pauseMedia = () => {
|
||||
for (let i = (_videos.value || []).length; i--;) {
|
||||
_videos.value[i].pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const setPlaybackRate = (rate) => {
|
||||
for (let i = (_videos.value || []).length; i--;) {
|
||||
_videos.value[i].playbackRate(rate)
|
||||
}
|
||||
}
|
||||
|
||||
const setContent = (content, append = false) => {
|
||||
if (!append || !imgList.value) {
|
||||
imgList.value = []
|
||||
}
|
||||
const par = new Parser({
|
||||
nodes:nodes.value,
|
||||
containerStyle:'',
|
||||
domain:'',
|
||||
useAnchor:0,
|
||||
lazyLoad:props.lazyLoad,
|
||||
pauseVideo:false,
|
||||
setTitle:false,
|
||||
scrollTable:false,
|
||||
selectable:props.selectable,
|
||||
previewImg:props.previewImg
|
||||
})
|
||||
const nodesas = par.parse(content)
|
||||
imgList.value = par.imgList
|
||||
|
||||
|
||||
nodes.value = append ? (nodes.value || []).concat(nodesas) : nodesas
|
||||
_videos.value = []
|
||||
nextTick(() => {
|
||||
emits('load')
|
||||
})
|
||||
|
||||
if (props.lazyLoad || imgList.value._unloadimgs < imgList.value.length / 2) {
|
||||
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
|
||||
let height = 0
|
||||
const callback = rect => {
|
||||
if (!rect || !rect.height) rect = {}
|
||||
// 350ms 总高度无变化就触发 ready 事件
|
||||
if (rect.height === height) {
|
||||
emits('ready', rect)
|
||||
} else {
|
||||
height = rect.height
|
||||
setTimeout(() => {
|
||||
getRect().then(callback).catch(callback)
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
getRect().then(callback).catch(callback)
|
||||
} else {
|
||||
// 未设置懒加载,等待所有图片加载完毕
|
||||
if (!imgList.value._unloadimgs) {
|
||||
getRect().then(rect => {
|
||||
emits('ready', rect)
|
||||
}).catch(() => {
|
||||
emits('ready', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
watch(() => props.value, () => {
|
||||
setContent(props.value)
|
||||
})
|
||||
onMounted(() => {
|
||||
if (props.value && !nodes.value.length) {
|
||||
setContent(props.value)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
._root {
|
||||
padding: 1px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 长按复制 */
|
||||
._select {
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<text
|
||||
@tap="onclick"
|
||||
v-if="!isImg"
|
||||
:class="['tmIcon', `${prefix}${iconName}`, isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{
|
||||
fontSize: iconSize,
|
||||
color: fontColor,
|
||||
fontFamily: fontFamily,
|
||||
fontStyle: 'normal',
|
||||
},
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></text>
|
||||
<image
|
||||
v-if="isImg"
|
||||
@tap="onclick"
|
||||
:src="iconName"
|
||||
:class="['tmIcon', isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{ width: iconSize, height: iconSize },
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></image>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { covetUniNumber } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 图标
|
||||
* @exportName tm-icon
|
||||
* @category 常用组件
|
||||
* @description 图标,可以是图片地址或者字体名称图标.也可以是你自定义的图标.
|
||||
* 如何自定义图标呢:你可以在iconfont网站勾选图标->添加购物车->添加项目->项目设置,勾选woff2,勾选base64->下载本地->解压得到iconfont.css
|
||||
* ->复制到你的app.vue全局中即可.然后使用图标时,font-family是你的图标名称比如myicon,prefix是图标前缀比如myicon-
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmIcon" });
|
||||
const { config } = useTmConfig();
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
"click",
|
||||
]);
|
||||
const attrs = defineProps({
|
||||
_style:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
_class:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
/**
|
||||
* 图标名称,可以是图片地址
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 图标大小,默认32rpx
|
||||
*/
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: "32",
|
||||
},
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "#333333",
|
||||
},
|
||||
/**
|
||||
* 暗黑时的图标颜色
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 图标是否旋转
|
||||
*/
|
||||
spin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
rotate: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**字体图标名称,如果你要显示你自己的名称时,需要改成你自己的图标名称. */
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: "remixicon",
|
||||
},
|
||||
/**图标名称前缀,默认为ri-,没有你可以为空 */
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "ri-",
|
||||
},
|
||||
});
|
||||
|
||||
const iconName = computed(() => attrs.name);
|
||||
const iconSize = computed(() => covetUniNumber(attrs.size, "rpx"));
|
||||
const isImg = computed(() => iconName.value.includes("."));
|
||||
const isSpin = computed(() => attrs.spin);
|
||||
const rotateDeg = computed(() => attrs.rotate);
|
||||
const _style = computed(() => attrs._style);
|
||||
const _class = computed(() => attrs._class);
|
||||
const fontColor = computed(() => {
|
||||
let color = attrs.color;
|
||||
if (config.mode == "dark" && attrs.darkColor) {
|
||||
color = attrs.darkColor;
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
});
|
||||
const onclick = () => emits("click");
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmIcon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
transition: transform 0.3s linear;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.tmIconSpin {
|
||||
animation: tmIconAniSPin 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes tmIconAniSPin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<view @click="prevImage" class="tmImage" :class="[idBox]" ref="tmImage" :id="idBox"
|
||||
:style="{ width: _place_size.width, height: _place_size.height }">
|
||||
<!-- @vue-skip -->
|
||||
<view class="tmImageBox"
|
||||
:style="{ width: _img_box_size.width, height: _img_box_size.height, borderRadius: _round, pointerEvent: 'none' }">
|
||||
<view v-if="isLoading || isError" class="tmImagePlace" :style="{ backgroundColor: _placeBgColor }">
|
||||
<tm-icon :size="iconSize" v-if="isError" color="error" name="landscape-line"></tm-icon>
|
||||
<tm-icon :size="iconSize" v-if="isLoading" name="loader-line" color="primary" :spin="true"></tm-icon>
|
||||
</view>
|
||||
|
||||
<image v-if="!isError" class="tmImageImg" :lazy-load="lazy" :draggable="draggable"
|
||||
:showMenuByLongpress="showMenuByLongpress"
|
||||
:class="[isLoading ? 'tmImageImgAbs' : '', fadeShow ? 'tmImageFadeShow' : '']" :mode="_model"
|
||||
:style="[_styleMap]" :src="_src" @error="imgError">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, onUpdated, nextTick } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
/**
|
||||
* @displayName 图片
|
||||
* @exportName tm-image
|
||||
* @category 展示组件
|
||||
* @description 宽高可以设置,支持百分比,px,rpx,如果你用了auto,%这种就要注意:如果你把组件放到了容器中,容器设置为v-show,display:none,visible这种,
|
||||
* 那你一定要跟随vif组件,不然尺寸是不对的。因为小程序无法监测这种变化的情况,导致无法设置尺寸。如果你设定了固定尺寸则不用担心这个问题。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmBadge' });
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 宽度,默认100%
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 高度,auto,%,rpx,px,string number
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
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<"fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill">,
|
||||
default: "fill"
|
||||
},
|
||||
/**
|
||||
* 点击后是否预览图片
|
||||
*/
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 预览占位比例
|
||||
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
|
||||
* 当图片加载成功后,使用正确的原图片比例设置。
|
||||
* 默认是5/4=1.25
|
||||
*/
|
||||
ratio: {
|
||||
type: Number,
|
||||
default: 1.25
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: '0'
|
||||
},
|
||||
/**
|
||||
* 加载和失败时的图标大小。
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "36"
|
||||
},
|
||||
/**
|
||||
* 占位背景色
|
||||
*/
|
||||
placeBgColor: {
|
||||
type: String,
|
||||
default: "#F5F5F5"
|
||||
},
|
||||
/**
|
||||
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
|
||||
*/
|
||||
placeDarkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示过渡动画
|
||||
*/
|
||||
fadeShow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片懒加载
|
||||
*/
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否允许拖动。
|
||||
*/
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showMenuByLongpress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>();
|
||||
|
||||
const id = ref(`tmImage-${getUid()}`);
|
||||
const idBox = ref(`tmImage-${getUid()}`);
|
||||
const isLoading = ref(true);
|
||||
const isError = ref(false);
|
||||
const reload = ref(0);
|
||||
const imgrealWidth = ref(0);
|
||||
const imgrealHeight = ref(0);
|
||||
const boxWidth = ref(0);
|
||||
const boxHeight = ref(0);
|
||||
|
||||
const isLoaded = ref(false);
|
||||
const tid = ref(0);
|
||||
const androidAndWebUrl = ref("");
|
||||
|
||||
const tid2 = ref(0);
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
|
||||
const _model = computed(() => props.model);
|
||||
const _placeBgColor = computed(() => {
|
||||
let bgcolor = props.placeBgColor;
|
||||
if (config.mode == 'dark') {
|
||||
bgcolor = props.placeDarkBgColor;
|
||||
if (props.placeDarkBgColor == '') {
|
||||
bgcolor = config.inputDarkColor;
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor);
|
||||
});
|
||||
|
||||
const _round = computed(() => covetUniNumber(props.round, config.unit));
|
||||
const _src = computed(() => props.src);
|
||||
|
||||
const _place_size = computed(() => ({
|
||||
width: covetUniNumber(props.width, config.unit),
|
||||
height: covetUniNumber(props.height, config.unit),
|
||||
}));
|
||||
|
||||
|
||||
|
||||
const _img_box_size = computed(() => {
|
||||
if (imgrealHeight.value > 0) {
|
||||
return _img_size.value;
|
||||
}
|
||||
|
||||
let _w = props.width;
|
||||
let _h = props.height;
|
||||
let us_w = covetUniNumber(props.width, config.unit);
|
||||
let us_h = covetUniNumber(props.height, config.unit);
|
||||
|
||||
if (typeof props.width == 'string' && props.width.lastIndexOf('%') > -1 || props.width == 'auto') {
|
||||
us_w = '100%';
|
||||
_w = '100%';
|
||||
}
|
||||
|
||||
if (typeof props.height == 'string' && props.height.lastIndexOf('%') > -1 || props.height == 'auto' || isError.value) {
|
||||
if (boxHeight.value >= 5) {
|
||||
_h = `${boxHeight.value}px`;
|
||||
} else {
|
||||
if (typeof props.width == 'string' &&props.width.lastIndexOf('%') > -1 || props.width == 'auto') {
|
||||
if (props.height == '100%') {
|
||||
_h = `${props.ratio * boxHeight.value}px`;
|
||||
} else {
|
||||
_h = `${props.ratio * boxWidth.value}px`;
|
||||
}
|
||||
} else {
|
||||
_h = `${props.ratio * parseFloat(us_w)}${getUnit(us_w)}`;
|
||||
}
|
||||
}
|
||||
return { width: _w, height: _h };
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h };
|
||||
});
|
||||
|
||||
const _img_size = computed(() => {
|
||||
let us_w = covetUniNumber(props.width, config.unit);
|
||||
let us_h = covetUniNumber(props.height, config.unit);
|
||||
|
||||
if (!isLoaded.value) {
|
||||
return { width: "300px", height: "300px" };
|
||||
}
|
||||
|
||||
if (boxWidth.value > 0) {
|
||||
let ratio = boxWidth.value / imgrealWidth.value;
|
||||
|
||||
if (props.height == 'auto') {
|
||||
us_h = `${ratio * imgrealHeight.value}px`;
|
||||
}
|
||||
|
||||
if (props.width == 'auto') {
|
||||
us_w = `${boxWidth.value}px`;
|
||||
}
|
||||
|
||||
if (typeof props.height == 'string'&&props.height.lastIndexOf('%') > -1) {
|
||||
us_h = `${boxHeight.value}px`;
|
||||
}
|
||||
if (typeof props.width == 'string'&&props.width.lastIndexOf('%') > -1) {
|
||||
us_w = `${boxWidth.value}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h };
|
||||
});
|
||||
|
||||
const _styleMap = computed(() => {
|
||||
let styleMap: Record<string, any> = {}
|
||||
styleMap["width"] = _img_size.value.width
|
||||
styleMap["height"] = _img_size.value.height
|
||||
styleMap["transform"] = isLoading.value ? 'scale(0.1)' : 'scale(1)'
|
||||
styleMap["visibility"] = isLoading.value ? 'visible' : (!isError.value ? 'visible' : 'hidden')
|
||||
styleMap["opacity"] = isLoading.value ? '0' : '1'
|
||||
styleMap["border-radius"] = _round.value
|
||||
return styleMap;
|
||||
});
|
||||
|
||||
const prevImage = () => {
|
||||
emit('click');
|
||||
if (props.preview) {
|
||||
uni.previewImage({
|
||||
current: props.src,
|
||||
urls: [props.src]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const imgLoad = () => {
|
||||
uni.getImageInfo({
|
||||
src: _src.value,
|
||||
fail(_) {
|
||||
console.log('error');
|
||||
isError.value = true;
|
||||
isLoading.value = false;
|
||||
},
|
||||
success(result: any) {
|
||||
isLoading.value = false;
|
||||
isError.value = false;
|
||||
isLoaded.value = true;
|
||||
imgrealWidth.value = result.width;
|
||||
imgrealHeight.value = result.height;
|
||||
androidAndWebUrl.value = result.path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const imgError = () => {
|
||||
isError.value = true;
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const getNodes = (): Promise<any> => {
|
||||
return new Promise((res) => {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".tmImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as UniApp.NodeInfo;
|
||||
boxWidth.value = nodeinfo.width!;
|
||||
boxHeight.value = nodeinfo.height!;
|
||||
res(true)
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
const resize = (): Promise<any> => {
|
||||
isLoaded.value = false;
|
||||
return new Promise((res) => {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".tmImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as UniApp.NodeInfo;
|
||||
boxWidth.value = nodeinfo.width!;
|
||||
boxHeight.value = nodeinfo.height!;
|
||||
isLoaded.value = true;
|
||||
res(true)
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getNodes();
|
||||
imgLoad();
|
||||
|
||||
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value);
|
||||
clearTimeout(tid2.value);
|
||||
});
|
||||
|
||||
watch(() => props.src, () => {
|
||||
imgLoad();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.tmImage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tmImageBox {
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.tmImageImg {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes animateFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tmImageFadeShow {
|
||||
animation: animateFadeIn 0.2s linear forwards;
|
||||
}
|
||||
|
||||
.tmImagePlace {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.tmImageImgAbs {
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,778 @@
|
||||
<template>
|
||||
<view class="tmInput" :style="{
|
||||
width: _width,
|
||||
height: _height,
|
||||
minHeight: _minHeight,
|
||||
color: _fontColor,
|
||||
}">
|
||||
<!--
|
||||
@slot 左侧插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<text v-if="_attrs.leftText !== ''" :style="{ marginRight: '24rpx', fontSize: _fontSize }">{{
|
||||
_attrs.leftText
|
||||
}}</text>
|
||||
</slot>
|
||||
|
||||
<view class="tmInputWrap" :style="[
|
||||
{
|
||||
backgroundColor: _color,
|
||||
fontSize: _fontSize,
|
||||
minHeight: _minHeight,
|
||||
borderRadius: _round,
|
||||
},
|
||||
_focusHighlight
|
||||
|
||||
]" :class="[_attrs.inputClass]">
|
||||
<!--
|
||||
@slot 输入框左侧图标插槽
|
||||
-->
|
||||
<slot name="leftIcon">
|
||||
<tm-icon v-if="_attrs.leftIcon" :size="_attrs.fontSize" _style="margin-left:24rpx" :name="leftIcon"
|
||||
:color="_attrs.iconColor || config.color"></tm-icon>
|
||||
</slot>
|
||||
<view class="tmInputWrapBox"
|
||||
:style="[_attrs.type != 'textarea' ? { height: _minHeight } : { minHeight: _minHeight }]">
|
||||
<input v-if="_attrs.type != 'textarea'" @keyboardheightchange="onkeyboardheightchange"
|
||||
@confirm="onconfirm" @focus="onfocus" @blur="onblur" @input="oninput" :value="nowValue"
|
||||
@change="onchange" :password="_attrs.password && showPasswordEye" :disabled="_attrs.disabled"
|
||||
:maxlength="_attrs.maxlength" :cursorSpacing="_attrs.cursorSpacing" :focus="_attrs.focus"
|
||||
:confirmType="_attrs.confirmType" :confirmHld="_attrs.confirmHold" :cursor="_attrs.cursor"
|
||||
:type="_attrs.type" :cursorColor="_currorColor" :selectionStart="_attrs.selectionStart"
|
||||
:selectionEnd="_attrs.selectionEnd" :adjustPosition="_attrs.adjustPosition"
|
||||
:autoBlur="_attrs.autoBlur" :ignoreCompositionEvent="_attrs.ignoreCompositionEvent"
|
||||
:alwaysEmbed="_attrs.alwaysEmbed" :holdKeyboard="_attrs.holdKeyboard"
|
||||
:safePasswordCertPath="_attrs.safePasswordCertPath" :safePasswordLength="_attrs.safePasswordLength"
|
||||
:safePasswordTimeStamp="_attrs.safePasswordTimeStamp" :safePasswordNonce="_attrs.safePasswordNonce"
|
||||
:safePasswordSalt="_attrs.safePasswordSalt" :safePasswordCustomHash="_attrs.safePasswordCustomHash"
|
||||
:randomNumber="_attrs.randomNumber" :controlled="_attrs.controlled"
|
||||
:alwaysSystem="_attrs.alwaysSystem" :inputmode="_attrs.inputmode" class="tmInputInput tmInputText"
|
||||
:placeholderStyle="`textAlign: ${_attrs.align};fontSize:${_fontSize};` + _attrs.placeholderStyle"
|
||||
:placeholder="_attrs.placeholder" :placeholderClass="_attrs.placeholderClass" :style="[
|
||||
{
|
||||
textAlign: _attrs.align,
|
||||
padding: `0px ${_inputPadding}`,
|
||||
color: _inputFontColor,
|
||||
},
|
||||
_attrs.inputStyle
|
||||
|
||||
]" />
|
||||
<!-- @vue-ignore -->
|
||||
<textarea v-else @keyboardheightchange="onkeyboardheightchange" @confirm="onconfirm" @focus="onfocus"
|
||||
@blur="onblur" @input="oninput" @linechange="onlinechange" :autoHeight="_attrs.autoHeight"
|
||||
:fixed="_attrs.fixed" :showConfirmBar="_attrs.showConfirmBar" :value="nowValue"
|
||||
:disabled="_attrs.disabled" :maxlength="_attrs.maxlength" :cursorSpacing="_attrs.cursorSpacing"
|
||||
:focus="_attrs.focus" :confirmHld="_attrs.confirmHold" :cursor="_attrs.cursor"
|
||||
:cursorColor="_currorColor" :selectionStart="_attrs.selectionStart"
|
||||
:selectionEnd="_attrs.selectionEnd" :adjustPosition="_attrs.adjustPosition"
|
||||
:autoBlur="_attrs.autoBlur" :ignoreCompositionEvent="_attrs.ignoreCompositionEvent"
|
||||
:holdKeyboard="_attrs.holdKeyboard" :inputmode="_attrs.inputmode"
|
||||
:placeholderStyle="`textAlign: ${_attrs.align};fontSize:${_fontSize};` + _attrs.placeholderStyle"
|
||||
:placeholder="_attrs.placeholder" :placeholderClass="_attrs.placeholderClass"
|
||||
class="tmInputInput tmInputTextArea" :style="[
|
||||
{
|
||||
width: 'auto',
|
||||
textAlign: _attrs.align,
|
||||
padding: `${_inputPadding}`,
|
||||
color: _inputFontColor,
|
||||
},
|
||||
_attrs.autoHeight ? { minHeight: _attrs.autoHeight } : { height: _minHeight },
|
||||
_attrs.inputStyle
|
||||
]">
|
||||
|
||||
</textarea>
|
||||
<!-- <view class="tmInputPlaceholder" :class="[showPlaceholder&&!isFocus?'tmInputPlaceholderOn':'tmInputPlaceholderOff',_attrs.placeholderClass]" :style="[
|
||||
{
|
||||
textAlign: _attrs.align,
|
||||
fontSize: _fontSize,
|
||||
lineHeight: _minHeight,
|
||||
padding: `0px ${_inputPadding}`,
|
||||
},
|
||||
_attrs.placeholderStyle,
|
||||
]">
|
||||
{{ _attrs.placeholder }}
|
||||
</view> -->
|
||||
<view class="tmInputShowChar" v-if="_attrs.maxlength > -1 && _attrs.showCharCount">
|
||||
<!--
|
||||
@slot 输入框右侧显示字数
|
||||
-->
|
||||
<slot name="maxlength">
|
||||
{{ String(nowValue).length + '/' + _attrs.maxlength }}
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<tm-icon :size="_attrs.fontSize" @click="onclear" _style="padding:16rpx;" :color="_clearColor"
|
||||
v-if="_attrs.showClear" name="close-circle-fill"></tm-icon>
|
||||
<tm-icon v-if="_attrs.showEye" :size="_attrs.fontSize" @click="showPasswordEye = !showPasswordEye"
|
||||
_style="padding:16rpx;" :color="showPasswordEye ? _clearColor : config.color"
|
||||
:name="showPasswordEye ? 'eye-off-line' : 'eye-line'"></tm-icon>
|
||||
<!--
|
||||
@slot 输入框内右侧图标
|
||||
-->
|
||||
<slot name="rightIcon">
|
||||
<tm-icon :size="_attrs.fontSize" v-if="_attrs.rightIcon"
|
||||
:_style="_attrs.showClear ? 'margin-right:24rpx' : 'margin:0 24rpx'" :name="rightIcon"
|
||||
:color="_attrs.iconColor || config.color"></tm-icon>
|
||||
</slot>
|
||||
</view>
|
||||
<!--
|
||||
@slot 输入框右侧
|
||||
-->
|
||||
<slot name="right">
|
||||
<text v-if="_attrs.rightText != ''" :style="{ marginLeft: '24rpx', fontSize: _fontSize }">{{
|
||||
_attrs.rightText
|
||||
}}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { type ComputedRef, type PropType, computed, nextTick, ref, watchEffect } from "vue";
|
||||
import {
|
||||
arrayNumberValid,
|
||||
arrayNumberValidByStyleMP,
|
||||
covetUniNumber,
|
||||
linearValid,
|
||||
} from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import {
|
||||
getDefaultColor,
|
||||
setTextColorLightByDark
|
||||
} from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 输入框
|
||||
* @exportName tm-input
|
||||
* @category 表单组件
|
||||
* @description 可以全局配置圆角风格.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmInput" });
|
||||
const { config } = useTmConfig();
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 输入框行数变化时触发(文本哉才有)
|
||||
* @property {object} evt {height: 0, heightRpx: 0, lineCount: 0}
|
||||
*/
|
||||
'linechange',
|
||||
/**
|
||||
* 键盘高度变化时触发
|
||||
* @property {number} height 键盘高度
|
||||
*/
|
||||
'keyboardheightchange',
|
||||
/**
|
||||
* 点击完成按钮时触发
|
||||
* @property {string} value 输入的值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 失去焦点时触发
|
||||
*/
|
||||
'blur',
|
||||
/**
|
||||
* 获取焦点时触发
|
||||
*/
|
||||
'focus',
|
||||
/**
|
||||
* 清除事件
|
||||
*/
|
||||
'clear',
|
||||
/**
|
||||
* 输入时候触发
|
||||
* @property {string} value 输入的值
|
||||
*/
|
||||
"input",
|
||||
/**
|
||||
* 等同v-model
|
||||
* @property {string} value 输入的值
|
||||
*/
|
||||
"update:modelValue",
|
||||
]);
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 输入框的值
|
||||
*/
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 输入框自定义的style样式
|
||||
*/
|
||||
inputStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输入框自定义的class样式
|
||||
*/
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输入类型,可选值:text,number,idcard,digit,tel,safe-password,nickname,
|
||||
* textarea:文本域
|
||||
*/
|
||||
type: {
|
||||
type: String as PropType<
|
||||
"text" | "number" | "idcard" | "digit" | "tel" | "safe-password" | "nickname" | "textarea"
|
||||
>,
|
||||
deffault: "",
|
||||
},
|
||||
/**
|
||||
* 是否是密码框
|
||||
*/
|
||||
password: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 显示密码小眼睛
|
||||
*/
|
||||
showEye: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 输入框的占位文字
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入或选择",
|
||||
},
|
||||
/**
|
||||
* 输入框的占位文字样式
|
||||
*/
|
||||
placeholderStyle: {
|
||||
type: String,
|
||||
default: "color: #b1b1b1;",
|
||||
},
|
||||
/**
|
||||
* 输入框的占位样式
|
||||
*/
|
||||
placeholderClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 输入框的对齐方式,可选值:left,right,center
|
||||
*/
|
||||
align: {
|
||||
type: String as PropType<"left" | "right" | "center">,
|
||||
default: "left",
|
||||
},
|
||||
/**
|
||||
* 输入框背景
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 输入框暗黑背景,空值取全局的配置
|
||||
* 提供会覆盖全局的配色。默认是透明
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: "transparent",
|
||||
},
|
||||
/**
|
||||
* 输入框字体大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "32",
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "auto",
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "auto",
|
||||
},
|
||||
minHeight: {
|
||||
type: String,
|
||||
default: "88",
|
||||
},
|
||||
/**
|
||||
* 输入框内的左右间隙
|
||||
*/
|
||||
inputPadding: {
|
||||
type: String,
|
||||
default: "24"
|
||||
},
|
||||
/**
|
||||
* 输入框圆角,默认空值取全局的配置
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示清除按钮
|
||||
*/
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 清除按钮颜色
|
||||
*/
|
||||
clearColor: {
|
||||
type: String,
|
||||
default: "#b1b1b1"
|
||||
},
|
||||
/**
|
||||
* 是否显示字符字统计
|
||||
*/
|
||||
showCharCount: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 字体颜色
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 暗黑模式字体颜色,如果不填写自动反转
|
||||
*/
|
||||
darkFontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 激活时的字体颜色
|
||||
* 空值取全局主题色,并且darkFontColor会失效.
|
||||
* 如果填写none则不生效
|
||||
*/
|
||||
focusFontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左图标的名称
|
||||
*/
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 左图标的颜色
|
||||
* 默认空值取全局的主题色。
|
||||
*/
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 右图标的名称
|
||||
*/
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 右文字
|
||||
*/
|
||||
rightText: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 右文字
|
||||
*/
|
||||
leftText: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否聚焦
|
||||
*/
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 键盘右下解按钮文字,可以取值:send,search,next,done
|
||||
* 只对input有效,对textarea无效
|
||||
*/
|
||||
confirmType: {
|
||||
type: String as PropType<"send" | "search" | "next" | "done">,
|
||||
default: "done"
|
||||
},
|
||||
/**
|
||||
* 光标与键盘的距离,单位px
|
||||
*/
|
||||
cursorSpacing: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 输入框的最大长度
|
||||
*/
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
confirmHold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cursor: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 光标颜色,默认空值取全局配置
|
||||
*/
|
||||
cursorColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 光标起始位置
|
||||
*/
|
||||
selectionStart: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
/**
|
||||
* 光标结束位置
|
||||
*/
|
||||
selectionEnd: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
/**
|
||||
* 是否自动聚焦
|
||||
*/
|
||||
adjustPosition: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否自动失焦
|
||||
*/
|
||||
autoBlur: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ignoreCompositionEvent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 强制 input 处于同层状态
|
||||
*/
|
||||
alwaysEmbed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* focus时,点击页面的时候不收起键盘
|
||||
*/
|
||||
holdKeyboard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
safePasswordCertPath: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
safePasswordLength: {
|
||||
type: Number,
|
||||
default: NaN
|
||||
},
|
||||
safePasswordTimeStamp: {
|
||||
type: Number,
|
||||
default: NaN
|
||||
},
|
||||
safePasswordNonce: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
safePasswordSalt: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
safePasswordCustomHash: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
randomNumber: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
controlled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
alwaysSystem: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* input 输入模式
|
||||
*/
|
||||
inputmode: {
|
||||
type: String as PropType<"url" | "email" | "search" | "tel" | "text" | "numeric" | "none" | "decimal">,
|
||||
default: "text"
|
||||
},
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showConfirmBar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否高亮聚焦当前输入框
|
||||
* 会使用全局的主题色为边框色。
|
||||
*/
|
||||
focusHighlight: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 高亮聚焦当前输入框的样式
|
||||
* 默认为自动全局2px solid 主题色,如果填写了以你的为准。
|
||||
*/
|
||||
focusHighlightStyle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
});
|
||||
const _attrs = computed(() => attrs);
|
||||
const nowValue = ref<string | number>("");
|
||||
const isFocus = ref<boolean>(false);
|
||||
|
||||
|
||||
const showPlaceholder = computed(() => {
|
||||
return nowValue.value == "" ? true : false;
|
||||
});
|
||||
const showPasswordEye = ref(_attrs.value.password)
|
||||
const _color = computed(() => {
|
||||
let color = attrs.color;
|
||||
if (color == "") {
|
||||
color = config.inputLightColor
|
||||
}
|
||||
if (config.mode == "dark") {
|
||||
color = attrs.darkBgColor;
|
||||
if (color == "") {
|
||||
color = config.inputDarkColor;
|
||||
}
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
});
|
||||
const _focusHighlight = computed(() => {
|
||||
let defaultsolid = 'border:solid 2px transparent;' + _attrs.value.inputStyle
|
||||
if (!isFocus.value) return defaultsolid;
|
||||
return attrs.focusHighlight ? (attrs.focusHighlightStyle || `border:solid 2px ${getDefaultColor(config.color)} !important`) : defaultsolid;
|
||||
})
|
||||
const _fontColor = computed(() => {
|
||||
let fontColor = attrs.fontColor;
|
||||
if (config.mode == "dark") {
|
||||
fontColor = attrs.darkFontColor;
|
||||
if (fontColor == "") {
|
||||
fontColor = setTextColorLightByDark(attrs.fontColor)
|
||||
}
|
||||
}
|
||||
return getDefaultColor(fontColor)
|
||||
})
|
||||
const _currorColor = computed(() => {
|
||||
return getDefaultColor(_attrs.value.cursorColor || config.color)
|
||||
})
|
||||
const _inputFontColor = computed(() => {
|
||||
if (!isFocus.value) return _fontColor.value
|
||||
if (attrs.fontColor == 'none') return _fontColor.value;
|
||||
return getDefaultColor(attrs.focusFontColor || config.color)
|
||||
})
|
||||
const _fontSize = computed(() => {
|
||||
return covetUniNumber(attrs.fontSize, config.unit);
|
||||
});
|
||||
const _width = computed(() => {
|
||||
return covetUniNumber(attrs.width, config.unit);
|
||||
});
|
||||
const _height = computed(() => {
|
||||
return covetUniNumber(attrs.height, config.unit);
|
||||
});
|
||||
const _minHeight = computed(() => {
|
||||
return covetUniNumber(attrs.minHeight, config.unit);
|
||||
});
|
||||
const _inputPadding = computed(() => {
|
||||
return covetUniNumber(attrs.inputPadding, config.unit);
|
||||
});
|
||||
const _round = computed((): string | undefined => {
|
||||
let round = attrs.round;
|
||||
if (round === '' && config.inputRadius !== '') {
|
||||
round = config.inputRadius;
|
||||
}
|
||||
if (round === '') return undefined
|
||||
return arrayNumberValid(round, '0px').join(' ')
|
||||
});
|
||||
const _clearColor = computed(() => getDefaultColor(attrs.clearColor))
|
||||
const oninput = (evt: Event) => {
|
||||
// @ts-ignore
|
||||
let value: string | number = evt.detail.value;
|
||||
let pvalue = value
|
||||
if (attrs.type == "number" || attrs.type == "digit" || attrs.type == "tel") {
|
||||
pvalue = typeof pvalue == "number" ? pvalue.toString() : pvalue;
|
||||
try {
|
||||
let oladvalue = parseFloat(pvalue);
|
||||
if (isNaN(oladvalue) || /[^0-9.]+/.test(pvalue)) {
|
||||
pvalue = ''
|
||||
}
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
nowValue.value = value;
|
||||
|
||||
nextTick(() => {
|
||||
nowValue.value = pvalue;
|
||||
emits("input", pvalue);
|
||||
emits("update:modelValue", pvalue);
|
||||
})
|
||||
return value
|
||||
};
|
||||
const onclear = () => {
|
||||
nowValue.value = '';
|
||||
emits('update:modelValue', '')
|
||||
emits("clear")
|
||||
}
|
||||
const onfocus = () => {
|
||||
isFocus.value = true;
|
||||
};
|
||||
const onblur = () => {
|
||||
isFocus.value = false;
|
||||
};
|
||||
const onconfirm = (evt: any) => {
|
||||
emits("confirm", evt.detail.value);
|
||||
};
|
||||
const onkeyboardheightchange = (evt: any) => {
|
||||
emits('keyboardheightchange', evt.detail.height);
|
||||
};
|
||||
const onlinechange = (evt: any) => {
|
||||
emits('linechange', evt.detail);
|
||||
}
|
||||
const onchange = (evt: any) => {
|
||||
// 用来监测微信输入昵称的值。
|
||||
let val = evt.detail.value;
|
||||
if (_attrs.value.type == 'nickname' && val != nowValue.value && val) {
|
||||
nowValue.value = val;
|
||||
emits("input", val);
|
||||
emits("update:modelValue", val);
|
||||
}
|
||||
}
|
||||
watchEffect(() => {
|
||||
// if(nowValue.value==attrs.modelValue) return;
|
||||
nowValue.value = attrs.modelValue;
|
||||
});
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmInputShowChar {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 16rpx;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tmInput {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmInputWrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
transition-property: border-color;
|
||||
transition-duration: 0.3s;
|
||||
transition-timing-function: linear;
|
||||
border-color: transparent;
|
||||
|
||||
}
|
||||
|
||||
.tmInputWrapBox {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tmInputInput {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tmInputPlaceholder {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
display: -webkit-box;
|
||||
white-space: inherit;
|
||||
text-wrap: nowrap;
|
||||
opacity: 0;
|
||||
transition: all 0.2s cubic-bezier(.42, .38, .15, .93);
|
||||
|
||||
&.tmInputPlaceholderOn {
|
||||
opacity: 1;
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
&.tmInputPlaceholderOff {
|
||||
opacity: 0;
|
||||
transform: translateY(50rpx);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,601 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, onUpdated, nextTick } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
/**
|
||||
* @displayName 对话框
|
||||
* @exportName tm-modal
|
||||
* @category 反馈组件
|
||||
* @description 可全局统一更改风格。使用时注意下:如果height:auto,将失去内容滚动效果。如果要内容滚动height必定是一个值。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmModal' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题"
|
||||
},
|
||||
/**
|
||||
* 显示底部操作栏
|
||||
*/
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 250
|
||||
},
|
||||
/**
|
||||
* 取消按钮的文本
|
||||
*/
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消"
|
||||
},
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
*/
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "确认"
|
||||
},
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 宽,百分比,Px,rpx,auto都支持
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: "84%"
|
||||
},
|
||||
/**
|
||||
* 宽,百分比,Px,rpx,auto都支持
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: "350"
|
||||
},
|
||||
/**
|
||||
* 可以是百分比,px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: {
|
||||
type: [String, Number],
|
||||
default: "80%"
|
||||
},
|
||||
/**
|
||||
* 是否禁用内部的scroll标签
|
||||
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
|
||||
* 但如果没有指定高,内容自动的话,高是自动的。
|
||||
* 有这个属性是因为截止4.03scroll-view里面放input不会上推键盘,及内部的view touchMove会失效。
|
||||
*/
|
||||
disabledScroll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 容器背景色
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的容器背景色,不填写的话取sheetDarkColor
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: "1105"
|
||||
},
|
||||
/**
|
||||
* 内容区域的间隙
|
||||
*/
|
||||
contentPadding: {
|
||||
type: [String, Number],
|
||||
default: "32"
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
'click',
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
'close',
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
'open',
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
'beforeOpen',
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
'beforeClose',
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
'update:show',
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
'confirm'
|
||||
])
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const actinon = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("tmModal" + getUid())
|
||||
const wrapId = ref("tmModal" + getUid())
|
||||
const tid = ref(0)
|
||||
const tid2 = ref(0)
|
||||
const windtop = ref(0)
|
||||
const tantiaoTrue = ref(false)
|
||||
const _customStyle = computed(() => props.customStyle)
|
||||
const _show = computed(() => props.show)
|
||||
const _showClose = computed(() => props.showClose)
|
||||
const _duration = computed(() => props.duration)
|
||||
const _title = computed(() => props.title)
|
||||
const _showTitle = computed(() => props.showTitle)
|
||||
const _round = computed(() => {
|
||||
if (props.round == "") {
|
||||
return covetUniNumber(config.modalRadius, config.unit)
|
||||
}
|
||||
return covetUniNumber(props.round, config.unit)
|
||||
})
|
||||
const _contentHeight = ref('auto')
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const _c_width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
const _c_height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
const _showFooter = computed(() => props.showFooter)
|
||||
const _maxHeight = computed(() => covetUniNumber(props.maxHeight, config.unit))
|
||||
const _contentPadding = computed(() => covetUniNumber(props.contentPadding, config.unit))
|
||||
const _showCancel = computed(() => props.showCancel)
|
||||
const _cancelText = computed(() => props.cancelText)
|
||||
const _confirmText = computed(() => props.confirmText)
|
||||
const _animationFun = computed(() => config.animation)
|
||||
const __height = computed(() => {
|
||||
let h = '100%'
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtop.value}px)`
|
||||
// #endif
|
||||
return h
|
||||
})
|
||||
const _bgColor = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
|
||||
return getDefaultColor(config.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
})
|
||||
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (newVal) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifndef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight - sys.windowTop
|
||||
windtop.value = sys.windowTop
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight
|
||||
// #endif
|
||||
|
||||
if (_show.value) {
|
||||
showAlert()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
clearTimeout(tid2.value)
|
||||
})
|
||||
|
||||
const overflayMoveTouch = (evt: TouchEvent) => {
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
const cancelEvt = () => {
|
||||
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
emit('cancel')
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const confirmEvt = () => {
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
emit('confirm')
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const onClickOverflowy = (evt: Event) => {
|
||||
evt.stopPropagation()
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
emit("click")
|
||||
if (!props.overlayClick){
|
||||
clearTimeout(tid2.value)
|
||||
tantiaoTrue.value = true
|
||||
tid2.value = setTimeout(() => {
|
||||
tantiaoTrue.value = false
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const closeAlert = () => {
|
||||
if (actinon.value) return
|
||||
if (status.value == 'close') return
|
||||
actinon.value = true
|
||||
status.value = 'close'
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emit('beforeClose')
|
||||
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}
|
||||
|
||||
const showAlert = () => {
|
||||
if (actinon.value) return
|
||||
if (status.value == 'open') return
|
||||
showOverflay.value = true
|
||||
actinon.value = true
|
||||
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emit('beforeOpen')
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
status.value = 'open'
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
|
||||
const openDrawer = () => {
|
||||
showAlert()
|
||||
}
|
||||
|
||||
const onEnd = () => {
|
||||
actinon.value = false
|
||||
|
||||
if (status.value == 'close') {
|
||||
showOverflay.value = false
|
||||
/**
|
||||
* 关闭时执行
|
||||
*/
|
||||
emit('close')
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
emit('update:show', false)
|
||||
} else {
|
||||
/**
|
||||
* 打开执行的事件
|
||||
*/
|
||||
emit('open')
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const maskerMove = (evt: TouchEvent) => {
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer">
|
||||
<!--
|
||||
@slot 标签触发显示遮罩,免于使用变量控制
|
||||
@binding {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="show"></slot>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport to="uni-app">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
<view @click="onClickOverflowy" @touchmove="maskerMove" ref="tmModalWrap" v-if="showOverflay"
|
||||
:id="id" class="tmModalWrap tmModalWrap_center"
|
||||
:class="[status == 'open' ? 'tmModalWrap_on' : 'tmModalWrap_off']"
|
||||
:style="[{ top: windtop + 'px', zIndex: zIndex, width: '100%', height: __height, 'transition-timing-function': _animationFun }, _customStyle]">
|
||||
|
||||
<view @click.stop="" ref="tmModalWrapContent" class="tmModalWrapContent "
|
||||
:class="[status == 'open' ? 'tmModalWrapContent_on' : 'tmModalWrapContent_off',tantiaoTrue?'tmModalWrapContent_Tantiao':'']" :id="wrapId"
|
||||
:style="{
|
||||
width: _c_width,
|
||||
height: _c_height,
|
||||
maxWidth: '750px',
|
||||
borderRadius: _round,
|
||||
maxHeight: _maxHeight != '' ? _maxHeight : '100%',
|
||||
backgroundColor: _bgColor,
|
||||
transitionDuration: _duration + 'ms',
|
||||
'transition-timing-function': _animationFun
|
||||
}">
|
||||
<view ref="tmModalWrapBox" class="tmModalWrapBox" :style="{ borderRadius: _round }">
|
||||
<view class="tmModalXclose">
|
||||
<tm-icon v-if="_showClose" @click="closeAlert" color="#dcdcdc" size="52"
|
||||
name="close-circle-fill"></tm-icon>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<view v-if="_showTitle" class="tmModalTitleBox">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
@binding {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="show">
|
||||
<tm-text font-size="34" class="tmModaltitleBoxTitle">{{ _title }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!disabledScroll&&_c_height!='auto'" style="flex: 1;" class="tmModalWrapBoxContent" >
|
||||
<scroll-view style="height:100%;position:absolute;width:100%" :scroll-y="true"
|
||||
:rebound="false">
|
||||
<view :style="{ padding: `0px ${_contentPadding} 0px ${_contentPadding}` }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view v-else :style="{ height: '100%', padding: _contentPadding }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="showFooter" class="tmModalFooter" :style="{ backgroundColor: _bgColor }">
|
||||
<!--
|
||||
@slot 底部操作栏
|
||||
-->
|
||||
<slot name="footer">
|
||||
<tm-button @click="cancelEvt" v-if="_showCancel" skin="thin"
|
||||
style="margin-right: 16px;flex:1">{{ _cancelText }}</tm-button>
|
||||
<tm-button @click="confirmEvt" style="flex:1">{{ _confirmText }}</tm-button>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.tmModalWrapBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background-color: white; */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.tmModalFooter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
/* background-color: white; */
|
||||
padding: 24rpx;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmModalXclose {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 18rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tmModaltitleBoxTitle {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.tmModalTitleBox {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tmModaltitleBox {
|
||||
max-width: 350px;
|
||||
overflow: hidden;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tmModalWrap_center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.tmModalWrapContent {
|
||||
transition-property: transform, opacity;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.tmModalWrapContent_on {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
|
||||
.tmModalWrapContent_off {
|
||||
transform: scale(0.54);
|
||||
opacity: 0;
|
||||
}
|
||||
.tmModalWrapBoxContent{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmModalWrap {
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
transition-duration: 350ms;
|
||||
transition-property: background-color;
|
||||
}
|
||||
|
||||
.tmModalWrap_on {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.tmModalWrap_off {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
@keyframes tantiao {
|
||||
0%{
|
||||
transform: scale(1);
|
||||
}
|
||||
60%{
|
||||
transform: scale(1.05);
|
||||
}
|
||||
75%{
|
||||
transform: scale(0.95);
|
||||
}
|
||||
85%{
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100%{
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.tmModalWrapContent_Tantiao{
|
||||
animation: tantiao 0.15s linear;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,419 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, ComponentInstance, onUpdated, nextTick, VueElement, inject, watchEffect } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj, isBlackAndWhite, setBgColorLightByDark } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
|
||||
/**
|
||||
* @displayName 标题导航
|
||||
* @exportName tm-navbar
|
||||
* @category 导航组件
|
||||
* @description 标题导航,用途页面头部标题。可以默认透明,滚动实现背景可变的标题导航。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmNavbar' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 当滚动页面时,导航栏位置
|
||||
* 与滚动之间的距离差比(0~1)
|
||||
* 0表示在顶部,1表示已经超过了导航高
|
||||
* 主要是用来类型滚动时设置导航顶部的一些布局变化。
|
||||
*/
|
||||
'fiexdChange',
|
||||
/**
|
||||
* 初始化完成向下发送一个事件用于告知本组件实际的高
|
||||
* @param {number} height -单位px,组件高。
|
||||
*/
|
||||
'init'
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 是否底部占位内容,如果为false底部悬空。你的页面内容将自动压在导航栏的底部。
|
||||
*/
|
||||
isPlace: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 背景颜色,注意这是静态时的背景色。
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
/**
|
||||
* 暗黑的背景颜色。
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: "#000000"
|
||||
},
|
||||
/**
|
||||
* 背景颜色,这是滑动时超过指定本状态栏高度时自动渐变到此颜色
|
||||
* 如果为空时,不会有动态背景
|
||||
* 如果提供的是白或者黑,暗黑时自动取反。
|
||||
*/
|
||||
activeBgColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 返回按钮的颜色.默认是取titleColor,如果你单独定义了
|
||||
* 以你定义的为准
|
||||
*/
|
||||
backColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: '标题'
|
||||
},
|
||||
/**
|
||||
* 默认标题颜色,暗黑是取白,如果有其它需求建议插槽。
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
/**
|
||||
* 动态悬浮时标题颜色,如果为暗黑时,你提供的颜色为白或者黑会反色
|
||||
* 如果提供的是彩色自动加深或者提亮
|
||||
*/
|
||||
titleActiveColor: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
/**
|
||||
* 标题文字大小
|
||||
*/
|
||||
titleFontSize: {
|
||||
type: String,
|
||||
default: '34',
|
||||
},
|
||||
/**
|
||||
* 右边的宽度。
|
||||
*/
|
||||
lrWidth: {
|
||||
type: String,
|
||||
default: '100',
|
||||
},
|
||||
/**
|
||||
* 左边的宽度。
|
||||
*/
|
||||
llWidth: {
|
||||
type: String,
|
||||
default: '100',
|
||||
},
|
||||
/**
|
||||
* 层级。
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 90,
|
||||
},
|
||||
/**
|
||||
* 左边按钮默认点击返回上页。
|
||||
* 但如果上页返回失败(通常见于直接程序启动本面,无法进行上页返回时)
|
||||
* 失败后返回的页面。默认是首页。
|
||||
*/
|
||||
backErrorPath: {
|
||||
type: String,
|
||||
default: '/pages/index/index'
|
||||
},
|
||||
/**
|
||||
* 是否显示返回按钮。
|
||||
*/
|
||||
showNavBack: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 渐变背景,如果提供,上面的BgColor背景和暗黑背景将失效。
|
||||
* 例:['to right','#ff667f','#ff5416']
|
||||
*/
|
||||
linearGradient: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 动态悬浮时的渐变背景,
|
||||
* 提供后上面的 activeBgColor的背景和暗黑背景将失效。
|
||||
* 例:['to right','#ff667f','#ff5416']
|
||||
*/
|
||||
linearActiveGradient: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 静态在顶部时是否透明背景
|
||||
*/
|
||||
staticTransparent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 如果需要悬浮滚动请在外部页面中监听onPageScroll事件,并把值传到这里。
|
||||
*/
|
||||
scrollTop:{
|
||||
type:Number,
|
||||
default:0
|
||||
}
|
||||
})
|
||||
|
||||
const statusBarHeight = ref(44)
|
||||
const navbarHeight = 50
|
||||
const isFiexd = ref(false)
|
||||
const fiexRatio = ref(1)
|
||||
const _stop = computed(()=>props.scrollTop)
|
||||
|
||||
const _linearGradient = computed((): string => {
|
||||
if (props.linearGradient.length < 3) return '';
|
||||
let str = props.linearGradient.join(",");
|
||||
return `linear-gradient(${str})`
|
||||
})
|
||||
|
||||
const _linearActiveGradient = computed((): string => {
|
||||
if (props.linearActiveGradient.length < 3) return '';
|
||||
let str = props.linearActiveGradient.join(",");
|
||||
return `linear-gradient(${str})`
|
||||
})
|
||||
|
||||
const _lrWidth = computed((): string => covetUniNumber(props.lrWidth, config.unit))
|
||||
const _llWidth = computed((): string => covetUniNumber(props.llWidth, config.unit))
|
||||
const _titleFontSize = computed((): string => covetUniNumber(props.titleFontSize, config.unit))
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
let bgcolor = props.bgColor
|
||||
if (config.mode == 'dark') {
|
||||
bgcolor = props.darkBgColor == "" ? "#000000" : props.darkBgColor
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
})
|
||||
|
||||
const _activeBgColor = computed((): string => {
|
||||
let color = _bgColor.value
|
||||
if (props.activeBgColor != "") {
|
||||
color = getDefaultColor(props.activeBgColor)
|
||||
}
|
||||
let isba = isBlackAndWhite(props.titleActiveColor)
|
||||
if (isba) {
|
||||
if (config.mode == 'dark') {
|
||||
color = setBgColorLightByDark(color)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _titleColor = computed((): string => {
|
||||
if (config.mode == 'dark') return "#ffffff"
|
||||
return getDefaultColor(props.titleColor)
|
||||
})
|
||||
|
||||
const _backColor = computed((): string => {
|
||||
if (props.backColor != '') return getDefaultColor(props.backColor)
|
||||
return _titleColor.value
|
||||
})
|
||||
|
||||
const _titleActiveColor = computed((): string => {
|
||||
let isba = isBlackAndWhite(props.titleActiveColor)
|
||||
if (isba) {
|
||||
if (config.mode == 'dark') return "#ffffff"
|
||||
}
|
||||
return getDefaultColor(props.titleActiveColor)
|
||||
})
|
||||
|
||||
const _title = computed((): string => props.title)
|
||||
|
||||
const _styleMap = computed(() => {
|
||||
let maps: Record<string, string | number> = {};
|
||||
if (props.staticTransparent) {
|
||||
maps['opacity'] = fiexRatio.value
|
||||
} else {
|
||||
maps['opacity'] = '1'
|
||||
|
||||
}
|
||||
maps['height'] = (statusBarHeight.value + navbarHeight) + 'px'
|
||||
|
||||
if (isFiexd.value) {
|
||||
if (_linearActiveGradient.value != '') {
|
||||
maps['background-image'] = _linearActiveGradient.value
|
||||
|
||||
} else if (_activeBgColor.value != '') {
|
||||
maps['background-color'] = _activeBgColor.value
|
||||
|
||||
} else {
|
||||
maps['background-color'] = _bgColor.value
|
||||
|
||||
}
|
||||
} else {
|
||||
if (_linearGradient.value != '') {
|
||||
maps['background-image'] = _linearGradient.value
|
||||
} else {
|
||||
maps['background-color'] = _bgColor.value
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return maps
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
let sys = uni.getSystemInfoSync();
|
||||
statusBarHeight.value = sys.statusBarHeight || 0
|
||||
emits('init', statusBarHeight.value + navbarHeight)
|
||||
if (props.staticTransparent) {
|
||||
fiexRatio.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
watchEffect(() => {
|
||||
let barheight = statusBarHeight.value + navbarHeight;
|
||||
isFiexd.value = _stop.value > 0
|
||||
if (props.staticTransparent) {
|
||||
fiexRatio.value = Math.max(Math.min(_stop.value / barheight, 1), 0)
|
||||
}
|
||||
emits("fiexdChange", fiexRatio.value)
|
||||
})
|
||||
|
||||
const backGo = () => {
|
||||
// #ifdef WEB
|
||||
window.history.back()
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
uni.navigateBack({
|
||||
fail() {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view class="tmNavbar" :style="{ zIndex: zIndex }">
|
||||
|
||||
<view :key="_linearActiveGradient" :style="_styleMap" class="tmNavbarBg"></view>
|
||||
<view class="tmNavbarContentBox">
|
||||
<view :style="{ height: (statusBarHeight) + 'px' }"></view>
|
||||
<view class="tmNavbarContent" :style="{ height: (navbarHeight) + 'px' }">
|
||||
<view class="tmNavbarLeft" :style="{ width: _llWidth }">
|
||||
<view v-if="showNavBack" @click="backGo" class="tmNavbarBack" style="padding-left: 8px;">
|
||||
<tm-icon size="52" name="arrow-left-s-line"
|
||||
:color="isFiexd ? _titleActiveColor : _backColor"></tm-icon>
|
||||
</view>
|
||||
<!--
|
||||
@slot 左边插槽
|
||||
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
|
||||
-->
|
||||
<slot name="left" :isFiexd="isFiexd"></slot>
|
||||
</view>
|
||||
<view class="tmNavbarTtitle">
|
||||
<!--
|
||||
@slot 标题插槽
|
||||
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
|
||||
-->
|
||||
<slot name="title">
|
||||
<text class="xNvbarTtitleOverls"
|
||||
:style="{ fontSize: _titleFontSize, fontWeight: 'bold', color: isFiexd ? _titleActiveColor : _titleColor }">{{
|
||||
_title }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="tmNavbarRight" :style="{ width: _lrWidth }">
|
||||
<!--
|
||||
@slot 右边插槽
|
||||
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
|
||||
-->
|
||||
<slot name="right" :isFiexd="isFiexd"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="isPlace" :style="{ height: (statusBarHeight + navbarHeight) + 'px' }"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xNvbarTtitleOverls {
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tmNavbarContentBox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tmNavbarBack {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmNavbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tmNavbarContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmNavbarLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmNavbarRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmNavbarTtitle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,543 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, PropType } from 'vue';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { tmDate, type tmDateTypeTime, createDate } from '../../libs/tmDate';
|
||||
import pickerItem from './../tm-picker-view/picker-item.vue';
|
||||
|
||||
type PICKER_ITEM_INFO = Record<string, any>
|
||||
|
||||
/**
|
||||
* @displayName 日期选择器
|
||||
* @exportName tm-picker-date
|
||||
* @category 表单组件
|
||||
* @description 可以设置为年月日,并设置精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmPickerDate' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type coverValue = {
|
||||
value: string[],
|
||||
str: string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
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有效
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD"
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => ['年', '月', '日', '时', '分', '秒']
|
||||
},
|
||||
/**
|
||||
* 是否懒加载内部内容。
|
||||
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
|
||||
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
|
||||
*/
|
||||
lazyContent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认触发
|
||||
* @param {string} date 当前选中时间id值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const show = ref(false);
|
||||
const nowValue = ref<string[]>([]);
|
||||
const _modelValueIndex = ref<number[]>([])
|
||||
const nowValueStr = ref('');
|
||||
const startDate = ref(new tmDate().subtraction(1, 'y'));
|
||||
const endDate = ref(new tmDate());
|
||||
const dateList = ref<PICKER_ITEM_INFO[][]>([]);
|
||||
const yanchiDuration = ref(false);
|
||||
const _lazyContent = computed(() => props.lazyContent);
|
||||
const _start_date = computed(() => props.start ? new tmDate(props.start) : startDate.value);
|
||||
const _end_date = computed(() => props.end ? new tmDate(props.end) : endDate.value);
|
||||
const _getDateType = computed((): tmDateTypeTime => {
|
||||
let isType: tmDateTypeTime = 's';
|
||||
if (props.type == 'year') isType = 'y';
|
||||
if (props.type == 'month') isType = 'm';
|
||||
if (props.type == 'day') isType = 'd';
|
||||
if (props.type == 'hour') isType = 'h';
|
||||
if (props.type == 'minute') isType = 'M';
|
||||
return isType;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const defaultModelvalue = (newvalue: string, showStr: boolean) => {
|
||||
let isType = _getDateType.value;
|
||||
let selfnowValue = new tmDate(newvalue);
|
||||
if (selfnowValue.isBetweenOf(_start_date.value, '<=', isType)) {
|
||||
selfnowValue = _start_date.value;
|
||||
}
|
||||
if (selfnowValue.isBetweenOf(_end_date.value, '>=', isType)) {
|
||||
|
||||
selfnowValue = _end_date.value;
|
||||
}
|
||||
|
||||
let stp = getRangByDateTime(selfnowValue);
|
||||
nowValue.value = stp.value;
|
||||
nowValueStr.value = stp.str;
|
||||
dateList.value = getTimeTreeByStartAndEnd(_start_date.value, _end_date.value);
|
||||
_modelValueIndex.value = getIndexsByids(stp.value)
|
||||
|
||||
if (showStr) {
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getIndexsByids = (ids: string[]) => {
|
||||
let indexs = [] as number[];
|
||||
for (let i = 0; i < dateList.value.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
let index = item.findIndex(el => parseInt(el.id) == parseInt(ids[i]))
|
||||
indexs.push(Math.max(0, index))
|
||||
}
|
||||
return indexs
|
||||
}
|
||||
|
||||
|
||||
const getTimeTreeByStartAndEnd = (start: tmDate, end: tmDate): PICKER_ITEM_INFO[][] => {
|
||||
let startCopy = start.getClone();
|
||||
let nowDate = new tmDate(nowValueStr.value)
|
||||
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: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
|
||||
function getD(type: string, s: number, n: number) {
|
||||
if (type == 'm') {
|
||||
|
||||
for (let i = s; i <= n; i++) {
|
||||
months.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i + 1)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'd') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
days.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'h') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
hours.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'M') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
minutes.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 's') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
seconds.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getDnumber(type: tmDateTypeTime, target: tmDateTypeTime): 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 = 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, getNowTypeLenIndex()) as PICKER_ITEM_INFO[][];
|
||||
}
|
||||
const indexToHex = (i: number) => {
|
||||
let n = i.toString()
|
||||
if (n.length == 1) return '0' + n;
|
||||
return n;
|
||||
}
|
||||
const getRangByDateTime = (d: tmDate) => {
|
||||
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;
|
||||
}
|
||||
const getNowTypeLenIndex = () => {
|
||||
let index = 6
|
||||
if (props.type == 'year') {
|
||||
index = 1;
|
||||
} else if (props.type == 'month') {
|
||||
index = 2;
|
||||
} else if (props.type == 'day') {
|
||||
index = 3;
|
||||
} else if (props.type == 'hour') {
|
||||
index = 4;
|
||||
} else if (props.type == 'minute') {
|
||||
index = 5;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const openShow = () => {
|
||||
show.value = true;
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit('update:modelShow', true);
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit('update:modelShow', false);
|
||||
cancelResetDataCol();
|
||||
if (_lazyContent.value) {
|
||||
yanchiDuration.value = false;
|
||||
}
|
||||
}
|
||||
const cancelResetDataCol = () => {
|
||||
defaultModelvalue(new tmDate(props.modelValue).format('YYYY/MM/DD HH:mm:ss'), false)
|
||||
}
|
||||
const onOpen = () => {
|
||||
yanchiDuration.value = true;
|
||||
}
|
||||
const fillNumber = (n: string) => {
|
||||
if (parseInt(n) > 9) return n;
|
||||
return "0" + n
|
||||
}
|
||||
const stringArValuCoverToString = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
|
||||
let newsday = new tmDate(nowValue.value[0] + "-" + (parseInt(nowValue.value[1]) + 1).toString() + "-1")
|
||||
let days = parseInt(nowValue.value[2])
|
||||
days = days >= newsday.getMonthCountDay() ? newsday.getMonthCountDay() : days
|
||||
nowValue.value.splice(2, 1, days.toString())
|
||||
|
||||
return fillNumber(nowValue.value[0]) + "-" + fillNumber((parseInt(nowValue.value[1]) + 1).toString())
|
||||
+ "-" + fillNumber(nowValue.value[2])
|
||||
+ " " + fillNumber(nowValue.value[3]) + ":"
|
||||
+ fillNumber(nowValue.value[4]) + ":"
|
||||
+ fillNumber(nowValue.value[5])
|
||||
}
|
||||
const change = (ixs: number[]) => {
|
||||
let nowvalstrs = [] as string[]
|
||||
for (let i = 0; i < ixs.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
nowvalstrs.push(item[ixs[i]].title)
|
||||
}
|
||||
|
||||
let str = nowvalstrs.join('/');
|
||||
let stp = getRangByDateTime(new tmDate(str));
|
||||
|
||||
defaultModelvalue(stp.str, false)
|
||||
|
||||
// onConfirm()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
emit('cancel');
|
||||
cancelResetDataCol();
|
||||
}
|
||||
const formatTimeDate = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
let sp = props.format;
|
||||
sp = sp.replace(/YYYY/g, fillNumber(nowValue.value[0]))
|
||||
sp = sp.replace(/MM/g, fillNumber((parseInt(nowValue.value[1]) + 1).toString()))
|
||||
sp = sp.replace(/DD/g, fillNumber(nowValue.value[2]))
|
||||
sp = sp.replace(/hh/g, fillNumber(nowValue.value[3]))
|
||||
sp = sp.replace(/mm/g, fillNumber(nowValue.value[4]))
|
||||
sp = sp.replace(/ss/g, fillNumber(nowValue.value[5]))
|
||||
return sp;
|
||||
}
|
||||
const onConfirm = () => {
|
||||
nowValueStr.value = stringArValuCoverToString();
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValueStr.value);
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
|
||||
emit('confirm', nowValueStr.value);
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == '') return;
|
||||
let isType = _getDateType.value;
|
||||
if (new tmDate(newvalue).isBetweenOf(new tmDate(nowValueStr.value), '=', isType)) return;
|
||||
|
||||
defaultModelvalue(new tmDate(newvalue).format('YYYY/MM/DD HH:mm:ss'), true);
|
||||
});
|
||||
|
||||
watch(() => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
yanchiDuration.value = _lazyContent.value ? false : true;
|
||||
let nowValue = new tmDate(props.modelValue);
|
||||
|
||||
defaultModelvalue(nowValue.format('YYYY/MM/DD HH:mm:ss'), props.modelValue != '');
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@binding {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<tm-drawer @open="onOpen" :widthCoverCenter="true" :disabledScroll="true" :title="title" @close="onClose"
|
||||
@confirm="onConfirm" @cancel="onCancel" :showFooter="true" v-model:show="show" :show-close="true" size="850">
|
||||
<view v-if="yanchiDuration&&show" class="tmPickerDateWrap">
|
||||
<picker-item @change-deep="change" :selected-index="_modelValueIndex" :list="dateList"
|
||||
:cell-units="cellUnits.slice(0, dateList.length)"></picker-item>
|
||||
</view>
|
||||
<tm-icon v-if="!yanchiDuration" size="42" color="error" spin name="loader-line"></tm-icon>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmPickerDateWrap {
|
||||
/* display: flex;
|
||||
flex-direction: row; */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,326 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, type VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import pickerItem from './picker-item.vue';
|
||||
|
||||
const { config } = useTmConfig()
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
|
||||
|
||||
const boxHeight = ref(0)
|
||||
const id = ('tmPickerItem-' + getUid())
|
||||
const nowCurrentIndex = ref([0])
|
||||
let tid = 0
|
||||
let tid2 = 1
|
||||
|
||||
const emits = defineEmits(['changeDeep', 'countChange'])
|
||||
const props = defineProps({
|
||||
|
||||
list: {
|
||||
type: Array as PropType<TM_PICKER_X_ITEM[]>,
|
||||
default: (): TM_PICKER_X_ITEM[] => [] as TM_PICKER_X_ITEM[]
|
||||
},
|
||||
wrapWight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
parentIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
selectedIndex: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: (): number[] => [] as number[]
|
||||
},
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 自定义标识id
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 自定义标识文本字段名
|
||||
*/
|
||||
rangText: {
|
||||
type: String,
|
||||
default: "title"
|
||||
}
|
||||
})
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = covetUniNumber(props.fontSize, config.unit)
|
||||
|
||||
if (config.fontSizeScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(props.fontSize)
|
||||
})
|
||||
|
||||
const _parentIndex = computed((): number => props.parentIndex)
|
||||
|
||||
const _wrapWight = computed((): number => props.wrapWight)
|
||||
|
||||
const _selectedIndex = computed((): number[] => props.selectedIndex)
|
||||
|
||||
const _unitsName = computed((): string => {
|
||||
if (props.cellUnits.length == 0) return ""
|
||||
if (props.cellUnits.length < _parentIndex.value) return ""
|
||||
return props.cellUnits[_parentIndex.value]! as string
|
||||
})
|
||||
|
||||
const _cellUnits = computed((): string[] => props.cellUnits as string[])
|
||||
|
||||
const _list = computed((): TM_PICKER_X_ITEM[] => props.list.slice(0))
|
||||
|
||||
const _nowChildren = computed((): TM_PICKER_X_ITEM[] => {
|
||||
let index = Math.max(0, Math.min(nowCurrentIndex.value[0], _list.value.length - 1))
|
||||
if (_list.value.length == 0 && index > _list.value.length - 1) return [] as TM_PICKER_X_ITEM[]
|
||||
let item = _list.value[index]
|
||||
return item?.children ?? []
|
||||
})
|
||||
|
||||
const _isDark = computed((): boolean => config.mode == 'dark')
|
||||
|
||||
const _maskStyle = computed((): string => {
|
||||
if (_isDark.value) {
|
||||
return 'background-image:linear-gradient(180deg,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0)),linear-gradient(0deg, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0))'
|
||||
}
|
||||
return 'background-image:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,0)),linear-gradient(0deg, rgba(255,255,255,0),rgba(255,255,255,0))'
|
||||
})
|
||||
const _selectedMaskStyle = computed((): string => {
|
||||
return _isDark.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'
|
||||
})
|
||||
|
||||
const getIndexByid = (): number => {
|
||||
let index = 0
|
||||
|
||||
if (_selectedIndex.value.length == 0) return index;
|
||||
if (_parentIndex.value > _selectedIndex.value.length) return index
|
||||
|
||||
index = _selectedIndex.value[_parentIndex.value]
|
||||
|
||||
index = Math.max(0, Math.min(index, _list.value.length - 1))
|
||||
return index;
|
||||
}
|
||||
const getIdByindex = (): string | null => {
|
||||
let id = null;
|
||||
if (_list.value.length == 0) return id
|
||||
if (nowCurrentIndex.value.length == 0) return id;
|
||||
let index = Math.max(0, Math.min(nowCurrentIndex.value[0], _list.value.length - 1))
|
||||
return _list.value[index].id
|
||||
|
||||
}
|
||||
const mchange = (indexs: number[]) => {
|
||||
|
||||
let index = indexs[0];
|
||||
let parentIndex = _parentIndex.value;
|
||||
let ids = _selectedIndex.value.slice(0)
|
||||
|
||||
|
||||
|
||||
if (parentIndex <= ids.length - 1) {
|
||||
ids.splice(parentIndex, 1, index)
|
||||
} else {
|
||||
for (let i = 0; i < _parentIndex.value; i++) {
|
||||
ids.push(0)
|
||||
}
|
||||
ids.push(index)
|
||||
|
||||
}
|
||||
|
||||
emits("changeDeep", ids)
|
||||
}
|
||||
const change = (s: number[]) => {
|
||||
emits("changeDeep", s)
|
||||
}
|
||||
|
||||
const pushDeepCount = (n: number) => {
|
||||
|
||||
emits("countChange", n)
|
||||
}
|
||||
const onChange = (event: any) => {
|
||||
if (event.detail.value.length == 0 || _list.value.length == 0) return;
|
||||
let indexs = event.detail.value!;
|
||||
if (indexs.join('') == nowCurrentIndex.value.join('')) return;
|
||||
let index = indexs[0]
|
||||
index = Math.max(0, Math.min(index, _list.value.length - 1))
|
||||
let item = _list.value[index]
|
||||
|
||||
if (item.disabled) {
|
||||
index = nowCurrentIndex.value[0] > index ? index + 1 : index - 1;
|
||||
index = Math.max(_list.value.length, index)
|
||||
index = Math.min(0, index)
|
||||
indexs = [index]
|
||||
}
|
||||
nowCurrentIndex.value = indexs;
|
||||
|
||||
mchange(indexs)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
pushDeepCount(_parentIndex.value)
|
||||
nowCurrentIndex.value = [getIndexByid()] as number[]
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
clearTimeout(tid2)
|
||||
})
|
||||
|
||||
watch([() => props.selectedIndex], () => {
|
||||
nowCurrentIndex.value = [getIndexByid()] as number[]
|
||||
|
||||
}, { deep: true })
|
||||
watch(() => props.list, () => {
|
||||
nowCurrentIndex.value = [getIndexByid()] as number[]
|
||||
mchange(nowCurrentIndex.value.slice(0))
|
||||
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view class="tmPickerViewUnit" v-if="_unitsName != ''">
|
||||
<tm-text font-size="24" class="tmPickerViewUnitText">{{ _unitsName }}</tm-text>
|
||||
</view>
|
||||
<view class="tmPickerView">
|
||||
<view style="padding: 0 2.5px;">
|
||||
<picker-view :immediate-change="false" :value="nowCurrentIndex" @change="onChange"
|
||||
:style="{ height: '250px', width: (_wrapWight - 5) + 'px' }" :mask-style="`${_maskStyle}`"
|
||||
:indicator-style="`height:50px;border:none;border-radius:10px`">
|
||||
<picker-view-column>
|
||||
<view :style="{ background: nowCurrentIndex[0] == index ? _selectedMaskStyle : 'none' }"
|
||||
v-for="(item, index) in _list" :key="index" class="tmPickerViewWrapCoumn">
|
||||
<text class="tmPickerViewWrapCoumnText" :style="{
|
||||
fontSize: _fontSize,
|
||||
lineHeight: 1,
|
||||
fontWeight: nowCurrentIndex[0] == index ? 'bold' : 'inherit',
|
||||
opacity: item.disabled ? 0.4 : (nowCurrentIndex[0] == index ? 1 : 0.6),
|
||||
color: _isDark ? 'rgba(255,255,255,0.8)' : '#000000'
|
||||
}">
|
||||
{{ item[rangText] }}
|
||||
</text>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
|
||||
</view>
|
||||
|
||||
<picker-item :rangKey="rangKey" :rangText="rangText" @countChange="pushDeepCount" :font-size="fontSize"
|
||||
:cellUnits="_cellUnits" @changeDeep="change" :selectedIndex="_selectedIndex" :wrapWight="_wrapWight"
|
||||
:parentIndex="_parentIndex + 1" v-if="_nowChildren.length > 0" :list="_nowChildren">
|
||||
</picker-item>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.uni-picker-view-indicator:after,
|
||||
.uni-picker-view-indicator:before {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tmPickerViewUnit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tmPickerViewUnitText {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tmPickerViewWrapCoumnText {
|
||||
margin: 0 6px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
|
||||
}
|
||||
|
||||
.tmPickerView {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tmPickerViewWrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmPickerContent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tmPickerMasker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xPickErBar {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
margin: 0 3px;
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.tmPickerContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: left, right, top, bottom;
|
||||
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
|
||||
}
|
||||
|
||||
.tmPickerViewWrapCoumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
/* background-color: #f5f5f5; */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,242 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import pickerItem from './picker-item.vue';
|
||||
|
||||
const { config } = useTmConfig()
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
|
||||
|
||||
const boxHeight = ref(0)
|
||||
const id = ('tmPickerItem-' + getUid())
|
||||
const nowCurrentIndex = ref([0])
|
||||
let tid = 0
|
||||
let tid2 = 1
|
||||
|
||||
const emits = defineEmits(['changeDeep'])
|
||||
const props = defineProps({
|
||||
|
||||
list: {
|
||||
type: Array as PropType<TM_PICKER_X_ITEM[][]>,
|
||||
default: (): TM_PICKER_X_ITEM[] => [] as TM_PICKER_X_ITEM[]
|
||||
},
|
||||
|
||||
|
||||
selectedIndex: {
|
||||
type: Array as PropType<number[]>,
|
||||
default: (): number[] => [] as number[]
|
||||
},
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 自定义标识id
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 自定义标识文本字段名
|
||||
*/
|
||||
rangText: {
|
||||
type: String,
|
||||
default: "title"
|
||||
},
|
||||
})
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = covetUniNumber(props.fontSize, config.unit)
|
||||
|
||||
if (config.fontSizeScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(props.fontSize)
|
||||
})
|
||||
|
||||
|
||||
|
||||
const refreshKey = ref(12)
|
||||
|
||||
const _cellUnits = computed((): string[] => props.cellUnits as string[])
|
||||
|
||||
const _list = computed((): TM_PICKER_X_ITEM[][] => props.list.slice(0))
|
||||
|
||||
|
||||
const _isDark = computed((): boolean => config.mode == 'dark')
|
||||
|
||||
const _maskStyle = computed((): string => {
|
||||
if (_isDark.value) {
|
||||
return 'background-image:linear-gradient(180deg,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0)),linear-gradient(0deg, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0))'
|
||||
}
|
||||
return 'background-image:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,0)),linear-gradient(0deg, rgba(255,255,255,0),rgba(255,255,255,0))'
|
||||
})
|
||||
const _selectedMaskStyle = computed((): string => {
|
||||
return _isDark.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'
|
||||
})
|
||||
|
||||
|
||||
|
||||
const onChange = (event: any) => {
|
||||
|
||||
if (event.detail.value.length == 0 || _list.value.length == 0) return;
|
||||
let indexs = event.detail.value!;
|
||||
if (indexs.join('') == nowCurrentIndex.value.join('')) return;
|
||||
nowCurrentIndex.value = indexs
|
||||
emits("changeDeep", indexs)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nowCurrentIndex.value = props.selectedIndex.slice(0)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
clearTimeout(tid2)
|
||||
})
|
||||
|
||||
watch(() => props.selectedIndex, (newvalue: number[]) => {
|
||||
if (newvalue.join('') == nowCurrentIndex.value.join('')) return;
|
||||
nowCurrentIndex.value = newvalue.slice(0)
|
||||
}, { deep: true })
|
||||
watch(_list, (newvalue) => {
|
||||
refreshKey.value += 1;
|
||||
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view class="tmPickerViewUnit" v-if="_cellUnits.length > 0">
|
||||
<view v-for="(item, index) in _cellUnits" :key="index" style="flex: 1;text-align: center;">
|
||||
<tm-text font-size="24" class="tmPickerViewUnitText">{{ item }}</tm-text>
|
||||
</view>
|
||||
</view>
|
||||
<picker-view :key="refreshKey" :immediate-change="true" :value="nowCurrentIndex" @change="onChange"
|
||||
:style="{ height: '250px', width: '100%' }" :mask-style="`${_maskStyle}`"
|
||||
:indicator-style="`height:50px;border:none;border-radius:10px`">
|
||||
<picker-view-column v-for="(item2, index2) in _list" :key="index2">
|
||||
<view
|
||||
:style="{ background: nowCurrentIndex[index2] == index ? _selectedMaskStyle : 'none', margin: '0 4px' }"
|
||||
v-for="(item, index) in item2" :key="index" class="tmPickerViewWrapCoumn">
|
||||
<text class="tmPickerViewWrapCoumnText" :style="{
|
||||
fontSize: _fontSize,
|
||||
lineHeight: 1,
|
||||
fontWeight: nowCurrentIndex[index2] == index ? 'bold' : 'inherit',
|
||||
opacity: item.disabled ? 0.4 : (nowCurrentIndex[index2] == index ? 1 : 0.6),
|
||||
color: _isDark ? 'rgba(255,255,255,0.8)' : '#000000'
|
||||
}">
|
||||
{{ item[rangText] || "" }}
|
||||
</text>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
</view>
|
||||
</template>
|
||||
<style>
|
||||
.uni-picker-view-indicator:after,
|
||||
.uni-picker-view-indicator:before {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tmPickerViewUnit {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tmPickerViewUnitText {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tmPickerViewWrapCoumnText {
|
||||
margin: 0 6px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
|
||||
}
|
||||
|
||||
.tmPickerView {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tmPickerViewWrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmPickerContent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tmPickerMasker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xPickErBar {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 10px;
|
||||
margin: 0 3px;
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.tmPickerContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: left, right, top, bottom;
|
||||
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
|
||||
}
|
||||
|
||||
.tmPickerViewWrapCoumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
/* background-color: #f5f5f5; */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import pickerItem from './picker-item.vue';
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
type TM_PICKER_ITEM_INFO = Record<string, any>
|
||||
const proxy = getCurrentInstance()?.proxy ?? null;
|
||||
|
||||
/**
|
||||
* @displayName 选择器容器
|
||||
* @exportName tm-picker-view
|
||||
* @category 表单组件
|
||||
* @description 可单列或者多列
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmPickerView' });
|
||||
const { config } = useTmConfig()
|
||||
const wrapTotalWidth = ref(0)
|
||||
const nowValue = ref<Array<string | number>>([])
|
||||
const _modelValueIndex = ref<Array<number>>([])
|
||||
const okNodeInfo = ref(false)
|
||||
const tid = ref(0)
|
||||
const parentIndex = ref(0)
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 选项变化时触发
|
||||
* @param {string[]} ids - 当前选中项的id
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 等同v-model:model-str
|
||||
* 只对外输出当前回选区的选中项的文本,不要外部改变此值。
|
||||
*/
|
||||
'update:modelStr',
|
||||
|
||||
'update:modelValue'
|
||||
|
||||
])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
/**
|
||||
* 数据项
|
||||
* 格式类型为:PICKER_ITEM_INFO
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<TM_PICKER_ITEM_INFO[]>,
|
||||
default: (): TM_PICKER_ITEM_INFO[] => [] as TM_PICKER_ITEM_INFO[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的id值
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的标题文本组
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 显示在顶部的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 项目的字体号大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "32"
|
||||
},
|
||||
/**
|
||||
* 自定义标识id
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 自定义标识文本字段名
|
||||
*/
|
||||
rangText: {
|
||||
type: String,
|
||||
default: "title"
|
||||
}
|
||||
})
|
||||
|
||||
const _cellUnits = computed(() => props.cellUnits)
|
||||
const _list = computed(() => props.list)
|
||||
const _maxDeep = computed(() => {
|
||||
if (_list.value.length == 0) return 0;
|
||||
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
|
||||
let deepIndex = 1;
|
||||
const node = list[0];
|
||||
if (node?.children?.length > 0) {
|
||||
deepIndex += getdiepWidth(node.children);
|
||||
}
|
||||
|
||||
return deepIndex;
|
||||
}
|
||||
return getdiepWidth(_list.value.slice(0));
|
||||
})
|
||||
const _deepWidth = computed(() => {
|
||||
if (_list.value.length == 0) return 0;
|
||||
if (_list.value.length == 0) return wrapTotalWidth.value;
|
||||
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
|
||||
let deepIndex = 1;
|
||||
const node = list[0];
|
||||
if (Array.isArray(node?.children) && node.children.length > 0) {
|
||||
deepIndex += getdiepWidth(node.children);
|
||||
}
|
||||
|
||||
return deepIndex;
|
||||
}
|
||||
return wrapTotalWidth.value / Math.max(1, getdiepWidth(_list.value))
|
||||
})
|
||||
const setChangeStrvmodel = () => {
|
||||
let listitem = getModelStr()
|
||||
let idvalue = [] as string[]
|
||||
let strs = [] as string[]
|
||||
listitem.forEach((el) => {
|
||||
idvalue.push(el[props.rangKey])
|
||||
strs.push(el[props.rangText])
|
||||
})
|
||||
|
||||
emits('update:modelStr', strs.join(","))
|
||||
}
|
||||
const oninitFun = () => {
|
||||
nowValue.value = props.modelValue.slice(0)
|
||||
_modelValueIndex.value = chuliIndex(getIdeByindex());
|
||||
setChangeStrvmodel()
|
||||
getNodeInfo();
|
||||
}
|
||||
const change = (ids: number[]) => {
|
||||
|
||||
_modelValueIndex.value = ids;
|
||||
let listitem = getModelStr()
|
||||
|
||||
let idvalue = [] as string[]
|
||||
let strs = [] as string[]
|
||||
listitem.forEach((el) => {
|
||||
idvalue.push(el[props.rangKey])
|
||||
strs.push(el[props.rangText])
|
||||
})
|
||||
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', idvalue)
|
||||
|
||||
emits('update:modelStr', strs.join(","))
|
||||
if (idvalue.join('') == props.modelValue.join('')) return;
|
||||
clearTimeout(tid.value);
|
||||
tid.value = setTimeout(function () {
|
||||
/**
|
||||
* 选项变化时触发
|
||||
* @param {string[]} ids 当前选中项的id
|
||||
*/
|
||||
emits('change', idvalue)
|
||||
|
||||
}, 30);
|
||||
}
|
||||
const chuliIndex = (indexs: number[]): number[] => {
|
||||
let ids = indexs.slice(0)
|
||||
let val = nowValue.value.slice(0)
|
||||
for (let i = 0; i < _maxDeep.value; i++) {
|
||||
if (i >= val.length) {
|
||||
ids.push(0)
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
const getIdeByindex = (): number[] => {
|
||||
let index = 0;
|
||||
let val = nowValue.value.slice(0)
|
||||
let indexs = [] as number[]
|
||||
function getIndex(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (val.length <= index || val.length == 0) return;
|
||||
let id = val[index]
|
||||
let sindex = 0
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let item = nodes[i]
|
||||
if (item[props.rangKey] == id) {
|
||||
sindex = i;
|
||||
indexs.push(sindex)
|
||||
if (item?.children?.length > 0) {
|
||||
index += 1
|
||||
getIndex(item.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getIndex(_list.value)
|
||||
return indexs;
|
||||
}
|
||||
const getModelStr = (): TM_PICKER_X_ITEM[] => {
|
||||
let ids = _modelValueIndex.value.slice(0)
|
||||
let index = 0;
|
||||
|
||||
let selectedList = [] as TM_PICKER_X_ITEM[]
|
||||
|
||||
|
||||
|
||||
function getStr(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (ids.length <= index || ids.length == 0) return;
|
||||
let idx = ids[index]
|
||||
idx = Math.max(0, Math.min(nodes.length - 1, idx));
|
||||
let item = nodes[idx];
|
||||
selectedList.push(item)
|
||||
if (item?.children?.length > 0) {
|
||||
index += 1
|
||||
getStr(item.children)
|
||||
}
|
||||
}
|
||||
getStr(_list.value)
|
||||
return selectedList
|
||||
}
|
||||
const conutChangeEnd = (n: number) => {
|
||||
|
||||
}
|
||||
const getNodeInfo = () => {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".tmPickerView")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as UniApp.NodeInfo
|
||||
wrapTotalWidth.value = nodeinfo.width!
|
||||
okNodeInfo.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
oninitFun();
|
||||
uni.$on('onResize', oninitFun)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value);
|
||||
uni.$off('onResize', oninitFun)
|
||||
})
|
||||
watch(() => props.modelValue, (newValue: Array<string | number>) => {
|
||||
if (newValue.join('') == nowValue.value.join('')) return;
|
||||
nowValue.value = newValue.slice(0);
|
||||
_modelValueIndex.value = chuliIndex(getIdeByindex());
|
||||
setChangeStrvmodel()
|
||||
}, { deep: true })
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmPickerView">
|
||||
<picker-item :rangKey="props.rangKey" :rangText="props.rangText" @countChange="conutChangeEnd"
|
||||
:font-size="props.fontSize" :cellUnits="_cellUnits" v-if="okNodeInfo" @changeDeep="change"
|
||||
:selectedIndex="_modelValueIndex" :parentIndex="parentIndex" :wrapWight="_deepWidth" :list="_list"
|
||||
class="tmPickerViewItem"></picker-item>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmPickerView {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,276 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import pickerItem from './picker-item.vue';
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
type TM_PICKER_ITEM_INFO = Record<string, any>
|
||||
const proxy = getCurrentInstance()?.proxy ?? null;
|
||||
|
||||
/**
|
||||
* @displayName 选择器容器
|
||||
* @exportName tm-picker-view
|
||||
* @category 表单组件
|
||||
* @description 可单列或者多列
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmPickerView' });
|
||||
const { config } = useTmConfig()
|
||||
const nowValue = ref<Array<string | number>>([])
|
||||
const _modelValueIndex = ref<Array<number>>([])
|
||||
const tid = ref(0)
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 选项变化时触发
|
||||
* @param {string[]} ids - 当前选中项的id
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 等同v-model:model-str
|
||||
* 只对外输出当前回选区的选中项的文本,不要外部改变此值。
|
||||
*/
|
||||
'update:modelStr',
|
||||
|
||||
'update:modelValue'
|
||||
|
||||
])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
/**
|
||||
* 数据项
|
||||
* 格式类型为:PICKER_ITEM_INFO
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<TM_PICKER_ITEM_INFO[]>,
|
||||
default: (): TM_PICKER_ITEM_INFO[] => [] as TM_PICKER_ITEM_INFO[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的id值
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array<string|number>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的标题文本组
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 显示在顶部的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 项目的字体号大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "32"
|
||||
},
|
||||
/**
|
||||
* 自定义标识id
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 自定义标识文本字段名
|
||||
*/
|
||||
rangText: {
|
||||
type: String,
|
||||
default: "title"
|
||||
}
|
||||
})
|
||||
|
||||
const _cellUnits = computed(() => props.cellUnits)
|
||||
const _list = computed(() => props.list)
|
||||
const _nowShowList = ref<Record<string,any>[][]>([])
|
||||
const _maxDeep = computed(() => {
|
||||
if (_list.value.length == 0) return 0;
|
||||
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
|
||||
let deepIndex = 1;
|
||||
const node = list[0];
|
||||
if (node?.children?.length > 0) {
|
||||
deepIndex += getdiepWidth(node.children);
|
||||
}
|
||||
return deepIndex;
|
||||
}
|
||||
return getdiepWidth(_list.value.slice(0));
|
||||
})
|
||||
|
||||
|
||||
const oninitFun = () => {
|
||||
nowValue.value = props.modelValue.slice(0)
|
||||
let currneinexs = getIndexsByids(nowValue.value).indexs;
|
||||
_modelValueIndex.value = nowValue.value.length==0||currneinexs.length==0?new Array(_maxDeep.value).fill(0):currneinexs;
|
||||
const {data,strs,ids} = getIdsByindexs(_modelValueIndex.value)
|
||||
_nowShowList.value = data.slice(0)
|
||||
if(nowValue.value.length==0){
|
||||
nowValue.value = ids.slice(0)
|
||||
nextTick(()=>{
|
||||
emits('update:modelStr', strs.join(","))
|
||||
emits('update:modelValue', ids.slice(0))
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
const change = (ixs: number[]) => {
|
||||
|
||||
const {ids,data,strs} = getIdsByindexs(ixs)
|
||||
_modelValueIndex.value = getIndexsByids(ids).indexs;
|
||||
_nowShowList.value = data.slice(0)
|
||||
nowValue.value = ids;
|
||||
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emits('update:modelValue', ids.slice(0))
|
||||
emits('update:modelStr', strs.join(","))
|
||||
|
||||
if (strs.join('') == props.modelValue.join('')) return;
|
||||
emits('change', ids.slice(0))
|
||||
}
|
||||
|
||||
|
||||
// 根据id返回索引
|
||||
const getIndexsByids = (ids:Array<string|number>):{indexs:Array<number>,data:TM_PICKER_X_ITEM[][],strs:string[]}=> {
|
||||
let index = 0;
|
||||
let val = ids.slice(0)
|
||||
let indexs = [] as number[]
|
||||
let data = [] as TM_PICKER_X_ITEM[][]
|
||||
let strs = [] as string[]
|
||||
|
||||
if(_list.value.length==0||ids.length==0) return {indexs:[],data:[],strs:[]}
|
||||
|
||||
function getIndex(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (val.length <= index || val.length == 0) return;
|
||||
let id = val[index]
|
||||
let sindex = 0
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let item = nodes[i]
|
||||
if (item[props.rangKey] == id) {
|
||||
sindex = i;
|
||||
indexs.push(sindex)
|
||||
data.push(nodes)
|
||||
strs.push(item[props.rangText])
|
||||
if (item?.children?.length > 0) {
|
||||
index += 1
|
||||
getIndex(item.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getIndex(_list.value)
|
||||
return {indexs,data,strs}
|
||||
}
|
||||
//根据索引返回ids
|
||||
const getIdsByindexs = (indexs:number[]):{ids:Array<string|number>,data:TM_PICKER_X_ITEM[][],strs:string[]} => {
|
||||
let ids = [] as string[]
|
||||
let data = [] as TM_PICKER_X_ITEM[][]
|
||||
let index = 0;
|
||||
let val = indexs.slice(0)
|
||||
let strs = [] as string[]
|
||||
|
||||
if(_list.value.length==0||indexs.length==0) return {ids:[],data:[],strs:[]}
|
||||
function getIds(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (val.length <= index || val.length == 0) return;
|
||||
let currentsIndex = val[index]
|
||||
let id = ''
|
||||
let str = ''
|
||||
let children = [];
|
||||
if(nodes.length-1 >= currentsIndex){
|
||||
id = nodes[currentsIndex][props.rangKey]
|
||||
str = nodes[currentsIndex][props.rangText]
|
||||
children = nodes[currentsIndex]?.children??[]
|
||||
}else{
|
||||
id = nodes[0][props.rangKey]
|
||||
str = nodes[0][props.rangText]
|
||||
children = nodes[0]?.children??[]
|
||||
}
|
||||
ids.push(id)
|
||||
data.push(nodes)
|
||||
strs.push(str)
|
||||
|
||||
if(children.length>0){
|
||||
index += 1
|
||||
getIds(children)
|
||||
}
|
||||
}
|
||||
getIds(_list.value)
|
||||
return {ids,data,strs};
|
||||
}
|
||||
|
||||
const getNowCurrent = ()=>{
|
||||
return getIdsByindexs(_modelValueIndex.value)
|
||||
}
|
||||
// 获取相关数据。
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
oninitFun();
|
||||
uni.$on('onResize', oninitFun)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value);
|
||||
uni.$off('onResize', oninitFun)
|
||||
})
|
||||
watch(() => props.modelValue, (newValue: Array<string | number>) => {
|
||||
if (newValue.join('') == nowValue.value.join('')) return;
|
||||
nowValue.value = newValue.slice(0);
|
||||
|
||||
let defauleindexs = nowValue.value.length==0?new Array(_maxDeep.value).fill(0):getIndexsByids(nowValue.value).indexs;
|
||||
_modelValueIndex.value = defauleindexs;
|
||||
|
||||
const {data,strs} = getIdsByindexs(_modelValueIndex.value)
|
||||
_nowShowList.value = data.slice(0)
|
||||
emits('update:modelStr', strs.join(","))
|
||||
|
||||
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => props.list, (newValue) => {
|
||||
oninitFun()
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({getIndexsByids,getIdsByindexs,getNowCurrent,onInit:oninitFun})
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmPickerView">
|
||||
<picker-item :rangKey="props.rangKey" :rangText="props.rangText"
|
||||
:font-size="props.fontSize" :cellUnits="_cellUnits" @changeDeep="change"
|
||||
:selectedIndex="_modelValueIndex" :list="_nowShowList"
|
||||
class="tmPickerViewItem"></picker-item>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmPickerView {
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
/**
|
||||
* 当前树有多少个被选中了。
|
||||
*/
|
||||
export function getTreeSelectedNum(item: TM_PICKER_X_ITEM[], target: Set<string>): number {
|
||||
let inx = 0;
|
||||
function jshz(tree: TM_PICKER_X_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: TM_PICKER_X_ITEM[], target: Set<string>): string[] {
|
||||
function jshz(tree: TM_PICKER_X_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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据给定的长度值,赋值默认值
|
||||
*/
|
||||
export function setDefaultByValueAr(list: TM_PICKER_X_ITEM[], value: string[], maxLen: number): string[] {
|
||||
let v = value;
|
||||
let pr = [] as string[];
|
||||
for (let j = 0; j < maxLen; j++) {
|
||||
pr.push("")
|
||||
}
|
||||
v = v.concat(pr).slice(0, maxLen)
|
||||
let defaultAr = [] as string[];
|
||||
function setvalue(item: TM_PICKER_X_ITEM[]) {
|
||||
if (item.length > 0) {
|
||||
defaultAr.push(item[0].id);
|
||||
if (item[0].children.length > 0) {
|
||||
setvalue(item[0].children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setvalue(list);
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (defaultAr.length - 1 >= i && v[i] == "") {
|
||||
v[i] = defaultAr[i]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通过 ID 查找节点的函数
|
||||
export function findNodeById(TreeNode: TM_PICKER_X_ITEM[], id: string): TM_PICKER_X_ITEM | null {
|
||||
let newnode = null as TM_PICKER_X_ITEM | null
|
||||
for (let i = 0; i < TreeNode.length; i++) {
|
||||
let node = TreeNode[i]
|
||||
if (node.id === id) {
|
||||
newnode = node;
|
||||
break;
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
const foundNode = findNodeById(node.children, id);
|
||||
if (foundNode != null) {
|
||||
newnode = foundNode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return newnode;
|
||||
}
|
||||
|
||||
|
||||
// 查找节点的上级节点
|
||||
export function findParentNode(nodes: TM_PICKER_X_ITEM[], targetNodeId: string): TM_PICKER_X_ITEM | null {
|
||||
function search(node: TM_PICKER_X_ITEM): TM_PICKER_X_ITEM | null {
|
||||
if (node.children.length > 0) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i];
|
||||
if (child.id === targetNodeId) {
|
||||
return node;
|
||||
} else {
|
||||
const foundParent = search(child);
|
||||
if (foundParent != null) {
|
||||
return foundParent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用for循环遍历根节点数组
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const root = nodes[i];
|
||||
const parentNode = search(root);
|
||||
if (parentNode != null) {
|
||||
return parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // 如果未找到目标节点的父节点,则返回undefined
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, type PropType, nextTick } from 'vue';
|
||||
import tmPickerView from '../tm-picker-view/tm-picker-view.vue';
|
||||
type TM_PICKER_X_ITEM = Record<string, any>
|
||||
type TM_PICKER_ITEM_INFO = Record<string, any>
|
||||
const pickerView = ref<InstanceType<typeof tmPickerView> | null>(null)
|
||||
/**
|
||||
* @displayName 选择器
|
||||
* @exportName tm-picker
|
||||
* @category 表单组件
|
||||
* @description 可单列或者多列
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmPicker' });
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
list: {
|
||||
type: Array as PropType<TM_PICKER_ITEM_INFO[]>,
|
||||
default: (): TM_PICKER_ITEM_INFO[] => [] as TM_PICKER_ITEM_INFO[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的id值
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Array<string | number>],
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 当前选中项的回显文本等同v-model:model-str
|
||||
* 请不要更改此值,此值只对外输出显示。
|
||||
* 如果空值,将内部首次递归渲染回显文本。如果你后台返回,就不会计算。
|
||||
* 因此如果对性能有要求的请务必让后台在首次显示时先回显文本,
|
||||
* 这样内部在第一次时不会递归计算回显文本,提高性能。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "请选择"
|
||||
},
|
||||
/**
|
||||
* 是否懒加载内部内容。
|
||||
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
|
||||
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
|
||||
*/
|
||||
lazyContent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 显示在顶部的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 自定义标识id
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 自定义标识文本字段名
|
||||
*/
|
||||
rangText: {
|
||||
type: String,
|
||||
default: "title"
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
'cancel',
|
||||
/**
|
||||
* 确认触发
|
||||
* @param {string[]} ids 当前选中项的id值
|
||||
*/
|
||||
'confirm',
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string[]} ids 当前选中项的id值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
'update:modelShow',
|
||||
/**
|
||||
* 等同v-model:model-str
|
||||
* 只对外输出当前回选区的选中项的文本,不要外部改变此值。
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const show = ref(false);
|
||||
const nowValue = ref<Array<string | number>>([]);
|
||||
const modelStrValue = ref("");
|
||||
const yanchiDuration = ref(false);
|
||||
|
||||
const _list = computed(() => props.list.slice(0));
|
||||
const _lazyContent = computed(() => props.lazyContent);
|
||||
const _cellUnits = computed(() => props.cellUnits);
|
||||
watch(() => props.modelValue, (newvalue: Array<string | number>) => {
|
||||
if (newvalue.join('') == nowValue.value.join('')) return;
|
||||
nowValue.value = newvalue.slice(0);
|
||||
|
||||
if (newvalue.length > 0 && _list.value.length > 0) {
|
||||
const { strs } = getIndexsByids(props.modelValue);
|
||||
emit('update:modelStr', strs.join(','));
|
||||
modelStrValue.value = strs.join(',');
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.modelShow, (newValue: boolean) => {
|
||||
if (newValue == show.value) return;
|
||||
show.value = newValue;
|
||||
});
|
||||
|
||||
watch(_list, () => {
|
||||
if (show.value) {
|
||||
return;
|
||||
}
|
||||
if (_list.value.length == 0) {
|
||||
emit('update:modelStr', '');
|
||||
modelStrValue.value = '';
|
||||
return;
|
||||
}
|
||||
if (_list.value.length > 0) {
|
||||
onSetDefaultStr();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nowValue.value = props.modelValue.slice(0);
|
||||
yanchiDuration.value = _lazyContent.value ? false : true;
|
||||
onSetDefaultStr();
|
||||
});
|
||||
|
||||
function onSetDefaultStr() {
|
||||
|
||||
if (props.modelStr == "" && _list.value.length > 0) {
|
||||
const { strs } = getIndexsByids(props.modelValue);
|
||||
|
||||
emit('update:modelStr', strs.join(','));
|
||||
modelStrValue.value = strs.join(',');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function openShow() {
|
||||
show.value = true;
|
||||
emit('update:modelShow', true);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit('update:modelShow', false);
|
||||
nowValue.value = props.modelValue.slice(0);
|
||||
if (_lazyContent.value) {
|
||||
yanchiDuration.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function strChange(str: string) {
|
||||
modelStrValue.value = str;
|
||||
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
yanchiDuration.value = true;
|
||||
|
||||
}
|
||||
|
||||
function mchange(ids: string[]) {
|
||||
emit('change', ids.slice(0));
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit('cancel');
|
||||
nowValue.value = props.modelValue.slice(0);
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
let ids = nowValue.value.slice(0);
|
||||
let str = [] as string[]
|
||||
if (ids.length == 0) {
|
||||
const temp = getNowCurrent();
|
||||
ids = temp.ids
|
||||
str = temp.strs
|
||||
} else {
|
||||
const temp = getIndexsByids(ids)
|
||||
str = temp.strs
|
||||
|
||||
}
|
||||
|
||||
nowValue.value = ids;
|
||||
modelStrValue.value = str.join(',');
|
||||
emit('update:modelValue', ids);
|
||||
emit('update:modelStr', modelStrValue.value);
|
||||
emit('confirm', ids);
|
||||
}
|
||||
|
||||
const getIndexsByids = (ids: Array<string | number>): { indexs: Array<number>, data: TM_PICKER_X_ITEM[][], strs: string[] } => {
|
||||
let index = 0;
|
||||
let val = ids.slice(0)
|
||||
let indexs = [] as number[]
|
||||
let data = [] as TM_PICKER_X_ITEM[][]
|
||||
let strs = [] as string[]
|
||||
|
||||
if (_list.value.length == 0 || ids.length == 0) return { indexs: [], data: [], strs: [] }
|
||||
|
||||
function getIndex(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (val.length <= index || val.length == 0) return;
|
||||
let id = val[index]
|
||||
let sindex = 0
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let item = nodes[i]
|
||||
if (item[props.rangKey] == id) {
|
||||
sindex = i;
|
||||
indexs.push(sindex)
|
||||
data.push(nodes)
|
||||
strs.push(item[props.rangText])
|
||||
if (item?.children?.length > 0) {
|
||||
index += 1
|
||||
getIndex(item.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getIndex(_list.value)
|
||||
return { indexs, data, strs }
|
||||
}
|
||||
//根据索引返回ids
|
||||
const getIdsByindexs = (indexs: number[]): { ids: Array<string | number>, data: TM_PICKER_X_ITEM[][], strs: string[] } => {
|
||||
let ids = [] as string[]
|
||||
let data = [] as TM_PICKER_X_ITEM[][]
|
||||
let index = 0;
|
||||
let val = indexs.slice(0)
|
||||
let strs = [] as string[]
|
||||
|
||||
if (_list.value.length == 0 || indexs.length == 0) return { ids: [], data: [], strs: [] }
|
||||
function getIds(nodes: TM_PICKER_X_ITEM[]) {
|
||||
if (val.length <= index || val.length == 0) return;
|
||||
let currentsIndex = val[index]
|
||||
let id = ''
|
||||
let str = ''
|
||||
let children = [];
|
||||
if (nodes.length - 1 >= currentsIndex) {
|
||||
id = nodes[currentsIndex][props.rangKey]
|
||||
str = nodes[currentsIndex][props.rangText]
|
||||
children = nodes[currentsIndex]?.children ?? []
|
||||
} else {
|
||||
id = nodes[0][props.rangKey]
|
||||
str = nodes[0][props.rangText]
|
||||
children = nodes[0]?.children ?? []
|
||||
}
|
||||
ids.push(id)
|
||||
data.push(nodes)
|
||||
strs.push(str)
|
||||
|
||||
if (children.length > 0) {
|
||||
index += 1
|
||||
getIds(children)
|
||||
}
|
||||
}
|
||||
getIds(_list.value)
|
||||
return { ids, data, strs };
|
||||
}
|
||||
const _maxDeep = computed(() => {
|
||||
if (_list.value.length == 0) return 0;
|
||||
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
|
||||
let deepIndex = 1;
|
||||
const node = list[0];
|
||||
if (node?.children?.length > 0) {
|
||||
deepIndex += getdiepWidth(node.children);
|
||||
}
|
||||
return deepIndex;
|
||||
}
|
||||
return getdiepWidth(_list.value.slice(0));
|
||||
})
|
||||
const getNowCurrent = () => {
|
||||
nowValue.value = props.modelValue.slice(0)
|
||||
let currneinexs = getIndexsByids(nowValue.value).indexs;
|
||||
let dinexs = nowValue.value.length == 0 || currneinexs.length == 0 ? new Array(_maxDeep.value).fill(0) : currneinexs;
|
||||
return getIdsByindexs(dinexs)
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
@binding {boolean} show - 控制打开关闭状态
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<tm-drawer @open="onOpen" :widthCoverCenter="true" :disabledScroll="true" max-height="80%" size="850" :title="title"
|
||||
@close="onClose" @confirm="onConfirm" @cancel="onCancel" :showFooter="true" v-model:show="show"
|
||||
:show-close="true">
|
||||
<tm-picker-view v-if="show" ref="pickerView" :rangKey="rangKey" :rangText="rangText" :cell-units="_cellUnits"
|
||||
@update:modelStr="strChange" @change="mchange" v-model="nowValue" :list="_list"></tm-picker-view>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, provide } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmCheckbox from '../tm-checkbox/tm-checkbox.vue';
|
||||
|
||||
/**
|
||||
* @displayName 多选框组
|
||||
* @exportName tm-radio-group
|
||||
* @page tm-radio
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmRadioGroup' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: "",
|
||||
},
|
||||
direction: {
|
||||
type: String as PropType<"row" | "column">,
|
||||
default: "row",
|
||||
},
|
||||
align: {
|
||||
type: String as PropType<"flex-start" | "center" | "flex-end">,
|
||||
default: "left"
|
||||
},
|
||||
/**
|
||||
* 排列时之间的间隙,如果是数组第一项是列间隙,第二项是行间隙
|
||||
*/
|
||||
gap: {
|
||||
type: [Number, String, Array<string | number>],
|
||||
default: 20,
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue']);
|
||||
|
||||
const checkvaluelist = ref<string | number | boolean>("");
|
||||
const isDestroy = ref(false);
|
||||
const id = "tmRadioGroup-" + getUid();
|
||||
|
||||
const _max = 1
|
||||
const _gap = computed(() => arrayNumber(props.gap).join(" "))
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
checkvaluelist.value = newValue;
|
||||
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
checkvaluelist.value = props.modelValue;
|
||||
isDestroy.value = false;
|
||||
|
||||
});
|
||||
|
||||
function addItem(item: string | number | boolean, ischange: boolean) {
|
||||
checkvaluelist.value = item
|
||||
if (ischange) {
|
||||
emits("update:modelValue", checkvaluelist.value);
|
||||
emits("change", checkvaluelist.value);
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(item: string | number | boolean) {
|
||||
if (checkvaluelist.value !== item) {
|
||||
checkvaluelist.value = item;
|
||||
emits("update:modelValue", checkvaluelist.value);
|
||||
emits("change", checkvaluelist.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
provide("tmRadioGroupValue", computed(() => checkvaluelist.value))
|
||||
|
||||
defineExpose({ addItem, removeItem })
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmCheckboxGroup"
|
||||
:style="{ 'flex-direction': direction, 'align-items': direction == 'row' ? 'flex-start' : undefined, 'justify-content': align, gap: _gap }">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tmCheckboxGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,368 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmRadioGroup from '../tm-radio-group/tm-radio-group.vue';
|
||||
import tmRadio from './tm-radio.vue';
|
||||
const proxy = getCurrentInstance()?.proxy as InstanceType<typeof tmRadio> | null;
|
||||
|
||||
/**
|
||||
* @displayName 单选框
|
||||
* @exportName tm-radio
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmRadio' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param {Boolean} check - 当前是否选中
|
||||
* @param {Number} value - 当前选中的值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前主题色,空值时取全局
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
/**
|
||||
* 当前未选中时主题色,空值时取全局
|
||||
*/
|
||||
unCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前未选中时的暗黑主题色
|
||||
*/
|
||||
darkUnCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 双向绑定值
|
||||
*/
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 选中的值,
|
||||
*/
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 未选中的值
|
||||
*/
|
||||
unCheckValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 选中的图标名称。
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "check-line"
|
||||
},
|
||||
/**
|
||||
* 右侧文字。
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否隐藏选中框。然后利用默认插槽自定义选中所有样式和状态。
|
||||
*/
|
||||
hiddenCheckbox: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: "42"
|
||||
},
|
||||
/**
|
||||
* 中间小图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "40"
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 选中的框在文本左边还是在右边
|
||||
*/
|
||||
labelDirection: {
|
||||
type: String as PropType<"left" | "right">,
|
||||
default: "left"
|
||||
},
|
||||
/**
|
||||
* 如果不要显示label时设置为false可以隐藏label并把label间隙删除,
|
||||
*/
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const nowValue = ref<string | number | boolean>('');
|
||||
const boxId = ref("tmCheckbox-" + getUid());
|
||||
const tid = ref(0);
|
||||
const isDestroy = ref(false);
|
||||
const parentVal = inject("tmRadioGroupValue", computed((): string | number | boolean => ""))
|
||||
const _color = computed(() => {
|
||||
if (props.color == "") return getDefaultColor(config.color);
|
||||
return getDefaultColor(props.color);
|
||||
});
|
||||
const _showLabel = computed(() => props.showLabel)
|
||||
const _unCheckColor = computed(() => {
|
||||
if (config.mode == 'dark' && props.darkUnCheckColor != '') {
|
||||
return getDefaultColor(props.darkUnCheckColor);
|
||||
}
|
||||
if (props.unCheckColor == "") return getDefaultColor(config.unRadioAndCheckBoxColor);
|
||||
return getDefaultColor(props.unCheckColor);
|
||||
});
|
||||
|
||||
const _isCheck = computed(() => {
|
||||
return nowValue.value === props.value;
|
||||
});
|
||||
|
||||
const _disabled = computed(() => {
|
||||
return props.disabled;
|
||||
});
|
||||
|
||||
const _label = computed(() => {
|
||||
return props.label;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const _size = computed(() => {
|
||||
let size = covetUniNumber(props.size, config.unit);
|
||||
if (config.fontSizeScale == 1) return size;
|
||||
let sizeNumber = parseInt(size);
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 24;
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(size);
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue === nowValue.value) return;
|
||||
nowValue.value = newValue;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
let t = this;
|
||||
isDestroy.value = false;
|
||||
|
||||
nextTick(() => {
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) {
|
||||
nowValue.value = props.modelValue;
|
||||
}
|
||||
// if (nowValue.value !== props.unCheckValue) {
|
||||
// pushDataToParent(false);
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
function boxClick() {
|
||||
let pelement = findParent(proxy!);
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
emit('click');
|
||||
if (_disabled.value) return;
|
||||
if (parentVal.value === props.value && pelement != null) return;
|
||||
if (_isCheck.value) {
|
||||
nowValue.value = props.unCheckValue;
|
||||
} else {
|
||||
nowValue.value = props.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前选中的值,等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param check {boolean} 当前是否选中
|
||||
* @param value {number} 当前选中的值
|
||||
*/
|
||||
emit('change', _isCheck.value, nowValue.value);
|
||||
if (_isCheck.value) {
|
||||
pushDataToParent(true);
|
||||
} else {
|
||||
removeValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setSelected(val: string | number | boolean) {
|
||||
if (props.value === val) {
|
||||
nowValue.value = props.value;
|
||||
|
||||
} else {
|
||||
nowValue.value = props.unCheckValue;
|
||||
}
|
||||
}
|
||||
|
||||
function pushDataToParent(isChange: boolean) {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement == null) return;
|
||||
let parent: InstanceType<typeof tmRadioGroup> = pelement!;
|
||||
if (typeof parent?.addItem != 'function') return;
|
||||
parent.addItem(nowValue.value, isChange);
|
||||
}
|
||||
function removeValue() {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement != null) return;
|
||||
let parent: InstanceType<typeof tmRadioGroup> = pelement!;
|
||||
if (typeof parent?.removeItem != 'function') return;
|
||||
parent.removeItem(props.value);
|
||||
}
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmRadioGroup") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmRadioGroup") return parents;
|
||||
return null;
|
||||
}
|
||||
|
||||
watch(parentVal, (newvalue: string | number | boolean) => {
|
||||
setSelected(newvalue)
|
||||
}, { deep: true })
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</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}`,
|
||||
width: _size,
|
||||
height: _size
|
||||
}">
|
||||
<view :class="['checkboxBoxIcon', 'checkboxBoxIcon_' + (nowValue ? 'on' : 'off')]">
|
||||
<tm-icon color="white" :name="icon" :size="iconSize"></tm-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view v-if="_showLabel" class="checkboxLabelBox" :class="[!hiddenCheckbox ? 'checkboxLabelBoxLeftSpace' : '']">
|
||||
<!--
|
||||
默认文本插槽
|
||||
@binding {boolean} checked - 是否选中
|
||||
-->
|
||||
<slot name="label" :checked="_isCheck">
|
||||
<tm-text :font-size="labelFontSize" class="checkboxLabel">{{ _label }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transition-property: background-color;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_on {
|
||||
opacity: 1;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_off {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.checkboxBox {
|
||||
border-radius: 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkboxLabelBoxLeftSpace {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
font-size: 28rpx;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 评分
|
||||
* @exportName tm-rate
|
||||
* @category 表单组件
|
||||
* @description 评分组件,只读和禁用等属性
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmRate' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前分值,等同v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 最大评分数量
|
||||
*/
|
||||
count: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/**
|
||||
* 选中的颜色,默认空值取全局值
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 未选中的颜色
|
||||
*/
|
||||
unColor: {
|
||||
type: String,
|
||||
default: "#cacaca"
|
||||
},
|
||||
/**
|
||||
* 未选中的暗黑背景颜色
|
||||
* 空时取InputDark表单颜色
|
||||
*/
|
||||
darkUnColor: {
|
||||
type: String,
|
||||
default: "#8b8b8b"
|
||||
},
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: {
|
||||
type: String,
|
||||
default: "42"
|
||||
},
|
||||
/**
|
||||
* 间隙
|
||||
*/
|
||||
space: {
|
||||
type: [String,Number],
|
||||
default: "8"
|
||||
},
|
||||
/**
|
||||
* 选中的图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "star-fill"
|
||||
},
|
||||
/**
|
||||
* 未选中的图标
|
||||
*/
|
||||
unicon: {
|
||||
type: String,
|
||||
default: "star-line"
|
||||
},
|
||||
/**
|
||||
* 是否只读状态
|
||||
*/
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否禁用状态
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否显示右侧评分值
|
||||
*/
|
||||
showScore: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 右侧文本分值文本的字号
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
/**
|
||||
* 是否开启半星
|
||||
* 开启半星后,自定的unicon和icon失效。
|
||||
*/
|
||||
half: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
const score = ref(0)
|
||||
const clickStartIndex = ref(-1)
|
||||
const clickStartStatus = ref(0)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue == score.value) return
|
||||
score.value = newValue
|
||||
if (props.half) {
|
||||
setDefaultHalfStatus()
|
||||
}
|
||||
})
|
||||
|
||||
const _disabled = computed(() => props.disabled)
|
||||
const _readonly = computed(() => props.readonly)
|
||||
const _count = computed(() => props.count)
|
||||
const _size = computed(() => props.size)
|
||||
const _fontSize = computed(() => {
|
||||
let fontSize = covetUniNumber(props.fontSize, config.unit)
|
||||
if (config.fontSizeScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _space = computed(() => covetUniNumber(props.space, config.unit))
|
||||
const _color = computed(() => props.color == "" ? getDefaultColor(config.color) : getDefaultColor(props.color))
|
||||
const _unColor = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkUnColor == '') {
|
||||
return getDefaultColor(config.inputDarkColor)
|
||||
} else {
|
||||
return getDefaultColor(props.darkUnColor)
|
||||
}
|
||||
}
|
||||
return getDefaultColor(props.unColor)
|
||||
})
|
||||
const _showScore = computed(() => props.showScore)
|
||||
|
||||
onMounted(() => {
|
||||
score.value = props.modelValue
|
||||
if (props.half) {
|
||||
setDefaultHalfStatus()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
})
|
||||
|
||||
function setDefaultHalfStatus() {
|
||||
if (score.value == 0) return
|
||||
clickStartIndex.value = score.value
|
||||
let fillCount = Math.ceil(score.value)
|
||||
clickStartStatus.value = score.value == (fillCount - 0.5) ? 1 : 2
|
||||
}
|
||||
|
||||
function isSelected(index : number) : boolean {
|
||||
if (props.half) {
|
||||
return score.value >= index - 0.5
|
||||
}
|
||||
return score.value >= index
|
||||
}
|
||||
|
||||
function isSelectedHalf(index : number) : string {
|
||||
if (score.value > index) {
|
||||
return 'star-fill'
|
||||
}
|
||||
if (score.value < index - 0.5) {
|
||||
return 'star-line'
|
||||
}
|
||||
|
||||
if (clickStartStatus.value == 1) {
|
||||
return 'star-half-fill'
|
||||
}
|
||||
if (clickStartStatus.value == 2) {
|
||||
return 'star-fill'
|
||||
}
|
||||
return 'star-line'
|
||||
}
|
||||
|
||||
function onClickHalf(index : number, type : number) {
|
||||
if (_readonly.value || _disabled.value) return
|
||||
|
||||
score.value = type == 1 ? index - 0.5 : index
|
||||
|
||||
clickStartStatus.value = type
|
||||
setUpdate(index)
|
||||
}
|
||||
|
||||
function onClick(index : number) {
|
||||
if (_readonly.value || _disabled.value) return
|
||||
score.value = index
|
||||
setUpdate(index)
|
||||
clickStartIndex.value = index
|
||||
}
|
||||
|
||||
function setUpdate(index : number) {
|
||||
/**
|
||||
* 同步当前分值,等同v-model
|
||||
* @param {number} score 评分
|
||||
*/
|
||||
emit('update:modelValue', score.value)
|
||||
/**
|
||||
* 评分变换时触发
|
||||
* @param {number} score 评分
|
||||
*/
|
||||
emit('change', score.value)
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmRate" :style="{opacity:_disabled?0.7:1}">
|
||||
<template v-if="!half">
|
||||
<view :ref="'rate-'+index" v-for="index in _count" :key="index">
|
||||
<tm-icon @click=" onClick(index)" :_style="`margin-right:${index==_count?'0rpx' : _space}`" :size="_size"
|
||||
:color="isSelected(index)?_color:_unColor" :dark-color="isSelected(index)?_color:_unColor"
|
||||
:name="isSelected(index)?icon:unicon"></tm-icon>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 开启半星 -->
|
||||
<template v-if="half">
|
||||
<view :ref="'rate-'+index" v-for="index in _count" :key="index" style='position:relative'>
|
||||
<tm-icon :_style="`marginRight:${index==_count?'0rpx' : _space};margin-left:${index==0?'0rpx' : _space}`"
|
||||
:size="_size" :color="isSelected(index)?_color:_unColor"
|
||||
:dark-color="isSelected(index)?_color:_unColor" :name="isSelectedHalf(index)"></tm-icon>
|
||||
<view @click="onClickHalf(index,1)" class="tmRateLeft"></view>
|
||||
<view @click="onClickHalf(index,2)" class="tmRateRight"></view>
|
||||
</view>
|
||||
</template>
|
||||
<!--
|
||||
@slot 文本分值的右侧插槽
|
||||
@binding {number} score - 当前分值
|
||||
-->
|
||||
<slot name="score" :score="score">
|
||||
<text class="tmRateText" v-if="_showScore" :style="{color:_color,fontSize:_fontSize}">
|
||||
{{score.toFixed(1)}}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmRateText {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.tmRateLeft {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px
|
||||
}
|
||||
|
||||
.tmRateRight {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
right: 0px
|
||||
}
|
||||
|
||||
.tmRate {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<!-- @vue-ignore -->
|
||||
<view @click="onClick" class="tmRow" :style="[
|
||||
{
|
||||
'flex-wrap': _attrs.wrap ? 'wrap' : 'nowrap',
|
||||
'justify-content': _attrs.justify,
|
||||
'align-items': _attrs.align,
|
||||
'gap': _gap
|
||||
}
|
||||
]">
|
||||
<!-- @slot 默认插槽,请允许放置tmCol组件 -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, PropType, provide } from 'vue';
|
||||
import {
|
||||
arrayNumber
|
||||
} from "../../libs/tool";
|
||||
/**
|
||||
* @displayName 布局Row
|
||||
* @exportName tm-row
|
||||
* @category 常用组件
|
||||
* @description 快速排版布局利器,内只能放置tmCol组件,默认是5列栅格,可以自行更改
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({
|
||||
name: "TmRow", options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
}
|
||||
});
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 默认列数,请不要动态修改此值。
|
||||
*/
|
||||
column: {
|
||||
type: [Number, String],
|
||||
default: 5
|
||||
},
|
||||
/**
|
||||
* 子元素左右对齐排列
|
||||
*/
|
||||
justify: {
|
||||
type: String as PropType<"flex-start" | "flex-end" | "center" | "space-between" | "space-around">,
|
||||
default: "start"
|
||||
},
|
||||
/**
|
||||
* 子元素上下对齐排列
|
||||
* 是align-items:值
|
||||
*/
|
||||
align: {
|
||||
type: String as PropType<"center" | "flex-start" | "flex-end" | "stretch" | "baseline">,
|
||||
default: "flex-start"
|
||||
},
|
||||
/**
|
||||
* 是否自动断行.
|
||||
*/
|
||||
wrap: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 上下左右间隙.
|
||||
* [row,col]:x表示纵向间隙,y表示横向间隙
|
||||
* col,[col]:表示横向和纵向之间的间隙.
|
||||
*/
|
||||
gutter: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: '0'
|
||||
}
|
||||
})
|
||||
const _attrs = computed(() => attrs);
|
||||
const tmRowColWidth = computed((): number => {
|
||||
let col = typeof attrs.column == 'string' ? parseInt(attrs.column) : attrs.column
|
||||
return col
|
||||
})
|
||||
const _gutter = computed(() => arrayNumber(_attrs.value.gutter))
|
||||
const _gap = computed(() => {
|
||||
|
||||
return _gutter.value.join(' ')
|
||||
})
|
||||
const onClick = ()=>{
|
||||
emits('click');
|
||||
}
|
||||
|
||||
provide("tmRowColWidth", tmRowColWidth)
|
||||
provide("tmRowColGutter", _gutter)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tmRow {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<view
|
||||
@click="OnClick"
|
||||
class="tmSheet"
|
||||
@touchstart="touchStart"
|
||||
@touchcancel="touchEnd"
|
||||
@touchend="touchEnd"
|
||||
@mousedown="touchStart"
|
||||
@mouseup="touchEnd"
|
||||
@mouseleave="touchEnd"
|
||||
:style="[
|
||||
{
|
||||
width: _width,
|
||||
height: _height,
|
||||
},
|
||||
buttonStyle,
|
||||
]"
|
||||
>
|
||||
<!-- @slot 默认插槽 -->
|
||||
<slot></slot>
|
||||
<view
|
||||
v-if="_attrs.loading"
|
||||
class="tmSheetLoading"
|
||||
:style="{ borderRadius: buttonStyle.borderRadius }"
|
||||
>
|
||||
<tm-icon name="loader-line" size="50" spin color="primary"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 容器
|
||||
* @exportName tm-sheet
|
||||
* @category 常用组件
|
||||
* @description 像一张纸张一样,用于包裹内容区域,可塑性高,可以快速用来布局.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmSheet' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @property {MouseEvent} evt 事件参数
|
||||
* */
|
||||
'click',
|
||||
])
|
||||
type PropsKeyType = keyof typeof attrs;
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 遵循规则:
|
||||
* string或者[x]:全部边线
|
||||
* [x,x]左右边线,上下边线
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
borderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: ""
|
||||
},
|
||||
darkBorderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: "transparent"
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderWidth: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: 0
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderStyle: {
|
||||
type: [String, Array<string>],
|
||||
default: "solid"
|
||||
},
|
||||
/** 规则同borderColor
|
||||
* 左上,右上,右下,左下
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 遵循规则:填写0关闭,填写空值取全局
|
||||
* string,number,或者[x]:全部
|
||||
* [x,x]左右,上下
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
margin: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 遵循规则:填写0关闭,填写空值取全局
|
||||
* string,number,或者[x]:全部
|
||||
* [x,x]左右,上下
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
padding: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否跟随主题背景色
|
||||
* 下方的color失效
|
||||
*/
|
||||
flowTheme: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 背景主题
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 自定义暗黑背景
|
||||
* 空值时取全局配置
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 渐变,前面的color,bgColor,darkBgColor失效 */
|
||||
linear: {
|
||||
type: [Array<string>],
|
||||
default: () => [],
|
||||
validator: (val: string[]) => {
|
||||
if (val.length == 0) return true;
|
||||
let pass = val.length == 3;
|
||||
if (!pass) {
|
||||
console.error("tmui提醒:linear 参数格式错误,请检查,必须长度为3");
|
||||
}
|
||||
return pass
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数字或者字符串时自动计算投影,它会投影大小
|
||||
* 数组时(必须为4),第一个x,第二为y,第三个为大小 ,第四个为投影颜色
|
||||
* 空字符串时,取全局配置,如果不想投影设置为none即可.
|
||||
*/
|
||||
shadow: {
|
||||
type: [String, Number, Array<string>],
|
||||
default: '',
|
||||
validator: (val: string | number | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length != 4) {
|
||||
console.error("tmui提醒:shadow 第一个x,第二为y,第三个为大小 ,第四个为投影颜色");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
const _attrs = computed(() => attrs);
|
||||
const OnClick = (evt: MouseEvent) => {
|
||||
if (_attrs.value.loading) return;
|
||||
emits('click', evt)
|
||||
}
|
||||
const _width = computed(() => {
|
||||
return covetUniNumber(attrs.width || 'auto')
|
||||
})
|
||||
const _height = computed(() => covetUniNumber(attrs.height || 'auto'))
|
||||
|
||||
const isHover = ref(false);
|
||||
const touchStart = () => {
|
||||
isHover.value = true;
|
||||
}
|
||||
const touchEnd = () => {
|
||||
isHover.value = false;
|
||||
}
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
let style = {
|
||||
borderColor: ``,
|
||||
borderWidth: ``,
|
||||
borderStyle: ``,
|
||||
background: ``,
|
||||
color: ``,
|
||||
borderRadius: ``,
|
||||
boxShadow: `none`,
|
||||
margin: '0px',
|
||||
padding: '0px'
|
||||
}
|
||||
let isDark = config.mode == 'dark';
|
||||
|
||||
let background = attrs.color
|
||||
let borderColor = arrayNumberValidByStyleBorderColor(attrs.borderColor)
|
||||
let borderWidth = arrayNumberValidByStyleMP(attrs.borderWidth)
|
||||
let borderStyle = arrayNumberValidByStyleBorderStyle(attrs.borderStyle)
|
||||
let borderRadius = arrayNumberValidByStyleMP(attrs.round || config.sheetRadius)
|
||||
let margin = arrayNumberValidByStyleMP(attrs.margin === '' ? config.sheetMargin : attrs.margin)
|
||||
let padding = arrayNumberValidByStyleMP(attrs.padding === '' ? config.sheetPadding : attrs.padding)
|
||||
let linear = linearValid(attrs.linear)
|
||||
|
||||
if (attrs.flowTheme) {
|
||||
background = config.color
|
||||
}
|
||||
if (isDark) {
|
||||
background = attrs.darkBgColor || config.sheetDarkColor
|
||||
}
|
||||
|
||||
let shadow = 'none'
|
||||
if (Array.isArray(attrs.shadow)) {
|
||||
shadow = attrs.shadow.join(' ')
|
||||
} else if (attrs.shadow !== '' && attrs.shadow != 'none') {
|
||||
let obj = getDefaultColorObj(attrs.color, attrs.color)
|
||||
shadow = `0px 0px ${covetUniNumber(attrs.shadow)} ${obj.default.shadow}`
|
||||
} else if (attrs.shadow === '') {
|
||||
if (Array.isArray(config.sheetShadow)) {
|
||||
shadow = config.sheetShadow.join(' ')
|
||||
} else if (config.sheetShadow && config.sheetShadow != 'none') {
|
||||
let obj = getDefaultColorObj(attrs.color, attrs.color)
|
||||
shadow = `0 4px ${covetUniNumber(config.sheetShadow)} ${obj.default.shadow}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
style.background = linear || getDefaultColor(background)
|
||||
style.borderColor = borderColor.join(' ')
|
||||
style.borderWidth = borderWidth.join(' ')
|
||||
style.borderStyle = borderStyle.join(' ')
|
||||
style.borderRadius = borderRadius.join(' ')
|
||||
style.margin = margin.join(' ')
|
||||
style.padding = padding.join(' ')
|
||||
style.boxShadow = shadow
|
||||
return style;
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmSheet {
|
||||
position: relative;
|
||||
|
||||
.tmSheetLoading {
|
||||
display: flex;
|
||||
border: 0px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
backdrop-filter: blur(3px);
|
||||
background-color: rgba(125, 125, 125, 0.2);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,361 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, ComponentInstance, onUpdated, nextTick, VueElement, inject, watchEffect } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj, isBlackAndWhite, setBgColorLightByDark } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
|
||||
/**
|
||||
* @displayName 步进器
|
||||
* @exportName tm-stepper
|
||||
* @category 表单组件
|
||||
* @description 可整数,小数
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmNavbar' });
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前值,可v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 最大值
|
||||
*/
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
/**
|
||||
* 组件宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 最小值
|
||||
*/
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 步进值
|
||||
*/
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 如果进步值是小数位需要设置此值
|
||||
*/
|
||||
decimalLen: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 按钮的颜色
|
||||
*/
|
||||
btnColor: {
|
||||
type: String,
|
||||
default: "info"
|
||||
},
|
||||
/**
|
||||
* 按钮的暗黑颜色
|
||||
* 空值读取全局的Input暗黑背景色
|
||||
*/
|
||||
darkBtnColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 输入框的背景色
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "info"
|
||||
},
|
||||
/**
|
||||
* 输入框的暗黑背景色
|
||||
* 空值读取全局的Input暗黑背景色
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 按钮的宽
|
||||
*/
|
||||
btnWidth: {
|
||||
type: String,
|
||||
default: "64"
|
||||
},
|
||||
/**
|
||||
* 输入框及按钮的高
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: "64"
|
||||
},
|
||||
/**
|
||||
* 按钮的圆角。
|
||||
*/
|
||||
round: {
|
||||
type: String,
|
||||
default: "8"
|
||||
},
|
||||
/**
|
||||
* 是否按钮与输入框独立开来
|
||||
* 不和输入框粘一起。
|
||||
*/
|
||||
splitBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 按钮文本颜色,暗黑时取白色
|
||||
*/
|
||||
btnFontColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 文本颜色,暗黑时取白色
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 文本文字大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 输入值或者点击按钮时触发
|
||||
* @param {number} str - 当前的值。
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 等同vmodel
|
||||
*/
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
const _value = ref(0)
|
||||
const _input_value = ref("")
|
||||
const addDomDisabeld = ref(false)
|
||||
const surDomDisabeld = ref(false)
|
||||
|
||||
const _round = computed((): string => {
|
||||
return covetUniNumber(props.round, config.unit)
|
||||
})
|
||||
|
||||
const _width = computed((): string => covetUniNumber(props.width, config.unit))
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = covetUniNumber(props.fontSize, config.unit);
|
||||
if (config.fontSizeScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 14
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _unFontSize = computed((): string => props.fontSize)
|
||||
|
||||
const _btnFontColor = computed((): string => getDefaultColor(props.btnFontColor))
|
||||
|
||||
const _fontColor = computed((): string => {
|
||||
if (config.mode == 'dark') return '#ffffff'
|
||||
return getDefaultColor(props.fontColor)
|
||||
})
|
||||
|
||||
const _height = computed((): string => covetUniNumber(props.height, config.unit))
|
||||
|
||||
const _btnWidth = computed((): string => covetUniNumber(props.btnWidth, config.unit))
|
||||
|
||||
const _splitBtn = computed((): boolean => props.splitBtn)
|
||||
|
||||
const _btnColor = computed((): string => {
|
||||
let color = getDefaultColor(props.btnColor)
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkBtnColor == "") {
|
||||
color = config.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(props.darkBtnColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
let color = getDefaultColor(props.bgColor)
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkBgColor == "") {
|
||||
color = config.inputDarkColor
|
||||
} else {
|
||||
color = getDefaultColor(props.darkBgColor)
|
||||
}
|
||||
}
|
||||
return color
|
||||
})
|
||||
|
||||
const _max = computed((): number => props.max)
|
||||
const _min = computed((): number => props.min)
|
||||
const _step = computed((): number => props.step)
|
||||
const _disabled = computed((): boolean => props.disabled)
|
||||
|
||||
watch(() => props.modelValue, (newval: number) => {
|
||||
if (newval == _value.value) return;
|
||||
setValue(newval, false)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setValue(props.modelValue, false)
|
||||
})
|
||||
|
||||
const isInRange = (value: number): boolean => {
|
||||
return value >= _min.value && value <= _max.value;
|
||||
}
|
||||
|
||||
const clampValue = (value: number): number => {
|
||||
return Math.min(Math.max(value, _min.value), _max.value);
|
||||
}
|
||||
|
||||
const getDecimalPlaces = (): number => {
|
||||
return props.decimalLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加
|
||||
*/
|
||||
const handleIncrement = () => {
|
||||
const newValue = Math.min(_value.value + _step.value, props.max);
|
||||
let val = clampValue(parseFloat(newValue.toFixed(props.decimalLen)));
|
||||
setValue(val, true)
|
||||
}
|
||||
|
||||
/**减 */
|
||||
const handleDecrement = () => {
|
||||
const newValue = Math.max(_value.value - _step.value, props.min);
|
||||
let val = clampValue(parseFloat(newValue.toFixed(props.decimalLen)));
|
||||
setValue(val, true)
|
||||
}
|
||||
|
||||
const handleInputChange = (event: any) => {
|
||||
_input_value.value = event.detail.value
|
||||
}
|
||||
|
||||
const inputBlur = () => {
|
||||
const inputValue = parseFloat(_input_value.value);
|
||||
if (!isNaN(inputValue)) {
|
||||
let val = clampValue(inputValue);
|
||||
setValue(val, true)
|
||||
} else {
|
||||
setValue(parseFloat(_min.value.toFixed(props.decimalLen)), true)
|
||||
}
|
||||
}
|
||||
|
||||
const setValue = (value: number, isEmit: boolean) => {
|
||||
const clampedValue = clampValue(value);
|
||||
_value.value = clampedValue;
|
||||
_input_value.value = _value.value.toString();
|
||||
addDomDisabeld.value = _value.value >= props.max
|
||||
surDomDisabeld.value = _value.value <= props.min
|
||||
|
||||
if (isEmit) {
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
emit("update:modelValue", clampedValue)
|
||||
/**
|
||||
* 输入值或者点击按钮时触发
|
||||
* @param {number} 当前的值。
|
||||
*/
|
||||
emit("change", clampedValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="xStepper" :style="{ width: _width, borderRadius: _round }">
|
||||
<view :hover-start-time="20" :hover-stay-time="150" :hover-class="addDomDisabeld ? '' : 'xStepperHoverbtn'"
|
||||
@click="handleDecrement" class="xStepperBtn"
|
||||
:style="{ backgroundColor: _btnColor, height: _height, width: _btnWidth, opacity: surDomDisabeld ? 0.6 : 1, borderRadius: _splitBtn ? '50px' : '0rpx' }">
|
||||
<tm-icon _style="pointer-events: none" :color="_btnFontColor" :size="_unFontSize"
|
||||
name="subtract-line"></tm-icon>
|
||||
</view>
|
||||
|
||||
<input @blur="inputBlur" @input="handleInputChange" :value="_input_value" class="xStepperInput"
|
||||
:style="{ backgroundColor: _splitBtn ? 'transparent' : _btnColor, height: _height, color: _fontColor, fontSize: _fontSize }"
|
||||
:type="decimalLen > 0 ? 'digit' : 'number'" />
|
||||
<view :hover-start-time="20" :hover-stay-time="150" :hover-class="addDomDisabeld ? '' : 'xStepperHoverbtn'"
|
||||
@click="handleIncrement" class="xStepperBtn"
|
||||
:style="{ backgroundColor: _btnColor, height: _height, width: _btnWidth, borderRadius: _splitBtn ? '50px' : '0rpx' }">
|
||||
<tm-icon :_style="` opacity: ${addDomDisabeld} ? 0.6 : 1,'pointerEvents': 'none'`" :color="_btnFontColor"
|
||||
:size="_unFontSize" name="add-line"></tm-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.xStepperBtnBtn {
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.xStepperHoverbtn {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.xStepper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xStepperInput {
|
||||
flex: 1;
|
||||
margin: 0 1px;
|
||||
padding: 0 5px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.xStepperBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch, PropType } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
/**
|
||||
* @displayName 粘性布局头
|
||||
* @exportName tm-sticky-header
|
||||
* @category 导航组件
|
||||
* @page tm-sticky
|
||||
* @description 只能是tm-sticky直接子节点,可以在节点内自由布局.可以多个放到tm-sticky中形成多个头置顶,当滚动到此位置时自动置顶.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmSwitch' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/** 顶部偏移距离,必须是px单位. */
|
||||
top: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
/** 顶部距离 */
|
||||
zIndex: {
|
||||
type: [Number, String],
|
||||
default: 2
|
||||
},
|
||||
})
|
||||
const windowTop = ref('0rpx')
|
||||
const _top = computed(() => covetUniNumber(props.top, 'px'))
|
||||
const _zIndex = computed(() => props.zIndex)
|
||||
|
||||
onMounted(() => {
|
||||
windowTop.value = uni.getSystemInfoSync().windowTop + 'px'
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tmStickyHeader {
|
||||
position: sticky;
|
||||
left: 0px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="tmStickyHeader" :style="{ top: `calc(${_top} + ${windowTop})`, zIndex: _zIndex }">
|
||||
<!--
|
||||
@slot 头部内容
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch, PropType } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
/**
|
||||
* @displayName 粘性布局
|
||||
* @exportName tm-sticky
|
||||
* @category 导航组件
|
||||
* @description 对于需要多个头定位的页面,可以使用粘性布局,将页面分为多个区域,每个区域可以单独设置粘性。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmStikcy' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tmSticky{
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="tmSticky">
|
||||
<!--
|
||||
@slot 正常布局内容,如果想粘性头,tm-sticky-header组件必须是直接子节点组件
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,371 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch, PropType } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
/**
|
||||
* @displayName 开关
|
||||
* @exportName tm-switch
|
||||
* @category 表单组件
|
||||
* @description 开关,用于直观的展示选项表单的选择。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmSwitch' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 激活时的背景色,空值时取全局的值。
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 未激活时的背景
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "info"
|
||||
},
|
||||
/**
|
||||
* 未激活时的暗黑背景
|
||||
* 空取inputDarkColor
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 按钮的背景色
|
||||
*/
|
||||
btnColor: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: {
|
||||
type: String as PropType<"small" | "normal" | "large">,
|
||||
default: "normal"
|
||||
},
|
||||
/**
|
||||
* 间隙,px单位
|
||||
*/
|
||||
space: {
|
||||
type: [Number, String],
|
||||
default: "2px"
|
||||
},
|
||||
/**
|
||||
* 当前打开的状态,默认为false
|
||||
* 等同v-model=""
|
||||
*/
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否加载中
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 开关文字数组第一个为开,后一个为关
|
||||
*/
|
||||
label: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => [],
|
||||
validator(val: string[]): boolean {
|
||||
if (!Array.isArray(val)) return false;
|
||||
if (val.length == 0) return true;
|
||||
if (val.length != 2) {
|
||||
console.error("x:必须长度为2")
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 圆角。空值时取全局值。
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 状态变换时触发。
|
||||
* @param status - 当前的开关状态
|
||||
*/
|
||||
(e: 'change', status: boolean): void;
|
||||
/**
|
||||
* 组件被点击时触发。
|
||||
* @param status - 当前的开关状态,这里的状态是在变更前。
|
||||
*/
|
||||
(e: 'click', status: boolean): void;
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const id = ref(`tmSwitch${getUid()}`);
|
||||
const boxwidth = ref(0);
|
||||
const boxheight = ref(0);
|
||||
const opened = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
opened.value = props.modelValue;
|
||||
getNodes();
|
||||
});
|
||||
|
||||
const _round = computed((): string => {
|
||||
if (props.round == "") {
|
||||
return covetUniNumber(config.switchRadius, config.unit)
|
||||
}
|
||||
return covetUniNumber(props.round, config.unit)
|
||||
});
|
||||
|
||||
const _bgColor = computed((): string => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkBgColor != '') return props.darkBgColor
|
||||
return config.inputDarkColor;
|
||||
}
|
||||
return getDefaultColor(props.bgColor)
|
||||
});
|
||||
|
||||
const _activeBgColor = computed((): string => {
|
||||
if (props.color == "") {
|
||||
return getDefaultColor(config.color)
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
});
|
||||
|
||||
const _btnColor = computed((): string => {
|
||||
return getDefaultColor(props.btnColor)
|
||||
});
|
||||
|
||||
const _size = computed((): string => {
|
||||
if (props.size == "small") return "44rpx"
|
||||
if (props.size == "large") return "76rpx"
|
||||
return "60rpx"
|
||||
});
|
||||
|
||||
const _fontSize = computed((): string => {
|
||||
if (props.size == "small") return "20rpx"
|
||||
if (props.size == "large") return "24rpx"
|
||||
return "22rpx"
|
||||
});
|
||||
|
||||
const _sizeWidth = computed((): string => {
|
||||
if (props.size == "small") return "88rpx"
|
||||
if (props.size == "large") return "152rpx"
|
||||
return "120rpx"
|
||||
});
|
||||
|
||||
const _space = computed((): number => {
|
||||
let space = covetUniNumber(props.space)
|
||||
let spaceUnit = getUnit(space)
|
||||
let spacenumber = parseInt(space)
|
||||
if (spaceUnit == 'rpx') {
|
||||
spacenumber = uni.upx2px(parseInt(space))
|
||||
}
|
||||
|
||||
return spacenumber
|
||||
});
|
||||
|
||||
const _contentWidth = computed((): number => {
|
||||
return boxwidth.value - _space.value * 2
|
||||
});
|
||||
|
||||
const _contentHeight = computed((): number => {
|
||||
return boxheight.value - _space.value * 2
|
||||
});
|
||||
|
||||
const _maxLeftPos = computed((): number => {
|
||||
return _contentWidth.value - _contentHeight.value
|
||||
});
|
||||
|
||||
const _label = computed((): string[] => {
|
||||
return props.label;
|
||||
});
|
||||
|
||||
const _loading = computed((): boolean => {
|
||||
return props.loading;
|
||||
});
|
||||
|
||||
const _disabled = computed((): boolean => {
|
||||
return props.disabled;
|
||||
});
|
||||
|
||||
const _animationFun = computed((): string => {
|
||||
return config.animation
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newvalue: boolean) => {
|
||||
if (newvalue != opened.value) {
|
||||
opened.value = newvalue;
|
||||
}
|
||||
});
|
||||
|
||||
function onClick() {
|
||||
/**
|
||||
* 组件被点击时触发。
|
||||
* @param status {boolean} 当前的开关状态,这里的状态是在变更前。
|
||||
*/
|
||||
emit("click", opened.value)
|
||||
if (_loading.value || _disabled.value) return;
|
||||
opened.value = !opened.value
|
||||
/**
|
||||
* 状态变换时触发。
|
||||
* @param status {boolean} 当前的开关状态
|
||||
*/
|
||||
emit("change", opened.value)
|
||||
/**
|
||||
* 等同v-model=""
|
||||
*/
|
||||
emit("update:modelValue", opened.value)
|
||||
}
|
||||
|
||||
function getNodes() {
|
||||
let width = covetUniNumber(_sizeWidth.value)
|
||||
let height = covetUniNumber(_size.value)
|
||||
let widthUnit = getUnit(width)
|
||||
let heightUnit = getUnit(height)
|
||||
if (widthUnit == 'rpx') {
|
||||
width = uni.upx2px(parseInt(width)) + ''
|
||||
}
|
||||
if (heightUnit == 'rpx') {
|
||||
height = uni.upx2px(parseInt(height)) + ''
|
||||
}
|
||||
boxwidth.value = parseInt(width);
|
||||
boxheight.value = parseInt(height);
|
||||
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view @click="onClick" :id="id" class="tmSwitch" ref="tmSwitch" :style="{
|
||||
height: boxheight+'px',
|
||||
width: boxwidth+'px',
|
||||
backgroundColor: _bgColor,
|
||||
borderRadius: _round
|
||||
}">
|
||||
<view :class="[_disabled ? 'tmSwitchDisabled' : '', opened ? 'tmSwitchBgOn' : 'tmSwitchOff']" class="tmSwitchBg"
|
||||
:style="{ backgroundColor: _activeBgColor, borderRadius: _round, 'transition-timing-function': _animationFun }">
|
||||
</view>
|
||||
<view :class="[_disabled ? 'tmSwitchDisabled' : '']" class="tmSwitchWrap" :style="{
|
||||
left: _space + 'px',
|
||||
top: _space + 'px',
|
||||
width: _contentWidth + 'px',
|
||||
height: _contentHeight + 'px',
|
||||
borderRadius: _round
|
||||
}">
|
||||
<view v-if="_label.length == 2" class="tmSwitchText">
|
||||
<text class="tmSwitchTextLeft" :style="{ fontSize: _fontSize }">{{ !opened ? '' : _label[0] }}</text>
|
||||
<text class="tmSwitchTextRight" :style="{ fontSize: _fontSize }">{{ opened ? '' : _label[1] }}</text>
|
||||
</view>
|
||||
<view class="tmSwitchBtn" :style="{
|
||||
width: _contentHeight + 'px',
|
||||
height: _contentHeight + 'px',
|
||||
transform: `translateX(${opened ? _maxLeftPos : 0}px)`,
|
||||
backgroundColor: _btnColor,
|
||||
borderRadius: _round,
|
||||
'transition-timing-function': _animationFun
|
||||
}">
|
||||
<tm-icon v-if="_loading" :spin="true" size="24" name="loader-line" :color="_activeBgColor"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tmSwitchDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: no-drop !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmSwitchTextLeft {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tmSwitchTextRight {
|
||||
color: #acacac;
|
||||
}
|
||||
|
||||
.tmSwitch {
|
||||
height: 64rpx;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmSwitchBg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition-duration: 350ms;
|
||||
transition-property: opacity, transform;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.tmSwitchBgOn {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.tmSwitchOff {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.tmSwitchWrap {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tmSwitchText {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmSwitchBtn {
|
||||
position: absolute;
|
||||
transition-duration: 350ms;
|
||||
left: 0px;
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,466 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, PropType, provide, nextTick } from 'vue';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { tmDate, type tmDateTypeTime, createDate } from '../../libs/tmDate';
|
||||
import { covetUniNumber, getUnit } from '../../libs/tool';
|
||||
import { getDefaultColor } from '../../libs/colors';
|
||||
|
||||
/**
|
||||
* @displayName 标签导航
|
||||
* @exportName tm-tabs
|
||||
* @category 导航组件
|
||||
* @description tabs标签导航组件,样式非常丰富可以完成你想要的自定义样式.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmTabs' });
|
||||
const { config } = useTmConfig()
|
||||
const emit = defineEmits([
|
||||
/** 变化时触发 */
|
||||
"change",
|
||||
/** 同步vmodel */
|
||||
"update:modelValue"
|
||||
])
|
||||
const props = defineProps(
|
||||
{
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 宽
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 高是必填,不可为auto。
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: "88"
|
||||
},
|
||||
/**
|
||||
* 排版模式
|
||||
* around:居中并且均分[数据多时会造成无法滚动]
|
||||
* between:均分在整个宽上(如想相项目宽一样,你可以设置属性width比如你有3个可以设置为33%)[数据多时会造成无法滚动]
|
||||
* start:左对齐[数据多时可滚动]
|
||||
* end:右对齐[数据多时可滚动]
|
||||
* center:中间对齐[数据多时会造成无法滚动]
|
||||
*/
|
||||
mode: {
|
||||
type: String as PropType<"around" | "between" | "start" | "end" | "center">,
|
||||
default: "start"
|
||||
},
|
||||
/**
|
||||
* 背景
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white"
|
||||
},
|
||||
/**
|
||||
* 暗黑时的背景,如果不填写读取sheetDark
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 文本激活时的颜色 ,空值默认取全局主题色
|
||||
*/
|
||||
activeTitleColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 文本默认颜色
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: "#888888"
|
||||
},
|
||||
/**
|
||||
* 文本默认的暗黑颜色,如果不填写取白色。
|
||||
*/
|
||||
darkTitleColor: {
|
||||
type: String,
|
||||
default: "#cacaca"
|
||||
},
|
||||
/**
|
||||
* 底部线条激活时的颜色,空值默认取全局主题色
|
||||
*/
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
lineHeight: {
|
||||
type: String,
|
||||
default: "3px"
|
||||
},
|
||||
/**
|
||||
* 底部的线条是与项目等宽还是固定默认的小宽度。
|
||||
*/
|
||||
lineFull: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否显示底部的线条
|
||||
*/
|
||||
showLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* id值,如果数据没有提供id属性,这里的id就是索引
|
||||
* 等同v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 标题字号
|
||||
*/
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 激活时的标题字号
|
||||
*/
|
||||
activeFontSize: {
|
||||
type: [String, Number],
|
||||
default: "32"
|
||||
},
|
||||
/**
|
||||
* 项目宽度,默认是auto,即自动根据标题内容自动撑开宽度。
|
||||
* 如果你想均分(适合不超过一行),比如你有5个标签,那么你就可以设置为"20%"
|
||||
*/
|
||||
itemWidth: {
|
||||
type: [String, Number],
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 标题的padding是左右的间隙。
|
||||
*/
|
||||
titlePadding: {
|
||||
type: [String, Number],
|
||||
default: "12px"
|
||||
},
|
||||
/**
|
||||
* 选中当前项是,移动的差值个数。比如你要让选中的居中,你一屏内
|
||||
* 刚好显示5个,你就设置为2,如果你一屏显示3个,就设置为1,就是你一屏内
|
||||
* 里面要显示的是单数,才好控制选中的始终是在中间位置 。
|
||||
*/
|
||||
moveCount: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
/**
|
||||
* 项目之间的间隙.如果你设定了itemwidth一般会有富余宽,两边自然有间隙,此时你应该设置为0以免造成不居中的假象.
|
||||
*/
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: 20
|
||||
},
|
||||
/**
|
||||
* 数据数组,可以混合类型可以是单类型
|
||||
* 数据格式比较灵活.
|
||||
* 如果数据是对象且出现了dot:true该项数组右上角出现角标红点
|
||||
* 如果dot:非boolean比如:dot:32会出现数字角标或者文本角标.如果为false或者空值为隐藏
|
||||
*/
|
||||
list: {
|
||||
type: [Array<Record<string, any> | string>],
|
||||
default: () => [],
|
||||
require: true
|
||||
},
|
||||
/**
|
||||
* 当你的list为对象时可用
|
||||
* 如果数据是string会直接取数据本身
|
||||
*/
|
||||
rangKey: {
|
||||
type: String,
|
||||
default: "title"
|
||||
},
|
||||
/**
|
||||
* id标识,用来vmodel双向绑定选中的值,
|
||||
* 如果你的list中不存在或者没有id标识
|
||||
* 那么取数组循环的索引值
|
||||
* */
|
||||
rangId: {
|
||||
type: String,
|
||||
default: "id"
|
||||
},
|
||||
/**
|
||||
* 读取角标的标识字段,默认是dot
|
||||
* */
|
||||
rangDot: {
|
||||
type: String,
|
||||
default: "dot"
|
||||
},
|
||||
/**
|
||||
* 角标的背景,默认是等同激活的标题颜色
|
||||
* 如果填写使用本值,文字颜色始终是白的.
|
||||
*/
|
||||
dotBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 默认的自定项目的style样式,可以用来完全覆盖默认的样式.
|
||||
*/
|
||||
customItemStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 默认的激活自定项目的style样式,可以用来完全覆盖默认的样式.
|
||||
*/
|
||||
activeItemStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
}
|
||||
)
|
||||
const _showLine = computed(() => props.showLine)
|
||||
|
||||
const _lineFull = computed(() => props.lineFull)
|
||||
const _customItemStyle = computed(() => props.customItemStyle)
|
||||
const _activeItemStyle = computed(() => props.activeItemStyle)
|
||||
|
||||
const _fontSize = computed(() => {
|
||||
let fontSize = covetUniNumber(props.fontSize, config.unit);
|
||||
if (config.fontSizeScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 30
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _itemWidth = computed(() => covetUniNumber(props.itemWidth, config.unit))
|
||||
|
||||
const _round = computed(() => covetUniNumber(props.round, config.unit))
|
||||
|
||||
const _activeFontSize = computed(() => {
|
||||
// covetUniNumber(props.activeFontSize, config.unit)
|
||||
let fontSize = covetUniNumber(props.activeFontSize, config.unit);
|
||||
if (config.fontSizeScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 32
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _lineHeight = computed(() => covetUniNumber(props.lineHeight, config.unit))
|
||||
const _gap = computed(() => covetUniNumber(props.gap, config.unit))
|
||||
const _height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
|
||||
const _width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
|
||||
const _color = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkColor != '') {
|
||||
return getDefaultColor(props.darkColor)
|
||||
} else {
|
||||
return getDefaultColor(config.sheetDarkColor)
|
||||
}
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
|
||||
const _activeTitleColor = computed(() => {
|
||||
if (props.activeTitleColor == "") return getDefaultColor(config.color);
|
||||
return getDefaultColor(props.activeTitleColor)
|
||||
})
|
||||
const _dotBgColor = computed(() => {
|
||||
if (props.dotBgColor == "") return _activeTitleColor.value
|
||||
return props.dotBgColor
|
||||
})
|
||||
|
||||
const _titleColor = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkTitleColor != '') {
|
||||
return getDefaultColor(props.darkTitleColor)
|
||||
} else {
|
||||
return "#ffffff"
|
||||
}
|
||||
}
|
||||
return getDefaultColor(props.titleColor)
|
||||
})
|
||||
|
||||
const _lineColor = computed(() => {
|
||||
if (props.lineColor == "") return _activeTitleColor.value;
|
||||
return getDefaultColor(props.lineColor)
|
||||
})
|
||||
|
||||
const activeName = ref(props.modelValue)
|
||||
const moveName = ref(props.modelValue)
|
||||
const _list = computed(() => props.list)
|
||||
const _listIds = computed(() => {
|
||||
let ids: Array<string | number> = _list.value.map((item, index) => {
|
||||
if (typeof item === 'string') return index
|
||||
let id = typeof item[props.rangId] == 'undefined' ? index : item[props.rangId]
|
||||
return id;
|
||||
})
|
||||
return ids;
|
||||
})
|
||||
const _mode = computed(() => {
|
||||
if (props.mode == 'around') return "space-around"
|
||||
if (props.mode == 'between') return "space-between"
|
||||
if (props.mode == 'center') return "center"
|
||||
if (props.mode == 'end') return "flex-end"
|
||||
return 'flex-start'
|
||||
})
|
||||
provide('tmTabsActiveName', computed((): string | number => activeName.value))
|
||||
|
||||
watch(() => props.modelValue, (newValue: string | number) => {
|
||||
if (newValue == activeName.value) return;
|
||||
toggleActivate(newValue)
|
||||
})
|
||||
const getValue = (item: Record<string, any> | string): string => {
|
||||
if (typeof item === 'string') return item;
|
||||
let value = item[props.rangKey] || ""
|
||||
return value;
|
||||
}
|
||||
const getDot = (item: Record<string, any> | string): boolean | string | number => {
|
||||
if (typeof item === 'string') return false;
|
||||
let value = typeof item[props.rangDot] == 'undefined' ? false : item[props.rangDot]
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* 切换选中
|
||||
*/
|
||||
const toggleActivate = (name: string | number, isEmits = false) => {
|
||||
activeName.value = name
|
||||
if (isEmits) {
|
||||
emit('update:modelValue', name)
|
||||
emit('change', name)
|
||||
}
|
||||
changMovename(name)
|
||||
}
|
||||
const changMovename = (name: string | number) => {
|
||||
let index = _listIds.value.findIndex(el => el == name)
|
||||
if (index == -1) return;
|
||||
index = index - props.moveCount
|
||||
index = Math.max(0, index)
|
||||
moveName.value = ''
|
||||
|
||||
nextTick(() => {
|
||||
moveName.value = _listIds.value[index];
|
||||
})
|
||||
}
|
||||
const tabsOnclick = (item: string | Record<string, any>, index: number) => {
|
||||
let name = _listIds.value[index];
|
||||
if (name == activeName.value) return;
|
||||
toggleActivate(name, true)
|
||||
}
|
||||
|
||||
defineExpose({ toggleActivate })
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmTabs" :style="{
|
||||
backgroundColor: _color,
|
||||
'border-radius': _round,
|
||||
height: _height,
|
||||
width: _width
|
||||
}
|
||||
">
|
||||
<scroll-view :show-scrollbar="false" enable-flex :scroll-into-view="'tabs_' + moveName" class="tmTabsWrapBox"
|
||||
:scroll-x="true">
|
||||
<view class="tmTabsWrap" :style="{ justifyContent: _mode }">
|
||||
<view @click="tabsOnclick(item, index)" class="tmTabsItem" :id="'tabs_' + _listIds[index]"
|
||||
v-for="(item, index) in _list" :key="index" :style="[
|
||||
{
|
||||
color: activeName == _listIds[index] ? _activeTitleColor : _titleColor,
|
||||
fontSize: activeName == _listIds[index] ? _activeFontSize : _fontSize,
|
||||
fontWeight: activeName == _listIds[index] ? 'bold' : 'normal',
|
||||
width: _itemWidth,
|
||||
padding: `0px ${_gap}`
|
||||
},
|
||||
activeName == _listIds[index] ? _activeItemStyle : _customItemStyle
|
||||
]">
|
||||
<tm-badge :bg-color="_dotBgColor" :label="getDot(item)">
|
||||
{{ getValue(item) }}
|
||||
</tm-badge>
|
||||
<view v-if="_showLine" class="tmTabsItemLiner" :style="{
|
||||
width: activeName == _listIds[index] ? (_lineFull ? '100%' : '36%') : '0%',
|
||||
height: _lineHeight,
|
||||
maxWidth: !_lineFull ? '36px' : 'auto',
|
||||
backgroundColor: activeName == _listIds[index] ? _lineColor : 'transparent'
|
||||
}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
<style lang="css">
|
||||
.tmTabsItem {
|
||||
/* padding: 0 20rpx; */
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmTabsItemLiner {
|
||||
/* height: 2px; */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 0px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
}
|
||||
|
||||
.tmTabs {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmTabsWrapBox {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.tmTabsWrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 100%;
|
||||
/* justify-content: flex-start; */
|
||||
align-items: center;
|
||||
/* width: auto; */
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<view @click="onClick" class="tmTag" :class="[_attrs.disabled ? 'tmTagLoading' : '']" :style="[
|
||||
{
|
||||
minWidth: _width,
|
||||
height: _height,
|
||||
fontSize: _fontSize,
|
||||
},
|
||||
buttonStyle,
|
||||
]">
|
||||
|
||||
<!--
|
||||
@slot icon图标
|
||||
-->
|
||||
<slot name="icon">
|
||||
<tm-icon v-if="_attrs.icon" :name="_attrs.icon" :size="_attrs.iconSize || _fontSize"
|
||||
:color="buttonStyle.color"></tm-icon>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, type PropType, computed, ref } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, linearValid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 标签
|
||||
* @exportName tm-tag
|
||||
* @category 常用组件
|
||||
* @description 标签组件,用来展示一些标签信息.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmTag' });
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 尺寸大小,可选值:s/m/n/g/xs
|
||||
*/
|
||||
size: {
|
||||
type: String as PropType<"s" | "g" | "xs" | "m" | "n">,
|
||||
default: "n"
|
||||
},
|
||||
/**
|
||||
* 样式
|
||||
* 可选值:thin/outlined/text/normal/dashed
|
||||
*/
|
||||
skin: {
|
||||
type: String as PropType<"thin" | "outlined" | "text" | "normal" | "dashed">,
|
||||
default: "normal"
|
||||
},
|
||||
/**
|
||||
* 遵循规则:
|
||||
* string或者[x]:全部边线
|
||||
* [x,x]左右边线,上下边线
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
borderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: ""
|
||||
},
|
||||
darkBorderColor: {
|
||||
type: [String, Array<string>],
|
||||
default: "transparent"
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderWidth: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: 1
|
||||
},
|
||||
/** 规则同borderColor */
|
||||
borderStyle: {
|
||||
type: [String, Array<string>],
|
||||
default: "solid"
|
||||
},
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
* 左上,右上,右下,左下
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: '10'
|
||||
},
|
||||
/**宽度 */
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**高度 */
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**开放能力 */
|
||||
openType: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**是否禁用 */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**图标名称 */
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**图标大小 */
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义主题颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义背景 */
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑背景 */
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义字体颜色 */
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑字体颜色 */
|
||||
darkFontColorColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 渐变,前面的color,bgColor,darkBgColor失效 */
|
||||
linear: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
validator: (val: string[]) => {
|
||||
if (val.length == 0) return true;
|
||||
let pass = val.length == 3;
|
||||
if (!pass) {
|
||||
console.error("tmui提醒:linear 参数格式错误,请检查,必须长度为3");
|
||||
}
|
||||
return pass
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数字或者字符串时自动计算投影,它会投影大小
|
||||
* 数组时(必须为4),第一个x,第二为y,第三个为大小 ,第四个为投影颜色
|
||||
* 空字符串时,取全局配置,如果不想投影设置为none即可.
|
||||
*/
|
||||
shadow: {
|
||||
type: [String, Number, Array<string>],
|
||||
default: '',
|
||||
validator: (val: string | number | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length != 4) {
|
||||
console.error("tmui提醒:shadow 第一个x,第二为y,第三个为大小 ,第四个为投影颜色");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
type PropsKeyType = keyof typeof attrs;
|
||||
|
||||
const covetButtonSize = (ops: PropsKeyType, s: TM.BUTTON_SIZE, size?: string | number) => {
|
||||
const widths = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '48'],
|
||||
['s', '76'],
|
||||
['m', '88'],
|
||||
['n', '100'],
|
||||
['g', '140']
|
||||
|
||||
])
|
||||
const heights = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '36'],
|
||||
['s', '48'],
|
||||
['m', '58'],
|
||||
['n', '68'],
|
||||
['g', '78']
|
||||
|
||||
])
|
||||
const fontSizes = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '20'],
|
||||
['s', '22'],
|
||||
['m', '24'],
|
||||
['n', '28'],
|
||||
['g', '32']
|
||||
])
|
||||
const rounds = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '8'],
|
||||
['s', '10'],
|
||||
['m', '10'],
|
||||
['n', '10'],
|
||||
['g', '12']
|
||||
])
|
||||
if (ops === 'width') {
|
||||
return covetUniNumber(widths.get(s)!)
|
||||
}
|
||||
if (ops === 'height') {
|
||||
return covetUniNumber(heights.get(s)!)
|
||||
}
|
||||
if (ops === 'round') {
|
||||
if (size && s == 'n') {
|
||||
return covetUniNumber(size!)
|
||||
}
|
||||
return covetUniNumber(rounds.get(s)!)
|
||||
}
|
||||
return covetUniNumber(fontSizes.get(s)!);
|
||||
}
|
||||
const _attrs = computed(() => attrs);
|
||||
const _width = computed(() => {
|
||||
return attrs.width === '' ? covetButtonSize('width', attrs.size) : covetUniNumber(attrs.width)
|
||||
})
|
||||
const _height = computed(() => attrs.height === '' ? covetButtonSize('height', attrs.size) : covetUniNumber(attrs.height))
|
||||
const _fontSize = computed(() => attrs.fontSize === '' ? covetButtonSize('fontSize', attrs.size) : covetUniNumber(attrs.fontSize))
|
||||
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
let style = {
|
||||
borderColor: ``,
|
||||
borderWidth: ``,
|
||||
borderStyle: ``,
|
||||
background: ``,
|
||||
color: ``,
|
||||
borderRadius: ``,
|
||||
boxShadow: `none`
|
||||
}
|
||||
let isDark = config.mode == 'dark';
|
||||
let color = attrs.color || config.color;
|
||||
let round = attrs.round === '' ? covetButtonSize('round', attrs.size, attrs.round) : attrs.round;
|
||||
let linear = linearValid(attrs.linear)
|
||||
let background = linear || (attrs.bgColor ? getDefaultColor(attrs.bgColor) : '')
|
||||
|
||||
let fontColor = attrs.fontColor ? getDefaultColor(attrs.fontColor) : ''
|
||||
|
||||
let borderWidth = arrayNumberValidByStyleMP(attrs.borderWidth)
|
||||
let borderStyle = arrayNumberValidByStyleBorderStyle(attrs.borderStyle).join(" ")
|
||||
let borderRadius = arrayNumberValidByStyleMP(attrs.round || config.sheetRadius)
|
||||
|
||||
|
||||
if (isDark) {
|
||||
if (attrs.darkBgColor) {
|
||||
background = getDefaultColor(attrs.darkBgColor)
|
||||
}
|
||||
if (attrs.darkFontColorColor) {
|
||||
fontColor = getDefaultColor(attrs.darkFontColorColor)
|
||||
}
|
||||
}
|
||||
|
||||
let obj = getDefaultColorObj(color, color)
|
||||
if (attrs.skin == 'text') {
|
||||
obj = getTextColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'thin') {
|
||||
obj = getThinColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'outlined' || attrs.skin == 'dashed') {
|
||||
obj = getOutlineColorObj(color, color)
|
||||
}
|
||||
if (attrs.skin == 'dashed') {
|
||||
borderStyle = 'dashed'
|
||||
}
|
||||
if (attrs.skin == 'outlined') {
|
||||
borderStyle = 'solid'
|
||||
}
|
||||
|
||||
if (obj) {
|
||||
let borderColor = arrayNumberValidByStyleBorderColor(attrs.borderColor || obj.default.borderColor)
|
||||
|
||||
style.color = linear ? '#ffffff' : fontColor || obj.default.fontColor;
|
||||
style.background = background || obj.default.background;
|
||||
style.borderStyle = borderStyle;
|
||||
style.borderColor = borderColor.join(" ");
|
||||
style.borderWidth = linear ? '0px' : borderWidth.join(" ")
|
||||
style.borderRadius = borderRadius.join(" ")
|
||||
let shadow = 'none'
|
||||
if (Array.isArray(attrs.shadow)) {
|
||||
shadow = attrs.shadow.join(' ')
|
||||
} else if (attrs.shadow !== '' && attrs.shadow != 'none') {
|
||||
shadow = `0 2px ${covetUniNumber(attrs.shadow)} ${obj.default.shadow}`
|
||||
}
|
||||
|
||||
style.boxShadow = shadow
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
if (_attrs.value.disabled) return;
|
||||
emits('click');
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmTag {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 12rpx;
|
||||
|
||||
&.tmTagLoading {
|
||||
opacity: 0.5;
|
||||
|
||||
.tmTagBlock,
|
||||
& {
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<view @click="OnClick" :style="[_style]" class="xTextLines">
|
||||
<!--
|
||||
@slot 默认文本插槽,如果使用插槽,那么相关特性功能将会失效。
|
||||
因此不建议使用插槽,还不如自己用text标签。
|
||||
-->
|
||||
<slot>
|
||||
<text v-if="_label.length == 0" :user-select="selectable" :selectable="selectable">{{ _attrs.label }}</text>
|
||||
</slot>
|
||||
<text @click="itemClick(item.text)" :selectable="selectable" :style="[
|
||||
{
|
||||
color: item.color,
|
||||
lineHeight: lineHeight,
|
||||
fontSize: _fontSize,
|
||||
},
|
||||
item.isHeightLight ? heightLightStyle : '',
|
||||
]" v-for="(item, index) in _label" :key="index">
|
||||
{{ item.text }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, setTextColorLightByDark, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
|
||||
|
||||
/**
|
||||
* @displayName 文本
|
||||
* @exportName tm-text
|
||||
* @category 常用组件
|
||||
* @description 允许正则高亮或者,词高亮.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmText' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const slots = useSlots()
|
||||
type ITEMINFO = {
|
||||
text: string,
|
||||
color: string,
|
||||
isHeightLight: boolean
|
||||
}
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
*文本点击
|
||||
* @property {String} word 点击的文本内容
|
||||
* */
|
||||
'click',
|
||||
/**
|
||||
*正则的文本被点击
|
||||
* @property {String} word 点击的文本内容
|
||||
* */
|
||||
'itemClick'
|
||||
])
|
||||
// 使用defineProps定义组件的props
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 文本内容
|
||||
* 这个prop用于接收显示在组件中的文本或数字
|
||||
*/
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 需要特别高亮的词
|
||||
*/
|
||||
heightLight: {
|
||||
type: [Array<string>],
|
||||
default: (): string[] => [] as string[]
|
||||
},
|
||||
/**
|
||||
* 高亮正则
|
||||
* 用于定义文本中哪些部分应该被高亮显示的正则表达式数组
|
||||
*/
|
||||
heightLightReg: {
|
||||
type: [Array<string>],
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 高亮文本的自定义样式
|
||||
*/
|
||||
heightLightStyle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 行数
|
||||
* 控制文本显示时应该占用的行数(如果组件支持此功能)
|
||||
*/
|
||||
lines: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
/**
|
||||
* 是否可选
|
||||
* 指示文本是否可以被用户选择(如通过长按选择)
|
||||
*/
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 文本颜色
|
||||
* 定义文本的主要颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "#333"
|
||||
},
|
||||
/**
|
||||
* 暗色模式颜色
|
||||
* 在暗色模式下文本的颜色(如果组件支持暗色模式)
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 高亮颜色
|
||||
* 文本中应该被高亮显示的部分的颜色
|
||||
*/
|
||||
heightLightColor: {
|
||||
type: String,
|
||||
default: "primary"
|
||||
},
|
||||
/**
|
||||
* 行高
|
||||
* 文本的行高设置
|
||||
*/
|
||||
lineHeight: {
|
||||
type: [String, Number],
|
||||
default: "1.5"
|
||||
},
|
||||
/**
|
||||
* 字体大小
|
||||
* 文本的字体大小设置
|
||||
*/
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
})
|
||||
const _attrs = computed(() => attrs)
|
||||
/**
|
||||
* 点击事件处理函数
|
||||
* @returns 无返回值
|
||||
*/
|
||||
const OnClick = () => {
|
||||
emits('click', attrs.label)
|
||||
|
||||
}
|
||||
/**
|
||||
* 正则的文本被点击
|
||||
* @param text 被点击的文本内容
|
||||
*/
|
||||
const itemClick = (text: string) => {
|
||||
emits('itemClick', text)
|
||||
}
|
||||
|
||||
const _color = computed((): string => {
|
||||
|
||||
let color = attrs.color;
|
||||
if (config.mode == 'dark') {
|
||||
if (attrs.darkColor != "") {
|
||||
color = attrs.darkColor!
|
||||
return getDefaultColor(color)
|
||||
}
|
||||
return setTextColorLightByDark(color)
|
||||
}
|
||||
return getDefaultColor(attrs.color)
|
||||
})
|
||||
|
||||
const _label = computed((): ITEMINFO[] => {
|
||||
let slotsdefault = slots.default;
|
||||
if (typeof slotsdefault == 'function') {
|
||||
if (slotsdefault().length > 0) return [] as ITEMINFO[];
|
||||
} else if (typeof slotsdefault == 'boolean') {
|
||||
if (slotsdefault) return [] as ITEMINFO[];
|
||||
}
|
||||
|
||||
let keywords = attrs.heightLight
|
||||
let albel = typeof attrs.label == 'number' ? attrs.label.toString() : attrs.label;
|
||||
|
||||
if ((keywords.length == 0 && attrs.heightLightReg.length == 0) || albel.length == 0) {
|
||||
return [{ text: albel, color: _color.value, isHeightLight: false } as ITEMINFO]
|
||||
}
|
||||
let regexxAr = [] as string[];
|
||||
|
||||
attrs.heightLightReg.forEach(function (reg: string) {
|
||||
let regex = new RegExp(reg, 'gi');
|
||||
let rulst = regex.exec(albel)
|
||||
while (rulst != null && Array.isArray(rulst)) {
|
||||
if (Array.isArray(rulst)) {
|
||||
let str = rulst[0]! as string;
|
||||
if (!regexxAr.includes(str)) {
|
||||
regexxAr.push(str)
|
||||
}
|
||||
|
||||
}
|
||||
rulst = regex.exec(albel)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
keywords = keywords.concat(regexxAr)
|
||||
|
||||
// 遍历关键词数组
|
||||
keywords.forEach(function (keyword: string) {
|
||||
// 创建一个正则表达式,使用'i'标志表示不区分大小写
|
||||
let regex = new RegExp(keyword, 'gi');
|
||||
// 使用replace方法将匹配到的关键词用带有高亮样式的span标签包裹起来
|
||||
albel = albel.replace(regex, `~-<span>${keyword}</span>~-`);
|
||||
});
|
||||
|
||||
let ps = albel.split('~-')
|
||||
let ar = [] as ITEMINFO[]
|
||||
ps.forEach((el: string) => {
|
||||
if (el.length > 0) {
|
||||
let start = el.indexOf('<span>')
|
||||
if (start > -1) {
|
||||
let end = el.lastIndexOf('</span>')
|
||||
ar.push({
|
||||
text: el.substring(start + 6, end),
|
||||
color: _heightLightColor.value,
|
||||
isHeightLight: true
|
||||
} as ITEMINFO)
|
||||
} else {
|
||||
ar.push({
|
||||
text: el,
|
||||
color: _color.value,
|
||||
isHeightLight: false
|
||||
} as ITEMINFO)
|
||||
}
|
||||
}
|
||||
})
|
||||
return ar;
|
||||
})
|
||||
const _heightLightColor = computed((): string => {
|
||||
return getDefaultColor(attrs.heightLightColor)
|
||||
})
|
||||
const _fontSize = computed((): string => {
|
||||
let fontSize = attrs.fontSize || config.fontSizeBase
|
||||
fontSize = covetUniNumber(fontSize, config.unit);
|
||||
if (config.fontSizeScale == 1) return fontSize;
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 28
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _style = computed((): { [key: string]: any } => {
|
||||
let obj: { [key: string]: any } = {}
|
||||
obj["-webkit-line-clamp"] = attrs.lines.toString()
|
||||
obj["lineHeight"] = attrs.lineHeight
|
||||
obj["fontSize"] = _fontSize.value
|
||||
obj["color"] = _color.value
|
||||
return obj
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,629 @@
|
||||
<template>
|
||||
<view class="tmUploadPhoto">
|
||||
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount&&props.addPos=='before'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
<view class="tmUploadPhotoItemAdd" :style="{borderRadius:_round}">
|
||||
<tm-icon :color="config.color" size="52" name="add-line"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="tmUploadPhotoItem" :style="{ width: `calc(${_width} - 10rpx)`, height: _height,borderRadius:_round }"
|
||||
v-for="(item, index) in list" :key="index">
|
||||
<image @click="imagePreve(index)" :src="item.path" :mode="props.mode"
|
||||
style="width: 100%;height: 100%;border-radius: 10rpx;"></image>
|
||||
<view class="tmUploadLabel" :style="{ color: STATUS_COLOR.get(item.status) || 'white' }">{{ item.statusText
|
||||
}}
|
||||
</view>
|
||||
<view @click="remove(index, item)" class="tmUploadDel"
|
||||
v-if="(item.status == TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS && _okFileIsDelete) || (item.status != TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS && item.status != TMUPLOAD_PHOTO_STATUS.UPLOADING)">
|
||||
<tm-icon color="error" size="36" name="close-circle-fill"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount&&props.addPos=='after'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
<view class="tmUploadPhotoItemAdd" :style="{borderRadius:_round}">
|
||||
<tm-icon :color="config.color" size="52" name="add-line"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType, computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { covetUniNumber } from '../../libs/tool';
|
||||
import { TMUPLOAD_PHOTO_STATUS } from '../../interface';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
|
||||
/**
|
||||
* @displayName 图片上传
|
||||
* @exportName tm-upload-photo
|
||||
* @category 表单组件
|
||||
* @description 样式丰富塑造性好
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmUploadPhoto' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
type BEFOREDELTYPE = (index: number, item: TM.TMUPLOAD_PHOTO_INFO) => Promise<boolean>
|
||||
type BEFORECOMPLETE = (res: any) => Promise<boolean>
|
||||
type CHOOSEFILE_FILES = {
|
||||
path: string,
|
||||
size: number
|
||||
}
|
||||
const STATUS_TEXT = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
[0, '待上传'],
|
||||
[1, '上传中'],
|
||||
[2, '上传成功'],
|
||||
[3, '上传失败'],
|
||||
[4, '已取消'],
|
||||
[5, '超出大小限制']
|
||||
])
|
||||
const STATUS_COLOR = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
[0, '#fff'],
|
||||
[1, '#0579FF'],
|
||||
[2, '#07d73d'],
|
||||
[3, '#f10226'],
|
||||
[4, '#F2F2F2'],
|
||||
[5, '#ffa61e']
|
||||
])
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 每次全部上传完时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 变化时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'delete',
|
||||
/**
|
||||
* 同步文件列表。
|
||||
*/
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 最大的可上传数量
|
||||
*/
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 9
|
||||
},
|
||||
/**
|
||||
* 图片项目圆角。
|
||||
*/
|
||||
round:{
|
||||
type:[String,Number],
|
||||
default:10
|
||||
},
|
||||
/**
|
||||
* 上传地址
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: "https://mockapi.eolink.com/LRViGGZ8e6c1e8b4a636cd82bca1eb15d2635ed8c74e774/admin/upload_pic/"
|
||||
},
|
||||
/**
|
||||
* 上传到服务器的名称字段
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "file"
|
||||
},
|
||||
/**
|
||||
* 上传到服务器的头文件
|
||||
*/
|
||||
header: {
|
||||
type: Object as PropType<Record<string, any> | null>,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 额外的表单数据。
|
||||
*/
|
||||
formData: {
|
||||
type: Object as PropType<Record<string, any> | null>,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 图片高,此处不可使用%单位
|
||||
*/
|
||||
imgHeight: {
|
||||
type: String,
|
||||
default: "110"
|
||||
},
|
||||
/**
|
||||
* 一行显示几列
|
||||
*/
|
||||
column: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/**
|
||||
* 上传成功的文件是否允许删除
|
||||
*/
|
||||
okFileIsDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 上传中的文件是否允许删除
|
||||
*/
|
||||
uploadingFileIsDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片上传的限制大小,默认是30mb(可以开启压缩,最后可能比较小)
|
||||
*/
|
||||
maxFileSize: {
|
||||
type: Number,
|
||||
default: 1024 * 1024 * 30
|
||||
},
|
||||
/**
|
||||
* 开发者服务器响应码,默认是200表示上传成功.
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array<Record<string, any> | string>,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 用于回显时,如果你提供的数组数据是对象,需要指定图片的地址.
|
||||
* 同时该对象数据会被保留到response字段中
|
||||
*/
|
||||
rangUrl: {
|
||||
type: String,
|
||||
default: 'url'
|
||||
},
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* 如果返回Promise<false>删除失败否则成功
|
||||
* 类型null|(index:number,item:XUPLOADFILE_FILE_INFO)=>Promise<boolean>
|
||||
*/
|
||||
beforeDel: {
|
||||
type: [Function, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 你需要原路返回参数提供的item
|
||||
* item可以自行修改响应内容,响应类型这样可以自己根据服务的内容判断
|
||||
* 是成功还是失败或者没有权限。修改其中的status来达到成功还是失败TMUPLOAD_PHOTO_STATUS
|
||||
*/
|
||||
beforeComplete: {
|
||||
type: [Function, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否自动上传
|
||||
*/
|
||||
autoStart: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片来源同官方的sourceType:
|
||||
* 'album','camera'
|
||||
*/
|
||||
sourceType: {
|
||||
type: [Array<"album" | "camera">],
|
||||
default: () => ["album", "camera"]
|
||||
},
|
||||
/**
|
||||
* 是否压缩
|
||||
*/
|
||||
compress: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 压缩后的缩放高,0表示不压缩高
|
||||
*/
|
||||
compressedHeight: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 压缩后的缩放高,0表示不压缩宽
|
||||
*/
|
||||
compressedWidth: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 压缩质量
|
||||
*/
|
||||
quality: {
|
||||
type: Number,
|
||||
default: 80
|
||||
},
|
||||
/**
|
||||
* 添加图片的位置
|
||||
* 'before'出现在前面
|
||||
* 'after'出现在后面
|
||||
*/
|
||||
addPos: {
|
||||
type: String as PropType<'before' | 'after'>,
|
||||
default: "after"
|
||||
},
|
||||
/**
|
||||
* 图片展示的缩放模式同官方.
|
||||
*/
|
||||
mode: {
|
||||
type: String,
|
||||
default: "scaleToFill"
|
||||
}
|
||||
})
|
||||
const list = ref<TM.TMUPLOAD_PHOTO_INFO[]>([])
|
||||
const _coloumn = computed(() => props.column)
|
||||
const _okFileIsDelete = computed(() => props.okFileIsDelete)
|
||||
const _height = computed(() => covetUniNumber(props.imgHeight))
|
||||
const _width = computed(() => 100 / props.column + '%')
|
||||
const _round = computed(() => covetUniNumber(props.round))
|
||||
const uploadIndex = ref(0);
|
||||
const uploading = ref(false)
|
||||
//可选择的图片数据。0表示已经达到最大数量
|
||||
const _selectedCount = computed(() => Math.max(0, Math.min(props.maxCount - list.value.length, 9)))
|
||||
const getUUID = () => Math.random() * 100 + '' + Date.now();
|
||||
/**
|
||||
* 转换为待上传列表的文件列表。
|
||||
* @param files 转换的数据
|
||||
*/
|
||||
const covertFileType = (files: CHOOSEFILE_FILES[]) => {
|
||||
let flist = files.map((item): TM.TMUPLOAD_PHOTO_INFO => {
|
||||
let status = item.size > props.maxFileSize ? TMUPLOAD_PHOTO_STATUS.EXCEEDS_SIZE_LIMIT : TMUPLOAD_PHOTO_STATUS.NOT;
|
||||
return {
|
||||
path: item.path,
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 0,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: null
|
||||
}
|
||||
})
|
||||
return flist;
|
||||
}
|
||||
/**
|
||||
* 转换外部文件.
|
||||
* @param files 转换的数据
|
||||
*/
|
||||
const covertFileTypeByModelvalue = (files: Array<Record<string, any> | string>) => {
|
||||
let flist = files.map((item): TM.TMUPLOAD_PHOTO_INFO => {
|
||||
let status = TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS;
|
||||
if (typeof item == 'string') {
|
||||
return {
|
||||
path: item,
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 100,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: item
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: item[props.rangUrl] || item['path'] || "",
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 100,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: item
|
||||
}
|
||||
})
|
||||
let nowfilesrc = list.value.map(item => item.path)
|
||||
let filst2 = flist.filter(item => !nowfilesrc.includes(item.path))
|
||||
return filst2;
|
||||
}
|
||||
/**
|
||||
* 选择图片
|
||||
*/
|
||||
const choose = () => {
|
||||
let resultfiles = [] as CHOOSEFILE_FILES[];
|
||||
let index = 0;
|
||||
// uni.showLoading({ title: '...' })
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: _selectedCount.value,
|
||||
sourceType: props.sourceType,
|
||||
fail(e) {
|
||||
// uni.hideLoading()
|
||||
|
||||
},
|
||||
success(res) {
|
||||
let files = res.tempFiles as any | any[];
|
||||
let flist = [] as CHOOSEFILE_FILES[];
|
||||
if (!Array.isArray(files)) {
|
||||
flist.push({
|
||||
path: files.path,
|
||||
size: files.size
|
||||
})
|
||||
} else {
|
||||
flist = files.map((item) => {
|
||||
return {
|
||||
path: item.path,
|
||||
size: item.size
|
||||
}
|
||||
});
|
||||
}
|
||||
resultfiles = flist
|
||||
if (resultfiles.length == 0) {
|
||||
// uni.hideLoading()
|
||||
return;
|
||||
}
|
||||
compressImage()
|
||||
},
|
||||
complete() {
|
||||
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
uni.chooseMedia({
|
||||
count: _selectedCount.value,
|
||||
mediaType: ["image"],
|
||||
sourceType: props.sourceType,
|
||||
fail(e) {
|
||||
// uni.hideLoading()
|
||||
console.log(e)
|
||||
},
|
||||
success(res) {
|
||||
let files = res.tempFiles as any | any[];
|
||||
let flist = [] as CHOOSEFILE_FILES[];
|
||||
if (!Array.isArray(files)) {
|
||||
flist.push({
|
||||
path: files.tempFilePath,
|
||||
size: files.size
|
||||
})
|
||||
} else {
|
||||
flist = files.map((item) => {
|
||||
return {
|
||||
path: item.tempFilePath,
|
||||
size: item.size
|
||||
}
|
||||
});
|
||||
}
|
||||
resultfiles = flist
|
||||
if (resultfiles.length == 0) {
|
||||
// uni.hideLoading()
|
||||
return;
|
||||
}
|
||||
compressImage()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
function compressImage() {
|
||||
// #ifdef H5
|
||||
// uni.hideLoading()
|
||||
// 结束。
|
||||
list.value = [...list.value, ...covertFileType(resultfiles)]
|
||||
emit('change', list.value.slice(0))
|
||||
if (props.autoStart) {
|
||||
startUpload();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
if (index >= resultfiles.length || !props.compress) {
|
||||
// uni.hideLoading()
|
||||
// 结束。
|
||||
list.value = [...list.value, ...covertFileType(resultfiles)]
|
||||
emit('change', list.value.slice(0))
|
||||
if (props.autoStart) {
|
||||
startUpload();
|
||||
}
|
||||
return
|
||||
}
|
||||
let item = resultfiles[index];
|
||||
uni.compressImage({
|
||||
src: item.path,
|
||||
quality: props.quality,
|
||||
width: props.compressedWidth,
|
||||
height: props.compressedWidth,
|
||||
success: (res) => {
|
||||
item.path = res.tempFilePath
|
||||
index += 1;
|
||||
compressImage();
|
||||
},
|
||||
fail() {
|
||||
index += 1;
|
||||
compressImage();
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 手动上传
|
||||
*/
|
||||
const startUpload = () => {
|
||||
if (uploading.value) {
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
uploadIndex.value = 0;
|
||||
upload();
|
||||
}
|
||||
const upload = () => {
|
||||
//结束上传。
|
||||
if (uploadIndex.value >= list.value.length) {
|
||||
uploading.value = false;
|
||||
emit('complete', list.value.slice(0))
|
||||
emit('update:modelValue', list.value.slice(0))
|
||||
return;
|
||||
}
|
||||
let item = list.value[uploadIndex.value];
|
||||
// 跳过上传中或已经上传成功的图片。
|
||||
if (item.status == TMUPLOAD_PHOTO_STATUS.EXCEEDS_SIZE_LIMIT || item.status == TMUPLOAD_PHOTO_STATUS.UPLOADING || item.status == TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS) {
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
return;
|
||||
}
|
||||
uni.uploadFile({
|
||||
url: props.url,
|
||||
filePath: item.path,
|
||||
name: props.name,
|
||||
formData: props.formData,
|
||||
header: props.header,
|
||||
success: async (res) => {
|
||||
let isComplete = true;
|
||||
if (typeof props.beforeComplete === 'function') {
|
||||
isComplete = await props.beforeComplete(res)
|
||||
}
|
||||
if (res.statusCode != 200 || !isComplete) {
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_FAILED;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
item.response = res.data;
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
return;
|
||||
}
|
||||
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS;
|
||||
item.response = res.data;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
},
|
||||
fail: (res) => {
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_FAILED;
|
||||
item.response = res.errMsg;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const remove = async (index: number, item: TM.TMUPLOAD_PHOTO_INFO) => {
|
||||
if (uploading.value) return;
|
||||
let isDele = true;
|
||||
if (typeof props.beforeDel === 'function') {
|
||||
isDele = await props.beforeDel(index, item)
|
||||
}
|
||||
if (!isDele) return;
|
||||
|
||||
list.value.splice(index, 1);
|
||||
emit('change', list.value.slice(0))
|
||||
emit('update:modelValue', list.value.slice(0))
|
||||
emit('delete',item)
|
||||
}
|
||||
const imagePreve = (index: number) => {
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: list.value.map(item => item.path)
|
||||
})
|
||||
}
|
||||
watch(() => props.modelValue, (val) => {
|
||||
let temlist = covertFileTypeByModelvalue(props.modelValue).slice(0);
|
||||
if (temlist.length == 0) return;
|
||||
list.value = [...list.value.slice(0), ...temlist]
|
||||
}, { immediate: true })
|
||||
|
||||
defineExpose({ startUpload, choose })
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.tmUploadDel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
padding: 3px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tmUploadLabel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5rpx;
|
||||
border-radius: 0 0 10rpx 10rpx;
|
||||
background-color: rgba(6, 8, 15, 0.46);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 0rpx;
|
||||
bottom: 0rpx;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tmUploadPhoto {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
gap: 5rpx;
|
||||
}
|
||||
|
||||
.tmUploadPhotoItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmUploadPhotoItemAdd {
|
||||
border: 1px solid #e6e7e7;
|
||||
background-color: #f3f3f3;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width:100%;
|
||||
height:100%;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<view class="tmUploadPhoto">
|
||||
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount && props.addPos == 'before'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
<view class="tmUploadPhotoItemAdd" :style="{borderRadius:_round}">
|
||||
<tm-icon :color="config.color" size="52" name="add-line"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="tmUploadPhotoItem" :style="{ width: `calc(${_width} - 10rpx)`, height: _height,borderRadius:_round }"
|
||||
v-for="(item, index) in list" :key="index">
|
||||
<video :autoplay="false" :controls="false" @click="imagePreve(index)" :src="item.path" :mode="props.mode"
|
||||
style="width: 100%;height: 100%;border-radius: 10rpx;"></video>
|
||||
<view class="tmUploadLabel" :style="{ color: STATUS_COLOR.get(item.status) || 'white' }">{{ item.statusText
|
||||
}}
|
||||
</view>
|
||||
<view @click="remove(index, item)" class="tmUploadDel"
|
||||
v-if="(item.status == TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS && _okFileIsDelete) || (item.status != TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS && item.status != TMUPLOAD_PHOTO_STATUS.UPLOADING)">
|
||||
<tm-icon color="error" size="36" name="close-circle-fill"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmUploadPhotoItem " v-if="_selectedCount && props.addPos == 'after'" @click="choose"
|
||||
:style="{ width: `calc(${_width} - 10rpx)`, height: _height }">
|
||||
<!--
|
||||
@slot 上传按钮插槽
|
||||
-->
|
||||
<slot>
|
||||
<view class="tmUploadPhotoItemAdd" :style="{borderRadius:_round}">
|
||||
<tm-icon :color="config.color" size="52" name="add-line"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType, computed, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { covetUniNumber } from '../../libs/tool';
|
||||
import { TMUPLOAD_PHOTO_STATUS } from '../../interface';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
|
||||
/**
|
||||
* @displayName 视频上传
|
||||
* @exportName tm-upload-video
|
||||
* @category 表单组件
|
||||
* @description 样式丰富塑造性好
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmUploadVideo' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
type BEFOREDELTYPE = (index: number, item: TM.TMUPLOAD_PHOTO_INFO) => Promise<boolean>
|
||||
type BEFORECOMPLETE = (res: any) => Promise<boolean>
|
||||
type CHOOSEFILE_FILES = {
|
||||
path: string,
|
||||
size: number
|
||||
}
|
||||
const STATUS_TEXT = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
[0, '待上传'],
|
||||
[1, '上传中'],
|
||||
[2, '上传成功'],
|
||||
[3, '上传失败'],
|
||||
[4, '已取消'],
|
||||
[5, '超出大小限制']
|
||||
])
|
||||
const STATUS_COLOR = new Map<TMUPLOAD_PHOTO_STATUS, string>([
|
||||
[0, '#fff'],
|
||||
[1, '#0579FF'],
|
||||
[2, '#07d73d'],
|
||||
[3, '#f10226'],
|
||||
[4, '#F2F2F2'],
|
||||
[5, '#ffa61e']
|
||||
])
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 每次全部上传完时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'complete',
|
||||
/**
|
||||
* 变化时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* @param {TMUPLOAD_PHOTO_INFO[]} 列表数据
|
||||
*/
|
||||
'delete',
|
||||
/**
|
||||
* 点击项目预览视频,有的回显视频可能需要授权播放,返回的可能是个视频图片,因此你需要通过事件自行创建预览窗口.
|
||||
*/
|
||||
'preview',
|
||||
/**
|
||||
* 同步文件列表。
|
||||
*/
|
||||
'update:modelValue'
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 最大的可上传数量
|
||||
*/
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 9
|
||||
},
|
||||
/**
|
||||
* 图片项目圆角。
|
||||
*/
|
||||
round:{
|
||||
type:[String,Number],
|
||||
default:10
|
||||
},
|
||||
/**
|
||||
* 上传地址
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: "https://mockapi.eolink.com/LRViGGZ8e6c1e8b4a636cd82bca1eb15d2635ed8c74e774/admin/upload_pic/"
|
||||
},
|
||||
/**
|
||||
* 上传到服务器的名称字段
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "file"
|
||||
},
|
||||
/**
|
||||
* 上传到服务器的头文件
|
||||
*/
|
||||
header: {
|
||||
type: Object as PropType<Record<string, any> | null>,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 额外的表单数据。
|
||||
*/
|
||||
formData: {
|
||||
type: Object as PropType<Record<string, any> | null>,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 图片高,此处不可使用%单位
|
||||
*/
|
||||
imgHeight: {
|
||||
type: String,
|
||||
default: "110"
|
||||
},
|
||||
/**
|
||||
* 一行显示几列
|
||||
*/
|
||||
column: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/**
|
||||
* 上传成功的文件是否允许删除
|
||||
*/
|
||||
okFileIsDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 上传中的文件是否允许删除
|
||||
*/
|
||||
uploadingFileIsDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片上传的限制大小,默认是30mb(可以开启压缩,最后可能比较小)
|
||||
*/
|
||||
maxFileSize: {
|
||||
type: Number,
|
||||
default: 1024 * 1024 * 30
|
||||
},
|
||||
/**
|
||||
* 最大拍摄时长不能超过60秒默认建议30秒
|
||||
*/
|
||||
maxDuration: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
/**
|
||||
* 摄像头类型,前置还是后置
|
||||
*/
|
||||
camera: {
|
||||
type: String as PropType<'back' | 'front'>,
|
||||
default: 'back'
|
||||
},
|
||||
/**
|
||||
* 开发者服务器响应码,默认是200表示上传成功.
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
/**
|
||||
* 等同v-model
|
||||
*/
|
||||
modelValue: {
|
||||
type: Array<Record<string, any> | string>,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 用于回显时,如果你提供的数组数据是对象,需要指定图片的地址.
|
||||
* 同时该对象数据会被保留到response字段中
|
||||
*/
|
||||
rangUrl: {
|
||||
type: String,
|
||||
default: 'url'
|
||||
},
|
||||
/**
|
||||
* 图片被删除时触发
|
||||
* 如果返回Promise<false>删除失败否则成功
|
||||
* 类型null|(index:number,item:XUPLOADFILE_FILE_INFO)=>Promise<boolean>
|
||||
*/
|
||||
beforeDel: {
|
||||
type: [Function, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 你需要原路返回参数提供的item
|
||||
* item可以自行修改响应内容,响应类型这样可以自己根据服务的内容判断
|
||||
* 是成功还是失败或者没有权限。修改其中的status来达到成功还是失败TMUPLOAD_PHOTO_STATUS
|
||||
*/
|
||||
beforeComplete: {
|
||||
type: [Function, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否自动上传
|
||||
*/
|
||||
autoStart: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片来源同官方的sourceType:
|
||||
* 'album','camera'
|
||||
*/
|
||||
sourceType: {
|
||||
type: [Array<"album" | "camera">],
|
||||
default: () => ["album", "camera"]
|
||||
},
|
||||
/**
|
||||
* 是否压缩
|
||||
*/
|
||||
compress: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 添加图片的位置
|
||||
* 'before'出现在前面
|
||||
* 'after'出现在后面
|
||||
*/
|
||||
addPos: {
|
||||
type: String as PropType<'before' | 'after'>,
|
||||
default: "after"
|
||||
},
|
||||
/**
|
||||
* 图片展示的缩放模式同官方.
|
||||
*/
|
||||
mode: {
|
||||
type: String,
|
||||
default: "scaleToFill"
|
||||
}
|
||||
})
|
||||
const list = ref<TM.TMUPLOAD_PHOTO_INFO[]>([])
|
||||
const _coloumn = computed(() => props.column)
|
||||
const _okFileIsDelete = computed(() => props.okFileIsDelete)
|
||||
const _height = computed(() => covetUniNumber(props.imgHeight))
|
||||
const _width = computed(() => 100 / props.column + '%')
|
||||
const _round = computed(() => covetUniNumber(props.round))
|
||||
|
||||
const uploadIndex = ref(0);
|
||||
const uploading = ref(false)
|
||||
//可选择的图片数据。0表示已经达到最大数量
|
||||
const _selectedCount = computed(() => Math.max(0, Math.min(props.maxCount - list.value.length, 9)))
|
||||
const getUUID = () => Math.random() * 100 + '' + Date.now();
|
||||
/**
|
||||
* 转换为待上传列表的文件列表。
|
||||
* @param files 转换的数据
|
||||
*/
|
||||
const covertFileType = (files: CHOOSEFILE_FILES[]) => {
|
||||
let flist = files.map((item): TM.TMUPLOAD_PHOTO_INFO => {
|
||||
let status = item.size > props.maxFileSize ? TMUPLOAD_PHOTO_STATUS.EXCEEDS_SIZE_LIMIT : TMUPLOAD_PHOTO_STATUS.NOT;
|
||||
return {
|
||||
path: item.path,
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 0,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: null
|
||||
}
|
||||
})
|
||||
return flist;
|
||||
}
|
||||
/**
|
||||
* 转换外部文件.
|
||||
* @param files 转换的数据
|
||||
*/
|
||||
const covertFileTypeByModelvalue = (files: Array<Record<string, any> | string>) => {
|
||||
let flist = files.map((item): TM.TMUPLOAD_PHOTO_INFO => {
|
||||
let status = TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS;
|
||||
if (typeof item == 'string') {
|
||||
return {
|
||||
path: item,
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 100,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: item
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: item[props.rangUrl] || item['path'] || "",
|
||||
id: getUUID(),
|
||||
status: status,
|
||||
progress: 100,
|
||||
statusText: STATUS_TEXT.get(status) || "",
|
||||
response: item
|
||||
}
|
||||
})
|
||||
let nowfilesrc = list.value.map(item => item.path)
|
||||
let filst2 = flist.filter(item => !nowfilesrc.includes(item.path))
|
||||
return filst2;
|
||||
}
|
||||
/**
|
||||
* 选择图片
|
||||
*/
|
||||
const choose = () => {
|
||||
let resultfiles = [] as CHOOSEFILE_FILES[];
|
||||
let index = 0;
|
||||
// uni.showLoading({ title: '...' })
|
||||
uni.chooseVideo({
|
||||
count: _selectedCount.value,
|
||||
sourceType: props.sourceType,
|
||||
compressed: props.compress,
|
||||
maxDuration: props.maxDuration,
|
||||
camera: props.camera,
|
||||
fail(e) {
|
||||
// uni.hideLoading()
|
||||
|
||||
},
|
||||
success(res) {
|
||||
console.log(res)
|
||||
let flist = [] as CHOOSEFILE_FILES[];
|
||||
flist.push({
|
||||
path: res.tempFilePath,
|
||||
size: res.size,
|
||||
})
|
||||
resultfiles = flist
|
||||
if (resultfiles.length == 0) {
|
||||
// uni.hideLoading()
|
||||
return;
|
||||
}
|
||||
compressImage()
|
||||
},
|
||||
complete() {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
function compressImage() {
|
||||
if (resultfiles.length == 0) return;
|
||||
list.value = [...list.value, ...covertFileType(resultfiles)]
|
||||
emit('change', list.value.slice(0))
|
||||
if (props.autoStart) {
|
||||
startUpload();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 手动上传
|
||||
*/
|
||||
const startUpload = () => {
|
||||
if (uploading.value) {
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
uploadIndex.value = 0;
|
||||
upload();
|
||||
}
|
||||
const upload = () => {
|
||||
//结束上传。
|
||||
if (uploadIndex.value >= list.value.length) {
|
||||
uploading.value = false;
|
||||
emit('complete', list.value.slice(0))
|
||||
emit('update:modelValue', list.value.slice(0))
|
||||
return;
|
||||
}
|
||||
let item = list.value[uploadIndex.value];
|
||||
// 跳过上传中或已经上传成功的图片。
|
||||
if (item.status == TMUPLOAD_PHOTO_STATUS.EXCEEDS_SIZE_LIMIT || item.status == TMUPLOAD_PHOTO_STATUS.UPLOADING || item.status == TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS) {
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
return;
|
||||
}
|
||||
uni.uploadFile({
|
||||
url: props.url,
|
||||
filePath: item.path,
|
||||
name: props.name,
|
||||
formData: props.formData,
|
||||
header: props.header,
|
||||
success: async (res) => {
|
||||
let isComplete = true;
|
||||
if (typeof props.beforeComplete === 'function') {
|
||||
isComplete = await props.beforeComplete(res)
|
||||
}
|
||||
if (res.statusCode != 200 || !isComplete) {
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_FAILED;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
item.response = res.data;
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
return;
|
||||
}
|
||||
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_SUCCESS;
|
||||
item.response = res.data;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
},
|
||||
fail: (res) => {
|
||||
item.status = TMUPLOAD_PHOTO_STATUS.UPLOAD_FAILED;
|
||||
item.response = res.errMsg;
|
||||
item.statusText = STATUS_TEXT.get(item.status) || ""
|
||||
uploadIndex.value += 1;
|
||||
upload();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const remove = async (index: number, item: TM.TMUPLOAD_PHOTO_INFO) => {
|
||||
if (uploading.value) return;
|
||||
let isDele = true;
|
||||
if (typeof props.beforeDel === 'function') {
|
||||
isDele = await props.beforeDel(index, item)
|
||||
}
|
||||
if (!isDele) return;
|
||||
|
||||
list.value.splice(index, 1);
|
||||
emit('change', list.value.slice(0))
|
||||
emit('update:modelValue', list.value.slice(0))
|
||||
emit('delete', item)
|
||||
}
|
||||
const imagePreve = (index: number) => {
|
||||
emit('preview', index)
|
||||
}
|
||||
watch(() => props.modelValue, (val) => {
|
||||
let temlist = covertFileTypeByModelvalue(props.modelValue).slice(0);
|
||||
if (temlist.length == 0) return;
|
||||
list.value = [...list.value.slice(0), ...temlist]
|
||||
}, { immediate: true })
|
||||
|
||||
defineExpose({ startUpload, choose })
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.tmUploadDel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
padding: 3px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tmUploadLabel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5rpx;
|
||||
border-radius: 0 0 10rpx 10rpx;
|
||||
background-color: rgba(6, 8, 15, 0.46);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 0rpx;
|
||||
bottom: 0rpx;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tmUploadPhoto {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
gap: 5rpx;
|
||||
}
|
||||
|
||||
.tmUploadPhotoItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmUploadPhotoItemAdd {
|
||||
border: 1px solid #e6e7e7;
|
||||
background-color: #f3f3f3;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch, PropType } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
/**
|
||||
* @displayName 开关
|
||||
* @exportName tm-switch
|
||||
* @category 表单组件
|
||||
* @description 开关,用于直观的展示选项表单的选择。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmSwitch' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
<template>
|
||||
|
||||
</template>
|
||||
Reference in new issue
Block a user