chore: format code
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import { ArrowLeft, Pencil, PlugConnection } from "@gravity-ui/icons";
|
import { ArrowLeft, Pencil, PlugConnection } from "@gravity-ui/icons";
|
||||||
import { api, type ChargePointDetail, type PaginatedTransactions } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
|
||||||
// ── Status maps ────────────────────────────────────────────────────────────
|
// ── Status maps ────────────────────────────────────────────────────────────
|
||||||
@@ -116,8 +116,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
|||||||
|
|
||||||
const txQuery = useQuery({
|
const txQuery = useQuery({
|
||||||
queryKey: ["chargePointTransactions", id, txPage],
|
queryKey: ["chargePointTransactions", id, txPage],
|
||||||
queryFn: () =>
|
queryFn: () => api.transactions.list({ page: txPage, limit: TX_LIMIT, chargePointId: id }),
|
||||||
api.transactions.list({ page: txPage, limit: TX_LIMIT, chargePointId: id }),
|
|
||||||
refetchInterval: 3_000,
|
refetchInterval: 3_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,7 +361,9 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
|||||||
<dt className="shrink-0 text-sm text-muted">充电桥状态</dt>
|
<dt className="shrink-0 text-sm text-muted">充电桥状态</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className={`size-2 rounded-full ${isOnline ? "bg-success animate-pulse" : "bg-muted"}`} />
|
<span
|
||||||
|
className={`size-2 rounded-full ${isOnline ? "bg-success animate-pulse" : "bg-muted"}`}
|
||||||
|
/>
|
||||||
<span className="text-sm text-foreground">{isOnline ? "在线" : "离线"}</span>
|
<span className="text-sm text-foreground">{isOnline ? "在线" : "离线"}</span>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@@ -337,7 +337,12 @@ export default function IdTagsPage() {
|
|||||||
const [deletingTag, setDeletingTag] = useState<string | null>(null);
|
const [deletingTag, setDeletingTag] = useState<string | null>(null);
|
||||||
const [claiming, setClaiming] = useState(false);
|
const [claiming, setClaiming] = useState(false);
|
||||||
|
|
||||||
const { data: idTagsData, isPending: loading, isFetching: refreshing, refetch } = useQuery({
|
const {
|
||||||
|
data: idTagsData,
|
||||||
|
isPending: loading,
|
||||||
|
isFetching: refreshing,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ["idTags"],
|
queryKey: ["idTags"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const [tagList, userList] = await Promise.all([
|
const [tagList, userList] = await Promise.all([
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from "react";
|
||||||
import Sidebar from '@/components/sidebar'
|
import Sidebar from "@/components/sidebar";
|
||||||
import { ReactQueryProvider } from '@/components/query-provider'
|
import { ReactQueryProvider } from "@/components/query-provider";
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -11,12 +11,10 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<main className="flex-1 overflow-y-auto pt-14 lg:pt-0">
|
<main className="flex-1 overflow-y-auto pt-14 lg:pt-0">
|
||||||
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||||
<ReactQueryProvider>
|
<ReactQueryProvider>{children}</ReactQueryProvider>
|
||||||
{children}
|
|
||||||
</ReactQueryProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,7 @@ function RecentTransactions({ txns }: { txns: Transaction[] }) {
|
|||||||
<span
|
<span
|
||||||
className={`flex size-7 shrink-0 items-center justify-center rounded-full ${active ? "bg-warning-soft" : "bg-success/10"}`}
|
className={`flex size-7 shrink-0 items-center justify-center rounded-full ${active ? "bg-warning-soft" : "bg-success/10"}`}
|
||||||
>
|
>
|
||||||
<Thunderbolt
|
<Thunderbolt className={`size-3.5 ${active ? "text-warning" : "text-success"}`} />
|
||||||
className={`size-3.5 ${active ? "text-warning" : "text-success"}`}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-foreground">
|
<p className="truncate text-sm font-medium text-foreground">
|
||||||
@@ -174,9 +172,7 @@ function ChargePointStatus({ cps }: { cps: ChargePoint[] }) {
|
|||||||
{chargingCount > 0 && (
|
{chargingCount > 0 && (
|
||||||
<p className="text-xs font-medium text-warning">{chargingCount} 充电中</p>
|
<p className="text-xs font-medium text-warning">{chargingCount} 充电中</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted">
|
<p className="text-xs text-muted">{online ? `${availableCount} 可用` : "离线"}</p>
|
||||||
{online ? `${availableCount} 可用` : "离线"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -283,9 +279,7 @@ export default function DashboardPage() {
|
|||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<StatusDot color="success" />
|
<StatusDot color="success" />
|
||||||
<span className="font-medium text-success">
|
<span className="font-medium text-success">{s?.onlineChargePoints ?? 0} 在线</span>
|
||||||
{s?.onlineChargePoints ?? 0} 在线
|
|
||||||
</span>
|
|
||||||
<span className="text-border">·</span>
|
<span className="text-border">·</span>
|
||||||
<span>{offlineCount} 离线</span>
|
<span>{offlineCount} 离线</span>
|
||||||
</>
|
</>
|
||||||
@@ -383,4 +377,3 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,27 +223,35 @@ export default function SettingsPage() {
|
|||||||
placeholder="对外显示的名称"
|
placeholder="对外显示的名称"
|
||||||
value={profileName}
|
value={profileName}
|
||||||
onChange={(e) => setProfileName(e.target.value)}
|
onChange={(e) => setProfileName(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter") void handleSaveProfile(); }}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") void handleSaveProfile();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TextField>
|
</TextField>
|
||||||
{profileError && (
|
{profileError && (
|
||||||
<Alert status="danger">
|
<Alert status="danger">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{profileError}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{profileError}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setProfileError("")} />
|
<CloseButton onPress={() => setProfileError("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{profileSuccess && (
|
{profileSuccess && (
|
||||||
<Alert status="success">
|
<Alert status="success">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{profileSuccess}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{profileSuccess}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setProfileSuccess("")} />
|
<CloseButton onPress={() => setProfileSuccess("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={savingProfile || !profileName.trim() || profileName === session?.user.name}
|
isDisabled={
|
||||||
|
savingProfile || !profileName.trim() || profileName === session?.user.name
|
||||||
|
}
|
||||||
onPress={handleSaveProfile}
|
onPress={handleSaveProfile}
|
||||||
>
|
>
|
||||||
{savingProfile ? <Spinner size="sm" color="current" /> : "保存"}
|
{savingProfile ? <Spinner size="sm" color="current" /> : "保存"}
|
||||||
@@ -292,20 +300,26 @@ export default function SettingsPage() {
|
|||||||
placeholder="再次输入新密码"
|
placeholder="再次输入新密码"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }}
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") void handleChangePassword();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TextField>
|
</TextField>
|
||||||
{pwError && (
|
{pwError && (
|
||||||
<Alert status="danger">
|
<Alert status="danger">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{pwError}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{pwError}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setPwError("")} />
|
<CloseButton onPress={() => setPwError("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{pwSuccess && (
|
{pwSuccess && (
|
||||||
<Alert status="success">
|
<Alert status="success">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{pwSuccess}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{pwSuccess}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setPwSuccess("")} />
|
<CloseButton onPress={() => setPwSuccess("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -369,7 +383,9 @@ export default function SettingsPage() {
|
|||||||
<div className="border-b border-border px-5 py-3">
|
<div className="border-b border-border px-5 py-3">
|
||||||
<Alert status="danger">
|
<Alert status="danger">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{error}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{error}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setError("")} />
|
<CloseButton onPress={() => setError("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,7 +394,9 @@ export default function SettingsPage() {
|
|||||||
<div className="border-b border-border px-5 py-3">
|
<div className="border-b border-border px-5 py-3">
|
||||||
<Alert status="success">
|
<Alert status="success">
|
||||||
<Alert.Indicator />
|
<Alert.Indicator />
|
||||||
<Alert.Content><Alert.Description>{success}</Alert.Description></Alert.Content>
|
<Alert.Content>
|
||||||
|
<Alert.Description>{success}</Alert.Description>
|
||||||
|
</Alert.Content>
|
||||||
<CloseButton onPress={() => setSuccess("")} />
|
<CloseButton onPress={() => setSuccess("")} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Button, Chip, Modal, Pagination, Spinner, Table } from "@heroui/react";
|
import { Button, Chip, Modal, Pagination, Spinner, Table } from "@heroui/react";
|
||||||
import { TrashBin } from "@gravity-ui/icons";
|
import { TrashBin } from "@gravity-ui/icons";
|
||||||
import { api, type PaginatedTransactions } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
|
||||||
const LIMIT = 15;
|
const LIMIT = 15;
|
||||||
@@ -27,7 +27,11 @@ export default function TransactionsPage() {
|
|||||||
const [stoppingId, setStoppingId] = useState<number | null>(null);
|
const [stoppingId, setStoppingId] = useState<number | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<number | null>(null);
|
const [deletingId, setDeletingId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data, isPending: loading, refetch } = useQuery({
|
const {
|
||||||
|
data,
|
||||||
|
isPending: loading,
|
||||||
|
refetch,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ["transactions", page, status],
|
queryKey: ["transactions", page, status],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
api.transactions.list({
|
api.transactions.list({
|
||||||
@@ -224,7 +228,8 @@ export default function TransactionsPage() {
|
|||||||
#{tx.id}
|
#{tx.id}
|
||||||
</span>
|
</span>
|
||||||
(储值卡:<span className="font-mono">{tx.idTag}</span>)。
|
(储值卡:<span className="font-mono">{tx.idTag}</span>)。
|
||||||
{!tx.stopTimestamp && "该记录仍进行中,删除同时将重置接口状态。"}
|
{!tx.stopTimestamp &&
|
||||||
|
"该记录仍进行中,删除同时将重置接口状态。"}
|
||||||
</p>
|
</p>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer className="flex justify-end gap-2">
|
<Modal.Footer className="flex justify-end gap-2">
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import "./globals.css";
|
|||||||
const fontSaira = Saira({
|
const fontSaira = Saira({
|
||||||
variable: "--font-saira",
|
variable: "--font-saira",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
})
|
});
|
||||||
|
|
||||||
const fontNotoSans = Noto_Sans({
|
const fontNotoSans = Noto_Sans({
|
||||||
variable: "--font-noto-sans",
|
variable: "--font-noto-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
})
|
});
|
||||||
|
|
||||||
const fontMono = Geist_Mono({
|
const fontMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-geist-mono",
|
||||||
@@ -19,7 +19,8 @@ const fontMono = Geist_Mono({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Helios EVCS",
|
title: "Helios EVCS",
|
||||||
description: "A modern EV charging station management system built with Next.js, Drizzle ORM, and OCPP.",
|
description:
|
||||||
|
"A modern EV charging station management system built with Next.js, Drizzle ORM, and OCPP.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export default function LoginPage() {
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<p className="mt-6 text-xs text-muted/60">OCPP 1.6-J Protocol • v0.1.0</p>
|
<p className="mt-6 text-xs text-muted/60">Helios EVCS</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
redirect('/dashboard')
|
redirect("/dashboard");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowRightFromSquare, PersonFill } from '@gravity-ui/icons'
|
import { ArrowRightFromSquare, PersonFill } from "@gravity-ui/icons";
|
||||||
import { signOut, useSession } from '@/lib/auth-client'
|
import { signOut, useSession } from "@/lib/auth-client";
|
||||||
|
|
||||||
export default function SidebarFooter() {
|
export default function SidebarFooter() {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
await signOut({ fetchOptions: { credentials: 'include' } })
|
await signOut({ fetchOptions: { credentials: "include" } });
|
||||||
router.push('/login')
|
router.push("/login");
|
||||||
router.refresh()
|
router.refresh();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-border p-3">
|
<div className="border-t border-border p-3">
|
||||||
@@ -27,7 +27,7 @@ export default function SidebarFooter() {
|
|||||||
{session.user.name || session.user.email}
|
{session.user.name || session.user.email}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs leading-tight text-muted capitalize">
|
<p className="truncate text-xs leading-tight text-muted capitalize">
|
||||||
{session.user.role ?? 'user'}
|
{session.user.role ?? "user"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,5 +44,5 @@ export default function SidebarFooter() {
|
|||||||
|
|
||||||
<p className="mt-2 px-2 text-[11px] text-muted/60">Helios EVCS</p>
|
<p className="mt-2 px-2 text-[11px] text-muted/60">Helios EVCS</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,40 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { CreditCard, Gear, ListCheck, Person, PlugConnection, Thunderbolt, Xmark, Bars } from '@gravity-ui/icons'
|
import {
|
||||||
import SidebarFooter from '@/components/sidebar-footer'
|
CreditCard,
|
||||||
import { useSession } from '@/lib/auth-client'
|
Gear,
|
||||||
|
ListCheck,
|
||||||
|
Person,
|
||||||
|
PlugConnection,
|
||||||
|
Thunderbolt,
|
||||||
|
Xmark,
|
||||||
|
Bars,
|
||||||
|
} from "@gravity-ui/icons";
|
||||||
|
import SidebarFooter from "@/components/sidebar-footer";
|
||||||
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/dashboard', label: '概览', icon: Thunderbolt, exact: true, adminOnly: false },
|
{ href: "/dashboard", label: "概览", icon: Thunderbolt, exact: true, adminOnly: false },
|
||||||
{ href: '/dashboard/charge-points', label: '充电桩', icon: PlugConnection, adminOnly: false },
|
{ href: "/dashboard/charge-points", label: "充电桩", icon: PlugConnection, adminOnly: false },
|
||||||
{ href: '/dashboard/transactions', label: '充电记录', icon: ListCheck, adminOnly: false },
|
{ href: "/dashboard/transactions", label: "充电记录", icon: ListCheck, adminOnly: false },
|
||||||
{ href: '/dashboard/id-tags', label: '储值卡', icon: CreditCard, adminOnly: false },
|
{ href: "/dashboard/id-tags", label: "储值卡", icon: CreditCard, adminOnly: false },
|
||||||
{ href: '/dashboard/users', label: '用户管理', icon: Person, adminOnly: true },
|
{ href: "/dashboard/users", label: "用户管理", icon: Person, adminOnly: true },
|
||||||
]
|
];
|
||||||
|
|
||||||
const settingsItems = [
|
const settingsItems = [{ href: "/dashboard/settings", label: "账号设置", icon: Gear }];
|
||||||
{ href: '/dashboard/settings', label: '账号设置', icon: Gear },
|
|
||||||
]
|
|
||||||
|
|
||||||
function NavContent({ pathname, isAdmin, onNavigate }: { pathname: string; isAdmin: boolean; onNavigate?: () => void }) {
|
function NavContent({
|
||||||
|
pathname,
|
||||||
|
isAdmin,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
pathname: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
onNavigate?: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
@@ -37,70 +52,68 @@ function NavContent({ pathname, isAdmin, onNavigate }: { pathname: string; isAdm
|
|||||||
<p className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-widest text-muted">
|
<p className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-widest text-muted">
|
||||||
管理
|
管理
|
||||||
</p>
|
</p>
|
||||||
{navItems.filter((item) => !item.adminOnly || isAdmin).map((item) => {
|
{navItems
|
||||||
|
.filter((item) => !item.adminOnly || isAdmin)
|
||||||
|
.map((item) => {
|
||||||
const isActive = item.exact
|
const isActive = item.exact
|
||||||
? pathname === item.href
|
? pathname === item.href
|
||||||
: pathname === item.href || pathname.startsWith(item.href + '/')
|
: pathname === item.href || pathname.startsWith(item.href + "/");
|
||||||
const Icon = item.icon
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
className={[
|
className={[
|
||||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? 'bg-accent/10 text-accent'
|
? "bg-accent/10 text-accent"
|
||||||
: 'text-muted hover:bg-surface-tertiary hover:text-foreground',
|
: "text-muted hover:bg-surface-tertiary hover:text-foreground",
|
||||||
].join(' ')}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<Icon className="size-4 shrink-0" />
|
<Icon className="size-4 shrink-0" />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
{isActive && (
|
{isActive && <span className="ml-auto h-1.5 w-1.5 rounded-full bg-accent" />}
|
||||||
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-accent" />
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
<p className="mb-1 mt-3 px-2 text-[11px] font-semibold uppercase tracking-widest text-muted">
|
<p className="mb-1 mt-3 px-2 text-[11px] font-semibold uppercase tracking-widest text-muted">
|
||||||
设置
|
设置
|
||||||
</p>
|
</p>
|
||||||
{settingsItems.map((item) => {
|
{settingsItems.map((item) => {
|
||||||
const isActive = pathname === item.href || pathname.startsWith(item.href + '/')
|
const isActive = pathname === item.href || pathname.startsWith(item.href + "/");
|
||||||
const Icon = item.icon
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
className={[
|
className={[
|
||||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? 'bg-accent/10 text-accent'
|
? "bg-accent/10 text-accent"
|
||||||
: 'text-muted hover:bg-surface-tertiary hover:text-foreground',
|
: "text-muted hover:bg-surface-tertiary hover:text-foreground",
|
||||||
].join(' ')}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<Icon className="size-4 shrink-0" />
|
<Icon className="size-4 shrink-0" />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
{isActive && (
|
{isActive && <span className="ml-auto h-1.5 w-1.5 rounded-full bg-accent" />}
|
||||||
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-accent" />
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<SidebarFooter />
|
<SidebarFooter />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false);
|
||||||
const { data: sessionData } = useSession()
|
const { data: sessionData } = useSession();
|
||||||
const isAdmin = sessionData?.user?.role === "admin"
|
const isAdmin = sessionData?.user?.role === "admin";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -124,18 +137,15 @@ export default function Sidebar() {
|
|||||||
|
|
||||||
{/* Mobile drawer overlay */}
|
{/* Mobile drawer overlay */}
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div className="fixed inset-0 z-40 bg-black/50 lg:hidden" onClick={() => setOpen(false)} />
|
||||||
className="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile drawer */}
|
{/* Mobile drawer */}
|
||||||
<aside
|
<aside
|
||||||
className={[
|
className={[
|
||||||
'fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-border bg-surface-secondary transition-transform duration-300 lg:hidden',
|
"fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-border bg-surface-secondary transition-transform duration-300 lg:hidden",
|
||||||
open ? 'translate-x-0' : '-translate-x-full',
|
open ? "translate-x-0" : "-translate-x-full",
|
||||||
].join(' ')}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -153,5 +163,5 @@ export default function Sidebar() {
|
|||||||
<NavContent pathname={pathname} isAdmin={isAdmin} />
|
<NavContent pathname={pathname} isAdmin={isAdmin} />
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,12 +156,15 @@ export const api = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
}),
|
}),
|
||||||
update: (id: string, data: {
|
update: (
|
||||||
|
id: string,
|
||||||
|
data: {
|
||||||
feePerKwh?: number;
|
feePerKwh?: number;
|
||||||
registrationStatus?: "Accepted" | "Pending" | "Rejected";
|
registrationStatus?: "Accepted" | "Pending" | "Rejected";
|
||||||
chargePointVendor?: string;
|
chargePointVendor?: string;
|
||||||
chargePointModel?: string;
|
chargePointModel?: string;
|
||||||
}) =>
|
},
|
||||||
|
) =>
|
||||||
apiFetch<ChargePoint>(`/api/charge-points/${id}`, {
|
apiFetch<ChargePoint>(`/api/charge-points/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
@@ -170,7 +173,12 @@ export const api = {
|
|||||||
apiFetch<{ success: true }>(`/api/charge-points/${id}`, { method: "DELETE" }),
|
apiFetch<{ success: true }>(`/api/charge-points/${id}`, { method: "DELETE" }),
|
||||||
},
|
},
|
||||||
transactions: {
|
transactions: {
|
||||||
list: (params?: { page?: number; limit?: number; status?: "active" | "completed"; chargePointId?: string }) => {
|
list: (params?: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
status?: "active" | "completed";
|
||||||
|
chargePointId?: string;
|
||||||
|
}) => {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
if (params?.page) q.set("page", String(params.page));
|
if (params?.page) q.set("page", String(params.page));
|
||||||
if (params?.limit) q.set("limit", String(params.limit));
|
if (params?.limit) q.set("limit", String(params.limit));
|
||||||
|
|||||||
Reference in New Issue
Block a user