refactor(transactions): extract responsive data list

This commit is contained in:
2026-09-28 14:27:32 +08:00
parent 2982d560ce
commit 1d780ba4d6
2 changed files with 177 additions and 39 deletions
+50 -39
View File
@@ -46,6 +46,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { DataList } from "@/components/data-list"
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -294,6 +295,7 @@ export function TransactionsView({
<Button <Button
render={<Link href="/bookkeeping" />} render={<Link href="/bookkeeping" />}
nativeButton={false}
size="default" size="default"
className="w-full shadow-xs sm:w-auto" className="w-full shadow-xs sm:w-auto"
> >
@@ -361,18 +363,21 @@ export function TransactionsView({
</div> </div>
{/* 分组流水卡片容器 */} {/* 分组流水卡片容器 */}
<div className="overflow-hidden rounded-lg border border-border/70 bg-card shadow-xs"> <DataList
columns="minmax(200px,2fr) minmax(120px,1.2fr) minmax(140px,1.2fr) minmax(140px,1.2fr) 80px 48px"
className="shadow-xs"
>
{/* PC 列表表头 */} {/* PC 列表表头 */}
<div className="hidden grid-cols-[minmax(200px,2fr)_minmax(120px,1.2fr)_minmax(140px,1.2fr)_minmax(140px,1.2fr)_80px_48px] items-center gap-3 border-b bg-muted/30 px-4 py-2 text-[11px] font-medium text-muted-foreground md:grid"> <DataList.Header>
<span>商户与描述</span> <DataList.Head>商户与描述</DataList.Head>
<span>结算渠道</span> <DataList.Head>结算渠道</DataList.Head>
<span className="text-right">发生金额 (原币)</span> <DataList.Head align="right">发生金额 (原币)</DataList.Head>
<span className="text-right">入账金额 (折算)</span> <DataList.Head align="right">入账金额 (折算)</DataList.Head>
<span className="text-center">场景</span> <DataList.Head align="center">场景</DataList.Head>
<span /> <DataList.Head />
</div> </DataList.Header>
<div className="divide-y divide-border/60"> <DataList.Body>
{group.items.map((txn) => { {group.items.map((txn) => {
const ch = txn.channelDetails?.[0] const ch = txn.channelDetails?.[0]
const isMultiCurrency = const isMultiCurrency =
@@ -435,12 +440,9 @@ export function TransactionsView({
) )
return ( return (
<div <DataList.Row key={txn.id}>
key={txn.id}
className="flex flex-col gap-2 p-3 transition-colors hover:bg-muted/30 md:grid md:grid-cols-[minmax(200px,2fr)_minmax(120px,1.2fr)_minmax(140px,1.2fr)_minmax(140px,1.2fr)_80px_48px] md:items-center md:gap-3 md:px-4 md:py-3"
>
{/* 移动端顶栏 (商户 + 更多操作在最右侧) / PC 端商户描述 */} {/* 移动端顶栏 (商户 + 更多操作在最右侧) / PC 端商户描述 */}
<div className="flex min-w-0 items-center justify-between gap-2 md:block"> <DataList.Cell className="flex min-w-0 items-center justify-between gap-2 md:block">
<div className="flex min-w-0 items-center gap-2.5"> <div className="flex min-w-0 items-center gap-2.5">
<div className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground md:hidden"> <div className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground md:hidden">
{renderChannelLogoOrIcon(ch)} {renderChannelLogoOrIcon(ch)}
@@ -474,23 +476,25 @@ export function TransactionsView({
<div className="shrink-0 md:hidden"> <div className="shrink-0 md:hidden">
{actionMenu} {actionMenu}
</div> </div>
</div> </DataList.Cell>
{/* 结算渠道 */} {/* 结算渠道 */}
<div className="hidden min-w-0 items-center gap-2 text-xs md:flex"> <DataList.Cell className="hidden min-w-0 items-center gap-2 text-xs md:flex">
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"> <div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
{renderChannelLogoOrIcon(ch)} {renderChannelLogoOrIcon(ch)}
</div> </div>
<span className="truncate text-muted-foreground"> <span className="truncate text-muted-foreground">
{ch?.displayName || "未关联渠道"} {ch?.displayName || "未关联渠道"}
</span> </span>
</div> </DataList.Cell>
{/* 发生金额 (原币) */} {/* 发生金额 (原币) */}
<div className="flex items-baseline justify-between md:block md:text-right"> <DataList.Cell
<span className="text-xs text-muted-foreground md:hidden"> label="发生金额"
发生金额 labelClassName="text-xs"
</span> align="right"
className="flex items-baseline justify-between md:block"
>
<div className="flex items-baseline gap-1 md:justify-end"> <div className="flex items-baseline gap-1 md:justify-end">
<span <span
className={`font-mono text-base font-semibold tracking-tight ${ className={`font-mono text-base font-semibold tracking-tight ${
@@ -506,13 +510,14 @@ export function TransactionsView({
{txn.txnCcy} {txn.txnCcy}
</span> </span>
</div> </div>
</div> </DataList.Cell>
{/* 入账金额 (折算) */} {/* 入账金额 (折算) */}
<div className="flex items-baseline justify-between text-xs md:block md:text-right"> <DataList.Cell
<span className="text-muted-foreground md:hidden"> label="入账折算"
入账折算 align="right"
</span> className="flex items-baseline justify-between text-xs md:block"
>
<div className="flex flex-col items-end gap-0.5"> <div className="flex flex-col items-end gap-0.5">
{isMultiCurrency ? ( {isMultiCurrency ? (
<> <>
@@ -537,25 +542,27 @@ export function TransactionsView({
</span> </span>
)} )}
</div> </div>
</div> </DataList.Cell>
{/* 场景徽标 */} {/* 场景徽标 */}
<div className="flex items-center justify-between md:justify-center"> <DataList.Cell
<span className="text-xs text-muted-foreground md:hidden"> label="场景类型"
场景类型 labelClassName="text-xs"
</span> align="center"
className="flex items-center justify-between md:justify-center"
>
{getSceneBadge(txn.txnScene, txn.dcFlag)} {getSceneBadge(txn.txnScene, txn.dcFlag)}
</div> </DataList.Cell>
{/* PC 端操作按钮 */} {/* PC 端操作按钮 */}
<div className="hidden md:flex md:justify-end"> <DataList.Cell className="hidden md:flex md:justify-end">
{actionMenu} {actionMenu}
</div> </DataList.Cell>
</div> </DataList.Row>
) )
})} })}
</div> </DataList.Body>
</div> </DataList>
</section> </section>
))} ))}
</div> </div>
@@ -579,7 +586,11 @@ export function TransactionsView({
清除筛选 清除筛选
</Button> </Button>
) : ( ) : (
<Button render={<Link href="/bookkeeping" />} size="sm"> <Button
render={<Link href="/bookkeeping" />}
nativeButton={false}
size="sm"
>
<PlusIcon data-icon="inline-start" /> <PlusIcon data-icon="inline-start" />
前往记账 前往记账
</Button> </Button>
+127
View File
@@ -0,0 +1,127 @@
import * as React from "react"
import { cn } from "cn"
// Intended consumers: transactions, channels, and accounts.
export type DataListRootProps = React.ComponentProps<"div"> & {
columns: string
}
function DataListRoot({
className,
columns,
style,
...props
}: DataListRootProps) {
return (
<div
data-slot="data-list"
className={cn(
"overflow-hidden rounded-lg border border-border/70 bg-card",
className
)}
style={{ ...style, "--data-list-cols": columns } as React.CSSProperties}
{...props}
/>
)
}
function DataListHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="data-list-header"
className={cn(
"hidden grid-cols-[var(--data-list-cols)] items-center gap-3 border-b bg-muted/30 px-4 py-2 text-[11px] font-medium text-muted-foreground md:grid",
className
)}
{...props}
/>
)
}
export type DataListHeadProps = React.ComponentProps<"span"> & {
align?: "left" | "center" | "right"
}
function DataListHead({
align = "left",
className,
...props
}: DataListHeadProps) {
return (
<span
data-slot="data-list-head"
className={cn(
align === "right" && "text-right",
align === "center" && "text-center",
className
)}
{...props}
/>
)
}
function DataListBody({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="data-list-body"
className={cn("divide-y divide-border/60", className)}
{...props}
/>
)
}
function DataListRow({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="data-list-row"
className={cn(
"flex flex-col gap-2 p-3 transition-colors hover:bg-muted/30 md:grid md:grid-cols-[var(--data-list-cols)] md:items-center md:gap-3 md:px-4 md:py-3",
className
)}
{...props}
/>
)
}
export type DataListCellProps = React.ComponentProps<"div"> & {
label?: React.ReactNode
labelClassName?: string
align?: "left" | "center" | "right"
}
function DataListCell({
align = "left",
children,
className,
labelClassName,
label,
...props
}: DataListCellProps) {
return (
<div
data-slot="data-list-cell"
className={cn(
align === "right" && "md:text-right",
align === "center" && "md:text-center",
className
)}
{...props}
>
{label != null && (
<span className={cn("text-muted-foreground md:hidden", labelClassName)}>
{label}
</span>
)}
{children}
</div>
)
}
export const DataList = Object.assign(DataListRoot, {
Header: DataListHeader,
Head: DataListHead,
Body: DataListBody,
Row: DataListRow,
Cell: DataListCell,
})