Compare commits
No commits in common. "107d95077100260458a6b092ab9f57d64b156d68" and "2b0741fdb9a2d119f0035db076fd77bde7f19779" have entirely different histories.
107d950771
...
2b0741fdb9
@ -39,12 +39,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-6 pt-[85px] pb-[23px]">
|
<div class="px-6 pt-[85px] pb-[23px]">
|
||||||
<Button
|
<Button
|
||||||
:disabled="isPaying"
|
|
||||||
@click="$emit('pay', selectedValue)"
|
@click="$emit('pay', selectedValue)"
|
||||||
class="h-[50px] w-full rounded-[32px] border-none bg-black text-[16px] font-black text-white transition-all hover:bg-black/90 active:scale-[0.98] disabled:opacity-50"
|
class="h-[50px] w-full rounded-[32px] border-none bg-black text-[16px] font-black text-white transition-all hover:bg-black/90 active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isPaying" class="mr-2 h-4 w-4 animate-spin" />
|
立即支付
|
||||||
{{ isPaying ? '正在支付...' : '立即支付' }}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +51,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Loader2 } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
interface PaymentOption {
|
interface PaymentOption {
|
||||||
label: string
|
label: string
|
||||||
@ -63,7 +60,6 @@ interface PaymentOption {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
methods: any[]
|
methods: any[]
|
||||||
selectedPlan: any
|
selectedPlan: any
|
||||||
isPaying?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const list = computed(() =>
|
const list = computed(() =>
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="user-center-skeleton custom-pulse" :class="[layout === 'desktop' ? 'w-full' : '']">
|
|
||||||
<!-- --- Header Part --- -->
|
|
||||||
<template v-if="type === 'header'">
|
|
||||||
<div v-if="layout === 'mobile'" class="mb-3 ml-[31px] flex h-[60px] items-center gap-3">
|
|
||||||
<div class="size-[60px] rounded-full bg-[#A8FF53]/60"></div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="h-5 w-40 rounded bg-[#A8FF53]/60"></div>
|
|
||||||
<div class="h-3 w-24 rounded bg-[#A8FF53]/40"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="mb-3 flex flex-col items-center">
|
|
||||||
<div class="size-[60px] rounded-full bg-[#A8FF53]/60"></div>
|
|
||||||
<div class="mt-2 h-6 w-32 rounded bg-[#A8FF53]/60"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- --- Devices Part --- -->
|
|
||||||
<template v-if="type === 'devices'">
|
|
||||||
<div v-if="layout === 'mobile'" class="h-[76px] w-full rounded-2xl bg-[#A8FF53]/15"></div>
|
|
||||||
<div v-else class="h-[160px] w-full rounded-2xl bg-[#A8FF53]/15"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- --- Plans Part --- -->
|
|
||||||
<template v-if="type === 'plans'">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="h-[120px] w-full rounded-2xl bg-black/15"></div>
|
|
||||||
<div class="h-[120px] w-full rounded-2xl bg-black/15"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- --- Payments Part --- -->
|
|
||||||
<template v-if="type === 'payments'">
|
|
||||||
<div class="h-[300px] w-full rounded-2xl bg-black/15"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- --- Full Layouts (Backward Compatibility) --- -->
|
|
||||||
<template v-if="!type">
|
|
||||||
<!-- Old Full Layout implementation if needed, but we'll use type-based now -->
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
type?: 'header' | 'devices' | 'plans' | 'payments'
|
|
||||||
layout?: 'mobile' | 'desktop'
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.custom-pulse {
|
|
||||||
animation: fast-pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fast-pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,26 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Main Neon Green Card -->
|
<!-- Main Neon Green Card -->
|
||||||
<div class="flex h-[678px] justify-center">
|
<div class="flex h-[678px]">
|
||||||
<div
|
<div
|
||||||
class="flex w-[345px] flex-col justify-between rounded-4xl border-5 border-white py-[32px] pb-[22px]"
|
class="flex w-[345px] flex-col justify-between rounded-4xl border-5 border-white py-[32px] pb-[22px]"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 flex min-h-[88px] flex-col items-center">
|
<div class="mb-3 flex flex-col items-center">
|
||||||
<Transition name="fade" mode="out-in">
|
<img src="../avatar.png" class="size-[60px]" alt="" />
|
||||||
<UserCenterSkeleton v-if="isUserLoading" type="header" layout="desktop" />
|
<div class="flex flex-col justify-center text-white">
|
||||||
<div v-else class="flex flex-col items-center">
|
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
||||||
<img src="../avatar.png" class="size-[60px]" alt="" />
|
</div>
|
||||||
<div class="flex flex-col justify-center text-white">
|
|
||||||
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5 px-[20px] text-white">
|
<div class="mb-5 px-[20px] text-white">
|
||||||
<Transition name="fade" mode="out-in">
|
<DeviceList :devices="devices" @refresh="emit('refresh')" />
|
||||||
<UserCenterSkeleton v-if="isUserLoading" type="devices" layout="desktop" />
|
|
||||||
<DeviceList v-else :devices="devices" @refresh="emit('refresh')" />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -51,34 +43,17 @@
|
|||||||
class="ml-2.5 flex items-center overflow-hidden rounded-4xl bg-[#A8FF53] pt-[32px] pb-[22px]"
|
class="ml-2.5 flex items-center overflow-hidden rounded-4xl bg-[#A8FF53] pt-[32px] pb-[22px]"
|
||||||
>
|
>
|
||||||
<div class="h-full w-[345px]">
|
<div class="h-full w-[345px]">
|
||||||
<Transition name="fade" mode="out-in">
|
<PlanCard :plans="plans" :currentPlanIndex="currentPlanIndex" @select="handlePlanSelect" />
|
||||||
<div v-if="isPlansLoading" class="p-8">
|
|
||||||
<UserCenterSkeleton type="plans" layout="desktop" />
|
|
||||||
</div>
|
|
||||||
<PlanCard
|
|
||||||
v-else
|
|
||||||
:plans="plans"
|
|
||||||
:currentPlanIndex="currentPlanIndex"
|
|
||||||
@select="handlePlanSelect"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx-[5px] h-[624px] w-[1px] bg-[url(@/pages/userCenter/DesktopLayout/Line-8.png)]"
|
class="mx-[5px] h-[624px] w-[1px] bg-[url(@/pages/userCenter/DesktopLayout/Line-8.png)]"
|
||||||
></div>
|
></div>
|
||||||
<div class="h-full w-[345px]">
|
<div class="h-full w-[345px]">
|
||||||
<Transition name="fade" mode="out-in">
|
<PaymentMethod
|
||||||
<div v-if="isPaymentsLoading" class="p-8">
|
:methods="payments"
|
||||||
<UserCenterSkeleton type="payments" layout="desktop" />
|
:selectedPlan="selectedPlan"
|
||||||
</div>
|
@pay="(id: number | string) => $emit('pay', id)"
|
||||||
<PaymentMethod
|
/>
|
||||||
v-else
|
|
||||||
:methods="payments"
|
|
||||||
:selectedPlan="selectedPlan"
|
|
||||||
:is-paying="isPaying"
|
|
||||||
@pay="(id: number | string) => $emit('pay', id)"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,7 +64,6 @@ import { computed } from 'vue'
|
|||||||
import PlanCard from '@/components/user-center/PlanCard.vue'
|
import PlanCard from '@/components/user-center/PlanCard.vue'
|
||||||
import DeviceList from '@/components/user-center/DeviceList.vue'
|
import DeviceList from '@/components/user-center/DeviceList.vue'
|
||||||
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
||||||
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@ -102,10 +76,6 @@ const props = defineProps<{
|
|||||||
userInfo: { email: string }
|
userInfo: { email: string }
|
||||||
selectedPlanId: string
|
selectedPlanId: string
|
||||||
selectedPlan: any
|
selectedPlan: any
|
||||||
isPaying: boolean
|
|
||||||
isUserLoading: boolean
|
|
||||||
isPlansLoading: boolean
|
|
||||||
isPaymentsLoading: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
||||||
@ -167,14 +137,4 @@ function logout() {
|
|||||||
.tracking-tight {
|
.tracking-tight {
|
||||||
font-family: 'Inter', 'PingFang SC', sans-serif;
|
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,53 +2,28 @@
|
|||||||
<!-- Main Neon Green Card -->
|
<!-- Main Neon Green Card -->
|
||||||
<div class="pt-[35px]">
|
<div class="pt-[35px]">
|
||||||
<div class="mb-3 ml-[31px] flex h-[60px] items-center gap-3">
|
<div class="mb-3 ml-[31px] flex h-[60px] items-center gap-3">
|
||||||
<Transition name="fade" mode="out-in">
|
<img src="../avatar.png" class="size-[60px]" alt="" />
|
||||||
<UserCenterSkeleton v-if="isUserLoading" type="header" layout="mobile" />
|
<div class="flex h-full flex-col justify-center text-white">
|
||||||
<div v-else class="flex items-center gap-3 text-white">
|
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
||||||
<img src="../avatar.png" class="size-[60px]" alt="" />
|
<div class="text-xs" :class="{ 'text-[#FF00B7]': expireDateInfo.highlight }">
|
||||||
<div class="flex h-full flex-col justify-center text-white">
|
{{ expireDateInfo.text }}
|
||||||
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
|
|
||||||
<div class="text-xs" :class="{ 'text-[#FF00B7]': expireDateInfo.highlight }">
|
|
||||||
{{ expireDateInfo.text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5 pr-[25px] pl-[21px] text-white">
|
<div class="mb-5 pr-[25px] pl-[21px] text-white">
|
||||||
<Transition name="fade" mode="out-in">
|
<DeviceList :devices="devices" @refresh="emit('refresh')" />
|
||||||
<UserCenterSkeleton v-if="isUserLoading" type="devices" layout="mobile" />
|
|
||||||
<DeviceList v-else :devices="devices" @refresh="emit('refresh')" />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden rounded-4xl bg-[#A8FF53]">
|
<div class="overflow-hidden rounded-4xl bg-[#A8FF53]">
|
||||||
<div class="pt-7">
|
<div class="pt-7">
|
||||||
<Transition name="fade" mode="out-in">
|
<PlanCard :plans="plans" :currentPlanIndex="currentPlanIndex" @select="handlePlanSelect" />
|
||||||
<div v-if="isPlansLoading" class="px-6">
|
|
||||||
<UserCenterSkeleton type="plans" layout="mobile" />
|
|
||||||
</div>
|
|
||||||
<PlanCard
|
|
||||||
v-else
|
|
||||||
:plans="plans"
|
|
||||||
:currentPlanIndex="currentPlanIndex"
|
|
||||||
@select="handlePlanSelect"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-7">
|
<div class="pt-7">
|
||||||
<Transition name="fade" mode="out-in">
|
<PaymentMethod
|
||||||
<div v-if="isPaymentsLoading" class="px-6">
|
:methods="payments"
|
||||||
<UserCenterSkeleton type="payments" layout="mobile" />
|
:selectedPlan="selectedPlan"
|
||||||
</div>
|
@pay="(id: number | string) => $emit('pay', id)"
|
||||||
<PaymentMethod
|
/>
|
||||||
v-else
|
|
||||||
:methods="payments"
|
|
||||||
:selectedPlan="selectedPlan"
|
|
||||||
:is-paying="isPaying"
|
|
||||||
@pay="(id: number | string) => $emit('pay', id)"
|
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -76,7 +51,6 @@ import { computed } from 'vue'
|
|||||||
import PlanCard from '@/components/user-center/PlanCard.vue'
|
import PlanCard from '@/components/user-center/PlanCard.vue'
|
||||||
import DeviceList from '@/components/user-center/DeviceList.vue'
|
import DeviceList from '@/components/user-center/DeviceList.vue'
|
||||||
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
|
||||||
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@ -89,10 +63,6 @@ const props = defineProps<{
|
|||||||
userInfo: { email: string }
|
userInfo: { email: string }
|
||||||
selectedPlanId: string
|
selectedPlanId: string
|
||||||
selectedPlan: any
|
selectedPlan: any
|
||||||
isPaying: boolean
|
|
||||||
isUserLoading: boolean
|
|
||||||
isPlansLoading: boolean
|
|
||||||
isPaymentsLoading: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
||||||
@ -154,14 +124,4 @@ function logout() {
|
|||||||
.tracking-tight {
|
.tracking-tight {
|
||||||
font-family: 'Inter', 'PingFang SC', sans-serif;
|
font-family: 'Inter', 'PingFang SC', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -22,46 +22,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col">
|
<!-- Main Neon Green Card -->
|
||||||
<!-- Main Neon Green Card -->
|
<div class="container md:hidden">
|
||||||
<div class="container md:hidden">
|
<MobileLayout
|
||||||
<MobileLayout
|
:already-subscribed="alreadySubscribed"
|
||||||
:already-subscribed="alreadySubscribed"
|
:devices="devices"
|
||||||
:devices="devices"
|
:plans="plans"
|
||||||
:plans="plans"
|
:payments="payments"
|
||||||
:payments="payments"
|
:user-info="userSubInfo"
|
||||||
:user-info="userSubInfo"
|
:selected-plan-id="selectedPlanId"
|
||||||
:selected-plan-id="selectedPlanId"
|
:selected-plan="activePlan"
|
||||||
:selected-plan="activePlan"
|
@select-plan="handlePlanSelect"
|
||||||
:is-paying="isPaying"
|
@pay="handlePay"
|
||||||
:is-user-loading="isUserLoading"
|
@refresh="init"
|
||||||
:is-plans-loading="isPlansLoading"
|
/>
|
||||||
:is-payments-loading="isPaymentsLoading"
|
</div>
|
||||||
@select-plan="handlePlanSelect"
|
<div class="container mx-auto hidden flex-1 items-center justify-center md:flex">
|
||||||
@pay="handlePay"
|
<DesktopLayout
|
||||||
@refresh="init"
|
:already-subscribed="alreadySubscribed"
|
||||||
/>
|
:devices="devices"
|
||||||
</div>
|
:plans="plans"
|
||||||
<div class="hidden flex-1 items-center justify-center md:flex md:pb-[50px]">
|
:payments="payments"
|
||||||
<div class="container mx-auto">
|
:user-info="userSubInfo"
|
||||||
<DesktopLayout
|
:selected-plan-id="selectedPlanId"
|
||||||
:already-subscribed="alreadySubscribed"
|
:selected-plan="activePlan"
|
||||||
:devices="devices"
|
@select-plan="handlePlanSelect"
|
||||||
:plans="plans"
|
@pay="handlePay"
|
||||||
:payments="payments"
|
@refresh="init"
|
||||||
:user-info="userSubInfo"
|
/>
|
||||||
:selected-plan-id="selectedPlanId"
|
|
||||||
:selected-plan="activePlan"
|
|
||||||
:is-paying="isPaying"
|
|
||||||
:is-user-loading="isUserLoading"
|
|
||||||
:is-plans-loading="isPlansLoading"
|
|
||||||
:is-payments-loading="isPaymentsLoading"
|
|
||||||
@select-plan="handlePlanSelect"
|
|
||||||
@pay="handlePay"
|
|
||||||
@refresh="init"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrderStatusDialog ref="orderStatusDialogRef" @close="handleStatusClose" @refresh="init" />
|
<OrderStatusDialog ref="orderStatusDialogRef" @close="handleStatusClose" @refresh="init" />
|
||||||
@ -74,7 +62,6 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import MobileLayout from './MobileLayout/index.vue'
|
import MobileLayout from './MobileLayout/index.vue'
|
||||||
import DesktopLayout from './DesktopLayout/index.vue'
|
import DesktopLayout from './DesktopLayout/index.vue'
|
||||||
import OrderStatusDialog from '@/components/user-center/OrderStatusDialog.vue'
|
import OrderStatusDialog from '@/components/user-center/OrderStatusDialog.vue'
|
||||||
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
|
||||||
import Logo from '@/pages/Home/logo.svg?component'
|
import Logo from '@/pages/Home/logo.svg?component'
|
||||||
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
|
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
@ -83,10 +70,6 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const selectedPlanId = ref('p2')
|
const selectedPlanId = ref('p2')
|
||||||
const isPaying = ref(false)
|
|
||||||
const isUserLoading = ref(true)
|
|
||||||
const isPlansLoading = ref(true)
|
|
||||||
const isPaymentsLoading = ref(true)
|
|
||||||
const orderStatusDialogRef = ref<InstanceType<typeof OrderStatusDialog> | null>(null)
|
const orderStatusDialogRef = ref<InstanceType<typeof OrderStatusDialog> | null>(null)
|
||||||
const selectedPayment = ref('alipay')
|
const selectedPayment = ref('alipay')
|
||||||
|
|
||||||
@ -149,7 +132,6 @@ const handlePlanSelect = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePay = (methodId: number | string) => {
|
const handlePay = (methodId: number | string) => {
|
||||||
if (isPaying.value) return
|
|
||||||
const plan = plans.value.find((p) => p.id === selectedPlanId.value)
|
const plan = plans.value.find((p) => p.id === selectedPlanId.value)
|
||||||
if (!plan) return
|
if (!plan) return
|
||||||
|
|
||||||
@ -165,78 +147,53 @@ const handlePay = (methodId: number | string) => {
|
|||||||
if (isRenewal) {
|
if (isRenewal) {
|
||||||
params.user_subscribe_id = already.id
|
params.user_subscribe_id = already.id
|
||||||
}
|
}
|
||||||
isPaying.value = true
|
console.log(params)
|
||||||
request
|
request.post(api, params).then((res: any) => {
|
||||||
.post(api, params)
|
request
|
||||||
.then((res: any) => {
|
.post('/api/v1/public/portal/order/checkout', {
|
||||||
request
|
orderNo: res.order_no,
|
||||||
.post('/api/v1/public/portal/order/checkout', {
|
returnUrl: `${window.location.origin}/user-center?order_no=${res.order_no}`,
|
||||||
orderNo: res.order_no,
|
})
|
||||||
returnUrl: `${window.location.origin}/user-center?order_no=${res.order_no}`,
|
.then((checkoutRes: any) => {
|
||||||
})
|
if (checkoutRes.type === 'url' && checkoutRes.checkout_url) {
|
||||||
.then((checkoutRes: any) => {
|
localStorage.setItem('pending_order_no', res.order_no)
|
||||||
if (checkoutRes.type === 'url' && checkoutRes.checkout_url) {
|
console.log('pending_order_no', res.order_no)
|
||||||
localStorage.setItem('pending_order_no', res.order_no)
|
setTimeout(() => {
|
||||||
console.log('pending_order_no', res.order_no)
|
window.location.href = checkoutRes.checkout_url
|
||||||
setTimeout(() => {
|
})
|
||||||
window.location.href = checkoutRes.checkout_url
|
}
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isPaying.value = false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
isPaying.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
function init() {
|
||||||
// 1. 用户信息 & 设备列表
|
// 用户信息 & 设备列表
|
||||||
isUserLoading.value = true
|
request.get('/api/v1/public/user/info').then((res: any) => {
|
||||||
request
|
devices.value = res.user_devices
|
||||||
.get('/api/v1/public/user/info')
|
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
|
||||||
.then((res: any) => {
|
if (emailInfo) {
|
||||||
devices.value = res.user_devices
|
userSubInfo.value.email = emailInfo.auth_identifier
|
||||||
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
|
}
|
||||||
if (emailInfo) {
|
})
|
||||||
userSubInfo.value.email = emailInfo.auth_identifier
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isUserLoading.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 2. 已订阅列表 (非阻塞主要展示)
|
// 已订阅列表
|
||||||
request.get('/api/v1/public/user/subscribe').then((res: any) => {
|
request.get('/api/v1/public/user/subscribe').then((res: any) => {
|
||||||
alreadySubscribed.value = res.list || []
|
alreadySubscribed.value = res.list || []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. 订阅套餐列表
|
// 订阅套餐列表
|
||||||
isPlansLoading.value = true
|
request.get('/api/v1/public/subscribe/list').then((res: any) => {
|
||||||
request
|
plans.value = mapPlans(res.list || [])
|
||||||
.get('/api/v1/public/subscribe/list')
|
if (plans.value.length > 0) {
|
||||||
.then((res: any) => {
|
selectedPlanId.value = plans.value[0].id
|
||||||
plans.value = mapPlans(res.list || [])
|
}
|
||||||
if (plans.value.length > 0) {
|
})
|
||||||
selectedPlanId.value = plans.value[0].id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isPlansLoading.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 4. 获取支付方式
|
// 获取支付方式
|
||||||
isPaymentsLoading.value = true
|
request.get('/api/v1/public/payment/methods').then((res: any) => {
|
||||||
request
|
console.log(res)
|
||||||
.get('/api/v1/public/payment/methods')
|
payments.value = res.list?.filter((p: any) => p.platform !== 'apple_iap') || []
|
||||||
.then((res: any) => {
|
})
|
||||||
payments.value = res.list?.filter((p: any) => p.platform !== 'apple_iap') || []
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isPaymentsLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -254,18 +211,6 @@ function handleStatusClose() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Simplified layout font */
|
/* Simplified layout font */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
|
||||||
|
|||||||
@ -52,68 +52,6 @@ interface ResponseType extends AxiosResponse {
|
|||||||
config: RequestConfig
|
config: RequestConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const ERROR_MESSAGES: Record<number | string, string> = {
|
|
||||||
'200': '成功',
|
|
||||||
'500': '内部服务器错误',
|
|
||||||
'10001': '数据库查询错误',
|
|
||||||
'10002': '数据库更新错误',
|
|
||||||
'10003': '数据库插入错误',
|
|
||||||
'10004': '数据库删除错误',
|
|
||||||
'20001': '用户已存在',
|
|
||||||
'20002': '用户不存在',
|
|
||||||
'20003': '用户密码错误',
|
|
||||||
'20004': '用户已禁用',
|
|
||||||
'20005': '余额不足',
|
|
||||||
'20006': '停止注册',
|
|
||||||
'20007': '未绑定Telegram',
|
|
||||||
'20008': '用户未绑定OAuth方式',
|
|
||||||
'20009': '邀请码错误',
|
|
||||||
'30001': '节点已存在',
|
|
||||||
'30002': '节点不存在',
|
|
||||||
'30003': '节点组已存在',
|
|
||||||
'30004': '节点组不存在',
|
|
||||||
'30005': '节点组不为空',
|
|
||||||
'400': '参数错误',
|
|
||||||
'40002': '用户令牌为空',
|
|
||||||
'40003': '用户令牌无效',
|
|
||||||
'40004': '用户令牌已过期',
|
|
||||||
'40005': '您还没有登录',
|
|
||||||
'401': '请求过多',
|
|
||||||
'50001': '优惠券不存在',
|
|
||||||
'50002': '优惠券已被使用',
|
|
||||||
'50003': '优惠券不匹配',
|
|
||||||
'60001': '订阅已过期',
|
|
||||||
'60002': '订阅不可用',
|
|
||||||
'60003': '用户已有订阅',
|
|
||||||
'60004': '订阅已被使用',
|
|
||||||
'60005': '单一订阅模式超出限制',
|
|
||||||
'60006': '订阅配额限制',
|
|
||||||
'70001': '验证码错误',
|
|
||||||
'80001': '队列入队错误',
|
|
||||||
'90001': '调试模式已启用',
|
|
||||||
'90002': '发送短信错误',
|
|
||||||
'90003': '短信功能未启用',
|
|
||||||
'90004': '电子邮件功能未启用',
|
|
||||||
'90005': '不支持的登录方式',
|
|
||||||
'90006': '身份验证器不支持此方式',
|
|
||||||
'90007': '电话区号为空',
|
|
||||||
'90008': '密码为空',
|
|
||||||
'90009': '区号为空',
|
|
||||||
'90010': '需要密码或验证码',
|
|
||||||
'90011': '电子邮件已存在',
|
|
||||||
'90012': '电话号码已存在',
|
|
||||||
'90013': '设备已存在',
|
|
||||||
'90014': '电话号码错误',
|
|
||||||
'90015': '此账户今日已达到发送次数限制',
|
|
||||||
'90017': '设备不存在',
|
|
||||||
'90018': '用户 ID 不匹配',
|
|
||||||
'61001': '订单不存在',
|
|
||||||
'61002': '支付方式未找到',
|
|
||||||
'61003': '订单状态错误',
|
|
||||||
'61004': '重置周期不足',
|
|
||||||
'61005': '存在没用完的流量',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Request {
|
export default class Request {
|
||||||
public axiosInstance: AxiosInstance
|
public axiosInstance: AxiosInstance
|
||||||
|
|
||||||
@ -155,26 +93,34 @@ export default class Request {
|
|||||||
|
|
||||||
if (config.data && !(config.data instanceof FormData)) {
|
if (config.data && !(config.data instanceof FormData)) {
|
||||||
const plainText = JSON.stringify(config.data)
|
const plainText = JSON.stringify(config.data)
|
||||||
|
// 加密后,config.data 会变成 { data: '...', time: '...' }
|
||||||
config.data = HiAesUtil.encryptData(plainText, encryptionKey)
|
config.data = HiAesUtil.encryptData(plainText, encryptionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.method?.toLowerCase() === 'get' || config.params) {
|
if (config.method?.toLowerCase() === 'get' || config.params) {
|
||||||
const paramsToEncrypt = config.params || {}
|
const paramsToEncrypt = config.params || {} // 为空则加密 "{}"
|
||||||
const plainParamsText = JSON.stringify(paramsToEncrypt)
|
const plainParamsText = JSON.stringify(paramsToEncrypt)
|
||||||
const encryptedParams = HiAesUtil.encryptData(plainParamsText, encryptionKey)
|
const encryptedParams = HiAesUtil.encryptData(plainParamsText, encryptionKey)
|
||||||
|
|
||||||
|
// 将原参数替换为加密后的 data 和 time 字段
|
||||||
config.params = {
|
config.params = {
|
||||||
data: encryptedParams.data,
|
data: encryptedParams.data,
|
||||||
time: encryptedParams.time,
|
time: encryptedParams.time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.data?.time) {
|
||||||
|
console.log(
|
||||||
|
'解密',
|
||||||
|
HiAesUtil.decryptData(config.data.data, config.data.time, encryptionKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
config.headers = mergeExtraConfig.formatHeader({
|
config.headers = mergeExtraConfig.formatHeader({
|
||||||
...this.config.headers,
|
...this.config.headers,
|
||||||
...config.headers,
|
...config.headers,
|
||||||
lang: 'zh_CN',
|
lang: 'zh_CN',
|
||||||
'login-type': 'device',
|
'login-type': 'device',
|
||||||
'user-agent': 'android',
|
|
||||||
...(mergeExtraConfig.withToken && {
|
...(mergeExtraConfig.withToken && {
|
||||||
[mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(),
|
[mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(),
|
||||||
}),
|
}),
|
||||||
@ -196,6 +142,7 @@ export default class Request {
|
|||||||
const { data, config } = response
|
const { data, config } = response
|
||||||
let responseData = response.data.data
|
let responseData = response.data.data
|
||||||
|
|
||||||
|
// 假设后端返回格式为 { data: "base64...", time: "..." }
|
||||||
if (responseData && responseData.data && responseData.time) {
|
if (responseData && responseData.data && responseData.time) {
|
||||||
try {
|
try {
|
||||||
const decryptedStr = HiAesUtil.decryptData(
|
const decryptedStr = HiAesUtil.decryptData(
|
||||||
@ -203,6 +150,7 @@ export default class Request {
|
|||||||
responseData.time,
|
responseData.time,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
)
|
)
|
||||||
|
// 解密后转化为 JSON 对象供后续业务使用
|
||||||
responseData = JSON.parse(decryptedStr)
|
responseData = JSON.parse(decryptedStr)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解密失败:', e)
|
console.error('解密失败:', e)
|
||||||
@ -210,32 +158,39 @@ export default class Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
axiosCanceler.removePending(config)
|
axiosCanceler.removePending(config)
|
||||||
|
if (data.code == 40004) {
|
||||||
if (data.code !== 200) {
|
toast.error('登录状态已失效, 请重新登录')
|
||||||
const msg = ERROR_MESSAGES[data.code] || response.data?.msg || data?.error || '未知错误'
|
redirectLogin()
|
||||||
|
return
|
||||||
if (data.code == 40004 || data.code == 40003 || data.code == 40005) {
|
}
|
||||||
toast.error(msg)
|
if (data.code == 70001) {
|
||||||
redirectLogin()
|
toast.error('验证码错误')
|
||||||
return
|
redirectLogin()
|
||||||
}
|
return
|
||||||
|
}
|
||||||
if (config.extraConfig?.handleResponse) {
|
const resData = config.extraConfig?.originResponseData ? response : responseData
|
||||||
this.errorReport(config.extraConfig.errorLevel ?? 2, msg)
|
if (config.extraConfig?.handleResponse) {
|
||||||
return Promise.reject({
|
if (data.code === 200) {
|
||||||
...data,
|
return resData
|
||||||
message: msg,
|
}
|
||||||
})
|
this.errorReport(
|
||||||
}
|
config.extraConfig.errorLevel ?? 2,
|
||||||
|
response.data?.msg ?? data?.error ?? '未知错误',
|
||||||
|
)
|
||||||
|
return Promise.reject({
|
||||||
|
...data,
|
||||||
|
message: response.data?.msg ?? data?.error ?? '未知错误',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return resData
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.extraConfig?.originResponseData ? response : responseData
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const status = error?.response?.status
|
const status = error?.response?.status
|
||||||
const code = error?.code
|
const code = error?.code
|
||||||
let message = error?.message
|
let message = error?.message
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
|
// message = '未登录或登录状态失效'
|
||||||
redirectLogin()
|
redirectLogin()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user