feat: refactor dashboard pages to use new Dashboard components for improved structure and consistency
This commit is contained in:
@@ -4,6 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { RefreshCwIcon, SearchIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page"
|
||||
import { ListPagination } from "@/components/list-pagination"
|
||||
import { OptionCombobox } from "@/components/option-combobox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -67,21 +72,6 @@ function actionBadgeVariant(action: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function hitlBadgeVariant(status: string) {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "secondary" as const
|
||||
case "confirmed":
|
||||
return "default" as const
|
||||
case "cancelled":
|
||||
return "outline" as const
|
||||
case "expired":
|
||||
return "destructive" as const
|
||||
default:
|
||||
return "secondary" as const
|
||||
}
|
||||
}
|
||||
|
||||
export default function DashboardAgentRunLogsPage() {
|
||||
const [keywordInput, setKeywordInput] = useState("")
|
||||
const [plannedActionInput, setPlannedActionInput] = useState("all")
|
||||
@@ -174,8 +164,20 @@ export default function DashboardAgentRunLogsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-[minmax(0,1.6fr)_repeat(5,minmax(0,0.7fr))_auto_auto] xl:items-center">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void loadData()}
|
||||
disabled={loading}
|
||||
className="w-full xl:w-auto"
|
||||
>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="relative min-w-0">
|
||||
<SearchIcon className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -240,18 +242,23 @@ export default function DashboardAgentRunLogsPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void loadData()}
|
||||
disabled={loading}
|
||||
className="w-full xl:w-auto"
|
||||
>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新列表
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border bg-background">
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={setPage}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!loading && result.results.length === 0 ? (
|
||||
<div className="py-14 text-center text-sm text-muted-foreground">
|
||||
暂无 Agent 运行日志
|
||||
@@ -352,21 +359,8 @@ export default function DashboardAgentRunLogsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t px-4 py-3">
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={setPage}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
<AgentRunLogDetailDialog
|
||||
open={detailOpen}
|
||||
logId={activeLogId}
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
import { useCallback, useEffect, useState, type KeyboardEvent } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
DashboardTableShell,
|
||||
DashboardTableStateRow,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page";
|
||||
import { ListPagination } from "@/components/list-pagination";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -247,14 +252,15 @@ export default function DashboardAgentsPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 p-4 lg:p-6">
|
||||
<div className="flex h-full flex-col gap-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-lg font-semibold">
|
||||
{selectedTeam ? selectedTeam.name : "客服档案"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end">
|
||||
<div className="relative min-w-72">
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<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" />
|
||||
<Input
|
||||
value={displayNameInput}
|
||||
@@ -271,7 +277,7 @@ export default function DashboardAgentsPage() {
|
||||
onChange={(event) => setAgentCodeInput(event.target.value)}
|
||||
onKeyDown={handleFilterKeyDown}
|
||||
placeholder="按客服工号筛选"
|
||||
className="w-full xl:w-48"
|
||||
className="w-full sm:w-44"
|
||||
/>
|
||||
<Select
|
||||
value={statusFilterInput}
|
||||
@@ -279,7 +285,7 @@ export default function DashboardAgentsPage() {
|
||||
setStatusFilterInput(value ?? "all")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full xl:w-36">
|
||||
<SelectTrigger className="w-full sm:w-36">
|
||||
<SelectValue>
|
||||
{serviceStatusOptions.find(
|
||||
(item) => item.value === statusFilterInput,
|
||||
@@ -302,14 +308,23 @@ export default function DashboardAgentsPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 space-y-4">
|
||||
<div className="overflow-hidden rounded-2xl border bg-background">
|
||||
</DashboardToolbar>
|
||||
<DashboardTableShell
|
||||
className="min-h-0"
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/40">
|
||||
<TableRow>
|
||||
@@ -431,33 +446,21 @@ export default function DashboardAgentsPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!loading && result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="py-12 text-center text-muted-foreground"
|
||||
>
|
||||
{selectedTeam
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={5}
|
||||
loading={loading}
|
||||
loadingText="正在加载客服档案..."
|
||||
emptyText={
|
||||
selectedTeam
|
||||
? "当前客服组下没有匹配的客服档案"
|
||||
: "没有匹配的客服档案"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
: "没有匹配的客服档案"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,12 @@ import {
|
||||
} from "react";
|
||||
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";
|
||||
@@ -449,16 +455,33 @@ export default function DashboardAIAgentsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void loadData()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建 AI Agent
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
value={nameInput}
|
||||
onChange={(event) => setNameInput(event.target.value)}
|
||||
onKeyDown={handleFilterKeyDown}
|
||||
placeholder="按名称筛选"
|
||||
className="w-full xl:w-56"
|
||||
className="w-full sm:w-56"
|
||||
/>
|
||||
<div className="w-full xl:w-52">
|
||||
<div className="w-full sm:w-52">
|
||||
<OptionCombobox
|
||||
value={statusInput}
|
||||
options={statusOptions}
|
||||
@@ -472,21 +495,22 @@ export default function DashboardAIAgentsPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void loadData()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新列表
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建 AI Agent
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="rounded-xl border bg-card">
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
limit={result.page.limit}
|
||||
total={result.page.total}
|
||||
onPageChange={(nextPage) => setPage(nextPage)}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@@ -507,15 +531,13 @@ export default function DashboardAIAgentsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={9}
|
||||
className="py-12 text-center text-muted-foreground"
|
||||
>
|
||||
{loading ? "正在加载 AI Agent..." : "暂无 AI Agent"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={9}
|
||||
loading={loading}
|
||||
loadingText="正在加载 AI Agent..."
|
||||
emptyText="暂无 AI Agent"
|
||||
/>
|
||||
) : null}
|
||||
<SortableContext
|
||||
items={result.results.map((item) => item.id)}
|
||||
@@ -536,20 +558,8 @@ export default function DashboardAIAgentsPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
<div className="border-t px-4 py-3">
|
||||
<ListPagination
|
||||
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>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
|
||||
<EditDialog
|
||||
open={dialogOpen}
|
||||
|
||||
@@ -29,6 +29,12 @@ import {
|
||||
import { useCallback, useEffect, useState, type CSSProperties } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardTableStateRow,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page";
|
||||
import { ListPagination } from "@/components/list-pagination";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -498,9 +504,26 @@ export default function DashboardAIConfigsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end">
|
||||
<div className="relative min-w-72">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
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" />
|
||||
<Input
|
||||
value={keywordInput}
|
||||
@@ -510,7 +533,7 @@ export default function DashboardAIConfigsPage() {
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full xl:w-40">
|
||||
<div className="w-full sm:w-40">
|
||||
<OptionCombobox
|
||||
value={modelTypeFilterInput}
|
||||
options={modelTypeFilterOptions}
|
||||
@@ -520,7 +543,7 @@ export default function DashboardAIConfigsPage() {
|
||||
onChange={setModelTypeFilterInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full xl:w-40">
|
||||
<div className="w-full sm:w-40">
|
||||
<OptionCombobox
|
||||
value={providerFilterInput}
|
||||
options={providerFilterOptions}
|
||||
@@ -530,7 +553,7 @@ export default function DashboardAIConfigsPage() {
|
||||
onChange={setProviderFilterInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full xl:w-32">
|
||||
<div className="w-full sm:w-32">
|
||||
<OptionCombobox
|
||||
value={statusFilterInput}
|
||||
options={listStatusOptions}
|
||||
@@ -544,21 +567,19 @@ export default function DashboardAIConfigsPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void loadData()}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新列表
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="rounded-2xl border bg-card">
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
limit={result.page.limit}
|
||||
total={result.page.total}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@@ -578,24 +599,13 @@ export default function DashboardAIConfigsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={8}
|
||||
className="py-10 text-center text-muted-foreground"
|
||||
>
|
||||
正在加载 AI 配置...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={8}
|
||||
className="py-10 text-center text-muted-foreground"
|
||||
>
|
||||
暂无 AI 配置数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={8}
|
||||
loading={loading}
|
||||
loadingText="正在加载 AI 配置..."
|
||||
emptyText="暂无 AI 配置数据"
|
||||
/>
|
||||
) : (
|
||||
<SortableContext
|
||||
items={result.results.map((item) => item.id)}
|
||||
@@ -617,16 +627,8 @@ export default function DashboardAIConfigsPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
limit={result.page.limit}
|
||||
total={result.page.total}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
|
||||
<EditDialog
|
||||
open={dialogOpen}
|
||||
|
||||
@@ -12,6 +12,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { type CustomerFormSavePayload } from "@/components/customer-form";
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardTableStateRow,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page";
|
||||
import { ListPagination } from "@/components/list-pagination";
|
||||
import {
|
||||
OptionCombobox,
|
||||
@@ -257,9 +263,16 @@ export default function DashboardCustomersPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end">
|
||||
<div className="relative min-w-72">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<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" />
|
||||
<Input
|
||||
value={keywordInput}
|
||||
@@ -320,13 +333,23 @@ export default function DashboardCustomersPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<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>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -341,15 +364,13 @@ export default function DashboardCustomersPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.results.length === 0 && !loading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={8}
|
||||
className="py-10 text-center text-muted-foreground"
|
||||
>
|
||||
暂无客户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={8}
|
||||
loading={loading}
|
||||
loadingText="正在加载客户数据..."
|
||||
emptyText="暂无客户数据"
|
||||
/>
|
||||
) : (
|
||||
result.results.map((item) => {
|
||||
const actionLoading = actionLoadingId === item.id;
|
||||
@@ -441,20 +462,8 @@ export default function DashboardCustomersPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={result.page.limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
|
||||
<EditDialog
|
||||
open={dialogOpen}
|
||||
|
||||
@@ -5,6 +5,11 @@ import { useRouter } from "next/navigation"
|
||||
import { BellIcon, CheckCheckIcon, RefreshCwIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page"
|
||||
import { ListPagination } from "@/components/list-pagination"
|
||||
import { useNotifications } from "@/components/notification-provider"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -104,15 +109,10 @@ export default function DashboardNotificationsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4 md:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">通知中心</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
查看工单、会话等业务流转提醒
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
|
||||
<RefreshCwIcon className={cn(loading && "animate-spin")} />
|
||||
刷新
|
||||
@@ -125,10 +125,9 @@ export default function DashboardNotificationsPage() {
|
||||
<CheckCheckIcon />
|
||||
全部已读
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
</>
|
||||
}
|
||||
>
|
||||
{readStatusOptions.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
@@ -138,9 +137,20 @@ export default function DashboardNotificationsPage() {
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border bg-background">
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
limit={result.page.limit}
|
||||
total={result.page.total}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{result.results.length > 0 ? (
|
||||
<div className="divide-y">
|
||||
{result.results.map((item) => {
|
||||
@@ -172,16 +182,7 @@ export default function DashboardNotificationsPage() {
|
||||
{loading ? "正在加载通知" : "暂无通知"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
limit={result.page.limit}
|
||||
total={result.page.total}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
} from "lucide-react"
|
||||
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"
|
||||
@@ -362,9 +368,22 @@ export default function DashboardSkillsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-end">
|
||||
<div className="relative min-w-72">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
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" />
|
||||
<Input
|
||||
value={nameInput}
|
||||
@@ -379,9 +398,9 @@ export default function DashboardSkillsPage() {
|
||||
onChange={(event) => setCodeInput(event.target.value)}
|
||||
onKeyDown={handleFilterKeyDown}
|
||||
placeholder="按编码筛选"
|
||||
className="w-full xl:w-56"
|
||||
className="w-full sm:w-56"
|
||||
/>
|
||||
<div className="w-full xl:w-36">
|
||||
<div className="w-full sm:w-36">
|
||||
<OptionCombobox
|
||||
value={statusFilterInput}
|
||||
options={statusFilterOptions}
|
||||
@@ -395,18 +414,23 @@ export default function DashboardSkillsPage() {
|
||||
<SearchIcon />
|
||||
查询
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => void loadData()} disabled={loading}>
|
||||
<RefreshCwIcon className={loading ? "animate-spin" : ""} />
|
||||
刷新列表
|
||||
</Button>
|
||||
<Button onClick={openCreateDialog}>
|
||||
<PlusIcon />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-hidden rounded-2xl border bg-background">
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/40">
|
||||
<TableRow>
|
||||
@@ -417,12 +441,13 @@ export default function DashboardSkillsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{!loading && result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
|
||||
没有匹配的 Skill
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={4}
|
||||
loading={loading}
|
||||
loadingText="正在加载 Skill..."
|
||||
emptyText="没有匹配的 Skill"
|
||||
/>
|
||||
) : null}
|
||||
{result.results.map((item) => (
|
||||
<SkillRow
|
||||
@@ -438,22 +463,8 @@ export default function DashboardSkillsPage() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="border-t px-4 py-3">
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={(nextLimit) => {
|
||||
setLimit(nextLimit)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
|
||||
<EditDialog
|
||||
open={dialogOpen}
|
||||
|
||||
@@ -5,6 +5,12 @@ import { useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardTableStateRow,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page"
|
||||
import { OptionCombobox, type ComboboxOption } from "@/components/option-combobox"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -244,48 +250,47 @@ export default function TicketsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{quickViews.map((view) => (
|
||||
<button
|
||||
key={view.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition",
|
||||
quickView === view.key
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border bg-background hover:bg-muted",
|
||||
)}
|
||||
onClick={() => setQuickView(view.key)}
|
||||
>
|
||||
<span className="truncate font-medium">{view.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-1.5 py-0.5 text-[11px] font-semibold tabular-nums",
|
||||
quickView === view.key ? "bg-primary-foreground/20" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{view.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => void loadData()} disabled={loading}>
|
||||
<RefreshCcwIcon className={cn("size-4", loading ? "animate-spin" : "")} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="button" onClick={() => setCreateOpen(true)}>
|
||||
<PlusIcon className="size-4" />
|
||||
新建工单
|
||||
</Button>
|
||||
</div>
|
||||
<DashboardPage>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{quickViews.map((view) => (
|
||||
<button
|
||||
key={view.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition",
|
||||
quickView === view.key
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border bg-background hover:bg-muted",
|
||||
)}
|
||||
onClick={() => setQuickView(view.key)}
|
||||
>
|
||||
<span className="truncate font-medium">{view.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-1.5 py-0.5 text-[11px] font-semibold tabular-nums",
|
||||
quickView === view.key ? "bg-primary-foreground/20" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{view.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<>
|
||||
<Button type="button" variant="outline" onClick={() => void loadData()} disabled={loading}>
|
||||
<RefreshCcwIcon className={cn("size-4", loading ? "animate-spin" : "")} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="button" onClick={() => setCreateOpen(true)}>
|
||||
<PlusIcon className="size-4" />
|
||||
新建工单
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
className="w-full sm:w-72"
|
||||
placeholder="搜索编号、标题或描述"
|
||||
@@ -333,9 +338,9 @@ export default function TicketsPage() {
|
||||
<SearchIcon className="size-4" />
|
||||
查询
|
||||
</Button>
|
||||
</div>
|
||||
</DashboardToolbar>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<DashboardTableShell>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -348,11 +353,12 @@ export default function TicketsPage() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tickets.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="py-12 text-center text-sm text-muted-foreground">
|
||||
{loading ? "加载中..." : "暂无工单"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<DashboardTableStateRow
|
||||
colSpan={5}
|
||||
loading={loading}
|
||||
loadingText="正在加载工单..."
|
||||
emptyText="暂无工单"
|
||||
/>
|
||||
) : (
|
||||
tickets.map((ticket) => (
|
||||
<TableRow key={ticket.id}>
|
||||
@@ -408,7 +414,7 @@ export default function TicketsPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
|
||||
<EditDialog
|
||||
open={createOpen}
|
||||
@@ -428,6 +434,6 @@ export default function TicketsPage() {
|
||||
}}
|
||||
onChanged={loadData}
|
||||
/>
|
||||
</div>
|
||||
</DashboardPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ import {
|
||||
KeyRoundIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
ShieldIcon,
|
||||
UserRoundIcon,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import {
|
||||
DashboardPage,
|
||||
DashboardTableShell,
|
||||
DashboardTableStateRow,
|
||||
DashboardToolbar,
|
||||
} from "@/components/dashboard-page"
|
||||
import { ListPagination } from "@/components/list-pagination"
|
||||
import {
|
||||
assignUserRoles,
|
||||
createUser,
|
||||
@@ -45,7 +51,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ListPagination } from "@/components/list-pagination"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -294,13 +299,16 @@ export default function DashboardUsersPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col gap-6 p-4 lg:p-6">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
|
||||
<Button onClick={() => setCreatingOpen(true)} disabled={loading}>
|
||||
<PlusIcon />
|
||||
添加用户
|
||||
</Button>
|
||||
<div className="relative min-w-72">
|
||||
<DashboardPage>
|
||||
<DashboardToolbar
|
||||
actions={
|
||||
<Button onClick={() => setCreatingOpen(true)} disabled={loading}>
|
||||
<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" />
|
||||
<Input
|
||||
value={keywordInput}
|
||||
@@ -313,9 +321,19 @@ export default function DashboardUsersPage() {
|
||||
<Button variant="outline" onClick={applyFilters} disabled={loading}>
|
||||
查询
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-hidden rounded-2xl border bg-background">
|
||||
</DashboardToolbar>
|
||||
<DashboardTableShell
|
||||
pagination={
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={result.page.limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/40">
|
||||
<TableRow>
|
||||
@@ -428,26 +446,18 @@ export default function DashboardUsersPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!loading && result.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground">
|
||||
没有匹配的用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{loading || result.results.length === 0 ? (
|
||||
<DashboardTableStateRow
|
||||
colSpan={6}
|
||||
loading={loading}
|
||||
loadingText="正在加载用户数据..."
|
||||
emptyText="没有匹配的用户数据"
|
||||
/>
|
||||
) : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<ListPagination
|
||||
page={result.page.page}
|
||||
total={result.page.total}
|
||||
limit={result.page.limit}
|
||||
loading={loading}
|
||||
onPageChange={handlePageChange}
|
||||
onLimitChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardTableShell>
|
||||
</DashboardPage>
|
||||
<CreateUserDrawer
|
||||
open={creatingOpen}
|
||||
saving={savingCreate}
|
||||
|
||||
@@ -17,32 +17,6 @@ export function DashboardPage({
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user