This commit is contained in:
parent
2a7e851cda
commit
ed2cd443ba
@ -1,34 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const statsList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const totalDownloads = computed(() => {
|
||||||
|
return statsList.value.reduce((acc, item) => acc + (item.visits || 0), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchDownloads() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/api/v1/public/user/agent/downloads')
|
||||||
|
statsList.value = res.list || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch downloads error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDownloads()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="lucid-glass-bar flex w-full flex-col justify-between rounded-4xl! border-1 border-white px-6 py-7"
|
class="lucid-glass-bar flex w-full flex-col justify-between rounded-4xl! border-1 border-white px-6 py-7"
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-center justify-between border-b-1 border-dashed pb-3">
|
<div class="mb-2 flex items-center justify-between border-b-1 border-dashed pb-3">
|
||||||
<div class="relative ml-1 text-base font-bold text-white">各端下载量</div>
|
<div class="relative ml-1 text-base font-bold text-white">各端下载量</div>
|
||||||
<div class="mr-1 text-2xl font-bold text-white tabular-nums">62</div>
|
<div class="mr-1 text-2xl font-bold text-white tabular-nums">{{ totalDownloads }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3 min-h-[100px]">
|
||||||
|
<div v-if="loading && statsList.length === 0" class="flex h-20 items-center justify-center">
|
||||||
|
<div class="h-6 w-6 animate-spin rounded-full border-2 border-[#ADFF5B] border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="item in stats"
|
v-for="item in statsList"
|
||||||
:key="item.label"
|
:key="item.platform"
|
||||||
class="flex items-center justify-between text-white/90"
|
class="flex items-center justify-between text-white/90"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="ml-1 text-xs font-medium">{{ item.label }}</span>
|
<span class="ml-1 text-xs font-medium">{{ item.platform }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-1 text-base font-semibold tabular-nums">{{ item.value }}</div>
|
<div class="mr-1 text-base font-semibold tabular-nums">{{ item.visits }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-white/40">相比前一个月+19%</div>
|
<div class="mt-2 text-xs text-white/40">实时下载统计数据</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const stats = [
|
|
||||||
{ label: 'iPhone/iPad', value: 42 },
|
|
||||||
{ label: 'Windows', value: 7 },
|
|
||||||
{ label: 'Android', value: 8 },
|
|
||||||
{ label: 'Mac', value: 15 },
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 flex-col overflow-hidden">
|
<div class="flex flex-1 flex-col overflow-hidden">
|
||||||
<!-- List Container -->
|
<!-- List Container -->
|
||||||
<div class="h-[470px]">
|
<div class="h-[470px] overflow-y-auto pr-1">
|
||||||
<div v-if="loading" class="flex h-40 items-center justify-center">
|
<div v-if="loading && list.length === 0" class="flex h-40 items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="h-8 w-8 animate-spin rounded-full border-4 border-[#ADFF5B] border-t-transparent"
|
class="h-8 w-8 animate-spin rounded-full border-4 border-[#ADFF5B] border-t-transparent"
|
||||||
></div>
|
></div>
|
||||||
@ -11,38 +11,34 @@
|
|||||||
v-else-if="list.length === 0"
|
v-else-if="list.length === 0"
|
||||||
class="flex h-40 items-center justify-center text-gray-500"
|
class="flex h-40 items-center justify-center text-gray-500"
|
||||||
>
|
>
|
||||||
暂无推荐数据
|
暂无成交数据
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-[10px]">
|
<div v-else class="space-y-[10px]">
|
||||||
<div
|
<div
|
||||||
v-for="order in list"
|
v-for="(order, index) in list"
|
||||||
:key="order.id"
|
:key="index"
|
||||||
class="rounded-[20px] bg-[#CECECF] py-2 text-[14px] font-normal text-black"
|
class="rounded-[20px] bg-[#CECECF] py-2 text-[14px] font-normal text-black"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-2 gap-y-1">
|
<div class="grid grid-cols-2 gap-y-1">
|
||||||
<!-- Order No -->
|
<!-- User Info -->
|
||||||
<div class="col-span-2 pl-4">
|
<div class="col-span-2 pl-4">
|
||||||
<div class="">用户ID</div>
|
<div class="text-xs text-gray-500">用户</div>
|
||||||
<div class="break-all">{{ order.order_no }}</div>
|
<div class="break-all font-medium">{{ order.user_email }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name and Status -->
|
<!-- ID and Amount -->
|
||||||
<div class="pl-4">
|
<div class="pl-4">
|
||||||
<div class="text-gray-500">名称</div>
|
<div class="text-xs text-gray-500">用户 ID</div>
|
||||||
<div class="whitespace-nowrap">{{ order.quantity }}天VPN服务</div>
|
<div>{{ order.user_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-gray-500">状态</div>
|
<div class="text-xs text-gray-500">成交金额</div>
|
||||||
<div>{{ statusText(order.status) }}</div>
|
<div class="font-bold text-[#222]">${{ order.amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Amount and Time -->
|
<!-- Time -->
|
||||||
<div class="pl-4">
|
<div class="col-span-2 pl-4">
|
||||||
<div class="text-gray-500">支付金额</div>
|
<div class="text-xs text-gray-500">成交时间</div>
|
||||||
<div>${{ order.amount / 100 }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden">
|
|
||||||
<div class="text-gray-500">支付时间</div>
|
|
||||||
<div class="whitespace-nowrap">{{ formatTime(order.created_at) }}</div>
|
<div class="whitespace-nowrap">{{ formatTime(order.created_at) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,14 +52,14 @@
|
|||||||
<button
|
<button
|
||||||
@click="changePage(1)"
|
@click="changePage(1)"
|
||||||
:disabled="page === 1"
|
:disabled="page === 1"
|
||||||
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<span class="text-lg font-bold text-[#848484]"><<</span>
|
<span class="text-lg font-bold text-[#848484]"><<</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="changePage(page - 1)"
|
@click="changePage(page - 1)"
|
||||||
:disabled="page === 1"
|
:disabled="page === 1"
|
||||||
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<span class="text-lg font-bold text-[#848484]"><</span>
|
<span class="text-lg font-bold text-[#848484]"><</span>
|
||||||
</button>
|
</button>
|
||||||
@ -78,14 +74,14 @@
|
|||||||
<button
|
<button
|
||||||
@click="changePage(page + 1)"
|
@click="changePage(page + 1)"
|
||||||
:disabled="page >= totalPages"
|
:disabled="page >= totalPages"
|
||||||
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<span class="text-lg font-bold text-[#848484]">></span>
|
<span class="text-lg font-bold text-[#848484]">></span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="changePage(totalPages)"
|
@click="changePage(totalPages)"
|
||||||
:disabled="page >= totalPages"
|
:disabled="page >= totalPages"
|
||||||
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity"
|
class="flex h-[30px] min-w-[30px] items-center justify-center rounded-full bg-[#EAEAEA] transition-opacity disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<span class="text-lg font-bold text-[#848484]">>></span>
|
<span class="text-lg font-bold text-[#848484]">>></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,3 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const data = ref([
|
||||||
|
{ label: '点击量', value: 0, key: 'clicks' },
|
||||||
|
{ label: '浏览量', value: 0, key: 'views' },
|
||||||
|
{ label: '付费数量', value: 0, key: 'paid_count' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const growthRate = ref('N/A')
|
||||||
|
|
||||||
|
async function fetchStats() {
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/api/v1/public/user/agent/realtime')
|
||||||
|
data.value = data.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: res[item.key] || 0,
|
||||||
|
}))
|
||||||
|
growthRate.value = res.growth_rate || 'N/A'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch proxy data error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStats()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="lucid-glass-bar flex w-full flex-col justify-between rounded-4xl! border-1 border-white px-6 py-7"
|
class="lucid-glass-bar flex w-full flex-col justify-between rounded-4xl! border-1 border-white px-6 py-7"
|
||||||
@ -17,14 +47,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-white/40">相比前一个月+32%</div>
|
<div class="mt-2 text-xs text-white/40">相比前一个月 {{ growthRate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const data = [
|
|
||||||
{ label: '点击量', value: 782 },
|
|
||||||
{ label: '浏览量', value: 699 },
|
|
||||||
{ label: '付费数量', value: 54 },
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -1,3 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
defineEmits(['show-history'])
|
||||||
|
|
||||||
|
const salesPage = ref({
|
||||||
|
total: 0,
|
||||||
|
list: [] as any[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function fetchSales() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/api/v1/public/user/invite/sales', {
|
||||||
|
page: 1,
|
||||||
|
size: 8,
|
||||||
|
})
|
||||||
|
salesPage.value = res
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch sales error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(timestamp: number) {
|
||||||
|
if (!timestamp) return '-'
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSales()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="lucid-glass-bar flex h-full w-full flex-col rounded-4xl! border-1 border-white px-6 py-7"
|
class="lucid-glass-bar flex h-full w-full flex-col rounded-4xl! border-1 border-white px-6 py-7"
|
||||||
@ -5,7 +52,7 @@
|
|||||||
<div class="mb-[20px] flex justify-between border-b-1 border-dashed pb-4">
|
<div class="mb-[20px] flex justify-between border-b-1 border-dashed pb-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 text-xl font-bold text-white">最近销售数据</div>
|
<div class="mb-1 text-xl font-bold text-white">最近销售数据</div>
|
||||||
<div class="text-sm text-white/40">本月已成交订单54人/次</div>
|
<div class="text-sm text-white/40">累计成交订单{{ salesPage.total }}人/次</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
@ -17,15 +64,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto pr-1">
|
<div class="flex-1 overflow-y-auto pr-1">
|
||||||
<div class="border-t border-dashed border-white/20 pt-4">
|
<div v-if="loading && salesPage.list.length === 0" class="flex h-20 items-center justify-center">
|
||||||
|
<div class="h-6 w-6 animate-spin rounded-full border-2 border-[#ADFF5B] border-t-transparent"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="border-t border-dashed border-white/20 pt-4">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in sales"
|
v-for="(item, index) in salesPage.list"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="mb-5 flex items-center justify-between last:mb-0"
|
class="mb-5 flex items-center justify-between last:mb-0"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm font-semibold text-white">{{ item.email }}</span>
|
<span class="text-sm font-semibold text-white">{{ item.user_email }}</span>
|
||||||
<span class="text-[10px] text-white/40">{{ item.time }}</span>
|
<span class="text-[10px] text-white/40">{{ formatTime(item.created_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-lg font-bold text-white tabular-nums">$ {{ item.amount }}</div>
|
<div class="text-lg font-bold text-white tabular-nums">$ {{ item.amount }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,16 +83,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
defineEmits(['show-history'])
|
|
||||||
|
|
||||||
const sales = [
|
|
||||||
{ email: '321x***.com', time: '2026/1/10 22:32', amount: '2.99' },
|
|
||||||
{ email: 'fafax***.com', time: '2026/1/10 22:32', amount: '44.99' },
|
|
||||||
{ email: 'kjkljl***.com', time: '2026/1/10 22:32', amount: '19.79' },
|
|
||||||
{ email: '098f***.com', time: '2026/1/10 22:32', amount: '2.99' },
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -48,10 +48,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col gap-[10px] px-6 pt-8 pb-9">
|
<div class="flex flex-col gap-[10px] px-6 pt-8 pb-9">
|
||||||
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
||||||
历史佣金总计:$ 1,326.99
|
历史佣金总计:$ {{ (inviteStats.friendly_count / 100).toFixed(2) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
<div class="h-[50px] w-full rounded-[32px] bg-[#222222] px-4 leading-[50px] font-medium">
|
||||||
历史推荐人数:123人
|
历史推荐人数:{{ inviteStats.history_count }}人
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -84,6 +84,10 @@ const userInfo = ref({
|
|||||||
share_link: '',
|
share_link: '',
|
||||||
commission: 0,
|
commission: 0,
|
||||||
})
|
})
|
||||||
|
const inviteStats = ref({
|
||||||
|
friendly_count: 0,
|
||||||
|
history_count: 0,
|
||||||
|
})
|
||||||
const isUserLoading = ref(true)
|
const isUserLoading = ref(true)
|
||||||
async function init() {
|
async function init() {
|
||||||
// 1. 用户信息 & 设备列表
|
// 1. 用户信息 & 设备列表
|
||||||
@ -101,6 +105,11 @@ async function init() {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
isUserLoading.value = false
|
isUserLoading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 2. 邀请统计
|
||||||
|
request.get('/api/v1/public/user/invite/stats').then((res: any) => {
|
||||||
|
inviteStats.value = res
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@ -19,9 +19,9 @@ export default defineConfig({
|
|||||||
// 将所有以 /api 开头的请求转发到目标服务器
|
// 将所有以 /api 开头的请求转发到目标服务器
|
||||||
// 1. 匹配所有以 /public 开头的请求
|
// 1. 匹配所有以 /public 开头的请求
|
||||||
'/api/v1': {
|
'/api/v1': {
|
||||||
target: 'https://hifastvpn.com',
|
target: 'https://tapi.hifast.biz/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
autoRewrite: true,
|
autoRewrite: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user