接口处理

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> <template>
<div> <div>
<div class="bg-[#A8FF53] px-6 pt-7 font-sans text-black"> <div class="bg-[#A8FF53] px-6 pt-7 font-sans text-black">
@ -57,16 +29,18 @@ const handleSelect = (value: number | string) => {
</div> </div>
<div class="px-6 pt-[55px]"> <div class="px-6 pt-[55px]">
<h2 class="mb-1 text-center text-2xl font-bold">订单信息</h2> <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 class="ml-[11px] flex items-center justify-between pt-[22px] text-[16px] font-semibold">
<div>订单金额</div> <div>订单金额</div>
<div>USD 44.99</div> <div>USD {{ selectedPlan?.price }}</div>
</div> </div>
</div> </div>
<div class="px-6 pt-[85px] pb-[23px]"> <div class="px-6 pt-[85px] pb-[23px]">
<Button <Button
@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 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> </Button>
@ -74,6 +48,35 @@ const handleSelect = (value: number | string) => {
</div> </div>
</template> </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> <style scoped>
/* 如果需要特殊的斜体感,可以在这里微调 */ /* 如果需要特殊的斜体感,可以在这里微调 */
span { span {

View File

@ -5,7 +5,9 @@
<img src="../avatar.png" class="size-[60px]" alt="" /> <img src="../avatar.png" class="size-[60px]" alt="" />
<div class="flex h-full flex-col justify-center text-white"> <div class="flex h-full flex-col justify-center text-white">
<div class="text-xl font-semibold">{{ userInfo.email }}</div> <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>
</div> </div>
@ -15,20 +17,25 @@
<div class="overflow-hidden rounded-4xl bg-[#A8FF53]"> <div class="overflow-hidden rounded-4xl bg-[#A8FF53]">
<PlanCard :plans="plans" :currentPlanIndex="currentPlanIndex" @select="handlePlanSelect" /> <PlanCard :plans="plans" :currentPlanIndex="currentPlanIndex" @select="handlePlanSelect" />
<div class="pt-2"> <div class="pt-2">
<PaymentMethod :methods="payments" /> <PaymentMethod
:methods="payments"
:selectedPlan="selectedPlan"
@pay="(id: number | string) => $emit('pay', id)"
/>
</div> </div>
</div> </div>
<div class="pt-8 pb-[calc(50px+env(safe-area-inset-bottom))]"> <div class="px-6 pt-8 pb-[calc(50px+env(safe-area-inset-bottom))]">
<Button <!-- <Button
variant="outline" 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]" 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 <Button
variant="outline" 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]" 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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' 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 { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { toast } from 'vue-sonner'
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps<{ const props = defineProps<{
devices: any[] devices: any[]
plans: any[] plans: any[]
payments: any[] payments: any[]
userInfo: { email: string; expireDate: string } alreadySubscribed: any[]
userInfo: { email: string }
selectedPlanId: string
selectedPlan: any
}>() }>()
// --- State --- const emit = defineEmits(['select-plan', 'pay'])
const selectedPlanId = ref('p2')
const selectedPayment = ref('alipay')
// --- Handlers --- // --- Handlers ---
const handlePlanSelect = (id: string) => { 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> </script>
<style scoped> <style scoped>

View File

@ -21,17 +21,22 @@
<!-- Main Neon Green Card --> <!-- Main Neon Green Card -->
<div class="px-[18px]"> <div class="px-[18px]">
<MobileLayout <MobileLayout
: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="activePlan"
@select-plan="handlePlanSelect"
@pay="handlePay"
/> />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import MobileLayout from './MobileLayout/index.vue' import MobileLayout from './MobileLayout/index.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'
@ -39,51 +44,125 @@ import request from '@/utils/request'
// --- State --- // --- State ---
const selectedPlanId = ref('p2') 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 devices = ref<any[]>([])
const plans = ref([]) const plans = ref<any[]>([])
const payments = ref([]) const payments = ref<any[]>([])
const alreadySubscribed = ref<any[]>([])
const userSubInfo = ref({ const userSubInfo = ref({
email: '', email: '',
expireDate: '', 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 --- // --- Handlers ---
const activePlan = computed(() => {
return plans.value.find((p) => p.id === selectedPlanId.value)
})
const handlePlanSelect = (id: string) => { const handlePlanSelect = (id: string) => {
selectedPlanId.value = id 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() { function init() {
// // &
// request.get('/public/user/devices').then((res: any) => { request.get('/api/v1/public/user/info').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')
devices.value = res.user_devices devices.value = res.user_devices
console.log(res.user_devices)
const emailInfo = res.auth_methods?.find((item: any) => item.auth_type === 'email')
if (emailInfo) { if (emailInfo) {
userSubInfo.value.email = emailInfo.auth_identifier userSubInfo.value.email = emailInfo.auth_identifier
} }
}) })
//
request.get('/public/subscribe/list').then((res: any) => { //
plans.value = res.list request.get('/api/v1/public/user/subscribe').then((res: any) => {
console.log(plans.value) 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) => { request.get('/api/v1/public/payment/methods').then((res: any) => {
payments.value = res.list || [] console.log(res)
payments.value = res.list?.filter((p: any) => p.platform !== 'apple_iap') || []
}) })
} }
init() init()

View File

@ -37,7 +37,9 @@ export class HiAesUtil {
*/ */
static encryptData(plainText, keyStr) { static encryptData(plainText, keyStr) {
// 生成 ISO8601 时间戳 (与 Dart DateTime.now().toIso8601String() 对应) // 生成 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 key = this._generateKey(keyStr)
const iv = this._generateIv(nonce, keyStr) const iv = this._generateIv(nonce, keyStr)

View File

@ -97,13 +97,34 @@ export default class Request {
config.data = HiAesUtil.encryptData(plainText, encryptionKey) 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({ config.headers = mergeExtraConfig.formatHeader({
...this.config.headers, ...this.config.headers,
...config.headers, ...config.headers,
lang: 'zh_CN',
'login-type': 'device',
...(mergeExtraConfig.withToken && { ...(mergeExtraConfig.withToken && {
[mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(), [mergeExtraConfig.tokenKey]: mergeExtraConfig.getToken(),
}), }),
}) } as any)
config.extraConfig = mergeExtraConfig config.extraConfig = mergeExtraConfig
if (mergeExtraConfig.cancelRepetition) { if (mergeExtraConfig.cancelRepetition) {
axiosCanceler.addPending(config) axiosCanceler.addPending(config)

View File

@ -14,4 +14,23 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': 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)
})
},
},
},
},
}) })