Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff3593f057 | |||
| 74422b1848 | |||
| 84f87cd168 |
@@ -38,17 +38,22 @@ func getBaseDeduction() float64 {
|
||||
return config.Global.Agent.SystemCdkPrice
|
||||
}
|
||||
|
||||
// getVipPrice 获取 VIP CDK 单张价格(元/张,系统设置,默认15元)
|
||||
func getVipPrice() float64 {
|
||||
// getVipCardPrice 获取 VIP 卡价格(元/张,系统设置)
|
||||
// key: vip_price_week(周卡7天,默认30元)/ vip_price_month(月卡30天,默认100元)
|
||||
// (单次运行基础扣费 6 元,周卡/月卡按次折算后再给折扣)
|
||||
func getVipCardPrice(key string, def float64) float64 {
|
||||
var cfg model.SystemConfig
|
||||
if database.DB.Where("`key` = ?", "vip_price").First(&cfg).Error == nil {
|
||||
if database.DB.Where("`key` = ?", key).First(&cfg).Error == nil {
|
||||
if v, err := strconv.ParseFloat(cfg.Value, 64); err == nil && v > 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 15.00
|
||||
return def
|
||||
}
|
||||
|
||||
func getVipWeekPrice() float64 { return getVipCardPrice("vip_price_week", 30.00) }
|
||||
func getVipMonthPrice() float64 { return getVipCardPrice("vip_price_month", 100.00) }
|
||||
|
||||
// GenerateCDK 生成CDK
|
||||
func GenerateCDK(c *fiber.Ctx) error {
|
||||
agentID := c.Locals("adminId").(uint)
|
||||
@@ -81,7 +86,6 @@ func GenerateCDK(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
baseDeduction := getBaseDeduction()
|
||||
vipPrice := getVipPrice()
|
||||
|
||||
var maxRuns int
|
||||
var vipDays int
|
||||
@@ -89,21 +93,23 @@ func GenerateCDK(c *fiber.Ctx) error {
|
||||
var costPerCDK float64
|
||||
|
||||
if req.Type == "vip" {
|
||||
// VIP CDK:默认一个月(30天),上限365天
|
||||
// VIP CDK:仅支持周卡(7天)/ 月卡(30天)两档
|
||||
if req.VipDays <= 0 {
|
||||
req.VipDays = 30
|
||||
}
|
||||
if req.VipDays > 365 {
|
||||
return utils.Fail(c, 400, "单张VIP CDK最多365天")
|
||||
}
|
||||
if req.VipDays%30 != 0 {
|
||||
return utils.Fail(c, 400, "VIP天数须为30的整数倍(一个月起)")
|
||||
if req.VipDays != 7 && req.VipDays != 30 {
|
||||
return utils.Fail(c, 400, "VIP CDK 仅支持 7 天(周卡)或 30 天(月卡)")
|
||||
}
|
||||
maxRuns = 0
|
||||
vipDays = req.VipDays
|
||||
months := req.VipDays / 30
|
||||
unitPrice = vipPrice * float64(months) * agent.Rate
|
||||
costPerCDK = vipPrice * float64(months)
|
||||
var vipPrice float64
|
||||
if req.VipDays == 7 {
|
||||
vipPrice = getVipWeekPrice()
|
||||
} else {
|
||||
vipPrice = getVipMonthPrice()
|
||||
}
|
||||
unitPrice = vipPrice * agent.Rate
|
||||
costPerCDK = vipPrice
|
||||
} else {
|
||||
// 次数 CDK
|
||||
if req.MaxRuns <= 0 {
|
||||
@@ -491,8 +497,10 @@ func GetUserVipInfo(c *fiber.Ctx) error {
|
||||
isVip := user.VipExpireAt != nil && user.VipExpireAt.After(time.Now())
|
||||
|
||||
return utils.Success(c, fiber.Map{
|
||||
"isVip": isVip,
|
||||
"vipExpireAt": vipExpireAt,
|
||||
"vipPrice": formatFloat(getVipPrice()),
|
||||
"isVip": isVip,
|
||||
"vipExpireAt": vipExpireAt,
|
||||
"vipWeekPrice": formatFloat(getVipWeekPrice()),
|
||||
"vipMonthPrice": formatFloat(getVipMonthPrice()),
|
||||
"vipPrice": formatFloat(getVipMonthPrice()), // 兼容旧字段:月卡价
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,8 +34,11 @@ func GetSettings(c *fiber.Ctx) error {
|
||||
if _, ok := settings["register_reward"]; !ok {
|
||||
settings["register_reward"] = "1.00"
|
||||
}
|
||||
if _, ok := settings["vip_price"]; !ok {
|
||||
settings["vip_price"] = "15.00"
|
||||
if _, ok := settings["vip_price_week"]; !ok {
|
||||
settings["vip_price_week"] = "30.00"
|
||||
}
|
||||
if _, ok := settings["vip_price_month"]; !ok {
|
||||
settings["vip_price_month"] = "100.00"
|
||||
}
|
||||
|
||||
return utils.Success(c, settings)
|
||||
@@ -63,7 +66,8 @@ func UpdateSetting(c *fiber.Ctx) error {
|
||||
"inviter_reward_per_invite": true,
|
||||
"invitee_reward_per_invite": true,
|
||||
"register_reward": true,
|
||||
"vip_price": true,
|
||||
"vip_price_week": true,
|
||||
"vip_price_month": true,
|
||||
// 邮件通知(SMTP)
|
||||
"smtp_host": true,
|
||||
"smtp_port": true,
|
||||
|
||||
@@ -135,7 +135,7 @@ export function redeemCDK(code: string, captchaId: string, captchaCode: string)
|
||||
|
||||
/** 当前用户 VIP 信息(是否 VIP、到期时间、系统配置单价) */
|
||||
export function getVipInfo() {
|
||||
return api.get<{ data: { isVip: boolean; vipExpireAt: string; vipPrice: string } }>(
|
||||
return api.get<{ data: { isVip: boolean; vipExpireAt: string; vipWeekPrice: string; vipMonthPrice: string; vipPrice: string } }>(
|
||||
'/cdk/vip-info'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ const steps: GuideStep[] = [
|
||||
descByKind: () =>
|
||||
'新用户注册即送奖励积分,邀请好友也能赚积分。\n积分满 1 后,点这里「兑换」按钮即可换成签到次数,1 积分 = 1 次签到,可重复兑换。',
|
||||
},
|
||||
{
|
||||
tab: 'profile',
|
||||
title: 'VIP 会员:签到不限次',
|
||||
targets: ['[data-guide="profile-vip-btn"]'],
|
||||
descByKind: () =>
|
||||
'普通签到次数:每执行一次任务扣 1 次,用完需要再兑换。\nVIP 会员:有效期内执行任务不限次数(无限次),不再扣签到次数。\n点「续费 VIP」输入 VIP 兑换码即可开通,到期时间自动顺延。',
|
||||
},
|
||||
{
|
||||
tab: 'home',
|
||||
title: '新建每日签到任务',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 复制文本到剪贴板(兼容 HTTP 环境与移动端)
|
||||
* 优先 Clipboard API(需 HTTPS/localhost),失败回退 execCommand
|
||||
* 返回是否复制成功
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
// 1) Clipboard API(仅安全上下文可用)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch {
|
||||
// 继续走 fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 隐藏 textarea + execCommand(HTTP / 老浏览器)
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
// 必须可编辑且不在屏幕外,否则 iOS Safari 会复制失败
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.top = '0'
|
||||
ta.style.left = '0'
|
||||
ta.style.opacity = '0'
|
||||
ta.setAttribute('readonly', '')
|
||||
document.body.appendChild(ta)
|
||||
ta.focus()
|
||||
ta.select()
|
||||
ta.setSelectionRange(0, text.length)
|
||||
let ok = false
|
||||
try {
|
||||
ok = document.execCommand('copy')
|
||||
} catch {
|
||||
ok = false
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
return ok
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import * as adminApi from '@/api/admin'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -121,8 +122,8 @@ async function handleGenerate() {
|
||||
return
|
||||
}
|
||||
if (generateType.value === 'vip') {
|
||||
if (generateVipDays.value <= 0 || generateVipDays.value > 365 || generateVipDays.value % 30 !== 0) {
|
||||
message.info('VIP 天数须为 30 的整数倍(30-360,一个月起)')
|
||||
if (generateVipDays.value !== 7 && generateVipDays.value !== 30) {
|
||||
message.info('VIP CDK 仅支持周卡(7 天)或月卡(30 天)')
|
||||
return
|
||||
}
|
||||
} else if (generateMaxRuns.value <= 0 || generateMaxRuns.value > 999) {
|
||||
@@ -170,57 +171,32 @@ function closeGenerateModal() {
|
||||
}
|
||||
|
||||
async function copyCode(code: string, index: number) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
copiedIndex.value = index
|
||||
setTimeout(() => { copiedIndex.value = null }, 2000)
|
||||
} catch {
|
||||
// fallback
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = code
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
if (await copyToClipboard(code)) {
|
||||
copiedIndex.value = index
|
||||
setTimeout(() => { copiedIndex.value = null }, 2000)
|
||||
} else {
|
||||
message.error('复制失败,请长按手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyResultCode(code: string, index: number) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
resultCopiedIdx.value = index
|
||||
setTimeout(() => { resultCopiedIdx.value = null }, 2000)
|
||||
} catch {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = code
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
if (await copyToClipboard(code)) {
|
||||
resultCopiedIdx.value = index
|
||||
setTimeout(() => { resultCopiedIdx.value = null }, 2000)
|
||||
} else {
|
||||
message.error('复制失败,请长按手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAllCodes() {
|
||||
if (resultCdks.value.length === 0) return
|
||||
const all = resultCdks.value.map((c) => c.code).join('\n')
|
||||
try {
|
||||
await navigator.clipboard.writeText(all)
|
||||
if (await copyToClipboard(all)) {
|
||||
copiedAll.value = true
|
||||
message.success(`已复制全部 ${resultCdks.value.length} 个CDK`)
|
||||
setTimeout(() => { copiedAll.value = false }, 2000)
|
||||
} catch {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = all
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
copiedAll.value = true
|
||||
setTimeout(() => { copiedAll.value = false }, 2000)
|
||||
} else {
|
||||
message.error('复制失败,请长按手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,14 +466,21 @@ function statusClass(s: string) {
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<Label class="mb-1 block text-sm text-gray-600">VIP 时长(天)</Label>
|
||||
<select v-model.number="generateVipDays" class="h-10 w-full rounded-md border border-gray-300 bg-white px-3 text-sm">
|
||||
<option :value="30">30 天(1 个月)</option>
|
||||
<option :value="60">60 天(2 个月)</option>
|
||||
<option :value="90">90 天(3 个月)</option>
|
||||
<option :value="180">180 天(半年)</option>
|
||||
<option :value="360">360 天(1 年)</option>
|
||||
</select>
|
||||
<Label class="mb-1 block text-sm text-gray-600">VIP 卡类型</Label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="h-10 rounded-md border text-sm font-medium transition-colors"
|
||||
:class="generateVipDays === 7 ? 'bg-amber-500 text-white border-amber-500' : 'bg-white text-gray-700 border-gray-300 hover:border-amber-400'"
|
||||
@click="generateVipDays = 7"
|
||||
>周卡 · 7天</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-10 rounded-md border text-sm font-medium transition-colors"
|
||||
:class="generateVipDays === 30 ? 'bg-amber-500 text-white border-amber-500' : 'bg-white text-gray-700 border-gray-300 hover:border-amber-400'"
|
||||
@click="generateVipDays = 30"
|
||||
>月卡 · 30天</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">兑换后从当前到期时间顺延,到期即享免扣次数执行任务</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -522,7 +505,7 @@ function statusClass(s: string) {
|
||||
</h2>
|
||||
<p class="text-xs text-gray-400 mt-0.5">
|
||||
共 {{ resultCdks.length }} 个CDK ·
|
||||
<template v-if="generateType === 'vip'">VIP {{ generateVipDays }} 天 · </template>
|
||||
<template v-if="generateType === 'vip'">{{ generateVipDays === 7 ? 'VIP 周卡 7 天' : 'VIP 月卡 30 天' }} · </template>
|
||||
<template v-else>每张 {{ generateMaxRuns }} 次 · </template>
|
||||
扣费
|
||||
<span class="font-mono text-gray-600">{{ resultCost.toFixed(2) }} 元</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Shield, RefreshCw, CheckCircle, Search, Eye, EyeOff, Copy } from 'lucid
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import api from '@/api/index'
|
||||
|
||||
interface PendingSchoolItem {
|
||||
@@ -102,10 +103,13 @@ async function handleMarkDone(record: PendingSchoolItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyText(text: string) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
async function copyText(text: string) {
|
||||
const ok = await copyToClipboard(text)
|
||||
if (ok) {
|
||||
message.success('已复制')
|
||||
})
|
||||
} else {
|
||||
message.error('复制失败,请长按手动复制')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const baseDeduction = ref('')
|
||||
const vipPrice = ref('')
|
||||
const vipWeekPrice = ref('')
|
||||
const vipMonthPrice = ref('')
|
||||
const inviterReward = ref('')
|
||||
const inviteeReward = ref('')
|
||||
const registerReward = ref('')
|
||||
@@ -40,7 +41,8 @@ async function loadSettings() {
|
||||
const res = await adminApi.getSettings()
|
||||
const settings = res.data?.data || {}
|
||||
baseDeduction.value = settings['base_deduction_per_run'] || '1.00'
|
||||
vipPrice.value = settings['vip_price'] || '15.00'
|
||||
vipWeekPrice.value = settings['vip_price_week'] || '30.00'
|
||||
vipMonthPrice.value = settings['vip_price_month'] || '100.00'
|
||||
inviterReward.value = settings['inviter_reward_per_invite'] || '0.50'
|
||||
inviteeReward.value = settings['invitee_reward_per_invite'] || '0.50'
|
||||
registerReward.value = settings['register_reward'] || '1.00'
|
||||
@@ -125,8 +127,13 @@ async function handleSave() {
|
||||
message.info('直接注册奖励积分请输入有效的非负数')
|
||||
return
|
||||
}
|
||||
const vipP = parseFloat(vipPrice.value)
|
||||
if (isNaN(vipP) || vipP <= 0) {
|
||||
const vipWeek = parseFloat(vipWeekPrice.value)
|
||||
if (isNaN(vipWeek) || vipWeek <= 0) {
|
||||
message.info('VIP 周卡售价请输入有效的正数')
|
||||
return
|
||||
}
|
||||
const vipMonth = parseFloat(vipMonthPrice.value)
|
||||
if (isNaN(vipMonth) || vipMonth <= 0) {
|
||||
message.info('VIP 月卡售价请输入有效的正数')
|
||||
return
|
||||
}
|
||||
@@ -134,7 +141,8 @@ async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await adminApi.updateSetting('base_deduction_per_run', deduction.toFixed(2))
|
||||
await adminApi.updateSetting('vip_price', vipP.toFixed(2))
|
||||
await adminApi.updateSetting('vip_price_week', vipWeek.toFixed(2))
|
||||
await adminApi.updateSetting('vip_price_month', vipMonth.toFixed(2))
|
||||
await adminApi.updateSetting('inviter_reward_per_invite', invReward.toFixed(2))
|
||||
await adminApi.updateSetting('invitee_reward_per_invite', ineeReward.toFixed(2))
|
||||
await adminApi.updateSetting('register_reward', regReward.toFixed(2))
|
||||
@@ -177,16 +185,30 @@ async function handleSave() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="mb-1 block text-sm text-gray-600">⭐ VIP 月卡售价(元 / 30天)</Label>
|
||||
<Label class="mb-1 block text-sm text-gray-600">⭐ VIP 周卡售价(元 / 7天)</Label>
|
||||
<Input
|
||||
v-model="vipPrice"
|
||||
v-model="vipWeekPrice"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
class="h-10 text-sm max-w-[200px]"
|
||||
/>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
用户在前台兑换 VIP CDK 时展示的售价,VIP 有效期内执行任务免扣次数
|
||||
生成 VIP 周卡 CDK 的单张售价,VIP 有效期内执行任务免扣次数
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label class="mb-1 block text-sm text-gray-600">⭐ VIP 月卡售价(元 / 30天)</Label>
|
||||
<Input
|
||||
v-model="vipMonthPrice"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
class="h-10 text-sm max-w-[200px]"
|
||||
/>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
生成 VIP 月卡 CDK 的单张售价,前台开通/续费 VIP 展示此价格
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ const redeemKind = ref<'count' | 'vip'>('count')
|
||||
// VIP 信息
|
||||
const isVip = ref(false)
|
||||
const vipExpireAt = ref('')
|
||||
const vipPrice = ref('')
|
||||
// 验证码
|
||||
const captchaId = ref('')
|
||||
const captchaImage = ref('')
|
||||
@@ -121,7 +120,6 @@ onMounted(async () => {
|
||||
const vipRes = await authApi.getVipInfo()
|
||||
isVip.value = vipRes.data.data.isVip
|
||||
vipExpireAt.value = vipRes.data.data.vipExpireAt
|
||||
vipPrice.value = vipRes.data.data.vipPrice
|
||||
} catch {
|
||||
// VIP 信息加载失败不阻塞页面
|
||||
}
|
||||
@@ -157,7 +155,6 @@ async function reloadVipInfo() {
|
||||
const vipRes = await authApi.getVipInfo()
|
||||
isVip.value = vipRes.data.data.isVip
|
||||
vipExpireAt.value = vipRes.data.data.vipExpireAt
|
||||
vipPrice.value = vipRes.data.data.vipPrice
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -268,7 +265,7 @@ async function handleRedeemInvite() {
|
||||
<img v-show="userInfo?.avatar && !avatarFailed" :src="userInfo?.avatar" class="w-full h-full object-cover" @error="avatarFailed = true" />
|
||||
<span v-if="!userInfo?.avatar || avatarFailed" class="text-3xl font-bold">{{ userInfo?.realName?.[0] || userInfo?.username?.[0] || '?' }}</span>
|
||||
</div>
|
||||
<div v-if="isVip" class="vip-badge absolute -right-3 -top-1.5">VIP</div>
|
||||
<div v-if="isVip" class="vip-badge absolute -right-3 -top-1.5 z-10 whitespace-nowrap">VIP</div>
|
||||
</div>
|
||||
<p class="text-xl font-bold">{{ userInfo?.realName || userInfo?.username || '--' }}</p>
|
||||
<p class="text-sm text-white/80 mt-1">{{ userInfo?.schoolName || '--' }}</p>
|
||||
@@ -283,10 +280,11 @@ async function handleRedeemInvite() {
|
||||
</span>
|
||||
<span v-else class="text-xs bg-white/20 rounded-full px-3 py-1.5 text-white/95">未开通 VIP</span>
|
||||
<button
|
||||
data-guide="profile-vip-btn"
|
||||
class="vip-btn text-xs font-semibold px-3.5 py-1.5 rounded-full text-amber-900 shadow"
|
||||
@click="openRedeemModal('vip')"
|
||||
>
|
||||
{{ isVip ? '续费 VIP' : `开通 VIP${vipPrice ? ` ¥${vipPrice}/月` : ''}` }}
|
||||
续费 VIP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,8 +299,8 @@ async function handleRedeemInvite() {
|
||||
<ClipboardCheck class="w-4 h-4 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-400">可签到次数</p>
|
||||
<p class="text-sm font-medium text-gray-900">{{ (userInfo?.signCount ?? 0) }} 次</p>
|
||||
<p class="text-xs text-gray-400">{{ isVip ? 'VIP 签到次数' : '可签到次数' }}</p>
|
||||
<p class="text-sm font-medium text-gray-900">{{ isVip ? '无限次' : `${userInfo?.signCount ?? 0} 次` }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -467,7 +465,7 @@ async function handleRedeemInvite() {
|
||||
<!-- 弹窗头部 -->
|
||||
<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' : '兑换签到次数' }}
|
||||
{{ redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数' }}
|
||||
</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" />
|
||||
@@ -476,9 +474,6 @@ async function handleRedeemInvite() {
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="px-5 pb-5 space-y-4">
|
||||
<p v-if="redeemKind === 'vip'" class="text-xs text-gray-400 leading-relaxed -mt-1">
|
||||
输入 VIP 兑换码即可开通/续费会员(默认一个月),VIP 有效期内执行任务不限次数、不再扣除签到次数
|
||||
</p>
|
||||
<!-- 兑换码输入 -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1.5">{{ redeemKind === 'vip' ? 'VIP 兑换码' : '兑换码' }}</label>
|
||||
@@ -549,9 +544,10 @@ async function handleRedeemInvite() {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* VIP 徽标:金色渐变 + 反光条扫过动画 */
|
||||
/* VIP 徽标:金色渐变 + 反光条扫过动画
|
||||
注意:不可设置 position,徽标由模板上的 Tailwind absolute 类控制浮层定位,
|
||||
若此处声明 position:relative 会覆盖 absolute,导致徽标挤进头像布局使其变形 */
|
||||
.vip-badge {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
|
||||
Reference in New Issue
Block a user