mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-24 20:14:26 +08:00
完成移动端邀请页面功能
This commit is contained in:
@@ -56,43 +56,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邀请方式 -->
|
||||
<div class="invite-methods">
|
||||
<h3 class="section-title">邀请方式</h3>
|
||||
<div class="methods-grid">
|
||||
<div class="method-item" @click="shareToWeChat">
|
||||
<div class="method-icon wechat">
|
||||
<i class="iconfont icon-wechat"></i>
|
||||
</div>
|
||||
<div class="method-name">微信分享</div>
|
||||
</div>
|
||||
<div class="method-item" @click="copyInviteLink">
|
||||
<div class="method-icon link">
|
||||
<i class="iconfont icon-link"></i>
|
||||
</div>
|
||||
<div class="method-name">复制链接</div>
|
||||
</div>
|
||||
<div class="method-item" @click="shareQRCode">
|
||||
<div class="method-icon qr">
|
||||
<i class="iconfont icon-qrcode"></i>
|
||||
</div>
|
||||
<div class="method-name">二维码</div>
|
||||
</div>
|
||||
<div class="method-item" @click="shareToFriends">
|
||||
<div class="method-icon more">
|
||||
<i class="iconfont icon-share"></i>
|
||||
</div>
|
||||
<div class="method-name">更多</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邀请码 -->
|
||||
<div class="invite-code-section">
|
||||
<div class="code-card">
|
||||
<div class="code-header">
|
||||
<span class="code-label">我的邀请码</span>
|
||||
<van-button size="small" type="primary" plain @click="copyInviteCode">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="copy-invite-code"
|
||||
:data-clipboard-text="inviteCode"
|
||||
>
|
||||
复制
|
||||
</van-button>
|
||||
</div>
|
||||
@@ -100,7 +75,12 @@
|
||||
<div class="code-link">
|
||||
<van-field v-model="inviteLink" readonly placeholder="邀请链接">
|
||||
<template #button>
|
||||
<van-button size="small" type="primary" @click="copyInviteLink">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="copy-invite-link"
|
||||
:data-clipboard-text="inviteLink"
|
||||
>
|
||||
复制链接
|
||||
</van-button>
|
||||
</template>
|
||||
@@ -113,70 +93,45 @@
|
||||
<div class="invite-records">
|
||||
<div class="records-header">
|
||||
<h3 class="section-title">邀请记录</h3>
|
||||
<van-button size="small" type="primary" plain @click="showAllRecords = !showAllRecords">
|
||||
{{ showAllRecords ? '收起' : '查看全部' }}
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<div class="records-list">
|
||||
<van-list
|
||||
v-model:loading="recordsLoading"
|
||||
:finished="recordsFinished"
|
||||
finished-text="没有更多记录"
|
||||
@load="loadInviteRecords"
|
||||
>
|
||||
<div v-for="record in displayRecords" :key="record.id" class="record-item">
|
||||
<div v-if="inviteRecords.length > 0">
|
||||
<div v-for="record in inviteRecords" :key="record.id" class="record-item">
|
||||
<div class="record-avatar">
|
||||
<van-image :src="record.avatar" round width="40" height="40" />
|
||||
<van-image
|
||||
:src="record.avatar || '/images/avatar/default.jpg'"
|
||||
round
|
||||
width="40"
|
||||
height="40"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
</div>
|
||||
<div class="record-info">
|
||||
<div class="record-name">{{ record.username }}</div>
|
||||
<div class="record-time">{{ formatTime(record.created_at) }}</div>
|
||||
</div>
|
||||
<div class="record-status">
|
||||
<van-tag :type="record.status === 'completed' ? 'success' : 'warning'">
|
||||
{{ record.status === 'completed' ? '已获得奖励' : '待获得奖励' }}
|
||||
</van-tag>
|
||||
<van-tag type="success">已获得奖励</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-empty
|
||||
v-if="!recordsLoading && inviteRecords.length === 0"
|
||||
description="暂无邀请记录"
|
||||
/>
|
||||
</van-list>
|
||||
<van-empty v-else description="暂无邀请记录" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码弹窗 -->
|
||||
<van-dialog
|
||||
v-model:show="showQRDialog"
|
||||
title="邀请二维码"
|
||||
:show-cancel-button="false"
|
||||
confirm-button-text="保存图片"
|
||||
@confirm="saveQRCode"
|
||||
>
|
||||
<div class="qr-content">
|
||||
<div ref="qrCodeRef" class="qr-code">
|
||||
<!-- 这里应该生成实际的二维码 -->
|
||||
<div class="qr-placeholder">
|
||||
<i class="iconfont icon-qrcode"></i>
|
||||
<p>邀请二维码</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="qr-tip">扫描二维码或长按保存分享给好友</p>
|
||||
</div>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { showNotify, showSuccessToast } from 'vant'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Clipboard from 'clipboard'
|
||||
|
||||
const router = useRouter()
|
||||
const userStats = ref({
|
||||
@@ -187,204 +142,126 @@ const userStats = ref({
|
||||
const inviteCode = ref('')
|
||||
const inviteLink = ref('')
|
||||
const inviteRecords = ref([])
|
||||
const recordsLoading = ref(false)
|
||||
const recordsFinished = ref(false)
|
||||
const showAllRecords = ref(false)
|
||||
const showQRDialog = ref(false)
|
||||
const qrCodeRef = ref()
|
||||
|
||||
// 奖励规则配置
|
||||
const rewardRules = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '好友注册',
|
||||
desc: '好友通过邀请链接成功注册',
|
||||
icon: 'icon-user-plus',
|
||||
color: '#1989fa',
|
||||
reward: 50,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '好友首次充值',
|
||||
desc: '好友首次充值任意金额',
|
||||
icon: 'icon-money',
|
||||
color: '#07c160',
|
||||
reward: 100,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '好友活跃使用',
|
||||
desc: '好友连续使用7天',
|
||||
icon: 'icon-star',
|
||||
color: '#ff9500',
|
||||
reward: 200,
|
||||
},
|
||||
])
|
||||
const rewardRules = ref([])
|
||||
|
||||
// 显示的记录(根据showAllRecords决定)
|
||||
const displayRecords = computed(() => {
|
||||
return showAllRecords.value ? inviteRecords.value : inviteRecords.value.slice(0, 5)
|
||||
})
|
||||
// clipboard实例
|
||||
const clipboard = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理clipboard实例
|
||||
if (clipboard.value) {
|
||||
clipboard.value.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
const initPage = async () => {
|
||||
try {
|
||||
const user = await checkSession()
|
||||
|
||||
// 生成邀请码和链接
|
||||
inviteCode.value = user.invite_code || generateInviteCode()
|
||||
inviteLink.value = `${location.origin}/register?invite=${inviteCode.value}`
|
||||
|
||||
// 获取用户邀请统计
|
||||
// 获取邀请统计(包含邀请码和链接)
|
||||
fetchInviteStats()
|
||||
|
||||
// 加载邀请记录
|
||||
loadInviteRecords()
|
||||
// 获取奖励规则
|
||||
fetchRewardRules()
|
||||
|
||||
// 一次性加载邀请记录
|
||||
await loadInviteRecords()
|
||||
|
||||
// 初始化clipboard
|
||||
initClipboard()
|
||||
} catch (error) {
|
||||
showLoginDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
const generateInviteCode = () => {
|
||||
return Math.random().toString(36).substr(2, 8).toUpperCase()
|
||||
}
|
||||
|
||||
const fetchInviteStats = () => {
|
||||
// 这里应该调用实际的API
|
||||
// httpGet('/api/user/invite/stats').then(res => {
|
||||
// userStats.value = res.data
|
||||
// })
|
||||
|
||||
// 临时使用模拟数据
|
||||
userStats.value = {
|
||||
inviteCount: Math.floor(Math.random() * 50),
|
||||
rewardTotal: Math.floor(Math.random() * 5000),
|
||||
todayInvite: Math.floor(Math.random() * 5),
|
||||
}
|
||||
}
|
||||
|
||||
const loadInviteRecords = () => {
|
||||
if (recordsFinished.value) return
|
||||
|
||||
recordsLoading.value = true
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
const mockRecords = generateMockRecords()
|
||||
inviteRecords.value.push(...mockRecords)
|
||||
|
||||
recordsLoading.value = false
|
||||
|
||||
// 模拟数据加载完成
|
||||
if (inviteRecords.value.length >= 20) {
|
||||
recordsFinished.value = true
|
||||
const fetchInviteStats = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/invite/stats')
|
||||
userStats.value = {
|
||||
inviteCount: res.data.invite_count,
|
||||
rewardTotal: res.data.reward_total,
|
||||
todayInvite: res.data.today_invite,
|
||||
}
|
||||
inviteCode.value = res.data.invite_code
|
||||
inviteLink.value = res.data.invite_link
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
// 使用默认值
|
||||
userStats.value = {
|
||||
inviteCount: 0,
|
||||
rewardTotal: 0,
|
||||
todayInvite: 0,
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const generateMockRecords = () => {
|
||||
const records = []
|
||||
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
records.push({
|
||||
id: Date.now() + i,
|
||||
username: names[i % names.length] + (i + 1),
|
||||
avatar: '/images/avatar/default.jpg',
|
||||
status: Math.random() > 0.3 ? 'completed' : 'pending',
|
||||
created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
const formatTime = (timeStr) => {
|
||||
const date = new Date(timeStr)
|
||||
const fetchRewardRules = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/invite/rules')
|
||||
rewardRules.value = res.data
|
||||
} catch (error) {
|
||||
console.error('获取奖励规则失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadInviteRecords = async () => {
|
||||
try {
|
||||
const res = await httpGet('/api/invite/list', {
|
||||
page: 1, // 加载第一页
|
||||
page_size: 20,
|
||||
})
|
||||
console.log('邀请记录API返回:', res.data) // 调试日志
|
||||
inviteRecords.value = res.data.items || []
|
||||
console.log('设置邀请记录:', inviteRecords.value) // 调试日志
|
||||
|
||||
// 调试头像信息
|
||||
if (inviteRecords.value.length > 0) {
|
||||
console.log('第一条记录头像:', inviteRecords.value[0].avatar)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取邀请记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp * 1000) // 转换为毫秒
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
// 分享到微信
|
||||
const shareToWeChat = () => {
|
||||
if (typeof WeixinJSBridge !== 'undefined') {
|
||||
// 在微信中分享
|
||||
WeixinJSBridge.invoke('sendAppMessage', {
|
||||
title: '邀请你使用AI创作平台',
|
||||
desc: '强大的AI工具,让创作更简单',
|
||||
link: inviteLink.value,
|
||||
imgUrl: `${location.origin}/images/share-logo.png`,
|
||||
})
|
||||
} else {
|
||||
// 复制链接提示
|
||||
copyInviteLink()
|
||||
showNotify({ type: 'primary', message: '请在微信中打开链接进行分享' })
|
||||
// 初始化clipboard
|
||||
const initClipboard = () => {
|
||||
// 销毁之前的实例
|
||||
if (clipboard.value) {
|
||||
clipboard.value.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// 复制邀请码
|
||||
const copyInviteCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteCode.value)
|
||||
showSuccessToast('邀请码已复制')
|
||||
} catch (err) {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = inviteCode.value
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
showSuccessToast('邀请码已复制')
|
||||
}
|
||||
}
|
||||
// 创建新的clipboard实例
|
||||
clipboard.value = new Clipboard('.copy-invite-code, .copy-invite-link')
|
||||
|
||||
// 复制邀请链接
|
||||
const copyInviteLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteLink.value)
|
||||
showSuccessToast('邀请链接已复制')
|
||||
} catch (err) {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = inviteLink.value
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
showSuccessToast('邀请链接已复制')
|
||||
}
|
||||
}
|
||||
clipboard.value.on('success', () => {
|
||||
showSuccessToast('复制成功')
|
||||
})
|
||||
|
||||
// 显示二维码
|
||||
const shareQRCode = () => {
|
||||
showQRDialog.value = true
|
||||
}
|
||||
|
||||
// 保存二维码
|
||||
const saveQRCode = () => {
|
||||
showNotify({ type: 'primary', message: '请长按二维码保存到相册' })
|
||||
}
|
||||
|
||||
// 更多分享方式
|
||||
const shareToFriends = () => {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: '邀请你使用AI创作平台',
|
||||
text: '强大的AI工具,让创作更简单',
|
||||
url: inviteLink.value,
|
||||
})
|
||||
} else {
|
||||
copyInviteLink()
|
||||
}
|
||||
clipboard.value.on('error', () => {
|
||||
showNotify({ type: 'danger', message: '复制失败' })
|
||||
})
|
||||
}
|
||||
|
||||
// 图片加载错误处理
|
||||
const onImageError = (e) => {
|
||||
e.target.src = '/images/img-holder.png'
|
||||
}
|
||||
|
||||
// 头像加载错误处理
|
||||
const onAvatarError = (e) => {
|
||||
e.target.src = '/images/avatar/default.jpg'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -393,8 +270,6 @@ const onImageError = (e) => {
|
||||
background: var(--van-background);
|
||||
|
||||
.invite-content {
|
||||
padding-top: 46px;
|
||||
|
||||
.invite-header {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
@@ -469,7 +344,6 @@ const onImageError = (e) => {
|
||||
}
|
||||
|
||||
.rules-section,
|
||||
.invite-methods,
|
||||
.invite-code-section,
|
||||
.invite-records {
|
||||
padding: 0 16px 16px;
|
||||
@@ -541,65 +415,6 @@ const onImageError = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
.methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
|
||||
.method-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 16px;
|
||||
background: var(--van-cell-background);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.wechat {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
&.link {
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
&.qr {
|
||||
background: #8b5cf6;
|
||||
}
|
||||
|
||||
&.more {
|
||||
background: #ff9500;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-code-section {
|
||||
.code-card {
|
||||
background: var(--van-cell-background);
|
||||
@@ -675,44 +490,6 @@ const onImageError = (e) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
.qr-code {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 16px;
|
||||
border: 1px solid var(--van-border-color);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--van-background-2);
|
||||
|
||||
.qr-placeholder {
|
||||
text-align: center;
|
||||
color: var(--van-gray-6);
|
||||
|
||||
.iconfont {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr-tip {
|
||||
font-size: 13px;
|
||||
color: var(--van-gray-6);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题优化
|
||||
|
||||
Reference in New Issue
Block a user