我的页面:次数并入会员卡、兑换 CDK 统一、邀请积分并入推广板块
ci/woodpecker/tag/build Pipeline was successful

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:
becomer
2026-10-21 16:15:59 +08:00
parent 651739af1b
commit 770b1754b4
+31 -38
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' 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 { themeMode, setThemeMode, type ThemeMode } from '@/composables/useTheme'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -80,7 +80,6 @@ const redeemCodeText = ref('')
const redeeming = ref(false) const redeeming = ref(false)
const redeemError = ref('') const redeemError = ref('')
// 弹窗打开来源:count=兑换次数 / vip=开通或续费VIP(仅影响标题文案) // 弹窗打开来源:count=兑换次数 / vip=开通或续费VIP(仅影响标题文案)
const redeemKind = ref<'count' | 'vip'>('count')
// VIP 信息 // VIP 信息
const isVip = ref(false) const isVip = ref(false)
const vipExpireAt = ref('') const vipExpireAt = ref('')
@@ -271,8 +270,7 @@ async function confirmRandomBind() {
} }
// ---- 兑换弹窗 ---- // ---- 兑换弹窗 ----
function openRedeemModal(kind: 'count' | 'vip' = 'count') { function openRedeemModal() {
redeemKind.value = kind
showRedeemModal.value = true showRedeemModal.value = true
redeemCodeText.value = '' redeemCodeText.value = ''
redeemError.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-if="isVip">{{ vipExpireShort ? `有效期至 ${vipExpireShort}` : '会员生效中' }}<template v-if="vipDaysLeft !== null"> · 剩 {{ vipDaysLeft }} 天</template></template>
<template v-else>开通后执行任务不限次数</template> <template v-else>开通后执行任务不限次数</template>
</p> </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> </div>
<!-- 统一为「兑换 CDK」:既能兑 VIP 也能兑次数,由 CDK 类型自动判断 -->
<button <button
data-guide="profile-vip-btn" data-guide="profile-vip-btn"
class="vip-btn press flex-shrink-0 text-sm font-semibold px-4 py-2 rounded-full shadow-md" 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> </button>
</div> </div>
</div> </div>
@@ -503,33 +507,6 @@ async function handleRedeemInvite() {
</span> </span>
</button> </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 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" /> <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> <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> 积分<template v-if="inviteeRewardPerInvite !== null">,TA 得 <span class="font-semibold text-purple-600">{{ fmtReward(inviteeRewardPerInvite) }}</span> 积分</template>
</p> </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>
</div> </div>
<!-- 邀请码 + 复制 --> <!-- 邀请码 + 复制 -->
@@ -669,13 +662,13 @@ async function handleRedeemInvite() {
wrapper-class="px-4" wrapper-class="px-4"
panel-class="max-w-sm" panel-class="max-w-sm"
:fade="true" :fade="true"
:title="redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数'" :title="'兑换 CDK'"
@update:open="(v: boolean) => { if (!v) closeRedeemModal() }" @update:open="(v: boolean) => { if (!v) closeRedeemModal() }"
> >
<!-- 弹窗头部 --> <!-- 弹窗头部 -->
<div class="flex items-center justify-between px-5 pt-5 pb-2"> <div class="flex items-center justify-between px-5 pt-5 pb-2">
<h3 class="text-base font-semibold text-gray-900"> <h3 class="text-base font-semibold text-gray-900">
{{ redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数' }} 兑换 CDK
</h3> </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"> <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" /> <X class="w-4 h-4" />
@@ -686,18 +679,18 @@ async function handleRedeemInvite() {
<div class="px-5 pb-5 space-y-4"> <div class="px-5 pb-5 space-y-4">
<!-- 邀请积分可免费兑换提示:引导用户关闭本弹窗,改用下方邀请信息的兑换按钮 --> <!-- 邀请积分可免费兑换提示:引导用户关闭本弹窗,改用下方邀请信息的兑换按钮 -->
<div <div
v-if="redeemKind === 'count' && canRedeem" v-if="canRedeem"
class="rounded-lg bg-amber-50 border border-amber-200 px-3 py-2.5" class="rounded-lg bg-amber-50 border border-amber-200 px-3 py-2.5"
> >
<p class="text-xs text-amber-700 leading-relaxed"> <p class="text-xs text-amber-700 leading-relaxed">
你的邀请奖励积分已满 1(当前 <span class="font-semibold">{{ fmtReward(inviteReward) }}</span>), 你的邀请奖励积分已满 1(当前 <span class="font-semibold">{{ fmtReward(inviteReward) }}</span>),
可免费兑换签到次数。请关闭本弹窗,点击下方「邀请信息」处的「兑换」按钮即可用积分兑换,无需兑换码。 可免费兑换签到次数。请关闭本弹窗,点击上方推广板块中的「免费换次数」按钮即可用积分兑换,无需兑换码。
</p> </p>
</div> </div>
<!-- 兑换码输入 --> <!-- 兑换码输入 -->
<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 <Input
v-model="redeemCodeText" v-model="redeemCodeText"
placeholder="请输入 CDK 兑换码" placeholder="请输入 CDK 兑换码"