feat: refactor dashboard components to use new layout structure and improve UI consistency
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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() {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user