Files
wenzi/frontend/components/MosquitoShareButton.vue

191 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>