This commit is contained in:
parent
a350863a3e
commit
fb7e5031c8
6
env.d.ts
vendored
6
env.d.ts
vendored
@ -5,3 +5,9 @@ declare module '*.vue' {
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '*.svg?component' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
@ -4,34 +4,93 @@
|
||||
<p class="mb-4 text-center text-sm font-[100] text-gray-600">*所有套餐均不限流量不限速度</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<template v-for="(plan, index) in plans" :key="plan.id">
|
||||
<!-- Special Trial Card -->
|
||||
<div
|
||||
v-for="(plan, index) in plans"
|
||||
:key="plan.id"
|
||||
v-if="index === 0 && trialPlan"
|
||||
@click="$emit('select', trialPlan.id)"
|
||||
class="relative h-[126px] cursor-pointer overflow-hidden rounded-[40px] border-4 border-black bg-[#A8FF53] transition-all"
|
||||
>
|
||||
<!-- Left content (Original Plan but dimmed/struck) -->
|
||||
<div class="py-4 pl-[45px] opacity-60">
|
||||
<div class="text-2xl font-bold text-black">{{ plan.days }}天</div>
|
||||
<div class="relative w-fit text-[40px] leading-none font-semibold text-black">
|
||||
${{ plan.price }}
|
||||
<div class="absolute top-1/2 left-0 h-1 w-full -translate-y-1/2 bg-black/60"></div>
|
||||
</div>
|
||||
<div class="text-sm text-black">约${{ plan.daily }}/天</div>
|
||||
</div>
|
||||
|
||||
<!-- Right overlay (Trial Info) -->
|
||||
<div
|
||||
class="absolute top-0 right-0 bottom-0 flex h-[120px] w-[126px] flex-col justify-center rounded-bl-[40px] bg-black/60 text-[#ADFF5B]"
|
||||
>
|
||||
<div class="ml-[4px] flex h-full w-full flex-col rounded-bl-[40px] bg-black">
|
||||
<div class="pt-2 pl-1">
|
||||
<div class="text-lg font-bold">新客尝鲜价</div>
|
||||
<div class="mb-1 text-xs">{{ trialCountdown }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-full flex-1 items-center justify-center rounded-bl-[40px] border-4 border-black bg-[#ADFF5B] text-4xl font-semibold"
|
||||
:class="isTrialSelected ? 'bg-black text-white' : 'bg-[#ADFF5B] text-black'"
|
||||
>
|
||||
${{ trialPlan.price }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Normal Card -->
|
||||
<div
|
||||
v-else
|
||||
@click="$emit('select', plan.id)"
|
||||
:class="[
|
||||
'relative h-[126px] cursor-pointer overflow-hidden rounded-[40px] border-4 border-black py-4 pl-[45px] transition-all',
|
||||
currentPlanIndex === index ? 'bg-black text-white' : 'bg-[#A8FF53] text-black',
|
||||
]"
|
||||
>
|
||||
<div class="text-2xl font-bold">{{ plan.days }}天</div>
|
||||
<div class="flex items-center gap-2 text-2xl font-bold">
|
||||
<Goods90Icon
|
||||
v-if="plan.days === 90"
|
||||
:class="[currentPlanIndex === index ? 'text-[#ADFF5B]' : 'text-black']"
|
||||
/>
|
||||
{{ plan.days }}天
|
||||
</div>
|
||||
<div class="text-[40px] leading-none font-semibold">${{ plan.price }}</div>
|
||||
<div :class="'text-sm'">约${{ plan.daily }}/天</div>
|
||||
<div
|
||||
v-if="plan.discount"
|
||||
:class="[currentPlanIndex === index ? 'bg-[#A8FF53]! text-black' : ' ']"
|
||||
class="absolute top-[20px] -right-[40px] h-[16px] w-[126px] origin-center rotate-45 bg-black text-center text-[14px] leading-[16px] font-[200] font-semibold text-[#ADFF5B]"
|
||||
:class="[
|
||||
currentPlanIndex === index ? 'bg-[#A8FF53]! text-black' : ' ',
|
||||
index > 1 ? 'top-[15px] -right-[30px] h-[35px]' : 'top-[20px] -right-[40px] h-[16px]',
|
||||
]"
|
||||
class="absolute h-[16px] w-[126px] origin-center rotate-45 bg-black text-center text-[14px] leading-[16px] font-[200] font-semibold text-[#ADFF5B]"
|
||||
>
|
||||
{{ plan.discount }}
|
||||
<span
|
||||
v-html="
|
||||
plan.discount + (index === 2 ? '<br>年销百万' : index === 3 ? '<br>最划算' : '')
|
||||
"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
import { computed } from 'vue'
|
||||
import Goods90Icon from './goods-90-icon.svg'
|
||||
const props = defineProps({
|
||||
plans: Array,
|
||||
currentPlanIndex: Number,
|
||||
trialPlan: Object,
|
||||
trialCountdown: String,
|
||||
selectedPlanId: [String, Number],
|
||||
})
|
||||
defineEmits(['select'])
|
||||
|
||||
const isTrialSelected = computed(() => {
|
||||
return props.trialPlan && props.selectedPlanId === props.trialPlan.id
|
||||
})
|
||||
</script>
|
||||
|
||||
3
src/components/user-center/goods-90-icon.svg
Normal file
3
src/components/user-center/goods-90-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9698 0.620247L14.559 6.21814C14.5851 6.27797 14.6271 6.32974 14.6804 6.36799C14.7338 6.40624 14.7967 6.42956 14.8623 6.43549L21.0502 7.139C21.254 7.16112 21.4469 7.24088 21.6059 7.3687C21.7649 7.49652 21.8832 7.66698 21.9466 7.85961C22.01 8.05223 22.0158 8.25884 21.9632 8.45462C21.9107 8.65041 21.8021 8.82705 21.6505 8.96334L17.063 13.126C17.0131 13.1689 16.9759 13.2244 16.9554 13.2866C16.9349 13.3488 16.9317 13.4153 16.9464 13.479L18.1814 19.5116C18.2232 19.7098 18.2063 19.9157 18.1325 20.1046C18.0587 20.2935 17.9312 20.4572 17.7654 20.5762C17.5996 20.6952 17.4026 20.7642 17.198 20.7751C16.9934 20.786 16.79 20.7382 16.6123 20.6375L11.1877 17.6127C11.1306 17.5799 11.0657 17.5627 10.9997 17.5627C10.9337 17.5627 10.8689 17.5799 10.8118 17.6127L5.38595 20.6375C5.20818 20.7379 5.00485 20.7855 4.80041 20.7745C4.59596 20.7634 4.39908 20.6943 4.23341 20.5754C4.06774 20.4565 3.94033 20.2929 3.86648 20.1042C3.79263 19.9155 3.77549 19.7097 3.8171 19.5116L5.05209 13.479C5.06671 13.4153 5.0636 13.3488 5.04307 13.2866C5.02255 13.2244 4.98538 13.1689 4.93551 13.126L0.348975 8.96286C0.197134 8.82666 0.0883434 8.65001 0.0356868 8.45417C-0.0169699 8.25832 -0.0112508 8.05162 0.0521563 7.85891C0.115563 7.6662 0.233959 7.49569 0.393104 7.36789C0.552249 7.24009 0.745368 7.16044 0.949237 7.13852L7.1381 6.43501C7.20376 6.42908 7.26659 6.40576 7.31997 6.36751C7.37335 6.32926 7.4153 6.27749 7.4414 6.21765L10.0297 0.620247C10.1137 0.435474 10.2499 0.278626 10.422 0.168585C10.5941 0.0585442 10.7947 0 10.9997 0C11.2047 0 11.4053 0.0585442 11.5774 0.168585C11.7495 0.278626 11.8858 0.435474 11.9698 0.620247Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -62,7 +62,10 @@
|
||||
<PlanCard
|
||||
v-else
|
||||
:plans="plans"
|
||||
:trial-plan="trialPlan"
|
||||
:trial-countdown="trialCountdown"
|
||||
:currentPlanIndex="currentPlanIndex"
|
||||
:selectedPlanId="selectedPlanId"
|
||||
@select="handlePlanSelect"
|
||||
/>
|
||||
</Transition>
|
||||
@ -111,6 +114,8 @@ const props = defineProps<{
|
||||
isUserLoading: boolean
|
||||
isPlansLoading: boolean
|
||||
isPaymentsLoading: boolean
|
||||
trialPlan: any
|
||||
trialCountdown: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['select-plan', 'pay', 'refresh', 'show-order-details'])
|
||||
|
||||
@ -34,7 +34,10 @@
|
||||
<PlanCard
|
||||
v-else
|
||||
:plans="plans"
|
||||
:trial-plan="trialPlan"
|
||||
:trial-countdown="trialCountdown"
|
||||
:currentPlanIndex="currentPlanIndex"
|
||||
:selectedPlanId="selectedPlanId"
|
||||
@select="handlePlanSelect"
|
||||
/>
|
||||
</Transition>
|
||||
@ -98,6 +101,8 @@ const props = defineProps<{
|
||||
isUserLoading: boolean
|
||||
isPlansLoading: boolean
|
||||
isPaymentsLoading: boolean
|
||||
trialPlan: any
|
||||
trialCountdown: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['select-plan', 'pay', 'refresh', 'show-order-details'])
|
||||
|
||||
@ -33,6 +33,8 @@
|
||||
:is-user-loading="isUserLoading"
|
||||
:is-plans-loading="isPlansLoading"
|
||||
:is-payments-loading="isPaymentsLoading"
|
||||
:trial-plan="trialPlan"
|
||||
:trial-countdown="trialCountdown"
|
||||
@select-plan="handlePlanSelect"
|
||||
@pay="handlePay"
|
||||
@refresh="init"
|
||||
@ -53,6 +55,8 @@
|
||||
:is-user-loading="isUserLoading"
|
||||
:is-plans-loading="isPlansLoading"
|
||||
:is-payments-loading="isPaymentsLoading"
|
||||
:trial-plan="trialPlan"
|
||||
:trial-countdown="trialCountdown"
|
||||
@select-plan="handlePlanSelect"
|
||||
@pay="handlePay"
|
||||
@refresh="init"
|
||||
@ -101,7 +105,77 @@ const userSubInfo = ref({
|
||||
expireDate: '',
|
||||
})
|
||||
|
||||
const rawPlansList = ref<any[]>([])
|
||||
const isTrial = ref(false)
|
||||
const trialPlan = ref<any>(null)
|
||||
const trialCountdown = ref('')
|
||||
let countdownTimer: any = null
|
||||
|
||||
// --- Data Mapping ---
|
||||
const updatePlans = () => {
|
||||
// First map all plans using the standard logic
|
||||
const allMappedPlans = mapPlans(rawPlansList.value)
|
||||
|
||||
if (isTrial.value && allMappedPlans.length > 0) {
|
||||
// Extract the last item as the trial plan (based on user requirement)
|
||||
const trial = allMappedPlans.pop()
|
||||
if (trial) {
|
||||
trial.discount = '新客尝鲜价' // Override discount text
|
||||
trialPlan.value = trial
|
||||
|
||||
// If we haven't selected a plan yet, or if the current selection is invalid, select the trial plan
|
||||
if (
|
||||
!selectedPlanId.value ||
|
||||
(allMappedPlans.length > 0 &&
|
||||
!allMappedPlans.find((p: any) => p.id === selectedPlanId.value))
|
||||
) {
|
||||
selectedPlanId.value = trial.id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trialPlan.value = null
|
||||
}
|
||||
|
||||
// The remaining plans are displayed in the normal list
|
||||
plans.value = allMappedPlans
|
||||
|
||||
// Fallback selection if trial is not active and no plan selected
|
||||
if (
|
||||
!isTrial.value &&
|
||||
plans.value.length > 0 &&
|
||||
!plans.value.find((p) => p.id === selectedPlanId.value)
|
||||
) {
|
||||
selectedPlanId.value = plans.value[0].id
|
||||
}
|
||||
}
|
||||
|
||||
const startCountdown = (createdAtStr: string) => {
|
||||
const createdAt = new Date(createdAtStr).getTime()
|
||||
const deadline = createdAt + 24 * 60 * 60 * 1000
|
||||
|
||||
const update = () => {
|
||||
const now = new Date().getTime()
|
||||
const diff = deadline - now
|
||||
|
||||
if (diff <= 0) {
|
||||
isTrial.value = false
|
||||
trialPlan.value = null
|
||||
updatePlans()
|
||||
if (countdownTimer) clearInterval(countdownTimer)
|
||||
return
|
||||
}
|
||||
|
||||
const h = Math.floor(diff / (1000 * 60 * 60))
|
||||
const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const s = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
|
||||
trialCountdown.value = `${h}h:${m}m:${s}s后失效`
|
||||
}
|
||||
|
||||
update()
|
||||
countdownTimer = setInterval(update, 1000)
|
||||
}
|
||||
|
||||
const mapPlans = (apiList: any[]) => {
|
||||
const flattened: any[] = []
|
||||
let globalIndex = 0
|
||||
@ -143,6 +217,9 @@ const mapPlans = (apiList: any[]) => {
|
||||
|
||||
// --- Handlers ---
|
||||
const activePlan = computed(() => {
|
||||
if (trialPlan.value && selectedPlanId.value === trialPlan.value.id) {
|
||||
return trialPlan.value
|
||||
}
|
||||
return plans.value.find((p) => p.id === selectedPlanId.value)
|
||||
})
|
||||
|
||||
@ -164,8 +241,10 @@ const handlePay = async (methodId: number | string) => {
|
||||
|
||||
try {
|
||||
// 2. 检查订阅状态以决定是“购买”还是“续费”
|
||||
const { list } = await request.get('/api/v1/public/user/subscribe', { includeExpired: 'all' })
|
||||
const existingSub = list.find((s: any) => s.subscribe_id === plan.planId)
|
||||
const { list } = (await request.get('/api/v1/public/user/subscribe', {
|
||||
includeExpired: 'all',
|
||||
})) as any
|
||||
const existingSub = list?.find((s: any) => s.subscribe_id === plan.planId)
|
||||
|
||||
const isRenewal = !!existingSub
|
||||
const api = isRenewal ? '/api/v1/public/order/renewal' : '/api/v1/public/order/purchase'
|
||||
@ -215,6 +294,36 @@ async function init() {
|
||||
userSubInfo.value.email = emailInfo.auth_identifier
|
||||
localStorage.setItem('UserEmail', emailInfo.auth_identifier)
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* */
|
||||
// mock 昨天中午 12:00 测试尝鲜价
|
||||
// const todayStart = new Date()
|
||||
// todayStart.setHours(0, 0, 0, 0)
|
||||
//
|
||||
// res.created_at = todayStart.getTime()
|
||||
|
||||
// Check trial eligibility
|
||||
if (res.created_at) {
|
||||
const createdAt = new Date(res.created_at).getTime()
|
||||
const now = new Date().getTime()
|
||||
if (now - createdAt < 24 * 60 * 60 * 1000) {
|
||||
request
|
||||
.get('/api/v1/public/order/list', {
|
||||
page: 1,
|
||||
size: 1,
|
||||
status: 5,
|
||||
})
|
||||
.then((orderRes: any) => {
|
||||
if (orderRes.list && orderRes.list.length === 0) {
|
||||
isTrial.value = true
|
||||
updatePlans()
|
||||
startCountdown(res.created_at)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
isUserLoading.value = false
|
||||
@ -230,10 +339,8 @@ async function init() {
|
||||
request
|
||||
.get('/api/v1/public/subscribe/list')
|
||||
.then((res: any) => {
|
||||
plans.value = mapPlans(res.list || [])
|
||||
if (plans.value.length > 0) {
|
||||
selectedPlanId.value = plans.value[0].id
|
||||
}
|
||||
rawPlansList.value = res.list || []
|
||||
updatePlans()
|
||||
})
|
||||
.finally(() => {
|
||||
isPlansLoading.value = false
|
||||
|
||||
@ -19,9 +19,9 @@ export default defineConfig({
|
||||
// 将所有以 /api 开头的请求转发到目标服务器
|
||||
// 1. 匹配所有以 /public 开头的请求
|
||||
'/api/v1': {
|
||||
target: 'https://hifastvpn.com',
|
||||
target: 'https://tapi.hifast.biz',
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
autoRewrite: true,
|
||||
// 3. 关键:将路径重写,在前面补上 /api/v1
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user