diff --git a/env.d.ts b/env.d.ts index 323c78a..2c481dc 100644 --- a/env.d.ts +++ b/env.d.ts @@ -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 +} diff --git a/src/components/user-center/PlanCard.vue b/src/components/user-center/PlanCard.vue index f3dbd76..251ff42 100644 --- a/src/components/user-center/PlanCard.vue +++ b/src/components/user-center/PlanCard.vue @@ -4,34 +4,93 @@

*所有套餐均不限流量不限速度

-
-
{{ plan.days }}天
-
${{ plan.price }}
-
约${{ plan.daily }}/天
+
diff --git a/src/components/user-center/goods-90-icon.svg b/src/components/user-center/goods-90-icon.svg new file mode 100644 index 0000000..6a34f88 --- /dev/null +++ b/src/components/user-center/goods-90-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/pages/UserCenter/DesktopLayout/index.vue b/src/pages/UserCenter/DesktopLayout/index.vue index 4d7f6a5..8ef8fd4 100644 --- a/src/pages/UserCenter/DesktopLayout/index.vue +++ b/src/pages/UserCenter/DesktopLayout/index.vue @@ -62,7 +62,10 @@ @@ -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']) diff --git a/src/pages/UserCenter/MobileLayout/index.vue b/src/pages/UserCenter/MobileLayout/index.vue index 56ff04d..76fd536 100644 --- a/src/pages/UserCenter/MobileLayout/index.vue +++ b/src/pages/UserCenter/MobileLayout/index.vue @@ -34,7 +34,10 @@ @@ -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']) diff --git a/src/pages/UserCenter/index.vue b/src/pages/UserCenter/index.vue index 2c39556..13c0a90 100644 --- a/src/pages/UserCenter/index.vue +++ b/src/pages/UserCenter/index.vue @@ -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([]) +const isTrial = ref(false) +const trialPlan = ref(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 diff --git a/vite.config.ts b/vite.config.ts index 6938a84..c43576b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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 },