javascript
复制代码
<!-- components/SvgIcon/SvgIcon.vue -->
<template>
<view
class="svg-icon"
:class="[customClass, { 'svg-icon--clickable': !disabled && !!$attrs.onClick }]"
:style="computedStyles"
@click="handleClick"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
<!-- 加载状态 -->
<view v-if="loading" class="svg-icon__loading">
<text class="loading-text">...</text>
</view>
<!-- 错误回退 -->
<text v-else-if="showFallback" class="svg-icon__fallback">□</text>
</view>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
// 定义 Props
const props = defineProps({
// 图标名称(对应 static/svg 目录下的文件名)
name: {
type: String,
required: true,
},
// 图标大小
size: {
type: [Number, String],
default: 88,
},
// 图标颜色
color: {
type: String,
default: 'unset',
},
disabledColor: {
type: String,
default: '#999999',
},
// 自定义类名
customClass: {
type: String,
default: '',
},
// 自定义样式
customStyle: {
type: Object,
default: () => ({}),
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 是否显示加载状态
loading: {
type: Boolean,
default: false,
},
})
// 定义 Emits
const emit = defineEmits(['click', 'error'])
// 响应式数据
const iconLoaded = ref(false)
const showFallback = ref(false)
const isTouching = ref(false)
// 计算属性
const iconPath = computed(() => {
return `/static/svg/${props.name}.svg`
})
const computedSize = computed(() => {
if (typeof props.size === 'number') {
return props.size + 'rpx'
}
if (
props.size.includes('rpx') ||
props.size.includes('px') ||
props.size.includes('em') ||
props.size.includes('%')
) {
return props.size
}
return props.size + 'rpx'
})
const computedStyles = computed(() => {
const baseStyles = {
width: computedSize.value,
height: computedSize.value,
color: props.disabled ? props.disabledColor : props.color,
'mask-image': `url(${iconPath.value})`,
'-webkit-mask-image': `url(${iconPath.value})`,
pointerEvents: props.disabled ? 'none' : 'auto',
}
return { ...baseStyles, ...props.customStyle }
})
// 方法
const handleClick = (event) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
const handleTouchStart = () => {
if (!props.disabled && !props.loading) {
isTouching.value = true
}
}
const handleTouchEnd = () => {
isTouching.value = false
}
// 图标加载处理
const checkIconExists = async () => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = iconPath.value
})
}
const loadIcon = async () => {
try {
const exists = await checkIconExists()
if (exists) {
iconLoaded.value = true
showFallback.value = false
} else {
iconLoaded.value = false
showFallback.value = true
emit('error', new Error(`图标不存在: ${props.name}`))
}
} catch (error) {
iconLoaded.value = false
showFallback.value = true
emit('error', error)
}
}
// 监听图标名称变化
watch(() => props.name, loadIcon, { immediate: true })
// 生命周期
onMounted(() => {
loadIcon()
})
</script>
<style scoped>
.svg-icon {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: currentColor;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
transition: all 0.3s ease;
flex-shrink: 0;
vertical-align: middle;
}
.svg-icon--clickable {
cursor: pointer;
}
.svg-icon--clickable:active {
opacity: 0.7;
transform: scale(0.95);
}
.svg-icon__loading {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.loading-text {
font-size: 12px;
color: inherit;
}
.svg-icon__fallback {
font-size: 24px;
color: #ccc;
}
</style>