接口处理

This commit is contained in:
2025-12-30 06:50:01 -08:00
parent c7bfc2729c
commit 346ad0a4ec
7 changed files with 252 additions and 73 deletions
+34 -31
View File
@@ -1,31 +1,3 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Button } from '@/components/ui/button'
interface PaymentOption {
label: string
value: number | string
}
const props = defineProps<{
methods: any[]
}>()
const list = computed(() =>
props.methods.map((item) => ({
label: item.name,
value: item.id,
}))
)
// 当前选中的值,默认选中第一个
const selectedValue = ref<number | string>(props.methods[0]?.id || 1)
const handleSelect = (value: number | string) => {
selectedValue.value = value
}
</script>
<template>
<div>
<div class="bg-[#A8FF53] px-6 pt-7 font-sans text-black">
@@ -57,16 +29,18 @@ const handleSelect = (value: number | string) => {
</div>
<div class="px-6 pt-[55px]">
<h2 class="mb-1 text-center text-2xl font-bold">订单信息</h2>
<div class="ml-[11px] pt-[22px] text-[16px] font-semibold">365天套餐</div>
<div class="ml-[11px] pt-[22px] text-[16px] font-semibold">
{{ selectedPlan?.days }}天套餐
</div>
<div class="ml-[11px] flex items-center justify-between pt-[22px] text-[16px] font-semibold">
<div>订单金额</div>
<div>USD 44.99</div>
<div>USD {{ selectedPlan?.price }}</div>
</div>
</div>
<div class="px-6 pt-[85px] pb-[23px]">
<Button
@click="$emit('pay', selectedValue)"
class="h-[50px] w-full rounded-[32px] border-none bg-black text-[16px] font-black text-white hover:bg-black/90"
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]"
>
立即支付
</Button>
@@ -74,6 +48,35 @@ const handleSelect = (value: number | string) => {
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Button } from '@/components/ui/button'
interface PaymentOption {
label: string
value: number | string
}
const props = defineProps<{
methods: any[]
selectedPlan: any
}>()
const list = computed(() =>
props.methods.map((item) => ({
label: item.name,
value: item.id,
})),
)
// 当前选中的值,默认选中第一个
const selectedValue = ref<number | string>(props.methods[0]?.id || 1)
const handleSelect = (value: number | string) => {
selectedValue.value = value
}
</script>
<style scoped>
/* 如果需要特殊的斜体感,可以在这里微调 */
span {
+67 -13
View File
@@ -5,7 +5,9 @@
<img src="../avatar.png" class="size-[60px]" alt="" />
<div class="flex h-full flex-col justify-center text-white">
<div class="text-xl font-semibold">{{ userInfo.email }}</div>
<div class="text-xs">到期时间 {{ userInfo.expireDate }}</div>
<div class="text-xs" :class="{ 'text-[#FF00B7]': expireDateInfo.highlight }">
{{ expireDateInfo.text }}
</div>
</div>
</div>
@@ -15,20 +17,25 @@
<div class="overflow-hidden rounded-4xl bg-[#A8FF53]">
<PlanCard :plans="plans" :currentPlanIndex="currentPlanIndex" @select="handlePlanSelect" />
<div class="pt-2">
<PaymentMethod :methods="payments" />
<PaymentMethod
:methods="payments"
:selectedPlan="selectedPlan"
@pay="(id: number | string) => $emit('pay', id)"
/>
</div>
</div>
<div class="pt-8 pb-[calc(50px+env(safe-area-inset-bottom))]">
<Button
<div class="px-6 pt-8 pb-[calc(50px+env(safe-area-inset-bottom))]">
<!-- <Button
variant="outline"
class="mb-[10px] h-[50px] w-full rounded-[32px] border-2 border-[#FF00B7] bg-transparent text-xl font-bold text-[#FF00B7] transition-all hover:bg-[#FF00FF]/90 active:scale-[0.98]"
>
注销账户
</Button>
</Button>-->
<Button
variant="outline"
@click="logout"
class="h-[50px] w-full rounded-[32px] border-2 border-white bg-transparent text-xl font-bold text-white transition-all hover:bg-white/90 active:scale-[0.98]"
>
退出登录
@@ -38,28 +45,75 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed } from 'vue'
import PlanCard from '@/components/user-center/PlanCard.vue'
import DeviceList from '@/components/user-center/DeviceList.vue'
import PaymentMethod from '@/components/user-center/PaymentMethod.vue'
import { Button } from '@/components/ui/button'
import { toast } from 'vue-sonner'
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps<{
devices: any[]
plans: any[]
payments: any[]
userInfo: { email: string; expireDate: string }
alreadySubscribed: any[]
userInfo: { email: string }
selectedPlanId: string
selectedPlan: any
}>()
// --- State ---
const selectedPlanId = ref('p2')
const selectedPayment = ref('alipay')
const emit = defineEmits(['select-plan', 'pay'])
// --- Handlers ---
const handlePlanSelect = (id: string) => {
selectedPlanId.value = id
emit('select-plan', id)
}
const currentPlanIndex = computed(() => {
return props.plans.findIndex((p) => p.id === props.selectedPlanId)
})
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 }
})
function logout() {
router.push('/')
localStorage.removeItem('Authorization')
toast.success('退出成功')
}
const currentPlanIndex = ref(3)
</script>
<style scoped>
+105 -26
View File
@@ -21,17 +21,22 @@
<!-- Main Neon Green Card -->
<div class="px-[18px]">
<MobileLayout
:already-subscribed="alreadySubscribed"
:devices="devices"
:plans="plans"
:payments="payments"
:user-info="userSubInfo"
:selected-plan-id="selectedPlanId"
:selected-plan="activePlan"
@select-plan="handlePlanSelect"
@pay="handlePay"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import MobileLayout from './MobileLayout/index.vue'
import Logo from '@/pages/Home/logo.svg?component'
import MobileLogo from '@/pages/Home/mobile-logo.svg?component'
@@ -39,51 +44,125 @@ import request from '@/utils/request'
// --- State ---
const selectedPlanId = ref('p2')
const selectedPayment = ref('alipay')
const selectedPayment = ref('alipay') // This variable is no longer directly used for payment selection in the UI, but kept for potential future use or if other parts of the app rely on it.
const devices = ref([])
const plans = ref([])
const payments = ref([])
const devices = ref<any[]>([])
const plans = ref<any[]>([])
const payments = ref<any[]>([])
const alreadySubscribed = ref<any[]>([])
const userSubInfo = ref({
email: '',
expireDate: '',
})
// --- Data Mapping ---
const mapPlans = (apiList: any[]) => {
const flattened: any[] = []
let globalIndex = 0
apiList.forEach((item: any) => {
const unitDays = item.unit_time === 'Day' ? 1 : 365
if (!item.discount || item.discount.length === 0) {
flattened.push({
id: item.id.toString(),
planId: item.id,
quantity: 1,
days: unitDays,
price: (item.unit_price / 100).toFixed(2),
daily: (item.unit_price / 100 / unitDays).toFixed(2),
discount: '',
})
globalIndex++
} else {
item.discount.forEach((opt: any) => {
const totalDays = opt.quantity * unitDays
const ratioToPay = opt.discount === 0 || opt.discount === undefined ? 100 : opt.discount
const totalPrice = (item.unit_price / 100) * opt.quantity * (ratioToPay / 100)
flattened.push({
id: `${item.id}-${opt.quantity}`,
planId: item.id,
quantity: opt.quantity,
days: totalDays,
price: totalPrice.toFixed(2),
daily: (totalPrice / totalDays).toFixed(2),
discount:
globalIndex > 0 && ratioToPay < 100 ? `${Math.ceil(100 - ratioToPay)}% off` : '',
})
globalIndex++
})
}
})
return flattened
}
// --- Handlers ---
const activePlan = computed(() => {
return plans.value.find((p) => p.id === selectedPlanId.value)
})
const handlePlanSelect = (id: string) => {
selectedPlanId.value = id
}
const handlePay = (methodId: number | string) => {
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 (isRenewal) {
params.user_subscribe_id = already.id
}
console.log(params)
request.post(api, params).then((res: any) => {
request
.post('/api/v1/public/portal/order/checkout', {
orderNo: res.order_no,
returnUrl: window.location.origin,
})
.then((checkoutRes: any) => {
if (checkoutRes.type === 'url' && checkoutRes.checkout_url) {
window.location.href = checkoutRes.checkout_url
}
})
})
}
function init() {
// 设备列表
// request.get('/public/user/devices').then((res: any) => {
// devices.value =
// res.list?.map((item: any) => ({
// id: item.id,
// name: item.user_agent || 'Unknown Device',
// type: item.user_agent?.toLowerCase().includes('android') ? 'mobile' : 'desktop',
// deviceId: item.id,
// })) || []
// })
// 用户信息
request.get('/public/user/info').then((res: any) => {
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
// 用户信息 & 设备列表
request.get('/api/v1/public/user/info').then((res: any) => {
devices.value = res.user_devices
console.log(res.user_devices)
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
if (emailInfo) {
userSubInfo.value.email = emailInfo.auth_identifier
}
})
// 订阅详情
request.get('/public/subscribe/list').then((res: any) => {
plans.value = res.list
console.log(plans.value)
// 已订阅列表
request.get('/api/v1/public/user/subscribe').then((res: any) => {
alreadySubscribed.value = res.list || []
})
// 订阅套餐列表
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
}
})
// 获取支付方式
request.get('/public/payment/methods').then((res: any) => {
payments.value = res.list || []
request.get('/api/v1/public/payment/methods').then((res: any) => {
console.log(res)
payments.value = res.list?.filter((p: any) => p.platform !== 'apple_iap') || []
})
}
init()
+3 -1
View File
@@ -37,7 +37,9 @@ export class HiAesUtil {
*/
static encryptData(plainText, keyStr) {
// 生成 ISO8601 时间戳 (与 Dart DateTime.now().toIso8601String() 对应)
const nonce = new Date().toISOString()
// Dart: 2025-12-30T21:05:06.075850 (no Z, 6 decimal places)
const now = new Date()
const nonce = now.toISOString().replace('Z', '') + '000'
const key = this._generateKey(keyStr)
const iv = this._generateIv(nonce, keyStr)
+22 -1
View File
@@ -97,13 +97,34 @@ export default class Request {
config.data = HiAesUtil.encryptData(plainText, encryptionKey)
}
if (config.method?.toLowerCase() === 'get' || config.params) {
const paramsToEncrypt = config.params || {} // 为空则加密 "{}"
const plainParamsText = JSON.stringify(paramsToEncrypt)
const encryptedParams = HiAesUtil.encryptData(plainParamsText, encryptionKey)
// 将原参数替换为加密后的 data 和 time 字段
config.params = {
data: encryptedParams.data,
time: encryptedParams.time,
}
}
if (config.data?.time) {
console.log(
'解密',
HiAesUtil.decryptData(config.data.data, config.data.time, encryptionKey),
)
}
config.headers = mergeExtraConfig.formatHeader({
...this.config.headers,
...config.headers,
lang: 'zh_CN',
'login-type': 'device',
...(mergeExtraConfig.withToken && {
[mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(),
}),
})
} as any)
config.extraConfig = mergeExtraConfig
if (mergeExtraConfig.cancelRepetition) {
axiosCanceler.addPending(config)