chore: sync local latest state and repository cleanup

This commit is contained in:
Your Name
2026-03-23 13:02:36 +08:00
parent f1ff3d629f
commit 2ef0f17961
493 changed files with 46912 additions and 7977 deletions

View File

@@ -27,6 +27,14 @@
<Icons name="trophy" class="mos-nav-icon" />
<span>排行</span>
</RouterLink>
<RouterLink
to="/profile"
class="mos-nav-item"
:class="{ active: route.path === '/profile' }"
>
<Icons name="user" class="mos-nav-icon" />
<span>我的</span>
</RouterLink>
</div>
</nav>
</div>

View File

@@ -84,6 +84,11 @@
<svg v-else-if="name === 'zap'" :class="iconClass" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>
<svg v-else-if="name === 'user'" :class="iconClass" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</template>
<script setup lang="ts">

View File

@@ -1,18 +1,18 @@
import { createApp } from 'vue'
import { createApp, type Plugin } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './styles/index.css'
import MosquitoEnhancedPlugin from '../../index'
import MosquitoEnhancedPlugin, { type MosquitoConfig } from '../../index'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(MosquitoEnhancedPlugin, {
app.use(MosquitoEnhancedPlugin as Plugin<MosquitoConfig>, {
baseUrl: import.meta.env.VITE_MOSQUITO_API_BASE_URL ?? '',
apiKey: import.meta.env.VITE_MOSQUITO_API_KEY ?? '',
userToken: import.meta.env.VITE_MOSQUITO_USER_TOKEN ?? ''
})
} as MosquitoConfig)
app.mount('#app')

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import ShareView from '../views/ShareView.vue'
import LeaderboardView from '../views/LeaderboardView.vue'
import ProfileView from '../views/ProfileView.vue'
const router = createRouter({
history: createWebHistory(),
@@ -20,6 +21,11 @@ const router = createRouter({
path: '/rank',
name: 'rank',
component: LeaderboardView
},
{
path: '/profile',
name: 'profile',
component: ProfileView
}
]
})

View File

