接口处理
This commit is contained in:
parent
c7bfc2729c
commit
346ad0a4ec
3
.env.dev
3
.env.dev
@ -1 +1,2 @@
|
||||
VITE_APP_BASE_URL=https://api.hifast.biz/v1
|
||||
# https://api.hifast.biz
|
||||
VITE_APP_BASE_URL=
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user