feat: refactor dashboard components to use new layout structure and improve UI consistency

This commit is contained in:
mlogclub
2026-05-23 23:16:01 +08:00
parent c5c3852a4b
commit aca50b9511
7 changed files with 454 additions and 311 deletions
+50 -36
View File
@@ -23,6 +23,12 @@ import {
type CreateAdminChannelPayload, type CreateAdminChannelPayload,
type PageResult, type PageResult,
} from "@/lib/api/admin" } from "@/lib/api/admin"
import {
DashboardPage,
DashboardTableShell,
DashboardTableStateRow,
DashboardToolbar,
} from "@/components/dashboard-page"
import { OptionCombobox } from "@/components/option-combobox" import { OptionCombobox } from "@/components/option-combobox"
import { EditDialog } from "./_components/edit" import { EditDialog } from "./_components/edit"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
@@ -205,16 +211,29 @@ export default function DashboardChannelsPage() {
return ( return (
<> <>
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 xl:flex-row xl:flex-wrap xl:items-center xl:justify-end"> <DashboardToolbar
actions={
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</>
}
>
<Input <Input
value={nameInput} value={nameInput}
onChange={(event) => setNameInput(event.target.value)} onChange={(event) => setNameInput(event.target.value)}
onKeyDown={handleFilterKeyDown} onKeyDown={handleFilterKeyDown}
placeholder="按渠道名称筛选" placeholder="按渠道名称筛选"
className="w-full xl:w-56" className="w-full sm:w-56"
/> />
<div className="relative min-w-72"> <div className="relative w-full sm:w-72">
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={channelIdInput} value={channelIdInput}
@@ -224,7 +243,7 @@ export default function DashboardChannelsPage() {
className="pl-9" className="pl-9"
/> />
</div> </div>
<div className="w-full xl:w-40"> <div className="w-full sm:w-40">
<OptionCombobox <OptionCombobox
value={channelTypeInput} value={channelTypeInput}
options={[...channelTypeOptions]} options={[...channelTypeOptions]}
@@ -234,7 +253,7 @@ export default function DashboardChannelsPage() {
onChange={setChannelTypeInput} onChange={setChannelTypeInput}
/> />
</div> </div>
<div className="w-full xl:w-36"> <div className="w-full sm:w-36">
<OptionCombobox <OptionCombobox
value={statusInput} value={statusInput}
options={[...statusOptions]} options={[...statusOptions]}
@@ -248,17 +267,23 @@ export default function DashboardChannelsPage() {
<SearchIcon /> <SearchIcon />
</Button> </Button>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}> </DashboardToolbar>
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</div>
<div className="rounded-xl border bg-card"> <DashboardTableShell
pagination={
<ListPagination
page={result.page.page}
limit={result.page.limit}
total={result.page.total}
loading={loading}
onPageChange={(nextPage) => setPage(nextPage)}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
}
>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -271,12 +296,13 @@ export default function DashboardChannelsPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{result.results.length === 0 ? ( {loading || result.results.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground"> colSpan={6}
{loading ? "正在加载接入渠道..." : "暂无接入渠道"} loading={loading}
</TableCell> loadingText="正在加载接入渠道..."
</TableRow> emptyText="暂无接入渠道"
/>
) : null} ) : null}
{result.results.map((item) => ( {result.results.map((item) => (
<TableRow key={item.id}> <TableRow key={item.id}>
@@ -338,20 +364,8 @@ export default function DashboardChannelsPage() {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<div className="border-t px-4 py-3"> </DashboardTableShell>
<ListPagination </DashboardPage>
page={result.page.page}
limit={result.page.limit}
total={result.page.total}
onPageChange={(nextPage) => setPage(nextPage)}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
</div>
</div>
</div>
<EditDialog <EditDialog
open={dialogOpen} open={dialogOpen}
+57 -57
View File
@@ -12,7 +12,14 @@ import {
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import {
DashboardPage,
DashboardTableShell,
DashboardTableStateRow,
DashboardToolbar,
} from "@/components/dashboard-page"
import { ListPagination } from "@/components/list-pagination" import { ListPagination } from "@/components/list-pagination"
import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group" import { ButtonGroup } from "@/components/ui/button-group"
@@ -23,13 +30,6 @@ 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Table, Table,
TableBody, TableBody,
@@ -62,13 +62,6 @@ const listStatusOptions = [
})), })),
] as const ] as const
function getStatusLabel(
value: string,
options: ReadonlyArray<{ value: string; label: string }>
) {
return options.find((item) => item.value === value)?.label ?? "请选择状态"
}
export default function DashboardCompaniesPage() { export default function DashboardCompaniesPage() {
const [nameInput, setNameInput] = useState("") const [nameInput, setNameInput] = useState("")
const [codeInput, setCodeInput] = useState("") const [codeInput, setCodeInput] = useState("")
@@ -194,9 +187,22 @@ export default function DashboardCompaniesPage() {
return ( return (
<> <>
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end"> <DashboardToolbar
<div className="relative min-w-72"> actions={
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</>
}
>
<div className="relative w-full sm:w-72">
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={nameInput} value={nameInput}
@@ -211,31 +217,37 @@ export default function DashboardCompaniesPage() {
onChange={(event) => setCodeInput(event.target.value)} onChange={(event) => setCodeInput(event.target.value)}
onKeyDown={handleFilterKeyDown} onKeyDown={handleFilterKeyDown}
placeholder="按公司编码筛选" placeholder="按公司编码筛选"
className="w-full xl:w-48" className="w-full sm:w-44"
/> />
<Select value={statusFilterInput} onValueChange={(v) => setStatusFilterInput(v ?? "all")}> <div className="w-full sm:w-36">
<SelectTrigger className="w-full xl:w-36"> <OptionCombobox
<SelectValue>{getStatusLabel(statusFilterInput, listStatusOptions)}</SelectValue> value={statusFilterInput}
</SelectTrigger> onChange={setStatusFilterInput}
<SelectContent> placeholder="全部状态"
{listStatusOptions.map((item) => ( options={[...listStatusOptions]}
<SelectItem key={item.value} value={item.value}> />
{item.label} </div>
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={applyFilters} disabled={loading}> <Button variant="outline" onClick={applyFilters} disabled={loading}>
<SearchIcon /> <SearchIcon />
</Button> </Button>
<Button onClick={openCreateDialog}> </DashboardToolbar>
<PlusIcon />
</Button>
</div>
<div className="overflow-hidden rounded-lg border bg-card"> <DashboardTableShell
pagination={
<ListPagination
page={result.page.page}
total={result.page.total}
limit={result.page.limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
}
>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -249,12 +261,13 @@ export default function DashboardCompaniesPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{result.results.length === 0 && !loading ? ( {loading || result.results.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={7} className="py-10 text-center text-muted-foreground"> colSpan={7}
loading={loading}
</TableCell> loadingText="正在加载公司数据..."
</TableRow> emptyText="暂无公司数据"
/>
) : ( ) : (
result.results.map((item) => { result.results.map((item) => {
const actionLoading = actionLoadingId === item.id const actionLoading = actionLoadingId === item.id
@@ -331,20 +344,8 @@ export default function DashboardCompaniesPage() {
)} )}
</TableBody> </TableBody>
</Table> </Table>
</div> </DashboardTableShell>
</DashboardPage>
<ListPagination
page={result.page.page}
total={result.page.total}
limit={result.page.limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
</div>
<EditDialog <EditDialog
open={dialogOpen} open={dialogOpen}
@@ -356,4 +357,3 @@ export default function DashboardCompaniesPage() {
</> </>
) )
} }
+57 -70
View File
@@ -5,22 +5,16 @@ import { KeyRoundIcon, RefreshCwIcon, RouteIcon, SearchIcon } from "lucide-react
import { toast } from "sonner" import { toast } from "sonner"
import { import {
fetchPermissions, DashboardPage,
type AdminPermission, DashboardTableShell,
type PageResult, DashboardTableStateRow,
} from "@/lib/api/admin" DashboardToolbar,
import { Status, StatusLabels } from "@/lib/generated/enums" } from "@/components/dashboard-page"
import { ListPagination } from "@/components/list-pagination"
import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { ListPagination } from "@/components/list-pagination"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Table, Table,
TableBody, TableBody,
@@ -29,6 +23,12 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import {
fetchPermissions,
type AdminPermission,
type PageResult,
} from "@/lib/api/admin"
import { Status, StatusLabels } from "@/lib/generated/enums"
const listStatusOptions = [ const listStatusOptions = [
{ value: "all", label: "全部状态" }, { value: "all", label: "全部状态" },
@@ -37,13 +37,6 @@ const listStatusOptions = [
{ value: String(Status.Deleted), label: StatusLabels[Status.Deleted] }, { value: String(Status.Deleted), label: StatusLabels[Status.Deleted] },
] as const ] as const
function getStatusLabel(
value: string,
options: ReadonlyArray<{ value: string; label: string }>
) {
return options.find((item) => item.value === value)?.label ?? "请选择状态"
}
export default function DashboardPermissionsPage() { export default function DashboardPermissionsPage() {
const [keywordInput, setKeywordInput] = useState("") const [keywordInput, setKeywordInput] = useState("")
const [groupNameInput, setGroupNameInput] = useState("") const [groupNameInput, setGroupNameInput] = useState("")
@@ -81,10 +74,6 @@ export default function DashboardPermissionsPage() {
void loadData() void loadData()
}, [loadData]) }, [loadData])
function handleStatusFilterChange(value: string | null) {
setStatusFilterInput(value ?? "all")
}
function applyFilters() { function applyFilters() {
setKeyword(keywordInput) setKeyword(keywordInput)
setGroupName(groupNameInput) setGroupName(groupNameInput)
@@ -109,9 +98,16 @@ export default function DashboardPermissionsPage() {
} }
return ( return (
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end"> <DashboardToolbar
<div className="relative min-w-72"> actions={
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
</Button>
}
>
<div className="relative w-full sm:w-72">
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={keywordInput} value={keywordInput}
@@ -126,34 +122,36 @@ export default function DashboardPermissionsPage() {
onChange={(event) => setGroupNameInput(event.target.value)} onChange={(event) => setGroupNameInput(event.target.value)}
onKeyDown={handleFilterKeyDown} onKeyDown={handleFilterKeyDown}
placeholder="按分组筛选" placeholder="按分组筛选"
className="w-full xl:w-48" className="w-full sm:w-44"
/> />
<Select <div className="w-full sm:w-36">
value={statusFilterInput} <OptionCombobox
onValueChange={handleStatusFilterChange} value={statusFilterInput}
> onChange={setStatusFilterInput}
<SelectTrigger className="w-full xl:w-36"> placeholder="全部状态"
<SelectValue>{getStatusLabel(statusFilterInput, listStatusOptions)}</SelectValue> options={[...listStatusOptions]}
</SelectTrigger> />
<SelectContent> </div>
{listStatusOptions.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={applyFilters} disabled={loading}> <Button variant="outline" onClick={applyFilters} disabled={loading}>
<SearchIcon /> <SearchIcon />
</Button> </Button>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}> </DashboardToolbar>
<RefreshCwIcon className={loading ? "animate-spin" : ""} /> <DashboardTableShell
pagination={
</Button> <ListPagination
</div> page={result.page.page}
<div className="space-y-4"> total={result.page.total}
<div className="overflow-hidden rounded-2xl border bg-background"> limit={limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
}
>
<Table> <Table>
<TableHeader className="bg-muted/40"> <TableHeader className="bg-muted/40">
<TableRow> <TableRow>
@@ -202,28 +200,17 @@ export default function DashboardPermissionsPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{!loading && result.results.length === 0 ? ( {loading || result.results.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={5} className="py-12 text-center text-muted-foreground"> colSpan={5}
loading={loading}
</TableCell> loadingText="正在加载权限数据..."
</TableRow> emptyText="没有匹配的权限数据"
/>
) : null} ) : null}
</TableBody> </TableBody>
</Table> </Table>
</div> </DashboardTableShell>
<ListPagination </DashboardPage>
page={result.page.page}
total={result.page.total}
limit={limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
</div>
</div>
) )
} }
+77 -84
View File
@@ -11,6 +11,32 @@ import {
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import {
DashboardPage,
DashboardTableShell,
DashboardTableStateRow,
DashboardToolbar,
} from "@/components/dashboard-page"
import { ListPagination } from "@/components/list-pagination"
import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { import {
createQuickReply, createQuickReply,
deleteQuickReply, deleteQuickReply,
@@ -23,32 +49,6 @@ import {
import { getEnumLabel, getEnumOptions } from "@/lib/enums" import { getEnumLabel, getEnumOptions } from "@/lib/enums"
import { Status, StatusLabels } from "@/lib/generated/enums" import { Status, StatusLabels } from "@/lib/generated/enums"
import { EditDialog } from "./_components/edit" import { EditDialog } from "./_components/edit"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { ListPagination } from "@/components/list-pagination"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const listStatusOptions = [ const listStatusOptions = [
{ value: "all", label: "全部状态" }, { value: "all", label: "全部状态" },
@@ -60,13 +60,6 @@ const listStatusOptions = [
})), })),
] as const ] as const
function getStatusLabel(
value: string,
options: ReadonlyArray<{ value: string; label: string }>
) {
return options.find((item) => item.value === value)?.label ?? "请选择状态"
}
export default function DashboardQuickRepliesPage() { export default function DashboardQuickRepliesPage() {
const [keywordInput, setKeywordInput] = useState("") const [keywordInput, setKeywordInput] = useState("")
const [groupNameInput, setGroupNameInput] = useState("") const [groupNameInput, setGroupNameInput] = useState("")
@@ -108,10 +101,6 @@ export default function DashboardQuickRepliesPage() {
void loadData() void loadData()
}, [loadData]) }, [loadData])
function handleStatusFilterChange(value: string | null) {
setStatusFilterInput(value ?? "all")
}
function applyFilters() { function applyFilters() {
setKeyword(keywordInput) setKeyword(keywordInput)
setGroupName(groupNameInput) setGroupName(groupNameInput)
@@ -221,9 +210,22 @@ export default function DashboardQuickRepliesPage() {
return ( return (
<> <>
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end"> <DashboardToolbar
<div className="relative min-w-72"> actions={
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : undefined} />
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</>
}
>
<div className="relative w-full sm:w-72">
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={keywordInput} value={keywordInput}
@@ -238,34 +240,36 @@ export default function DashboardQuickRepliesPage() {
onChange={(event) => setGroupNameInput(event.target.value)} onChange={(event) => setGroupNameInput(event.target.value)}
onKeyDown={handleFilterKeyDown} onKeyDown={handleFilterKeyDown}
placeholder="按分组筛选" placeholder="按分组筛选"
className="w-full xl:w-48" className="w-full sm:w-44"
/> />
<Select <div className="w-full sm:w-36">
value={statusFilterInput} <OptionCombobox
onValueChange={handleStatusFilterChange} value={statusFilterInput}
> onChange={setStatusFilterInput}
<SelectTrigger className="w-full xl:w-36"> placeholder="全部状态"
<SelectValue>{getStatusLabel(statusFilterInput, listStatusOptions)}</SelectValue> options={[...listStatusOptions]}
</SelectTrigger> />
<SelectContent> </div>
{listStatusOptions.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={applyFilters} disabled={loading}> <Button variant="outline" onClick={applyFilters} disabled={loading}>
<SearchIcon /> <SearchIcon />
</Button> </Button>
<Button onClick={openCreateDialog}> </DashboardToolbar>
<PlusIcon /> <DashboardTableShell
pagination={
</Button> <ListPagination
</div> page={result.page.page}
<div className="space-y-4"> total={result.page.total}
<div className="overflow-hidden rounded-2xl border bg-background"> limit={limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
}
>
<Table> <Table>
<TableHeader className="bg-muted/40"> <TableHeader className="bg-muted/40">
<TableRow> <TableRow>
@@ -282,7 +286,7 @@ export default function DashboardQuickRepliesPage() {
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell> <TableCell>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="mt-0.5 flex size-10 items-center justify-center rounded-2xl bg-muted text-muted-foreground"> <div className="mt-0.5 flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
<FileTextIcon className="size-4" /> <FileTextIcon className="size-4" />
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
@@ -345,29 +349,18 @@ export default function DashboardQuickRepliesPage() {
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{!loading && result.results.length === 0 ? ( {loading || result.results.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground"> colSpan={6}
loading={loading}
</TableCell> loadingText="正在加载快捷回复..."
</TableRow> emptyText="没有匹配的快捷回复"
/>
) : null} ) : null}
</TableBody> </TableBody>
</Table> </Table>
</div> </DashboardTableShell>
<ListPagination </DashboardPage>
page={result.page.page}
total={result.page.total}
limit={limit}
loading={loading}
onPageChange={handlePageChange}
onLimitChange={(nextLimit) => {
setLimit(nextLimit)
setPage(1)
}}
/>
</div>
</div>
<EditDialog <EditDialog
open={dialogOpen} open={dialogOpen}
saving={saving} saving={saving}
+53 -35
View File
@@ -41,6 +41,13 @@ import {
type PageResult, type PageResult,
updateRoleSort, updateRoleSort,
} from "@/lib/api/admin" } from "@/lib/api/admin"
import {
DashboardPage,
DashboardTableShell,
DashboardTableStateRow,
DashboardTableSummary,
DashboardToolbar,
} from "@/components/dashboard-page"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { AssignPermissionsDrawer } from "./_components/assign-permissions" import { AssignPermissionsDrawer } from "./_components/assign-permissions"
import { CreateRoleDrawer } from "./_components/create" import { CreateRoleDrawer } from "./_components/create"
@@ -311,23 +318,43 @@ export default function DashboardRolesPage() {
}, []) }, [])
return ( return (
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end"> <DashboardToolbar
<Button actions={
type="button" <>
onClick={() => setCreatingOpen(true)} <Button
disabled={loading || sorting} type="button"
> onClick={() => setCreatingOpen(true)}
<PlusIcon className="size-4" /> disabled={loading || sorting}
>
</Button> <PlusIcon className="size-4" />
<Button onClick={() => void loadRoles()} disabled={loading || sorting}>
<RefreshCwIcon className={cn((loading || sorting) && "animate-spin")} /> </Button>
<Button
</Button> variant="outline"
</div> onClick={() => void loadRoles()}
<div className="space-y-4"> disabled={loading || sorting}
<div className="overflow-hidden rounded-2xl border bg-background"> >
<RefreshCwIcon className={cn((loading || sorting) && "animate-spin")} />
</Button>
</>
}
>
<div className="text-sm text-muted-foreground">
</div>
</DashboardToolbar>
<DashboardTableShell
pagination={
<DashboardTableSummary>
<span>
{result.page.page} / {result.page.limit}
</span>
<span> {result.page.total} </span>
</DashboardTableSummary>
}
>
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
@@ -361,27 +388,18 @@ export default function DashboardRolesPage() {
/> />
))} ))}
</SortableContext> </SortableContext>
{!loading && result.results.length === 0 ? ( {loading || result.results.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={6}
colSpan={6} loading={loading}
className="py-12 text-center text-muted-foreground" loadingText="正在加载角色数据..."
> emptyText="暂无角色数据"
/>
</TableCell>
</TableRow>
) : null} ) : null}
</TableBody> </TableBody>
</Table> </Table>
</DndContext> </DndContext>
</div> </DashboardTableShell>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
{result.page.page} / {result.page.limit}
</span>
<span> {result.page.total} </span>
</div>
</div>
<AssignPermissionsDrawer <AssignPermissionsDrawer
open={!!assigningRole} open={!!assigningRole}
saving={savingPermissions} saving={savingPermissions}
@@ -398,6 +416,6 @@ export default function DashboardRolesPage() {
onOpenChange={handleCreateDrawerOpenChange} onOpenChange={handleCreateDrawerOpenChange}
onSubmit={handleCreateRole} onSubmit={handleCreateRole}
/> />
</div> </DashboardPage>
) )
} }
+39 -29
View File
@@ -44,6 +44,12 @@ import {
type Tag, type Tag,
type TagTree, type TagTree,
} from "@/lib/api/admin" } from "@/lib/api/admin"
import {
DashboardPage,
DashboardTableShell,
DashboardTableStateRow,
DashboardToolbar,
} from "@/components/dashboard-page"
import { EditDialog } from "./_components/edit" import { EditDialog } from "./_components/edit"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -531,9 +537,28 @@ export default function DashboardTagsPage() {
return ( return (
<> <>
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6"> <DashboardPage>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end"> <DashboardToolbar
<div className="relative min-w-72"> actions={
<>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
</Button>
<Button variant="outline" onClick={expandAll} disabled={loading}>
</Button>
<Button variant="outline" onClick={collapseAll} disabled={loading}>
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</>
}
>
<div className="relative w-full sm:w-72">
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
value={keywordInput} value={keywordInput}
@@ -558,23 +583,8 @@ export default function DashboardTagsPage() {
<SearchIcon /> <SearchIcon />
</Button> </Button>
<Button variant="outline" onClick={() => void loadData()} disabled={loading}> </DashboardToolbar>
<RefreshCwIcon className={loading ? "animate-spin" : ""} /> <DashboardTableShell>
</Button>
<Button variant="outline" onClick={expandAll} disabled={loading}>
</Button>
<Button variant="outline" onClick={collapseAll} disabled={loading}>
</Button>
<Button onClick={openCreateDialog}>
<PlusIcon />
</Button>
</div>
<div className="space-y-4">
<div className="overflow-hidden rounded-2xl border bg-background">
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
@@ -610,19 +620,19 @@ export default function DashboardTagsPage() {
/> />
))} ))}
</SortableContext> </SortableContext>
{!loading && flatList.length === 0 ? ( {loading || flatList.length === 0 ? (
<TableRow> <DashboardTableStateRow
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground"> colSpan={6}
loading={loading}
</TableCell> loadingText="正在加载标签..."
</TableRow> emptyText="没有匹配的标签"
/>
) : null} ) : null}
</TableBody> </TableBody>
</Table> </Table>
</DndContext> </DndContext>
</div> </DashboardTableShell>
</div> </DashboardPage>
</div>
<EditDialog <EditDialog
open={dialogOpen} open={dialogOpen}
saving={saving} saving={saving}
+121
View File
@@ -0,0 +1,121 @@
import type { ReactNode } from "react"
import { cn } from "@/lib/utils"
import { TableCell, TableRow } from "@/components/ui/table"
export function DashboardPage({
className,
children,
}: {
className?: string
children: ReactNode
}) {
return (
<div className={cn("flex flex-1 flex-col gap-5 p-4 lg:p-6", className)}>
{children}
</div>
)
}
export function DashboardPageHeader({
title,
description,
actions,
}: {
title: string
description?: ReactNode
actions?: ReactNode
}) {
return (
<div className="flex flex-col gap-3 border-b pb-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 space-y-1">
<h1 className="truncate text-xl font-semibold tracking-tight">{title}</h1>
{description ? (
<div className="text-sm leading-6 text-muted-foreground">
{description}
</div>
) : null}
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>
) : null}
</div>
)
}
export function DashboardToolbar({
className,
actions,
children,
}: {
className?: string
actions?: ReactNode
children: ReactNode
}) {
return (
<div
className={cn(
"flex flex-col gap-3 rounded-lg border bg-card p-3 lg:flex-row lg:items-center lg:justify-between",
className
)}
>
<div className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
{children}
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>
) : null}
</div>
)
}
export function DashboardTableShell({
className,
pagination,
children,
}: {
className?: string
pagination?: ReactNode
children: ReactNode
}) {
return (
<div className={cn("space-y-4", className)}>
<div className="overflow-hidden rounded-lg border bg-card text-card-foreground">
{children}
</div>
{pagination ? <div>{pagination}</div> : null}
</div>
)
}
export function DashboardTableStateRow({
colSpan,
loading,
loadingText = "正在加载数据...",
emptyText = "暂无数据",
}: {
colSpan: number
loading?: boolean
loadingText?: string
emptyText?: string
}) {
return (
<TableRow>
<TableCell colSpan={colSpan} className="py-12 text-center text-muted-foreground">
{loading ? loadingText : emptyText}
</TableCell>
</TableRow>
)
}
export function DashboardTableSummary({
children,
}: {
children: ReactNode
}) {
return (
<div className="flex items-center justify-between text-sm text-muted-foreground">
{children}
</div>
)
}