refactor(transactions): extract responsive data list
This commit is contained in:
@@ -46,6 +46,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { DataList } from "@/components/data-list"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -294,6 +295,7 @@ export function TransactionsView({
|
||||
|
||||
<Button
|
||||
render={<Link href="/bookkeeping" />}
|
||||
nativeButton={false}
|
||||
size="default"
|
||||
className="w-full shadow-xs sm:w-auto"
|
||||
>
|
||||
@@ -361,18 +363,21 @@ export function TransactionsView({
|
||||
</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 列表表头 */}
|
||||
<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">
|
||||
<span>商户与描述</span>
|
||||
<span>结算渠道</span>
|
||||
<span className="text-right">发生金额 (原币)</span>
|
||||
<span className="text-right">入账金额 (折算)</span>
|
||||
<span className="text-center">场景</span>
|
||||
<span />
|
||||
</div>
|
||||
<DataList.Header>
|
||||
<DataList.Head>商户与描述</DataList.Head>
|
||||
<DataList.Head>结算渠道</DataList.Head>
|
||||
<DataList.Head align="right">发生金额 (原币)</DataList.Head>
|
||||
<DataList.Head align="right">入账金额 (折算)</DataList.Head>
|
||||
<DataList.Head align="center">场景</DataList.Head>
|
||||
<DataList.Head />
|
||||
</DataList.Header>
|
||||
|
||||
<div className="divide-y divide-border/60">
|
||||
<DataList.Body>
|
||||
{group.items.map((txn) => {
|
||||
const ch = txn.channelDetails?.[0]
|
||||
const isMultiCurrency =
|
||||
@@ -435,12 +440,9 @@ export function TransactionsView({
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<DataList.Row key={txn.id}>
|
||||
{/* 移动端顶栏 (商户 + 更多操作在最右侧) / 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 size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground md:hidden">
|
||||
{renderChannelLogoOrIcon(ch)}
|
||||
@@ -474,23 +476,25 @@ export function TransactionsView({
|
||||
<div className="shrink-0 md:hidden">
|
||||
{actionMenu}
|
||||
</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">
|
||||
{renderChannelLogoOrIcon(ch)}
|
||||
</div>
|
||||
<span className="truncate text-muted-foreground">
|
||||
{ch?.displayName || "未关联渠道"}
|
||||
</span>
|
||||
</div>
|
||||
</DataList.Cell>
|
||||
|
||||
{/* 发生金额 (原币) */}
|
||||
<div className="flex items-baseline justify-between md:block md:text-right">
|
||||
<span className="text-xs text-muted-foreground md:hidden">
|
||||
发生金额
|
||||
</span>
|
||||
<DataList.Cell
|
||||
label="发生金额"
|
||||
labelClassName="text-xs"
|
||||
align="right"
|
||||
className="flex items-baseline justify-between md:block"
|
||||
>
|
||||
<div className="flex items-baseline gap-1 md:justify-end">
|
||||
<span
|
||||
className={`font-mono text-base font-semibold tracking-tight ${
|
||||
@@ -506,13 +510,14 @@ export function TransactionsView({
|
||||
{txn.txnCcy}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DataList.Cell>
|
||||
|
||||
{/* 入账金额 (折算) */}
|
||||
<div className="flex items-baseline justify-between text-xs md:block md:text-right">
|
||||
<span className="text-muted-foreground md:hidden">
|
||||
入账折算
|
||||
</span>
|
||||
<DataList.Cell
|
||||
label="入账折算"
|
||||
align="right"
|
||||
className="flex items-baseline justify-between text-xs md:block"
|
||||
>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
{isMultiCurrency ? (
|
||||
<>
|
||||
@@ -537,25 +542,27 @@ export function TransactionsView({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DataList.Cell>
|
||||
|
||||
{/* 场景徽标 */}
|
||||
<div className="flex items-center justify-between md:justify-center">
|
||||
<span className="text-xs text-muted-foreground md:hidden">
|
||||
场景类型
|
||||
</span>
|
||||
<DataList.Cell
|
||||
label="场景类型"
|
||||
labelClassName="text-xs"
|
||||
align="center"
|
||||
className="flex items-center justify-between md:justify-center"
|
||||
>
|
||||
{getSceneBadge(txn.txnScene, txn.dcFlag)}
|
||||
</div>
|
||||
</DataList.Cell>
|
||||
|
||||
{/* PC 端操作按钮 */}
|
||||
<div className="hidden md:flex md:justify-end">
|
||||
<DataList.Cell className="hidden md:flex md:justify-end">
|
||||
{actionMenu}
|
||||
</div>
|
||||
</div>
|
||||
</DataList.Cell>
|
||||
</DataList.Row>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DataList.Body>
|
||||
</DataList>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
@@ -579,7 +586,11 @@ export function TransactionsView({
|
||||
清除筛选
|
||||
</Button>
|
||||
) : (
|
||||
<Button render={<Link href="/bookkeeping" />} size="sm">
|
||||
<Button
|
||||
render={<Link href="/bookkeeping" />}
|
||||
nativeButton={false}
|
||||
size="sm"
|
||||
>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
前往记账
|
||||
</Button>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user