mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
压缩历史至 95081aff
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CountingNumber } from "@/components/animate-ui/primitives/texts/counting-number"
|
||||
import { BalanceReport } from "@/components/common/balance/balance-report"
|
||||
import { BalanceSummary } from "@/components/common/balance/balance-summary"
|
||||
import { BalanceTable } from "@/components/common/balance/balance-table"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
|
||||
/**
|
||||
* 积分余额主页面组件
|
||||
*
|
||||
* 负责组装积分余额页面的各个子组件,包括积分余额总览、积分余额摘要、近期活动和报告侧边栏
|
||||
*/
|
||||
export function BalanceMain() {
|
||||
const { user, loading } = useUser()
|
||||
|
||||
/** 计算总余额 */
|
||||
const totalBalance = React.useMemo(() => {
|
||||
return parseFloat(user?.available_balance || '0')
|
||||
}, [user?.available_balance])
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="flex items-center gap-2 pb-2">
|
||||
<h1 className="text-3xl">
|
||||
<span className="font-semibold">积分</span>
|
||||
<span className="pl-2">LDC</span>
|
||||
<span className="pl-2">
|
||||
{loading ? "-" : <CountingNumber number={totalBalance} decimalPlaces={2} />}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12 pt-4">
|
||||
<div className="lg:col-span-2 space-y-12">
|
||||
<section aria-labelledby="balance-summary-title">
|
||||
<h2 id="balance-summary-title" className="font-semibold mb-4">积分摘要</h2>
|
||||
<BalanceSummary />
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="recent-activity-title">
|
||||
<h2 id="recent-activity-title" className="font-semibold mb-2">近期活动</h2>
|
||||
<BalanceTable />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside className="lg:col-span-1" aria-labelledby="reports-title">
|
||||
<BalanceReport />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from "react"
|
||||
|
||||
/**
|
||||
* 余额报告组件
|
||||
*
|
||||
* 显示可下载或查看的报告列表
|
||||
*/
|
||||
export function BalanceReport() {
|
||||
return (
|
||||
<section className="space-y-4" aria-labelledby="reports-title">
|
||||
<h2 id="reports-title" className="font-semibold">报告</h2>
|
||||
<div className="text-sm text-muted-foreground">暂无报告</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
import { CountingNumber } from "@/components/animate-ui/primitives/texts/counting-number"
|
||||
import { useUser } from "@/contexts/user-context"
|
||||
|
||||
/** 颜色配置 - 统一管理主题色 */
|
||||
const COLORS = {
|
||||
available: "bg-primary/80",
|
||||
pending: "bg-zinc-400/80",
|
||||
emptyState: "bg-muted/50",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 计算积分余额百分比
|
||||
*/
|
||||
function calculatePercentages(available: number, total: number) {
|
||||
if (total <= 0) {
|
||||
return { available: 0, pending: 0 }
|
||||
}
|
||||
|
||||
const pending = total - available
|
||||
return {
|
||||
available: (available / total) * 100,
|
||||
pending: (pending / total) * 100,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全转换数字,避免 NaN
|
||||
*/
|
||||
function safeNumber(value: number): number {
|
||||
return typeof value === 'number' && !isNaN(value) ? value : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分余额摘要组件
|
||||
*
|
||||
* 显示积分余额的可视化摘要,包括可视化进度条和详细的积分余额分类列表
|
||||
*/
|
||||
export function BalanceSummary() {
|
||||
const { user, loading } = useUser()
|
||||
|
||||
const available = parseFloat(user?.available_balance || '0')
|
||||
const pending = parseFloat(user?.pending_balance || '0')
|
||||
const total = available + pending
|
||||
|
||||
const percentages = React.useMemo(
|
||||
() => calculatePercentages(available, total),
|
||||
[available, total]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={`w-full h-4 ${ COLORS.emptyState } rounded-sm overflow-hidden flex`}
|
||||
role="progressbar"
|
||||
aria-label="积分余额分布"
|
||||
aria-valuenow={percentages.available}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<div
|
||||
className={`${ COLORS.available } transition-all duration-300`}
|
||||
style={{ width: `${ percentages.available }%` }}
|
||||
title={`可用: ${ percentages.available.toFixed(1) }%`}
|
||||
/>
|
||||
<div
|
||||
className={`${ COLORS.pending } transition-all duration-300`}
|
||||
style={{ width: `${ percentages.pending }%` }}
|
||||
title={`未来积分: ${ percentages.pending.toFixed(1) }%`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs font-medium pb-2 border-b border-border/80">
|
||||
<span>积分类型</span>
|
||||
<span className="text-muted-foreground">值</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center font-bold text-sm pb-2 border-b border-border/80">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`size-2.5 ${ COLORS.available } rounded-xs`} aria-hidden="true" />
|
||||
<span>可用</span>
|
||||
</div>
|
||||
<span className="font-semibold">
|
||||
{loading ? "-" : (
|
||||
<CountingNumber
|
||||
number={safeNumber(available)}
|
||||
decimalPlaces={2}
|
||||
initiallyStable={true}
|
||||
inView={true}
|
||||
inViewOnce={true}
|
||||
/>
|
||||
)} LDC
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center font-bold text-sm pb-2 border-b border-border/80">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`size-2.5 ${ COLORS.pending } rounded-xs`} aria-hidden="true" />
|
||||
<span>未来积分</span>
|
||||
</div>
|
||||
<span className="font-semibold">
|
||||
{loading ? "-" : (
|
||||
<CountingNumber
|
||||
number={safeNumber(pending)}
|
||||
decimalPlaces={2}
|
||||
initiallyStable={true}
|
||||
inView={true}
|
||||
inViewOnce={true}
|
||||
/>
|
||||
)} LDC
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { TransactionTableList } from "@/components/common/general/table-data"
|
||||
import { TransactionProvider, useTransaction } from "@/contexts/transaction-context"
|
||||
import { DEFAULT_ORDER_TYPES, type OrderType } from "@/lib/services"
|
||||
import { formatLocalDate } from "@/lib/utils"
|
||||
|
||||
/** 标签触发器样式 */
|
||||
const TAB_TRIGGER_STYLES =
|
||||
"data-[state=active]:bg-transparent " +
|
||||
"data-[state=active]:shadow-none " +
|
||||
"data-[state=active]:border-0 " +
|
||||
"data-[state=active]:border-b-2 " +
|
||||
"data-[state=active]:border-indigo-500 " +
|
||||
"bg-transparent " +
|
||||
"rounded-none " +
|
||||
"border-0 " +
|
||||
"border-b-2 " +
|
||||
"border-transparent " +
|
||||
"px-0 " +
|
||||
"text-sm " +
|
||||
"font-bold " +
|
||||
"text-muted-foreground " +
|
||||
"data-[state=active]:text-indigo-500 " +
|
||||
"-mb-[2px] " +
|
||||
"relative " +
|
||||
"hover:text-foreground " +
|
||||
"transition-colors " +
|
||||
"flex-none"
|
||||
|
||||
/** 标签配置 - 数据驱动渲染 */
|
||||
const TABS = [
|
||||
{ value: "receive" as const, label: "积分收益" },
|
||||
{ value: "payment" as const, label: "积分消耗" },
|
||||
{ value: "community" as const, label: "社区划转" },
|
||||
{ value: "online" as const, label: "在线流转" },
|
||||
{ value: "all" as const, label: "所有活动" },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 计算最近30天的时间范围
|
||||
* 每次调用时重新计算,避免缓存过期时间
|
||||
*/
|
||||
function getTimeRange() {
|
||||
const now = new Date()
|
||||
now.setHours(23, 59, 59, 999)
|
||||
const start = new Date(now)
|
||||
start.setDate(start.getDate() - 30)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
return {
|
||||
startTime: formatLocalDate(start),
|
||||
endTime: formatLocalDate(now)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分余额活动表格组件
|
||||
*
|
||||
* 显示不同类型的积分余额活动记录,支持多标签切换和分页加载
|
||||
*/
|
||||
export function BalanceTable() {
|
||||
const [activeTab, setActiveTab] = React.useState<OrderType | "all">("all")
|
||||
const [timeRange, setTimeRange] = React.useState<{ startTime: string; endTime: string } | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeRange(getTimeRange())
|
||||
}, [])
|
||||
|
||||
if (!timeRange) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as OrderType | "all")}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="flex p-0 gap-4 rounded-none w-full bg-transparent justify-start border-b border-border overflow-x-auto overflow-y-hidden">
|
||||
{TABS.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={TAB_TRIGGER_STYLES}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-2">
|
||||
<TransactionProvider
|
||||
defaultParams={{
|
||||
page_size: 20,
|
||||
startTime: timeRange.startTime,
|
||||
endTime: timeRange.endTime,
|
||||
}}
|
||||
>
|
||||
<TransactionList
|
||||
type={activeTab === "all" ? undefined : activeTab}
|
||||
/>
|
||||
</TransactionProvider>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分余额活动列表组件
|
||||
*
|
||||
* 负责获取和显示积分余额活动数据
|
||||
*/
|
||||
const TransactionList = React.memo(function TransactionList({ type }: { type?: OrderType }) {
|
||||
const {
|
||||
transactions,
|
||||
loading,
|
||||
error,
|
||||
lastParams,
|
||||
fetchTransactions,
|
||||
} = useTransaction()
|
||||
|
||||
/** 当积分余额活动类型变化时重新加载数据 */
|
||||
React.useEffect(() => {
|
||||
fetchTransactions({
|
||||
page: 1,
|
||||
types: type ? [type] : DEFAULT_ORDER_TYPES,
|
||||
startTime: lastParams.startTime,
|
||||
endTime: lastParams.endTime,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [type])
|
||||
|
||||
return (
|
||||
<TransactionTableList
|
||||
loading={loading}
|
||||
error={error}
|
||||
transactions={transactions}
|
||||
onRetry={() => fetchTransactions({
|
||||
page: 1,
|
||||
types: type ? [type] : DEFAULT_ORDER_TYPES,
|
||||
startTime: lastParams.startTime,
|
||||
endTime: lastParams.endTime,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user