feat: RBAC controlling
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
} from "@heroui/react";
|
||||
import { ArrowLeft, Pencil, PlugConnection } from "@gravity-ui/icons";
|
||||
import { api, type ChargePointDetail, type PaginatedTransactions } from "@/lib/api";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
|
||||
// ── Status maps ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -182,6 +183,9 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
cp?.lastHeartbeatAt != null &&
|
||||
Date.now() - new Date(cp.lastHeartbeatAt).getTime() < (cp.heartbeatInterval ?? 60) * 3 * 1000;
|
||||
|
||||
const { data: sessionData } = useSession();
|
||||
const isAdmin = sessionData?.user?.role === "admin";
|
||||
|
||||
// ── Render: loading / not found ──────────────────────────────────────────
|
||||
|
||||
if (loading) {
|
||||
@@ -249,15 +253,18 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" onPress={openEdit}>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button size="sm" variant="secondary" onPress={openEdit}>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Device info */}
|
||||
{/* Device info — admin only */}
|
||||
{isAdmin && (
|
||||
<div className="rounded-xl border border-border bg-surface p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-foreground">设备信息</h2>
|
||||
<dl className="divide-y divide-border">
|
||||
@@ -280,8 +287,10 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Operation info */}
|
||||
{/* Operation info — admin only */}
|
||||
{isAdmin && (
|
||||
<div className="rounded-xl border border-border bg-surface p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-foreground">运行配置</h2>
|
||||
<dl className="divide-y divide-border">
|
||||
@@ -354,6 +363,38 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fee info — user only */}
|
||||
{!isAdmin && (
|
||||
<div className="rounded-xl border border-border bg-surface p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-foreground">电价信息</h2>
|
||||
<dl className="divide-y divide-border">
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<dt className="shrink-0 text-sm text-muted">单位电价</dt>
|
||||
<dd className="text-sm text-foreground">
|
||||
{cp.feePerKwh > 0 ? (
|
||||
<span>
|
||||
<span className="font-semibold">¥{(cp.feePerKwh / 100).toFixed(2)}</span>
|
||||
<span className="ml-1 text-xs text-muted">/kWh</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-success font-medium">免费</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<dt className="shrink-0 text-sm text-muted">充电桥状态</dt>
|
||||
<dd>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`size-2 rounded-full ${isOnline ? "bg-success animate-pulse" : "bg-muted"}`} />
|
||||
<span className="text-sm text-foreground">{isOnline ? "在线" : "离线"}</span>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connectors */}
|
||||
@@ -506,91 +547,93 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</div>
|
||||
|
||||
{/* Edit modal */}
|
||||
<Modal
|
||||
isOpen={editOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!editBusy) setEditOpen(open);
|
||||
}}
|
||||
>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container scroll="outside">
|
||||
<Modal.Dialog className="sm:max-w-md">
|
||||
<Modal.CloseTrigger />
|
||||
<Modal.Header>
|
||||
<Modal.Heading>编辑充电桩</Modal.Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">品牌</Label>
|
||||
<Input
|
||||
placeholder="Unknown"
|
||||
value={editForm.chargePointVendor}
|
||||
onChange={(e) =>
|
||||
setEditForm((f) => ({ ...f, chargePointVendor: e.target.value }))
|
||||
{isAdmin && (
|
||||
<Modal
|
||||
isOpen={editOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!editBusy) setEditOpen(open);
|
||||
}}
|
||||
>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container scroll="outside">
|
||||
<Modal.Dialog className="sm:max-w-md">
|
||||
<Modal.CloseTrigger />
|
||||
<Modal.Header>
|
||||
<Modal.Heading>编辑充电桩</Modal.Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">品牌</Label>
|
||||
<Input
|
||||
placeholder="Unknown"
|
||||
value={editForm.chargePointVendor}
|
||||
onChange={(e) =>
|
||||
setEditForm((f) => ({ ...f, chargePointVendor: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">型号</Label>
|
||||
<Input
|
||||
placeholder="Unknown"
|
||||
value={editForm.chargePointModel}
|
||||
onChange={(e) =>
|
||||
setEditForm((f) => ({ ...f, chargePointModel: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">注册状态</Label>
|
||||
<Select
|
||||
fullWidth
|
||||
selectedKey={editForm.registrationStatus}
|
||||
onSelectionChange={(key) =>
|
||||
setEditForm((f) => ({
|
||||
...f,
|
||||
registrationStatus: String(key) as EditForm["registrationStatus"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Popover>
|
||||
<ListBox>
|
||||
<ListBox.Item id="Accepted">Accepted</ListBox.Item>
|
||||
<ListBox.Item id="Pending">Pending</ListBox.Item>
|
||||
<ListBox.Item id="Rejected">Rejected</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Popover>
|
||||
</Select>
|
||||
</div>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">电价(分/kWh)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="0"
|
||||
value={editForm.feePerKwh}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, feePerKwh: e.target.value }))}
|
||||
/>
|
||||
</TextField>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">型号</Label>
|
||||
<Input
|
||||
placeholder="Unknown"
|
||||
value={editForm.chargePointModel}
|
||||
onChange={(e) =>
|
||||
setEditForm((f) => ({ ...f, chargePointModel: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">注册状态</Label>
|
||||
<Select
|
||||
fullWidth
|
||||
selectedKey={editForm.registrationStatus}
|
||||
onSelectionChange={(key) =>
|
||||
setEditForm((f) => ({
|
||||
...f,
|
||||
registrationStatus: String(key) as EditForm["registrationStatus"],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Popover>
|
||||
<ListBox>
|
||||
<ListBox.Item id="Accepted">Accepted</ListBox.Item>
|
||||
<ListBox.Item id="Pending">Pending</ListBox.Item>
|
||||
<ListBox.Item id="Rejected">Rejected</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Popover>
|
||||
</Select>
|
||||
</div>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">电价(分/kWh)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="0"
|
||||
value={editForm.feePerKwh}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, feePerKwh: e.target.value }))}
|
||||
/>
|
||||
</TextField>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onPress={() => setEditOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isDisabled={editBusy} onPress={handleEditSubmit}>
|
||||
{editBusy ? <Spinner size="sm" /> : "保存"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onPress={() => setEditOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isDisabled={editBusy} onPress={handleEditSubmit}>
|
||||
{editBusy ? <Spinner size="sm" /> : "保存"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user