This commit is contained in:
parent
5f9b23c112
commit
32bda7da80
@ -1,37 +1,34 @@
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
export { default as Button } from "./Button.vue"
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||||
secondary:
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
ghost:
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||||
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
"icon": "size-9",
|
icon: 'size-9',
|
||||||
"icon-sm": "size-8",
|
'icon-sm': 'size-8',
|
||||||
"icon-lg": "size-10",
|
'icon-lg': 'size-10',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -25,12 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="px-6 pt-8 pb-9">
|
<div class="px-6 pt-8 pb-9">
|
||||||
<!-- <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]"
|
@click="$emit('show-order-details')"
|
||||||
|
class="mb-[10px] h-[50px] w-full rounded-[32px] border-2 border-[#ADFF5B] bg-transparent text-xl font-bold text-[#ADFF5B] transition-all hover:bg-[#ADFF5B]/90 active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
注销账户
|
订单详情
|
||||||
</Button>-->
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -112,7 +113,7 @@ const props = defineProps<{
|
|||||||
isPaymentsLoading: boolean
|
isPaymentsLoading: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
const emit = defineEmits(['select-plan', 'pay', 'refresh', 'show-order-details'])
|
||||||
|
|
||||||
// --- Handlers ---
|
// --- Handlers ---
|
||||||
const handlePlanSelect = (id: string) => {
|
const handlePlanSelect = (id: string) => {
|
||||||
|
|||||||
@ -56,12 +56,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-6 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]"
|
@click="$emit('show-order-details')"
|
||||||
|
class="mb-[10px] h-[50px] w-full rounded-[32px] border-2 border-[#ADFF5B] bg-transparent text-xl font-bold text-[#ADFF5B] transition-all hover:bg-[#ADFF5B]/90 active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
注销账户
|
订单详情
|
||||||
</Button>-->
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -99,7 +100,7 @@ const props = defineProps<{
|
|||||||
isPaymentsLoading: boolean
|
isPaymentsLoading: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['select-plan', 'pay', 'refresh'])
|
const emit = defineEmits(['select-plan', 'pay', 'refresh', 'show-order-details'])
|
||||||
|
|
||||||
// --- Handlers ---
|
// --- Handlers ---
|
||||||
const handlePlanSelect = (id: string) => {
|
const handlePlanSelect = (id: string) => {
|
||||||
|
|||||||
163
src/pages/UserCenter/components/OrderDetails/OrderList.vue
Normal file
163
src/pages/UserCenter/components/OrderDetails/OrderList.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<!-- List Container -->
|
||||||
|
<div class="h-[470px]">
|
||||||
|
<div v-if="loading" class="flex h-40 items-center justify-center">
|
||||||
|
<div
|
||||||
|
class="h-8 w-8 animate-spin rounded-full border-4 border-[#ADFF5B] border-t-transparent"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="list.length === 0"
|
||||||
|
class="flex h-40 items-center justify-center text-gray-500"
|
||||||
|
>
|
||||||
|
暂无订单记录
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-[10px]">
|
||||||
|
<div
|
||||||
|
v-for="order in list"
|
||||||
|
:key="order.id"
|
||||||
|
class="rounded-[20px] bg-[#CECECF] py-2 text-[14px] font-normal text-black"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-y-1">
|
||||||
|
<!-- Order No -->
|
||||||
|
<div class="col-span-2 pl-4">
|
||||||
|
<div class="">订单号</div>
|
||||||
|
<div class="break-all">{{ order.order_no }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name and Status -->
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="text-gray-500">名称</div>
|
||||||
|
<div class="whitespace-nowrap">{{ order.quantity }}天VPN服务</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="text-gray-500">状态</div>
|
||||||
|
<div>{{ statusText(order.status) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Amount and Time -->
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="text-gray-500">支付金额</div>
|
||||||
|
<div>${{ order.amount }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div class="text-gray-500">支付时间</div>
|
||||||
|
<div class="whitespace-nowrap">{{ formatTime(order.created_at) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<div v-if="total > 0" class="flex flex-col items-center pt-[18px]">
|
||||||
|
<div class="mb-2 flex items-center gap-[10px]">
|
||||||
|
<button
|
||||||
|
@click="changePage(1)"
|
||||||
|
:disabled="page === 1"
|
||||||
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold text-[#848484]"><<</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="changePage(page - 1)"
|
||||||
|
:disabled="page === 1"
|
||||||
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold text-[#848484]"><</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex h-[30px] min-w-[65px] items-center justify-center rounded-full bg-[#EAEAEA] px-2"
|
||||||
|
>
|
||||||
|
<span class="text-base text-[#848484]">{{ page }}</span>
|
||||||
|
<span class="ml-1 text-base text-[#848484]">v</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="changePage(page + 1)"
|
||||||
|
:disabled="page >= totalPages"
|
||||||
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold text-[#848484]">></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="changePage(totalPages)"
|
||||||
|
:disabled="page >= totalPages"
|
||||||
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold text-[#848484]">>></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-[300] text-[#848484]">第 {{ page }} / {{ totalPages }} 页</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const size = ref(3)
|
||||||
|
const total = ref(0)
|
||||||
|
const list = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const totalPages = computed(() => Math.ceil(total.value / size.value) || 1)
|
||||||
|
|
||||||
|
async function fetchOrders() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/api/v1/public/order/list', {
|
||||||
|
page: page.value,
|
||||||
|
size: size.value,
|
||||||
|
status: 5,
|
||||||
|
})
|
||||||
|
list.value = res.list || []
|
||||||
|
total.value = res.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch orders error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(p: number) {
|
||||||
|
if (p < 1 || p > totalPages.value) return
|
||||||
|
page.value = p
|
||||||
|
fetchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText(status: number) {
|
||||||
|
const map: Record<number, string> = {
|
||||||
|
1: '待支付',
|
||||||
|
2: '已支付',
|
||||||
|
3: '已关闭',
|
||||||
|
4: '支付失败',
|
||||||
|
5: '已完成',
|
||||||
|
}
|
||||||
|
return map[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
if (!timestamp) return '-'
|
||||||
|
const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000)
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
60
src/pages/UserCenter/components/OrderDetails/index.vue
Normal file
60
src/pages/UserCenter/components/OrderDetails/index.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :open="isOpen" @update:open="setOpen">
|
||||||
|
<DialogContent
|
||||||
|
class="top-[94px] flex h-[calc(100vh-94px-50px)] w-[322px] translate-y-0 flex-col items-center justify-center border-none bg-transparent p-0 shadow-none outline-none focus:ring-0"
|
||||||
|
:showCloseButton="false"
|
||||||
|
>
|
||||||
|
<div class="relative flex h-[678px] w-full flex-col rounded-[32px] bg-[#DDDDDD] px-4 py-6">
|
||||||
|
<!-- Close Button -->
|
||||||
|
<button
|
||||||
|
@click="hide"
|
||||||
|
class="absolute top-6 right-6 z-10 flex size-8 items-center justify-center rounded-lg transition-colors hover:bg-black/5"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-black"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 class="mb-2 px-4 pt-8 text-center text-[20px] font-bold text-black">订单详情</h2>
|
||||||
|
|
||||||
|
<OrderList />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
import OrderList from './OrderList.vue'
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const setOpen = (value: boolean) => {
|
||||||
|
isOpen.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
isOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -36,6 +36,7 @@
|
|||||||
@select-plan="handlePlanSelect"
|
@select-plan="handlePlanSelect"
|
||||||
@pay="handlePay"
|
@pay="handlePay"
|
||||||
@refresh="init"
|
@refresh="init"
|
||||||
|
@show-order-details="orderDetailsModalRef?.show()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden flex-1 items-center justify-center md:flex md:pb-[50px]">
|
<div class="hidden flex-1 items-center justify-center md:flex md:pb-[50px]">
|
||||||
@ -55,12 +56,14 @@
|
|||||||
@select-plan="handlePlanSelect"
|
@select-plan="handlePlanSelect"
|
||||||
@pay="handlePay"
|
@pay="handlePay"
|
||||||
@refresh="init"
|
@refresh="init"
|
||||||
|
@show-order-details="orderDetailsModalRef?.show()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrderStatusDialog ref="orderStatusDialogRef" @close="handleStatusClose" @refresh="init" />
|
<OrderStatusDialog ref="orderStatusDialogRef" @close="handleStatusClose" @refresh="init" />
|
||||||
|
<OrderDetailsModal ref="orderDetailsModalRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -70,6 +73,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import MobileLayout from './MobileLayout/index.vue'
|
import MobileLayout from './MobileLayout/index.vue'
|
||||||
import DesktopLayout from './DesktopLayout/index.vue'
|
import DesktopLayout from './DesktopLayout/index.vue'
|
||||||
import OrderStatusDialog from '@/components/user-center/OrderStatusDialog.vue'
|
import OrderStatusDialog from '@/components/user-center/OrderStatusDialog.vue'
|
||||||
|
import OrderDetailsModal from './components/OrderDetails/index.vue'
|
||||||
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.vue'
|
import UserCenterSkeleton from '@/components/user-center/UserCenterSkeleton.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'
|
||||||
@ -85,6 +89,7 @@ const isUserLoading = ref(true)
|
|||||||
const isPlansLoading = ref(true)
|
const isPlansLoading = ref(true)
|
||||||
const isPaymentsLoading = ref(true)
|
const isPaymentsLoading = ref(true)
|
||||||
const orderStatusDialogRef = ref<InstanceType<typeof OrderStatusDialog> | null>(null)
|
const orderStatusDialogRef = ref<InstanceType<typeof OrderStatusDialog> | null>(null)
|
||||||
|
const orderDetailsModalRef = ref<InstanceType<typeof OrderDetailsModal> | null>(null)
|
||||||
const selectedPayment = ref('alipay')
|
const selectedPayment = ref('alipay')
|
||||||
|
|
||||||
const devices = ref<any[]>([])
|
const devices = ref<any[]>([])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user