1. 可执行次数并入会员卡那一行
- 普通用户:显示「可执行次数 N」,右侧按钮「兑换 CDK」
- VIP 用户:仍显示「VIP 会员 + 有效期/剩余天数」,
下方额外显示「可执行次数 不限」
2. 兑换入口统一
- 原「开通 VIP / 续费」按钮改为「兑换 CDK」,既能兑 VIP 也能兑次数
- 后端本就是同一接口:handleRedeem 依据响应 data.type
('vip' / 次数)分别处理,故后端无需改动
- 兑换弹窗标题/标签统一为「兑换 CDK」「CDK 兑换码」,
移除 redeemKind 状态(已无分支)
3. 删除下方数据三宫格
- 不再单独显示「可执行次数」格子(已并入会员卡)
- 删除「已邀请 N 位同学」格子
4. 邀请积分并入推广板块
- 在邀请卡内展示「邀请积分 N」+「免费换次数」按钮
- 原「换次数」文案改为「免费换次数」
- 积分不足时按钮禁用并显示「满 1 可换」
- 兑换弹窗内的引导文案同步指向新位置
顺带清理:移除随三宫格一起不再使用的 Users 图标导入。
npm run build 与 vue-tsc 均通过。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { LogOut, RefreshCw, Gift, X, Dices, TriangleAlert, SunMoon, Sun, Moon, Crown, Copy, ChevronRight, Smartphone, Users, Coins, UserPlus, GraduationCap, Headset, ShoppingCart, Flame } from 'lucide-vue-next'
|
||||
import { LogOut, RefreshCw, Gift, X, Dices, TriangleAlert, SunMoon, Sun, Moon, Crown, Copy, ChevronRight, Smartphone, Coins, UserPlus, GraduationCap, Headset, ShoppingCart, Flame } from 'lucide-vue-next'
|
||||
import { themeMode, setThemeMode, type ThemeMode } from '@/composables/useTheme'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -80,7 +80,6 @@ const redeemCodeText = ref('')
|
||||
const redeeming = ref(false)
|
||||
const redeemError = ref('')
|
||||
// 弹窗打开来源:count=兑换次数 / vip=开通或续费VIP(仅影响标题文案)
|
||||
const redeemKind = ref<'count' | 'vip'>('count')
|
||||
// VIP 信息
|
||||
const isVip = ref(false)
|
||||
const vipExpireAt = ref('')
|
||||
@@ -271,8 +270,7 @@ async function confirmRandomBind() {
|
||||
}
|
||||
|
||||
// ---- 兑换弹窗 ----
|
||||
function openRedeemModal(kind: 'count' | 'vip' = 'count') {
|
||||
redeemKind.value = kind
|
||||
function openRedeemModal() {
|
||||
showRedeemModal.value = true
|
||||
redeemCodeText.value = ''
|
||||
redeemError.value = ''
|
||||
@@ -465,13 +463,19 @@ async function handleRedeemInvite() {
|
||||
<template v-if="isVip">{{ vipExpireShort ? `有效期至 ${vipExpireShort}` : '会员生效中' }}<template v-if="vipDaysLeft !== null"> · 剩 {{ vipDaysLeft }} 天</template></template>
|
||||
<template v-else>开通后执行任务不限次数</template>
|
||||
</p>
|
||||
<!-- 可执行次数:普通用户与 VIP 都显示(VIP 为不限次数时显示「不限」) -->
|
||||
<p class="member-sub text-xs mt-1 flex items-center gap-1">
|
||||
<span class="opacity-75">可执行次数</span>
|
||||
<span class="font-bold tabular-nums">{{ isVip ? '不限' : fmtCount(userInfo?.signCount ?? 0) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- 统一为「兑换 CDK」:既能兑 VIP 也能兑次数,由 CDK 类型自动判断 -->
|
||||
<button
|
||||
data-guide="profile-vip-btn"
|
||||
class="vip-btn press flex-shrink-0 text-sm font-semibold px-4 py-2 rounded-full shadow-md"
|
||||
@click="openRedeemModal('vip')"
|
||||
@click="openRedeemModal()"
|
||||
>
|
||||
{{ isVip ? '续费' : '开通 VIP' }}
|
||||
兑换 CDK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -503,33 +507,6 @@ async function handleRedeemInvite() {
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- ======== 数据三宫格:可执行次数 / 邀请积分 / 已邀请 ======== -->
|
||||
<div class="grid grid-cols-3 gap-2.5 rise-in" style="animation-delay: 120ms">
|
||||
<button class="press stat-cell bg-white rounded-2xl border border-gray-100/80 shadow-sm py-3 text-center" @click="openRedeemModal('count')">
|
||||
<p class="text-xl font-bold tabular-nums text-gray-900 leading-tight">{{ isVip ? '∞' : fmtCount(userInfo?.signCount ?? 0) }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">{{ isVip ? '不限次数' : '可执行次数' }}</p>
|
||||
<p class="mt-1.5 inline-flex items-center gap-0.5 text-xs font-semibold text-brand-600"><Gift class="w-3 h-3" />兑换</p>
|
||||
</button>
|
||||
<button
|
||||
data-guide="profile-invite-redeem"
|
||||
class="press stat-cell bg-white rounded-2xl border border-gray-100/80 shadow-sm py-3 text-center disabled:active:scale-100"
|
||||
:disabled="redeemingInvite"
|
||||
@click="onRedeemInviteTap"
|
||||
>
|
||||
<p class="text-xl font-bold tabular-nums text-gray-900 leading-tight">{{ fmtReward(inviteReward) }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">邀请积分</p>
|
||||
<p class="mt-1.5 inline-flex items-center gap-0.5 text-xs font-semibold" :class="canRedeem ? 'text-amber-600' : 'text-gray-400'">
|
||||
<RefreshCw v-if="redeemingInvite" class="w-3 h-3 animate-spin" />
|
||||
<template v-else><Coins class="w-3 h-3" />{{ canRedeem ? '换次数' : '满 1 可换' }}</template>
|
||||
</p>
|
||||
</button>
|
||||
<div class="stat-cell bg-white rounded-2xl border border-gray-100/80 shadow-sm py-3 text-center">
|
||||
<p class="text-xl font-bold tabular-nums text-gray-900 leading-tight">{{ invitedCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">已邀请</p>
|
||||
<p class="mt-1.5 inline-flex items-center gap-0.5 text-xs font-medium text-gray-400"><Users class="w-3 h-3" />位同学</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======== 邀请好友卡 ======== -->
|
||||
<div data-guide="profile-invite-row" class="invite-card relative overflow-hidden hero-clip rounded-3xl p-4 rise-in" style="animation-delay: 160ms">
|
||||
<div class="pointer-events-none absolute -right-6 -bottom-10 w-32 h-32 rounded-full invite-glow" />
|
||||
@@ -541,6 +518,22 @@ async function handleRedeemInvite() {
|
||||
<span class="font-semibold text-purple-600">{{ inviterRewardPerInvite !== null ? fmtReward(inviterRewardPerInvite) : '一定' }}</span>
|
||||
积分<template v-if="inviteeRewardPerInvite !== null">,TA 得 <span class="font-semibold text-purple-600">{{ fmtReward(inviteeRewardPerInvite) }}</span> 积分</template>
|
||||
</p>
|
||||
<!-- 邀请积分(原三宫格内容并入推广板块):积分 + 免费换次数 -->
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs text-gray-400">邀请积分</span>
|
||||
<span class="text-base font-bold tabular-nums text-purple-600 leading-none">{{ fmtReward(inviteReward) }}</span>
|
||||
<button
|
||||
data-guide="profile-invite-redeem"
|
||||
class="press flex items-center gap-1 h-7 px-2.5 rounded-lg text-xs font-semibold disabled:opacity-60"
|
||||
:class="canRedeem ? 'invite-btn text-white shadow-sm shadow-purple-500/30' : 'bg-gray-100 text-gray-400'"
|
||||
:disabled="redeemingInvite || !canRedeem"
|
||||
@click="onRedeemInviteTap"
|
||||
>
|
||||
<RefreshCw v-if="redeemingInvite" class="w-3 h-3 animate-spin" />
|
||||
<Coins v-else class="w-3 h-3" />
|
||||
{{ canRedeem ? '免费换次数' : '满 1 可换' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 邀请码 + 复制 -->
|
||||
@@ -669,13 +662,13 @@ async function handleRedeemInvite() {
|
||||
wrapper-class="px-4"
|
||||
panel-class="max-w-sm"
|
||||
:fade="true"
|
||||
:title="redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数'"
|
||||
:title="'兑换 CDK'"
|
||||
@update:open="(v: boolean) => { if (!v) closeRedeemModal() }"
|
||||
>
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between px-5 pt-5 pb-2">
|
||||
<h3 class="text-base font-semibold text-gray-900">
|
||||
{{ redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数' }}
|
||||
兑换 CDK
|
||||
</h3>
|
||||
<button class="w-7 h-7 rounded-full bg-gray-100 flex items-center justify-center text-gray-400 hover:text-gray-600" @click="closeRedeemModal">
|
||||
<X class="w-4 h-4" />
|
||||
@@ -686,18 +679,18 @@ async function handleRedeemInvite() {
|
||||
<div class="px-5 pb-5 space-y-4">
|
||||
<!-- 邀请积分可免费兑换提示:引导用户关闭本弹窗,改用下方邀请信息的兑换按钮 -->
|
||||
<div
|
||||
v-if="redeemKind === 'count' && canRedeem"
|
||||
v-if="canRedeem"
|
||||
class="rounded-lg bg-amber-50 border border-amber-200 px-3 py-2.5"
|
||||
>
|
||||
<p class="text-xs text-amber-700 leading-relaxed">
|
||||
你的邀请奖励积分已满 1(当前 <span class="font-semibold">{{ fmtReward(inviteReward) }}</span>),
|
||||
可免费兑换签到次数。请关闭本弹窗,点击下方「邀请信息」处的「兑换」按钮即可用积分兑换,无需兑换码。
|
||||
可免费兑换签到次数。请关闭本弹窗,点击上方推广板块中的「免费换次数」按钮即可用积分兑换,无需兑换码。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 兑换码输入 -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1.5">{{ redeemKind === 'vip' ? 'VIP 兑换码' : '兑换码' }}</label>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1.5">CDK 兑换码</label>
|
||||
<Input
|
||||
v-model="redeemCodeText"
|
||||
placeholder="请输入 CDK 兑换码"
|
||||
|
||||
Reference in New Issue
Block a user