This commit is contained in:
dzc committed 2024-11-11 14:42:47 +08:00
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(/&amp;/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>
+36
View File
@@ -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>