压缩历史至 95081aff

This commit is contained in:
ryan
2026-06-08 20:34:27 +08:00
commit 8a782525de
435 changed files with 71146 additions and 0 deletions
@@ -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,
})}
/>
)
})