@@ -0,0 +1,241 @@
<template>
<section class="mx-auto max-w-md px-4 pb-28 pt-6 space-y-6">
<!-- Header -->
<header class="mos-hero relative overflow-hidden p-6">
<div class="relative z-10">
<h1 class="mos-title mt-4 text-2xl font-bold text-white">个人中心</h1>
<p class="mt-2 text-sm text-white/80">查看邀请记录和奖励明细</p>
</div>
</header>
<!-- Tabs -->
<div class="flex gap-2 border-b border-mosquito-line pb-2">
<button
v-for="tab in tabs"
:key="tab.key"
class="px-4 py-2 text-sm font-semibold rounded-t-lg transition"
:class="activeTab === tab.key
? 'bg-mosquito-accent/10 text-mosquito-brand border-b-2 border-mosquito-brand'
: 'text-mosquito-ink/70 hover:bg-mosquito-bg'"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<!-- Loading -->
<div v-if="loading" class="py-8 text-center text-mosquito-muted">
加载中...
</div>
<!-- 邀请记录 -->
<div v-else-if="activeTab === 'invites'" class="space-y-4">
<div v-if="invitedFriends.length === 0" class="mos-card p-6 text-center">
<div class="mos-empty-icon">
<Icons name="users" class="w-8 h-8" />
</div>
<h3 class="font-semibold text-mosquito-ink mt-2">暂无邀请记录</h3>
<p class="text-sm text-mosquito-muted mt-1">分享邀请链接后可查看</p>
<RouterLink to="/share" class="mos-btn mos-btn-primary mt-4">
去分享
</RouterLink>
</div>
<div v-else class="space-y-3">
<div v-for="friend in invitedFriends" :key="friend.nickname" class="mos-card p-4">
<div class="flex items-center justify-between">
<div>
<div class="font-semibold text-mosquito-ink">{{ friend.nickname }}</div>
<div class="text-xs text-mosquito-muted">{{ friend.maskedPhone || friend.phone || '-' }}</div>
</div>
<span :class="getStatusClass(friend.status)" class="px-2 py-1 rounded-full text-xs font-semibold">
{{ getStatusLabel(friend.status) }}
</span>
</div>
</div>
</div>
</div>
<!-- 奖励明细 -->
<div v-else-if="activeTab === 'rewards'" class="space-y-4">
<div v-if="rewards.length === 0" class="mos-card p-6 text-center">
<div class="mos-empty-icon">
<Icons name="gift" class="w-8 h-8" />
</div>
<h3 class="font-semibold text-mosquito-ink mt-2">暂无奖励记录</h3>
<p class="text-sm text-mosquito-muted mt-1">邀请好友后可获得奖励</p>
<RouterLink to="/share" class="mos-btn mos-btn-primary mt-4">
去分享
</RouterLink>
</div>
<div v-else class="space-y-3">
<div v-for="reward in rewards" :key="reward.createdAt" class="mos-card p-4">
<div class="flex items-center justify-between">
<div>
<div class="font-semibold text-mosquito-ink">{{ getRewardTypeLabel(reward.type) }}</div>
<div class="text-xs text-mosquito-muted">{{ formatDate(reward.createdAt) }}</div>
</div>
<div class="text-lg font-bold text-mosquito-brand">+{{ reward.points }} 积分</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { getUserIdFromToken, parseUserId } from '../../../shared/auth'
import Icons from '../components/Icons.vue'
const route = useRoute()
const apiKey = import.meta.env.VITE_MOSQUITO_API_KEY
const userToken = import.meta.env.VITE_MOSQUITO_USER_TOKEN
const routeUserId = computed(() => parseUserId(route.query.userId ?? route.params.userId))
const userId = computed(() => getUserIdFromToken(userToken) ?? routeUserId.value ?? 0)
const hasAuth = computed(() => Boolean(apiKey && userToken && userId.value))
const baseUrl = import.meta.env.VITE_MOSQUITO_API_BASE_URL || ''
const activeTab = ref('invites')
const loading = ref(false)
const invitedFriends = ref<any[]>([])
const rewards = ref<any[]>([])
const tabs = [
{ key: 'invites', label: '邀请记录' },
{ key: 'rewards', label: '奖励明细' }
]
const getAuthHeaders = () => ({
'Content-Type': 'application/json',
'X-API-Key': apiKey || '',
...(userToken ? { Authorization: `Bearer ${userToken}` } : {})
})
const getStatusClass = (status: string) => {
const map: Record<string, string> = {
'PENDING': 'bg-yellow-100 text-yellow-700',
'ACCEPTED': 'bg-green-100 text-green-700',
'REJECTED': 'bg-red-100 text-red-700',
'EXPIRED': 'bg-gray-100 text-gray-700'
}
return map[status] || 'bg-gray-100 text-gray-700'
}
const getStatusLabel = (status: string) => {
const map: Record<string, string> = {
'PENDING': '待接受',
'ACCEPTED': '已接受',
'REJECTED': '已拒绝',
'EXPIRED': '已过期'
}
return map[status] || status
}
const getRewardTypeLabel = (type: string) => {
const map: Record<string, string> = {
'INVITE': '邀请奖励',
'SHARE': '分享奖励',
'REGISTER': '注册奖励',
'CONVERSION': '转化奖励',
'BONUS': '额外奖励'
}
return map[type] || type
}
const formatDate = (value: string) => new Date(value).toLocaleString('zh-CN')
// 获取活动列表以获取当前活动ID
const getActivities = async () => {
const response = await fetch(`${baseUrl}/api/v1/activities`, {
headers: getAuthHeaders()
})
const payload = await response.json()
return payload?.data || []
}
const loadInvitedFriends = async () => {
loading.value = true
try {
const activities = await getActivities()
const activityId = activities[0]?.id
if (!activityId) {
invitedFriends.value = []
return
}
const params = new URLSearchParams({
activityId: String(activityId),
userId: String(userId.value),
page: '0',
size: '20'
})
const response = await fetch(`${baseUrl}/api/v1/me/invited-friends?${params}`, {
headers: getAuthHeaders()
})
const payload = await response.json()
invitedFriends.value = payload?.data || []
} catch (error) {
console.error('加载邀请记录失败:', error)
invitedFriends.value = []
} finally {
loading.value = false
}
}
const loadRewards = async () => {
loading.value = true
try {
const activities = await getActivities()
const activityId = activities[0]?.id
if (!activityId) {
rewards.value = []
return
}
const params = new URLSearchParams({
activityId: String(activityId),
userId: String(userId.value),
page: '0',
size: '20'
})
const response = await fetch(`${baseUrl}/api/v1/me/rewards?${params}`, {
headers: getAuthHeaders()
})
const payload = await response.json()
rewards.value = payload?.data || []
} catch (error) {
console.error('加载奖励记录失败:', error)
rewards.value = []
} finally {
loading.value = false
}
}
const loadData = async () => {
if (!hasAuth.value) {
return
}
if (activeTab.value === 'invites') {
await loadInvitedFriends()
} else {
await loadRewards()
}
}
onMounted(() => {
if (hasAuth.value) {
loadData()
}
})
// 切换 tab 时加载数据
import { watch } from 'vue'
watch(activeTab, () => {
loadData()
})
</script>

