171 lines
5.0 KiB
Vue
171 lines
5.0 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="mosquito-share-button">
|
|||
|
|
<button
|
|||
|
|
:class="buttonClasses"
|
|||
|
|
:disabled="loading || disabled"
|
|||
|
|
@click="handleClick"
|
|||
|
|
>
|
|||
|
|
<div v-if="loading" class="loading-spinner">
|
|||
|
|
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|||
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|||
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
<span v-else>{{ text }}</span>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<!-- Toast 通知 -->
|
|||
|
|
<div v-if="showToast" :class="toastClasses">
|
|||
|
|
<div class="flex items-center">
|
|||
|
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|||
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
|||
|
|
</svg>
|
|||
|
|
{{ toastMessage }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, computed, watch } from 'vue'
|
|||
|
|
import { useMosquito } from '../index'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
activityId: number
|
|||
|
|
userId: number
|
|||
|
|
template?: string
|
|||
|
|
text?: string
|
|||
|
|
disabled?: boolean
|
|||
|
|
variant?: 'default' | 'primary' | 'secondary' | 'success' | 'danger'
|
|||
|
|
size?: 'sm' | 'md' | 'lg'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
template: 'default',
|
|||
|
|
text: '分享给好友',
|
|||
|
|
disabled: false,
|
|||
|
|
variant: 'primary',
|
|||
|
|
size: 'md'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
copied: []
|
|||
|
|
error: [error: Error]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const { getShareUrl } = useMosquito()
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const showToast = ref(false)
|
|||
|
|
const toastMessage = ref('')
|
|||
|
|
const toastType = ref<'success' | 'error'>('success')
|
|||
|
|
const toastTimeout = ref<number>()
|
|||
|
|
|
|||
|
|
// 计算样式类
|
|||
|
|
const buttonClasses = computed(() => {
|
|||
|
|
const base = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'
|
|||
|
|
|
|||
|
|
const sizeClasses = {
|
|||
|
|
sm: 'px-3 py-1.5 text-sm',
|
|||
|
|
md: 'px-4 py-2 text-sm',
|
|||
|
|
lg: 'px-6 py-3 text-base'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const variantClasses = {
|
|||
|
|
default: 'bg-white text-mosquito-ink border border-mosquito-line hover:border-mosquito-accent focus:ring-mosquito-accent',
|
|||
|
|
primary: 'bg-mosquito-accent text-white hover:bg-mosquito-accent/90 focus:ring-mosquito-accent shadow-soft',
|
|||
|
|
secondary: 'bg-mosquito-bg text-mosquito-ink border border-mosquito-line hover:border-mosquito-accent focus:ring-mosquito-accent',
|
|||
|
|
success: 'bg-emerald-500 text-white hover:bg-emerald-600 focus:ring-emerald-500',
|
|||
|
|
danger: 'bg-rose-500 text-white hover:bg-rose-600 focus:ring-rose-500'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
base,
|
|||
|
|
sizeClasses[props.size],
|
|||
|
|
variantClasses[props.variant],
|
|||
|
|
{
|
|||
|
|
'opacity-50 cursor-not-allowed': props.disabled || loading.value
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const toastClasses = computed(() => {
|
|||
|
|
const tone = toastType.value === 'error' ? 'bg-rose-500' : 'bg-mosquito-accent'
|
|||
|
|
return [
|
|||
|
|
'fixed top-4 right-4 z-50 max-w-sm p-4 text-white rounded-lg shadow-lg transition-all duration-300 transform',
|
|||
|
|
`${tone} transform translate-x-0 opacity-100`
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 处理点击事件
|
|||
|
|
const handleClick = async () => {
|
|||
|
|
if (loading.value || props.disabled) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
loading.value = true
|
|||
|
|
const shareUrl = await getShareUrl(props.activityId, props.userId, props.template)
|
|||
|
|
|
|||
|
|
// 复制到剪贴板
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(shareUrl)
|
|||
|
|
showCopiedToast()
|
|||
|
|
emit('copied')
|
|||
|
|
} catch (clipboardError) {
|
|||
|
|
// 如果剪贴板API不可用,回退到传统方法
|
|||
|
|
const textArea = document.createElement('textarea')
|
|||
|
|
textArea.value = shareUrl
|
|||
|
|
document.body.appendChild(textArea)
|
|||
|
|
textArea.select()
|
|||
|
|
document.execCommand('copy')
|
|||
|
|
document.body.removeChild(textArea)
|
|||
|
|
showCopiedToast()
|
|||
|
|
emit('copied')
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取分享链接失败:', error)
|
|||
|
|
emit('error', error as Error)
|
|||
|
|
showToastMessage('获取分享链接失败,请稍后重试', 'error')
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示复制成功提示
|
|||
|
|
const showCopiedToast = () => {
|
|||
|
|
showToastMessage('分享链接已复制到剪贴板', 'success')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示消息提示
|
|||
|
|
const showToastMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
|||
|
|
toastMessage.value = message
|
|||
|
|
toastType.value = type
|
|||
|
|
showToast.value = true
|
|||
|
|
|
|||
|
|
// 清除之前的定时器
|
|||
|
|
if (toastTimeout.value) {
|
|||
|
|
clearTimeout(toastTimeout.value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的定时器
|
|||
|
|
toastTimeout.value = window.setTimeout(() => {
|
|||
|
|
showToast.value = false
|
|||
|
|
}, 3000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 组件卸载时清理定时器
|
|||
|
|
watch(() => showToast.value, (newVal) => {
|
|||
|
|
if (!newVal && toastTimeout.value) {
|
|||
|
|
clearTimeout(toastTimeout.value)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.loading-spinner {
|
|||
|
|
@apply flex items-center justify-center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mosquito-share-button {
|
|||
|
|
@apply inline-block;
|
|||
|
|
}
|
|||
|
|
</style>
|