diff --git a/src/pages/UserCenter/DesktopLayout/index.vue b/src/pages/UserCenter/DesktopLayout/index.vue
index bb1d8e6..783afcd 100644
--- a/src/pages/UserCenter/DesktopLayout/index.vue
+++ b/src/pages/UserCenter/DesktopLayout/index.vue
@@ -41,7 +41,10 @@
-
+
{{ expireDateInfo.text }}
@@ -91,6 +94,7 @@ import DeviceList from '@/components/user-center/DeviceList.vue'
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
import { Button } from '@/components/ui/button'
+import { formatExpireDate } from '../subscription'
import { toast } from 'vue-sonner'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -120,38 +124,7 @@ const currentPlanIndex = computed(() => {
const expireDateInfo = computed(() => {
const first = props.alreadySubscribed[0]
- let text = ''
- let highlight = false
-
- if (!first || !first.expireDate) {
- text = '尚未购买套餐'
- highlight = true
- } else {
- // 尝试解析日期,兼容多种格式
- const dateStr = first.expireDate.replace(/ /g, 'T')
- const expireDateTime = new Date(dateStr)
-
- if (isNaN(expireDateTime.getTime())) {
- text = '套餐信息无效'
- } else if (expireDateTime < new Date()) {
- const year = expireDateTime.getFullYear()
- const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
- const day = String(expireDateTime.getDate()).padStart(2, '0')
- text = `已于 ${year}/${month}/${day} 到期`
- highlight = true
- } else {
- const year = expireDateTime.getFullYear()
- const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
- const day = String(expireDateTime.getDate()).padStart(2, '0')
- const hour = String(expireDateTime.getHours()).padStart(2, '0')
- const minute = String(expireDateTime.getMinutes()).padStart(2, '0')
- const second = String(expireDateTime.getSeconds()).padStart(2, '0')
- text = `到期时间:${year}/${month}/${day} ${hour}:${minute}:${second}`
- highlight = false
- }
- }
-
- return { text, highlight }
+ return formatExpireDate(first)
})
function logout() {
diff --git a/src/pages/UserCenter/MobileLayout/index.vue b/src/pages/UserCenter/MobileLayout/index.vue
index fb33ce1..6fb9aba 100644
--- a/src/pages/UserCenter/MobileLayout/index.vue
+++ b/src/pages/UserCenter/MobileLayout/index.vue
@@ -8,7 +8,10 @@
{{ userInfo.email }}
-
+
{{ expireDateInfo.text }}
@@ -78,6 +81,7 @@ import DeviceList from '@/components/user-center/DeviceList.vue'
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
import { Button } from '@/components/ui/button'
+import { formatExpireDate } from '../subscription'
import { toast } from 'vue-sonner'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -107,38 +111,7 @@ const currentPlanIndex = computed(() => {
const expireDateInfo = computed(() => {
const first = props.alreadySubscribed[0]
- let text = ''
- let highlight = false
-
- if (!first || !first.expireDate) {
- text = '尚未购买套餐'
- highlight = true
- } else {
- // 尝试解析日期,兼容多种格式
- const dateStr = first.expireDate.replace(/ /g, 'T')
- const expireDateTime = new Date(dateStr)
-
- if (isNaN(expireDateTime.getTime())) {
- text = '套餐信息无效'
- } else if (expireDateTime < new Date()) {
- const year = expireDateTime.getFullYear()
- const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
- const day = String(expireDateTime.getDate()).padStart(2, '0')
- text = `已于 ${year}/${month}/${day} 到期`
- highlight = true
- } else {
- const year = expireDateTime.getFullYear()
- const month = String(expireDateTime.getMonth() + 1).padStart(2, '0')
- const day = String(expireDateTime.getDate()).padStart(2, '0')
- const hour = String(expireDateTime.getHours()).padStart(2, '0')
- const minute = String(expireDateTime.getMinutes()).padStart(2, '0')
- const second = String(expireDateTime.getSeconds()).padStart(2, '0')
- text = `到期时间:${year}/${month}/${day} ${hour}:${minute}:${second}`
- highlight = false
- }
- }
-
- return { text, highlight }
+ return formatExpireDate(first)
})
function logout() {
diff --git a/src/pages/UserCenter/index.vue b/src/pages/UserCenter/index.vue
index 64ffc36..a5f9a3e 100644
--- a/src/pages/UserCenter/index.vue
+++ b/src/pages/UserCenter/index.vue
@@ -74,6 +74,7 @@ import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
import Logo from '@/pages/Home/logo.svg?component'
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
import request from '@/utils/request'
+import { toast } from 'vue-sonner'
const route = useRoute()
const router = useRouter()
@@ -144,48 +145,57 @@ const handlePlanSelect = (id: string) => {
selectedPlanId.value = id
}
-const handlePay = (methodId: number | string) => {
+const handlePay = async (methodId: number | string) => {
if (isPaying.value) return
+
+ // 1. 查找套餐并校验
const plan = plans.value.find((p) => p.id === selectedPlanId.value)
- if (!plan) return
-
- const already = alreadySubscribed.value.find((s: any) => s.subscribe_id === plan.planId)
- const isRenewal = !!already
- const api = isRenewal ? '/api/v1/public/order/renewal' : '/api/v1/public/order/purchase'
- const params: any = {
- subscribe_id: plan.planId,
- quantity: plan.quantity,
- payment: methodId,
+ if (!plan) {
+ toast.error('请选择有效的订阅套餐')
+ return
}
- if (isRenewal) {
- params.user_subscribe_id = already.id
- }
isPaying.value = true
- request
- .post(api, params)
- .then((res: any) => {
- request
- .post('/api/v1/public/portal/order/checkout', {
- 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) {
- localStorage.setItem('pending_order_no', res.order_no)
- console.log('pending_order_no', res.order_no)
- setTimeout(() => {
- window.location.href = checkoutRes.checkout_url
- })
- }
- })
- .finally(() => {
- isPaying.value = false
- })
- })
- .catch(() => {
- isPaying.value = false
+
+ 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 isRenewal = !!existingSub
+ const api = isRenewal ? '/api/v1/public/order/renewal' : '/api/v1/public/order/purchase'
+
+ const orderParams = {
+ subscribe_id: plan.planId,
+ quantity: plan.quantity,
+ payment: methodId,
+ ...(isRenewal && { user_subscribe_id: existingSub.id }), // 仅续费时添加此字段
+ }
+
+ // 3. 创建订单
+ const orderRes: any = await request.post(api, orderParams)
+ if (!orderRes?.order_no) throw new Error('订单创建失败')
+
+ // 4. 获取支付收银台链接
+ const checkoutRes: any = await request.post('/api/v1/public/portal/order/checkout', {
+ orderNo: orderRes.order_no,
+ returnUrl: `${window.location.origin}/user-center?order_no=${orderRes.order_no}`,
})
+
+ // 5. 执行跳转
+ if (checkoutRes.type === 'url' && checkoutRes.checkout_url) {
+ localStorage.setItem('pending_order_no', orderRes.order_no)
+ window.location.href = checkoutRes.checkout_url
+ } else {
+ throw new Error('支付网关配置异常')
+ }
+ } catch (error: any) {
+ console.error('支付流程异常:', error)
+ // 使用你之前调整过字号的 Toaster 提示错误
+ toast.error(error.message || '发起支付失败,请稍后重试')
+ } finally {
+ isPaying.value = false
+ }
}
async function init() {
diff --git a/src/pages/UserCenter/subscription.ts b/src/pages/UserCenter/subscription.ts
new file mode 100644
index 0000000..f2465df
--- /dev/null
+++ b/src/pages/UserCenter/subscription.ts
@@ -0,0 +1,59 @@
+/**
+ * 订阅对象的接口定义(根据你的后端数据结构调整)
+ */
+interface SubscriptionItem {
+ expire_time?: string | number | null
+}
+
+export interface ExpireInfo {
+ text: string
+ highlight: boolean
+}
+
+/**
+ * 格式化订阅到期信息
+ * @param sub 整个订阅对象 item
+ */
+export function formatExpireDate(sub?: SubscriptionItem | null): ExpireInfo {
+ // 1. 处理对象不存在的情况
+ if (!sub || !sub.expire_time) {
+ return { text: '尚未购买套餐', highlight: true }
+ }
+
+ // 2. 解析日期(兼容 ISO 字符串和时间戳)
+ const timestamp = sub.expire_time
+ const isMs = typeof timestamp === 'number' && timestamp > 10000000000
+ const expireDate = new Date(
+ isMs ? timestamp : typeof timestamp === 'number' ? timestamp * 1000 : timestamp,
+ )
+
+ if (isNaN(expireDate.getTime())) {
+ return { text: '套餐信息无效', highlight: false }
+ }
+
+ // 3. 提取日期组件
+ const now = new Date()
+ const isExpired = expireDate < now
+
+ const y = expireDate.getFullYear()
+ const m = String(expireDate.getMonth() + 1).padStart(2, '0')
+ const d = String(expireDate.getDate()).padStart(2, '0')
+ const dateStr = `${y}/${m}/${d}`
+
+ // 4. 根据是否过期返回不同格式
+ if (isExpired) {
+ return {
+ text: `已于 ${dateStr} 到期`,
+ highlight: true,
+ }
+ }
+
+ const hh = String(expireDate.getHours()).padStart(2, '0')
+ const mm = String(expireDate.getMinutes()).padStart(2, '0')
+ const ss = String(expireDate.getSeconds()).padStart(2, '0')
+
+ return {
+ text: `到期时间:${dateStr} ${hh}:${mm}:${ss}`,
+ highlight: false,
+ }
+}