feat: add grid view for IdTagsPage and toggle button
This commit is contained in:
@@ -24,9 +24,11 @@ import {
|
|||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import { parseDate } from "@internationalized/date";
|
import { parseDate } from "@internationalized/date";
|
||||||
import { ArrowRotateRight, Pencil, Plus, TrashBin } from "@gravity-ui/icons";
|
import { ArrowRotateRight, Pencil, Plus, TrashBin } from "@gravity-ui/icons";
|
||||||
|
import { LayoutGrid, List } from "lucide-react";
|
||||||
import { api, type IdTag, type UserRow } from "@/lib/api";
|
import { api, type IdTag, type UserRow } from "@/lib/api";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import dayjs from "@/lib/dayjs";
|
import dayjs from "@/lib/dayjs";
|
||||||
|
import { IdTagCard } from "@/components/id-tag-card";
|
||||||
|
|
||||||
const statusColorMap: Record<string, "success" | "danger" | "warning"> = {
|
const statusColorMap: Record<string, "success" | "danger" | "warning"> = {
|
||||||
Accepted: "success",
|
Accepted: "success",
|
||||||
@@ -396,6 +398,7 @@ export default function IdTagsPage() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deletingTag, setDeletingTag] = useState<string | null>(null);
|
const [deletingTag, setDeletingTag] = useState<string | null>(null);
|
||||||
const [claiming, setClaiming] = useState(false);
|
const [claiming, setClaiming] = useState(false);
|
||||||
|
const [viewMode, setViewMode] = useState<"table" | "grid">("table");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: idTagsData,
|
data: idTagsData,
|
||||||
@@ -505,6 +508,19 @@ export default function IdTagsPage() {
|
|||||||
>
|
>
|
||||||
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
|
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onPress={() => setViewMode(viewMode === "table" ? "grid" : "table")}
|
||||||
|
aria-label={viewMode === "table" ? "切换到卡片视图" : "切换到列表视图"}
|
||||||
|
>
|
||||||
|
{viewMode === "table" ? (
|
||||||
|
<LayoutGrid className="size-4" />
|
||||||
|
) : (
|
||||||
|
<List className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Button size="sm" variant="secondary" onPress={openCreate}>
|
<Button size="sm" variant="secondary" onPress={openCreate}>
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
@@ -547,69 +563,46 @@ export default function IdTagsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table>
|
{/* ── Grid view ─────────────────────────────────────────── */}
|
||||||
<Table.ScrollContainer>
|
{(!isAdmin || viewMode === "grid") && (
|
||||||
<Table.Content aria-label="储值卡列表" className="min-w-200">
|
<>
|
||||||
<Table.Header>
|
{loading ? (
|
||||||
<Table.Column isRowHeader>卡号</Table.Column>
|
<div className="flex justify-center py-16">
|
||||||
<Table.Column>状态</Table.Column>
|
<Spinner />
|
||||||
<Table.Column>余额</Table.Column>
|
</div>
|
||||||
{isAdmin && <Table.Column>关联用户</Table.Column>}
|
) : tags.length === 0 ? (
|
||||||
<Table.Column>有效期</Table.Column>
|
<div className="rounded-2xl border border-border px-6 py-14 text-center text-sm text-muted">
|
||||||
<Table.Column>父卡号</Table.Column>
|
暂无储值卡
|
||||||
<Table.Column>创建时间</Table.Column>
|
</div>
|
||||||
{isAdmin && <Table.Column className="text-end">操作</Table.Column>}
|
) : (
|
||||||
</Table.Header>
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<Table.Body
|
|
||||||
renderEmptyState={() => (
|
|
||||||
<div className="py-8 text-center text-sm text-muted">
|
|
||||||
{loading ? "加载中…" : "暂无储值卡"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const owner = users.find((u) => u.id === tag.userId);
|
const owner = users.find((u) => u.id === tag.userId);
|
||||||
return (
|
return (
|
||||||
<Table.Row key={tag.idTag} id={tag.idTag}>
|
<div key={tag.idTag} className="space-y-2">
|
||||||
<Table.Cell className="font-mono font-medium">{tag.idTag}</Table.Cell>
|
<IdTagCard
|
||||||
<Table.Cell>
|
idTag={tag.idTag}
|
||||||
<Chip
|
balance={tag.balance}
|
||||||
color={statusColorMap[tag.status] ?? "warning"}
|
layout={tag.cardLayout ?? undefined}
|
||||||
size="sm"
|
skin={tag.cardSkin ?? undefined}
|
||||||
variant="soft"
|
/>
|
||||||
>
|
|
||||||
{tag.status}
|
|
||||||
</Chip>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="font-mono">¥{fenToYuan(tag.balance)}</Table.Cell>
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Table.Cell className="text-sm">
|
<div className="flex items-center justify-between px-1">
|
||||||
{owner ? (
|
<div className="flex min-w-0 items-center gap-1.5">
|
||||||
<span title={owner.email}>
|
<Chip
|
||||||
{owner.name ?? owner.username ?? owner.email}
|
color={statusColorMap[tag.status] ?? "warning"}
|
||||||
</span>
|
size="sm"
|
||||||
) : (
|
variant="soft"
|
||||||
<span className="text-muted">—</span>
|
>
|
||||||
)}
|
{tag.status}
|
||||||
</Table.Cell>
|
</Chip>
|
||||||
)}
|
{owner && (
|
||||||
<Table.Cell>
|
<span className="truncate text-xs text-muted">
|
||||||
{tag.expiryDate ? (
|
{owner.name ?? owner.username ?? owner.email}
|
||||||
dayjs(tag.expiryDate).format("YYYY/M/D")
|
</span>
|
||||||
) : (
|
)}
|
||||||
<span className="text-muted">—</span>
|
</div>
|
||||||
)}
|
<div className="flex shrink-0 gap-1">
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="font-mono">
|
|
||||||
{tag.parentIdTag ?? <span className="text-muted">—</span>}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="text-sm">
|
|
||||||
{dayjs(tag.createdAt).format("YYYY/M/D HH:mm:ss")}
|
|
||||||
</Table.Cell>
|
|
||||||
{isAdmin && (
|
|
||||||
<Table.Cell>
|
|
||||||
<div className="flex justify-end gap-1">
|
|
||||||
{/* Edit button */}
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
@@ -647,7 +640,6 @@ export default function IdTagsPage() {
|
|||||||
</Modal.Container>
|
</Modal.Container>
|
||||||
</Modal.Backdrop>
|
</Modal.Backdrop>
|
||||||
</Modal>
|
</Modal>
|
||||||
{/* Delete button */}
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={deletingTag === tag.idTag}
|
isDisabled={deletingTag === tag.idTag}
|
||||||
@@ -699,15 +691,180 @@ export default function IdTagsPage() {
|
|||||||
</Modal.Backdrop>
|
</Modal.Backdrop>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</Table.Cell>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Table.Row>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Table.Body>
|
</div>
|
||||||
</Table.Content>
|
)}
|
||||||
</Table.ScrollContainer>
|
</>
|
||||||
</Table>
|
)}
|
||||||
|
|
||||||
|
{/* ── Table view (admin only) ────────────────────────────────── */}
|
||||||
|
{isAdmin && viewMode === "table" && (
|
||||||
|
<Table>
|
||||||
|
<Table.ScrollContainer>
|
||||||
|
<Table.Content aria-label="储值卡列表" className="min-w-200">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Column isRowHeader>卡号</Table.Column>
|
||||||
|
<Table.Column>状态</Table.Column>
|
||||||
|
<Table.Column>余额</Table.Column>
|
||||||
|
{isAdmin && <Table.Column>关联用户</Table.Column>}
|
||||||
|
<Table.Column>有效期</Table.Column>
|
||||||
|
<Table.Column>父卡号</Table.Column>
|
||||||
|
<Table.Column>创建时间</Table.Column>
|
||||||
|
{isAdmin && <Table.Column className="text-end">操作</Table.Column>}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body
|
||||||
|
renderEmptyState={() => (
|
||||||
|
<div className="py-8 text-center text-sm text-muted">
|
||||||
|
{loading ? "加载中…" : "暂无储值卡"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const owner = users.find((u) => u.id === tag.userId);
|
||||||
|
return (
|
||||||
|
<Table.Row key={tag.idTag} id={tag.idTag}>
|
||||||
|
<Table.Cell className="font-mono font-medium">{tag.idTag}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Chip
|
||||||
|
color={statusColorMap[tag.status] ?? "warning"}
|
||||||
|
size="sm"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
{tag.status}
|
||||||
|
</Chip>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-mono">¥{fenToYuan(tag.balance)}</Table.Cell>
|
||||||
|
{isAdmin && (
|
||||||
|
<Table.Cell className="text-sm">
|
||||||
|
{owner ? (
|
||||||
|
<span title={owner.email}>
|
||||||
|
{owner.name ?? owner.username ?? owner.email}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">—</span>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
)}
|
||||||
|
<Table.Cell>
|
||||||
|
{tag.expiryDate ? (
|
||||||
|
dayjs(tag.expiryDate).format("YYYY/M/D")
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">—</span>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-mono">
|
||||||
|
{tag.parentIdTag ?? <span className="text-muted">—</span>}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="text-sm">
|
||||||
|
{dayjs(tag.createdAt).format("YYYY/M/D HH:mm:ss")}
|
||||||
|
</Table.Cell>
|
||||||
|
{isAdmin && (
|
||||||
|
<Table.Cell>
|
||||||
|
<div className="flex justify-end gap-1">
|
||||||
|
{/* Edit button */}
|
||||||
|
<Modal>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="tertiary"
|
||||||
|
onPress={() => openEdit(tag)}
|
||||||
|
>
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Modal.Backdrop>
|
||||||
|
<Modal.Container scroll="outside">
|
||||||
|
<Modal.Dialog className="sm:max-w-105">
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Heading>编辑储值卡</Modal.Heading>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className="space-y-3">
|
||||||
|
<TagFormBody
|
||||||
|
form={form}
|
||||||
|
setForm={setForm}
|
||||||
|
isEdit={true}
|
||||||
|
users={users}
|
||||||
|
tags={tags}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer className="flex justify-end gap-2">
|
||||||
|
<Button slot="close" variant="ghost">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button isDisabled={saving} slot="close" onPress={handleSave}>
|
||||||
|
{saving ? <Spinner size="sm" /> : "保存"}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal.Dialog>
|
||||||
|
</Modal.Container>
|
||||||
|
</Modal.Backdrop>
|
||||||
|
</Modal>
|
||||||
|
{/* Delete button */}
|
||||||
|
<Modal>
|
||||||
|
<Button
|
||||||
|
isDisabled={deletingTag === tag.idTag}
|
||||||
|
isIconOnly
|
||||||
|
size="sm"
|
||||||
|
variant="danger-soft"
|
||||||
|
>
|
||||||
|
{deletingTag === tag.idTag ? (
|
||||||
|
<Spinner size="sm" />
|
||||||
|
) : (
|
||||||
|
<TrashBin className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Modal.Backdrop>
|
||||||
|
<Modal.Container scroll="outside">
|
||||||
|
<Modal.Dialog className="sm:max-w-96">
|
||||||
|
<Modal.CloseTrigger />
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Heading>确认删除储值卡</Modal.Heading>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p className="text-sm text-muted">
|
||||||
|
将永久删除储值卡{" "}
|
||||||
|
<span className="font-mono font-medium text-foreground">
|
||||||
|
{tag.idTag}
|
||||||
|
</span>
|
||||||
|
,此操作不可恢复。
|
||||||
|
</p>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer className="flex justify-end gap-2">
|
||||||
|
<Button slot="close" variant="ghost">
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
slot="close"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={deletingTag === tag.idTag}
|
||||||
|
onPress={() => handleDelete(tag.idTag)}
|
||||||
|
>
|
||||||
|
{deletingTag === tag.idTag ? (
|
||||||
|
<Spinner size="sm" />
|
||||||
|
) : (
|
||||||
|
"确认删除"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal.Dialog>
|
||||||
|
</Modal.Container>
|
||||||
|
</Modal.Backdrop>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
)}
|
||||||
|
</Table.Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Content>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user