View File

@@ -34,10 +34,10 @@
<div class="flex flex-wrap items-center gap-3">
<template v-if="hasAuth">
<MosquitoShareButton :activity-id="activityId" :user-id="userId" />
<button class="mos-btn mos-btn-accent !py-2 !px-4">
<MosquitoShareButton :activity-id="activityId" :user-id="userId" @copied="handleCopied" @error="handleCopyError" />
<button class="mos-btn mos-btn-accent !py-2 !px-4" @click="handleCopyLink">
<Icons name="copy" class="w-4 h-4" />
复制链接
{{ copyButtonText }}
</button>
</template>
<template v-else>
@@ -125,7 +125,7 @@ type ActivitySummary = {
}
const route = useRoute()
const { getActivities } = useMosquito()
const { getActivities, getShareUrl } = useMosquito()
const apiKey = import.meta.env.VITE_MOSQUITO_API_KEY
const userToken = import.meta.env.VITE_MOSQUITO_USER_TOKEN
const routeUserId = computed(() => parseUserId(route.query.userId ?? route.params.userId))
@@ -134,6 +134,8 @@ const activityId = ref(1)
const activityLabel = computed(() => `活动 #${activityId.value}`)
const loadError = ref('')
const hasAuth = computed(() => Boolean(apiKey && userToken && userId.value))
const copyButtonText = ref('复制链接')
const copyFeedback = ref<'success' | 'error' | null>(null)
const guideSteps = [
'点击"分享给好友"生成专属链接',
@@ -141,6 +143,65 @@ const guideSteps = [
'回到首页查看最新排行和奖励进度'
]
// 复制链接处理
const handleCopyLink = async () => {
try {
const shareResponse = await getShareUrl(activityId.value, userId.value, 'default')
let urlToCopy: string
if (shareResponse && typeof shareResponse === 'object') {
if (shareResponse.originalUrl) {
urlToCopy = shareResponse.originalUrl
} else if (shareResponse.path) {
urlToCopy = shareResponse.path.startsWith('http')
? shareResponse.path
: `${window.location.origin}${shareResponse.path}`
} else {
throw new Error('分享链接响应格式异常')
}
} else {
throw new Error('分享链接响应格式异常')
}
// 复制到剪贴板
try {
await navigator.clipboard.writeText(urlToCopy)
showCopyFeedback('success')
} catch {
// 回退到传统方法
const textArea = document.createElement('textarea')
textArea.value = urlToCopy
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
showCopyFeedback('success')
}
} catch (error) {
console.error('复制链接失败:', error)
showCopyFeedback('error')
}
}
// 显示复制反馈
const showCopyFeedback = (type: 'success' | 'error') => {
copyFeedback.value = type
copyButtonText.value = type === 'success' ? '已复制!' : '复制失败'
setTimeout(() => {
copyButtonText.value = '复制链接'
copyFeedback.value = null
}, 2000)
}
// MosquitoShareButton 回调处理
const handleCopied = () => {
showCopyFeedback('success')
}
const handleCopyError = () => {
showCopyFeedback('error')
}
const loadActivity = async () => {
if (!hasAuth.value) {
return