Files
2026-09-24 16:25:22 +08:00

430 lines
10 KiB
Plaintext

<template>
<view class="xAvatarGroup" ref="xAvatarGroup" :class="[_flat?'xAvatarGroupWrap':'xAvatarGroupNoWrap']">
<template v-for="(item,index) in _list" :key="index">
<image @click="itemClick(item,index)" v-if="item.isImg" :style="{
width:_size,
height:_size,
borderRadius:_round,
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter
}" class="xAvatarGroupImage" :src="item.name" :mode="props.model"></image>
<view @click="itemClick(item,index)" v-else :style="{
width:_size,
height:_size,
borderRadius:_round,
marginRight:((index+1) == column&&_showCount) ?'0px':_gutter,
marginBottom:Math.ceil((index+1)/column)==row?'0px':_gutter,
backgroundColor:item.bgColor
}" class="xAvatarGroupImage xAvatarGroupImageBytext">
<x-text v-if="item.name!=''" :font-size="props.fontSize"
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor">
{{item.name}}
</x-text>
<x-icon v-if="item.name==''" :font-size="props.fontSize"
:color="(_randomBgColor?'white':props.fontColor)" :dark-color="props.darkFontColor"
:name="props.placeIcon"></x-icon>
</view>
</template>
<!--
@slot more更多插槽,如果使用了这个插槽moreClick事件会丢失,请自己写在自己的布局上。
-->
<slot name="more">
<view @click="moreClick" class="xAvatarGroupImageMore" :style="{
width:_size,
height:_size,
borderRadius:_round,
backgroundColor:_BgColor
}" v-if="_showCount">
<x-text :font-size="props.fontSize" :color="props.fontColor"
:dark-color="props.darkFontColor">{{_count==0?(_list.length>99?99:_list.length)+'+':(_count>99?99:_count)+'+'}}</x-text>
</view>
</slot>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import { type PropType } from "vue"
import { getDefaultColor, hslaToCss } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { xProvitae } from "../../config/xConfig.uts"
type XAVATARGROUPITEMTYPE = {
name : string,
isImg : boolean,
bgColor : string
}
/**
* @name 头像组 xAvatarGroup
* @page /pages/index/avatar-group
* @category 展示组件
* @description 平铺和堆叠方式。如果想要单头像建议使用:xSheet+xImage配合,+xBadge达到效果,因此我不再提供单头像组件,没有意义。
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xAvatarGroup" })
let resizeObserver = null as UniResizeObserver | null
const xAvatarGroup = ref<UniElement | null>(null)
const emits = defineEmits([
/**
* 头像被点击时
* @param {number} index 当前索引
* @param {string} src 当前图片地址
*/
"click",
/**
* 最后一个数字头像more被点击时
*/
"moreClick"
])
const props = defineProps({
/**
* 头像列表,也可以是文本数组,也可以是空字符串数组
*/
list: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
/**
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
*/
size: {
type: String,
default: '32'
},
/**
* 最多显示几个头像。
*/
maxCount: {
type: Number,
default: 5
},
/**
* 圆角
*/
round: {
type: String,
default: '16'
},
/**
* 平铺或者堆叠时的间隙或者前推差值。
* 不允许使用auto,%只能数字或者带单位的数字2px,2rpx这种
*/
gutter: {
type: String,
default: '16'
},
/**
* 显示类型见:
* https://doc.dcloud.net.cn/uni-app-x/component/image.html#%E5%B1%9E%E6%80%A7
*/
model: {
type: String,
default: "scaleToFill"
},
/**
* 显示在最后一个时,显示的数字。如果为0取list的长度
*/
count: {
type: Number,
default: 0
},
/**
* 是否显示最后一个数字头像
*/
showCount: {
type: Boolean,
default: true
},
/**
* 是否平铺,如果否就是堆叠。是就是正常排列。
*/
flat: {
type: Boolean,
default: false
},
/**
* 如果为文本头像时的背景
*/
bgColor: {
type: String,
default: "#f5f5f5"
},
/**
* 如果为文本头像时的暗黑背景
* 空时默认取inputDarkBgcolor
*/
darkBgColor: {
type: String,
default: ""
},
/**
* 如果为文本头像时的文字颜色
*/
fontColor: {
type: String,
default: "#a6a6a6"
},
/**
* 如果为文本头像时的暗黑背景
* 空时默认取inputDarkBgcolor
*/
darkFontColor: {
type: String,
default: "#ffffff"
},
/**
* 字号
*/
fontSize: {
type: String,
default: "14"
},
/**
* 文本头像时,是否随机背景色
*/
randomBgColor: {
type: Boolean,
default: false
},
/**
* 如果当图片或者文本为空时的图片占位符
* 可以是图片地址或者图标名称
*/
placeIcon: {
type: String,
default: 'user-3-fill'
}
})
const _isFileImg = (name : string) : boolean => {
if (name.lastIndexOf(".") > -1 ||
name.indexOf("ftp:") > -1 ||
name.indexOf("https:") > -1 ||
name.indexOf("http:") > -1 ||
name.indexOf("data:image") > -1
) {
return true;
}
return false;
}
const _randomBgColor = computed(() : boolean => {
return props.randomBgColor
})
const _BgColor = computed(() : string => {
let bgcolor = props.bgColor;
if (xConfig.dark == 'dark') {
bgcolor = props.darkBgColor;
if (bgcolor == '') {
bgcolor = xConfig.inputDarkColor
}
}
return getDefaultColor(bgcolor)
})
const UpperEn = new Map<string, string>([
['a', 'A'],
['b', 'B'],
['c', 'C'],
['d', 'D'],
['e', 'E'],
['f', 'F'],
['g', 'G'],
['h', 'H'],
['i', 'I'],
['j', 'J'],
['k', 'K'],
['l', 'L'],
['m', 'M'],
['n', 'N'],
['o', 'O'],
['p', 'P'],
['q', 'Q'],
['r', 'R'],
['s', 'S'],
['t', 'T'],
['u', 'U'],
['v', 'V'],
['w', 'W'],
['x', 'X'],
['y', 'Y'],
['z', 'Z']
]);
const _list = computed(() : XAVATARGROUPITEMTYPE[] => {
let nowlist = props.list.slice(0)
if (props.maxCount > 0) {
nowlist = nowlist.slice(0, props.maxCount)
}
let compeltedList = [] as XAVATARGROUPITEMTYPE[];
for (let i = 0; i < nowlist.length; i++) {
let name = nowlist[i]
let isImg = _isFileImg(name);
let bgColor = ''
if (!isImg) {
name = name.slice(0, 1)
if (UpperEn.get(name) != null) {
name = UpperEn.get(name)!
}
let h = Math.random() * 360
let s = 78
let l = 62
bgColor = hslaToCss({ h: parseInt(h.toFixed(0)), s, l, a: 1 } as UTSJSONObject)
}
compeltedList.push({
name,
isImg,
bgColor: props.randomBgColor ? bgColor : _BgColor.value
} as XAVATARGROUPITEMTYPE)
}
return compeltedList;
})
const _size = computed(() : string => checkIsCssUnit(props.size, xConfig.unit))
const _sizeBypx = computed(() : number => {
let rpx = checkIsCssUnit(props.size, xConfig.unit);
let unit = getUnit(rpx)
let zhi = parseFloat(rpx)
if (unit == 'rpx') {
zhi = uni.rpx2px(parseFloat(rpx))
}
return zhi
})
const _showCount = computed(() : boolean => props.showCount)
const _round = computed(() : string => checkIsCssUnit(props.round, xConfig.unit))
const _flat = computed(() : boolean => props.flat)
const _gutter = computed(() : string => {
if (props.flat) {
return checkIsCssUnit(props.gutter, xConfig.unit)
}
return '-' + checkIsCssUnit(props.gutter, xConfig.unit)
})
const _gutterBypx = computed(() : number => {
let rpx = checkIsCssUnit(props.gutter, xConfig.unit);
let unit = getUnit(rpx)
let zhi = parseFloat(rpx)
if (unit == 'rpx') {
zhi = uni.rpx2px(parseFloat(rpx))
}
return zhi
})
const _count = computed(() : number => props.count)
const totalWidth = ref(0)
const column = ref(1)
const row = ref(0)
let tid = 0
const calcColumn = (width : number) => {
totalWidth.value = width;
if (_sizeBypx.value > 0) {
column.value = Math.floor((totalWidth.value + _gutterBypx.value) / (_sizeBypx.value + _gutterBypx.value * (props.flat ? 1 : -1)));
}
if (column.value > 0 && _list.value.length > 0) {
row.value = Math.ceil(props.list.length / column.value)
}
}
const getNodeinfo = () => {
uni.createSelectorQuery()
.in(getCurrentInstance()?.proxy)
.select(".xAvatarGroup")
.boundingClientRect((res : any) => {
let node = res as NodeInfo
calcColumn(node.width!)
})
.exec()
}
const createrObr = () => {
// #ifndef MP
let ele = xAvatarGroup.value as UniElement | null;
if (ele == null) return;
if (resizeObserver == null) {
resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
entries.forEach(entry => {
if (entry.target == ele) {
clearTimeout(tid)
tid = setTimeout(function () {
let bund = ele!.getBoundingClientRect()
calcColumn(bund.width)
}, 150);
}
})
})
}
resizeObserver!.observe(ele!)
// #endif
}
const removeObr = () => {
// #ifndef MP
resizeObserver?.disconnect()
// #endif
}
const itemClick = (item : XAVATARGROUPITEMTYPE, index : number) => {
emits('click', item.name, index)
}
const moreClick = () => {
emits('moreClick')
}
onMounted(() => {
getNodeinfo()
// #ifndef MP
createrObr();
// #endif
})
onUnmounted(() => {
clearTimeout(tid)
// #ifndef MP
removeObr()
// #endif
})
</script>
<style>
.xAvatarGroup {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.xAvatarGroupImage {
position: relative;
/* transition-duration: 200ms;
transition-property: margin-right, margin-bottom;
transition-timing-function: linear; */
}
.xAvatarGroupImageMore {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 5;
}
.xAvatarGroupImageBytext {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.xAvatarGroupWrap {
flex-wrap: wrap;
}
.xAvatarGroupNoWrap {
flex-wrap: nowrap;
}
</style>