接口处理

This commit is contained in:
speakeloudest 2025-12-30 06:50:01 -08:00
parent c7bfc2729c
commit 346ad0a4ec
7 changed files with 252 additions and 73 deletions

View File

@ -1 +1,2 @@
VITE_APP_BASE_URL=https://api.hifast.biz/v1
# https://api.hifast.biz
VITE_APP_BASE_URL=

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 {

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>

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()

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)

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)

View File

@ -14,4 +14,23 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
proxy: {
// 将所有以 /api 开头的请求转发到目标服务器
// 1. 匹配所有以 /public 开头的请求
'/api/v1': {
target: 'https://api.hifast.biz',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
autoRewrite: true,
// 3. 关键:将路径重写,在前面补上 /api/v1
// 验证请求是否进入代理,可以在终端看到打印
configure: (proxy, _options) => {
proxy.on('proxyReq', (proxyReq, req, _res) => {
console.log('代理请求:', req.method, req.url, ' -> ', proxyReq.path)
})
},
},
},
},
})