提现记录和退费数据展示
site-dist-deploy / build-and-deploy (push) Failing after 11m50s

This commit is contained in:
2026-06-18 11:23:09 +08:00
parent 11bc49d1e0
commit 104e04a8cc
9 changed files with 276 additions and 92 deletions
+15 -1
View File
@@ -17,12 +17,23 @@
<!-- Right Column --> <!-- Right Column -->
<div class="flex h-full w-[345px] flex-col gap-[20px]"> <div class="flex h-full w-[345px] flex-col gap-[20px]">
<ProxyData /> <ProxyData />
<div class="flex-1"> <div class="">
<SalesData @show-history="orderDetailsModalRef?.show()" /> <SalesData @show-history="orderDetailsModalRef?.show()" />
</div> </div>
<div class="">
<RefundData @show-history="refundDetailsModalRef?.show()" />
</div>
</div> </div>
<OrderDetailsModal ref="orderDetailsModalRef" /> <OrderDetailsModal ref="orderDetailsModalRef" />
<WithdrawalLogDialog
ref="refundDetailsModalRef"
title="退费记录"
biz-type="commission_refund"
amount-label="退费金额"
empty-text="暂无退费记录"
show-content
/>
</div> </div>
</template> </template>
@@ -33,9 +44,12 @@ import DownloadStats from '@/pages/UserCenter/components/DownloadStats/index.vue
import QuickTools from '@/pages/UserCenter/components/QuickTools/index.vue' import QuickTools from '@/pages/UserCenter/components/QuickTools/index.vue'
import ProxyData from '@/pages/UserCenter/components/ProxyData/index.vue' import ProxyData from '@/pages/UserCenter/components/ProxyData/index.vue'
import SalesData from '@/pages/UserCenter/components/SalesData/index.vue' import SalesData from '@/pages/UserCenter/components/SalesData/index.vue'
import RefundData from '@/pages/UserCenter/components/RefundData/index.vue'
import OrderDetailsModal from '@/pages/UserCenter/components/OrderDetails/index.vue' import OrderDetailsModal from '@/pages/UserCenter/components/OrderDetails/index.vue'
import WithdrawalLogDialog from '@/pages/UserCenter/components/UserInfo/components/WithdrawalLogDialog.vue'
const orderDetailsModalRef = ref<InstanceType<typeof OrderDetailsModal> | null>(null) const orderDetailsModalRef = ref<InstanceType<typeof OrderDetailsModal> | null>(null)
const refundDetailsModalRef = ref<InstanceType<typeof WithdrawalLogDialog> | null>(null)
</script> </script>
<style scoped></style> <style scoped></style>
@@ -139,7 +139,7 @@ function statusText(status: number) {
function formatTime(timestamp: number) { function formatTime(timestamp: number) {
if (!timestamp) return '-' if (!timestamp) return '-'
const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000) const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000)
return date.toLocaleString('en-US', { return date.toLocaleString('zh-CN', {
month: 'numeric', month: 'numeric',
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',
@@ -6,7 +6,7 @@
<div class="relative ml-1 pb-2 text-base font-bold text-white">本月链接转化</div> <div class="relative ml-1 pb-2 text-base font-bold text-white">本月链接转化</div>
</div> </div>
<div class="mt-2 flex flex-col gap-2"> <div class="flex flex-col">
<div <div
v-for="item in data" v-for="item in data"
:key="item.label" :key="item.label"
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="mt-2 text-xs text-white/40">相比前一个月 {{ growthRate }}</div> <div class="mt-1 text-xs text-white/40">相比前一个月 {{ growthRate }}</div>
</div> </div>
</template> </template>
@@ -0,0 +1,94 @@
<template>
<div
class="lucid-glass-bar flex h-full w-full flex-col rounded-4xl! border-1 border-white px-6 py-7"
>
<div class="mb-[20px] flex justify-between border-b-1 border-dashed pb-4">
<div>
<div class="mb-1 text-base font-bold text-white">退费数据</div>
</div>
<Button
variant="link"
class="h-auto p-0 text-sm font-medium text-white/60 underline hover:text-white"
@click="$emit('show-history')"
>
查看全部
</Button>
</div>
<div class="flex-1 overflow-y-auto pr-1">
<div v-if="loading" class="flex h-10 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="">
<div
v-for="(item, index) in salesPage.list"
:key="index"
class="mb-3 flex items-center justify-between last:mb-0"
>
<div class="flex flex-col">
<span class="text-sm font-semibold text-white">{{ item.id }}</span>
<span class="text-[10px] text-white/40">{{ formatTime(item.created_at) }}</span>
</div>
<div class="text-lg font-bold text-white tabular-nums">
$ {{ formatAmount(item.amount) }}
</div>
</div>
<div v-if="!salesPage.list.length">暂无数据</div>
</div>
</div>
</div>
</template>
<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[],
})
function formatAmount(amount: number) {
return ((amount || 0) / 100).toFixed(2)
}
function formatTime(timestamp: number) {
if (!timestamp) return '-'
const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000)
return date.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
}
const loading = ref(false)
async function fetchSales() {
loading.value = true
try {
const res: any = await request.get('/api/v1/public/user/withdrawal_log', {
page: 1,
size: 2,
biz_type: 'commission_refund',
})
salesPage.value = res
} catch (error) {
console.error('Fetch sales error:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchSales()
})
</script>
@@ -2,7 +2,7 @@
<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"
> >
<div class="mb-[20px] flex justify-between border-b-1 border-dashed pb-4"> <div class="mb-[10px] flex justify-between border-b-1 border-dashed pb-4">
<div> <div>
<div class="mb-1 text-base font-bold text-white">本月销售数据</div> <div class="mb-1 text-base font-bold text-white">本月销售数据</div>
<div class="text-sm text-white/40">本月已成交{{ salesPage.total }}</div> <div class="text-sm text-white/40">本月已成交{{ salesPage.total }}</div>
@@ -17,10 +17,7 @@
</div> </div>
<div class="flex-1 overflow-y-auto pr-1"> <div class="flex-1 overflow-y-auto pr-1">
<div <div v-if="loading" class="flex h-[100px] items-center justify-center">
v-if="loading && salesPage.list.length === 0"
class="flex h-20 items-center justify-center"
>
<div <div
class="h-6 w-6 animate-spin rounded-full border-2 border-[#ADFF5B] border-t-transparent" class="h-6 w-6 animate-spin rounded-full border-2 border-[#ADFF5B] border-t-transparent"
></div> ></div>
@@ -29,14 +26,15 @@
<div <div
v-for="(item, index) in salesPage.list" v-for="(item, index) in salesPage.list"
:key="index" :key="index"
class="mb-5 flex items-center justify-between last:mb-0" class="mb-3 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.user_hash }}</span> <span class="text-sm font-semibold text-white">{{ item.user_hash }}</span>
<span class="text-[10px] text-white/40">{{ formatTime(item.update_at) }}</span> <span class="text-[10px] text-white/40">{{ formatTime(item.updated_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>
<div v-if="!salesPage.list.length">暂无数据</div>
</div> </div>
</div> </div>
</div> </div>
@@ -69,7 +67,7 @@ async function fetchSales() {
const end_time = Math.floor(endOfMonth.getTime() / 1000) const end_time = Math.floor(endOfMonth.getTime() / 1000)
const res: any = await request.get('/api/v1/public/user/invite/sales', { const res: any = await request.get('/api/v1/public/user/invite/sales', {
page: 1, page: 1,
size: 8, size: 3,
start_time, start_time,
end_time, end_time,
}) })
@@ -36,7 +36,7 @@
:value="t" :value="t"
class="py-3 text-sm focus:bg-[#ADFF5B] focus:text-black" class="py-3 text-sm focus:bg-[#ADFF5B] focus:text-black"
> >
{{ t === 'USDT(TRC20)' ? 'USDT' : t }} {{ t }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -129,7 +129,10 @@
@click="fileInputRef?.click()" @click="fileInputRef?.click()"
> >
<template v-if="values.avatar"> <template v-if="values.avatar">
<img :src="values.avatar" class="max-h-[300px] w-full rounded-2xl object-contain" /> <img
:src="receiptPreviewUrl || values.avatar"
class="max-h-[300px] w-full rounded-2xl object-contain"
/>
<div <div
class="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity group-hover:opacity-100" class="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity group-hover:opacity-100"
> >
@@ -140,6 +143,13 @@
<Upload class="mb-3 h-12 w-12 text-white/20" /> <Upload class="mb-3 h-12 w-12 text-white/20" />
<span class="text-base font-medium text-white/30">点击上传高清收款二维码</span> <span class="text-base font-medium text-white/30">点击上传高清收款二维码</span>
</template> </template>
<div
v-if="isUploadingReceipt"
class="absolute inset-0 flex items-center justify-center bg-black/70"
>
<Loader2 class="mr-2 h-6 w-6 animate-spin text-[#ADFF5B]" />
<span class="text-sm font-bold text-white">上传中</span>
</div>
</div> </div>
<FormMessage class="ml-4 text-red-400" /> <FormMessage class="ml-4 text-red-400" />
</FormItem> </FormItem>
@@ -148,7 +158,7 @@
<div class="flex justify-center pt-2"> <div class="flex justify-center pt-2">
<Button <Button
type="submit" type="submit"
:disabled="isPending" :disabled="isPending || isUploadingReceipt"
class="h-[30px] w-[100px] rounded-full bg-[#ADFF5B] text-sm font-black text-black shadow-[0_10px_30px_rgba(173,255,91,0.3)] transition-all hover:scale-105 hover:bg-[#9ded4e] active:scale-95 disabled:opacity-50" class="h-[30px] w-[100px] rounded-full bg-[#ADFF5B] text-sm font-black text-black shadow-[0_10px_30px_rgba(173,255,91,0.3)] transition-all hover:scale-105 hover:bg-[#9ded4e] active:scale-95 disabled:opacity-50"
> >
<Loader2 v-if="isPending" class="mr-2 h-7 w-7 animate-spin" /> <Loader2 v-if="isPending" class="mr-2 h-7 w-7 animate-spin" />
@@ -189,64 +199,88 @@ const emit = defineEmits(['confirm'])
const open = ref(false) const open = ref(false)
const isPending = ref(false) // 手动管理加载状态 const isPending = ref(false) // 手动管理加载状态
const isUploadingReceipt = ref(false)
const receiptPreviewUrl = ref('')
const fileInputRef = ref<HTMLInputElement | null>(null) const fileInputRef = ref<HTMLInputElement | null>(null)
const ACCOUNT_TYPE = ['USDT(TRC20)', '微信', '支付宝'] as const const ACCOUNT_TYPE = ['USDT(TRC20)', '微信', '支付宝'] as const
const RECEIPT_UPLOAD_BIZ_TYPE = 'app-package'
const WITHDRAW_METHOD = {
支付宝: 1,
微信: 2,
'USDT(TRC20)': 3,
} as const
const compressImage = (file: File, quality = 0.7, maxWidth = 1024): Promise<string> => { type FileUploadResponse =
return new Promise((resolve, reject) => { | string
const reader = new FileReader() | {
reader.readAsDataURL(file) url?: string
reader.onload = (e) => { file_url?: string
const img = new Image() full_url?: string
img.src = e.target?.result as string path?: string
img.onload = () => { uri?: string
const canvas = document.createElement('canvas') file?: string
let width = img.width src?: string
let height = img.height
if (width > maxWidth) {
height = (maxWidth / width) * height
width = maxWidth
} }
canvas.width = width const revokeReceiptPreview = () => {
canvas.height = height if (receiptPreviewUrl.value) {
URL.revokeObjectURL(receiptPreviewUrl.value)
const ctx = canvas.getContext('2d') receiptPreviewUrl.value = ''
ctx?.drawImage(img, 0, 0, width, height)
const compressedBase64 = canvas.toDataURL('image/jpeg', quality)
resolve(compressedBase64)
} }
img.onerror = reject
} }
reader.onerror = reject
}) const getUploadedFileUrl = (data: FileUploadResponse) => {
if (typeof data === 'string') return data
return (
data.url ||
data.file_url ||
data.full_url ||
data.path ||
data.uri ||
data.file ||
data.src ||
''
)
}
const uploadReceiptCode = async (file: File) => {
const formData = new FormData()
formData.append('biz_type', RECEIPT_UPLOAD_BIZ_TYPE)
formData.append('file', file, file.name)
const data = await request.post<FormData, FileUploadResponse>(
'/api/v1/public/file/upload',
formData,
)
return getUploadedFileUrl(data)
} }
const onFileChange = async (e: Event) => { const onFileChange = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0] const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return if (!file) return
try { if (!file.type.startsWith('image/')) {
// 压缩图片 toast.error('请上传图片文件')
const compressedBase64 = await compressImage(file)
// 计算压缩后的大小
const base64Length = compressedBase64.split(',')[1].length
const sizeInBytes = base64Length * (3 / 4)
if (sizeInBytes > 2 * 1024 * 1024) {
// 限制 2MB
toast.error('图片过大,压缩后仍超过 2MB')
return return
} }
setFieldValue('avatar', compressedBase64) try {
isUploadingReceipt.value = true
const uploadedUrl = await uploadReceiptCode(file)
if (!uploadedUrl) {
toast.error('上传失败,请重试')
return
}
revokeReceiptPreview()
receiptPreviewUrl.value = URL.createObjectURL(file)
setFieldValue('avatar', uploadedUrl)
} catch (err) { } catch (err) {
console.error('图片处理失败:', err) console.error('收款码上传失败:', err)
toast.error('图片处理失败,请重试') toast.error('上传失败,请重试')
} finally { } finally {
isUploadingReceipt.value = false
// 重置 input,允许重新选择同一张图 // 重置 input,允许重新选择同一张图
if (e.target) { if (e.target) {
;(e.target as HTMLInputElement).value = '' ;(e.target as HTMLInputElement).value = ''
@@ -282,6 +316,7 @@ const { handleSubmit, resetForm, values, setFieldValue } = useForm({
// --- 暴露给父组件的方法 --- // --- 暴露给父组件的方法 ---
const show = () => { const show = () => {
revokeReceiptPreview()
resetForm() resetForm()
open.value = true open.value = true
} }
@@ -307,29 +342,16 @@ const onSubmit = handleSubmit(async (val) => {
return return
} }
// 3. 检查是否有未完成工单
const data = await request.get<any>('/api/v1/public/ticket/list', {
page: 1,
size: 1,
status: 1,
})
if (data?.list?.length > 0) {
toast.info('已有待处理申请')
return
}
// 3. 执行提现请求 // 3. 执行提现请求
const description = await request.post('/api/v1/public/user/commission_withdraw', {
val.type === 'USDT(TRC20)' ? `${val.type}-${val.account}` : `${val.type}-${val.avatar}` amount: Math.round(amount * 100),
method: WITHDRAW_METHOD[val.type],
await request.post('/api/v1/public/ticket/', { account: val.type === 'USDT(TRC20)' ? val.account : '',
title: `提现-${val.money}`, qr_code_url: val.type === 'USDT(TRC20)' ? '' : val.avatar,
description,
issue_type: 1,
}) })
// 4. 成功后处理 // 4. 成功后处理
toast.success('提交成功') toast.success('提现申请已提交,请等待审核')
open.value = false open.value = false
emit('confirm') // 触发父组件刷新 info 接口 emit('confirm') // 触发父组件刷新 info 接口
} catch (error) { } catch (error) {
@@ -25,9 +25,15 @@
</svg> </svg>
</button> </button>
<h2 class="mb-2 px-4 pt-8 text-center text-[20px] font-bold text-black">提现记录</h2> <h2 class="mb-2 px-4 pt-8 text-center text-[20px] font-bold text-black">{{ title }}</h2>
<WithdrawalLogList ref="logListRef" /> <WithdrawalLogList
ref="logListRef"
:biz-type="bizType"
:amount-label="amountLabel"
:empty-text="emptyText"
:show-content="showContent"
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -38,6 +44,23 @@ import { nextTick, ref } from 'vue'
import { Dialog, DialogContent } from '@/components/ui/dialog' import { Dialog, DialogContent } from '@/components/ui/dialog'
import WithdrawalLogList from './WithdrawalLogList.vue' import WithdrawalLogList from './WithdrawalLogList.vue'
withDefaults(
defineProps<{
title?: string
bizType?: string
amountLabel?: string
emptyText?: string
showContent?: boolean
}>(),
{
title: '提现记录',
bizType: '',
amountLabel: '提现金额',
emptyText: '暂无提现记录',
showContent: false,
},
)
const isOpen = ref(false) const isOpen = ref(false)
const logListRef = ref<InstanceType<typeof WithdrawalLogList> | null>(null) const logListRef = ref<InstanceType<typeof WithdrawalLogList> | null>(null)
@@ -10,7 +10,7 @@
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"
> >
暂无提现记录 {{ emptyText }}
</div> </div>
<div v-else class="space-y-[10px]"> <div v-else class="space-y-[10px]">
<div <div
@@ -18,9 +18,23 @@
:key="item.id" :key="item.id"
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 v-if="showContent" class="grid grid-cols-2 gap-y-2">
<div class="pl-4"> <div class="pl-4">
<div class="text-xs text-gray-500">提现金额</div> <div class="text-xs text-gray-500">{{ amountLabel }}</div>
<div class="font-bold text-[#222]">${{ formatAmount(item.amount) }}</div>
</div>
<div>
<div class="text-xs text-gray-500">退费时间</div>
<div class="whitespace-nowrap">{{ formatTime(item.created_at) }}</div>
</div>
<div class="col-span-2 px-4">
<div class="text-xs text-gray-500">说明</div>
<div class="break-words leading-snug">{{ item.content || '-' }}</div>
</div>
</div>
<div v-else class="grid grid-cols-2 gap-y-1">
<div class="pl-4">
<div class="text-xs text-gray-500">{{ amountLabel }}</div>
<div class="font-bold text-[#222]">${{ formatAmount(item.amount) }}</div> <div class="font-bold text-[#222]">${{ formatAmount(item.amount) }}</div>
</div> </div>
<div> <div>
@@ -94,11 +108,27 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import request from '@/utils/request' import request from '@/utils/request'
const props = withDefaults(
defineProps<{
bizType?: string
amountLabel?: string
emptyText?: string
showContent?: boolean
}>(),
{
bizType: '',
amountLabel: '提现金额',
emptyText: '暂无提现记录',
showContent: false,
},
)
interface WithdrawalLog { interface WithdrawalLog {
id: number id: number
amount: number amount: number
status: number status: number
method: number method: number
content?: string
reason?: string reason?: string
created_at: number created_at: number
} }
@@ -117,6 +147,7 @@ async function fetchLogs() {
const res: any = await request.get('/api/v1/public/user/withdrawal_log', { const res: any = await request.get('/api/v1/public/user/withdrawal_log', {
page: page.value, page: page.value,
size: size.value, size: size.value,
...(props.bizType ? { biz_type: props.bizType } : {}),
}) })
list.value = res.list || [] list.value = res.list || []
total.value = res.total || 0 total.value = res.total || 0
@@ -165,7 +196,7 @@ function formatAmount(amount: number) {
function formatTime(timestamp: number) { function formatTime(timestamp: number) {
if (!timestamp) return '-' if (!timestamp) return '-'
const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000) const date = new Date(timestamp > 10000000000 ? timestamp : timestamp * 1000)
return date.toLocaleString('en-US', { return date.toLocaleString('zh-CN', {
month: 'numeric', month: 'numeric',
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',
@@ -16,7 +16,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mt-7 mb-1 ml-[18px] font-semibold text-[#ADFF5B]">专属代理链接</div> <div class="mt-7 mb-1 ml-[18px] flex justify-between font-semibold text-[#ADFF5B]">
专属代理链接
<Button
variant="link"
class="mr-6 h-auto p-0 text-sm font-bold text-[#999] underline outline-none hover:text-[#666]"
@click="withdrawalLogDialogRef?.show()"
>
提现记录
</Button>
</div>
<div <div
class="mb-[10px] flex h-[50px] w-full items-center justify-between rounded-[32px] bg-[#ADFF5B] px-4 font-medium text-black" class="mb-[10px] flex h-[50px] w-full items-center justify-between rounded-[32px] bg-[#ADFF5B] px-4 font-medium text-black"
> >
@@ -27,15 +37,7 @@
class="flex min-h-[90px] w-full items-center justify-between rounded-[25px] bg-[#ADFF5B] px-4 font-medium text-black" class="flex min-h-[90px] w-full items-center justify-between rounded-[25px] bg-[#ADFF5B] px-4 font-medium text-black"
> >
<div> <div>
<div class="flex items-center text-xl font-semibold"> <div class="flex items-center text-xl font-semibold">佣金账户余额</div>
佣金账户余额<!--<Button
variant="link"
class="ml-2 h-auto p-0 text-sm font-bold text-black/55 underline hover:text-black"
@click="withdrawalLogDialogRef?.show()"
>
提现记录
</Button>-->
</div>
<div class="text-3xl font-black"> <div class="text-3xl font-black">
$ {{ (userInfo.commission / 100 || 0).toFixed(2) }} $ {{ (userInfo.commission / 100 || 0).toFixed(2) }}
</div> </div>