191 lines
5.9 KiB
Vue
191 lines
5.9 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, type ShortenResponse } 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 shareResponse = await getShareUrl(props.activityId, props.userId, props.template)
|
||
|
||
// 从 ShortenResponse 对象中提取正确的 URL
|
||
// 优先使用 originalUrl,否则拼接 baseUrl + path
|
||
let urlToCopy: string
|
||
if (shareResponse && typeof shareResponse === 'object') {
|
||
const shortenResponse = shareResponse as ShortenResponse
|
||
if (shortenResponse.originalUrl) {
|
||
urlToCopy = shortenResponse.originalUrl
|
||
} else if (shortenResponse.path) {
|
||
// 需要从配置中获取 baseUrl,这里做个兼容处理
|
||
// 如果 path 是完整URL直接使用,否则需要拼接
|
||
urlToCopy = shortenResponse.path.startsWith('http')
|
||
? shortenResponse.path
|
||
: `${window.location.origin}${shortenResponse.path}`
|
||
} else {
|
||
throw new Error('分享链接响应格式异常')
|
||
}
|
||
} else {
|
||
throw new Error('分享链接响应格式异常')
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
try {
|
||
await navigator.clipboard.writeText(urlToCopy)
|
||
showCopiedToast()
|
||
emit('copied')
|
||
} catch (clipboardError) {
|
||
// 如果剪贴板API不可用,回退到传统方法
|
||
const textArea = document.createElement('textarea')
|
||
textArea.value = urlToCopy
|
||
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>
|