visionA 雲端版前端 — 沿用 local-tool 既有 UI(原則 4:先抄 local-tool)+
新增雲端特有的登入 / 配對 / 設定流程,含以下整合階段:
- Phase 0:13 頁 + 30+ 元件 + 雛形 banner
- dashboard / devices / models / workspace / clusters / settings 等頁
- AppShell + Sidebar + Header + tokens + i18n(中英雙語 96 keys)
- API client + 5 stores + 3 hooks
- Phase 0.6:OIDC redirect 改造
- login 頁改為 OIDC redirect(`window.location.href = /api/auth/login`)
- register 改說明頁、account 改唯讀(user 資料來源是 MC)
- api client 改 cookie session(credentials: include)+ 完全清掉 localStorage
- Phase 0.7:stage 部署 + nil guard
- getApiBaseUrl() 修:browser 環境視為 same-origin(與 login 頁一致)
- login 頁加「已登入 → router.replace('/')」effect
- User type email/name 改 optional(MC id_token 不一定回 email/name claim)
- header.tsx UserMenu displayName 4 層 fallback:name → email → id → i18n
- 雛形 banner 文案更新(已接 Innovedus 帳號中心)+ 版號 Phase 0.7
驗證:pnpm lint / test (125/125) / build 全綠
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* DeviceCard — 裝置卡片(雲端版)
|
||
*
|
||
* 來源:`local-tool/frontend/src/components/devices/device-card.tsx`(雲端版改造)
|
||
*
|
||
* 對齊:
|
||
* - `.autoflow/03-design/components.md` §5(Devices 元件)
|
||
* - `.autoflow/03-design/pages.md` §5(裝置列表)
|
||
* - `.autoflow/03-design/flows/flow-offline-handling.md` §4.1
|
||
*
|
||
* 改動:
|
||
* - 右上角狀態徽章改為 `RemoteDeviceBadge`(雲端版語意)
|
||
* - 離線(remoteStatus != online)時卡片 opacity-75 + 操作按鈕 disabled
|
||
* - 「工作區」按鈕只在 remoteStatus=online 時顯示(flow-offline-handling §4.1)
|
||
* - 移除 local-tool 的 connect/disconnect 按鈕(雲端版這些走 `/devices/[id]` 詳情頁操作)
|
||
* - 卡片本體包成 Link 到 `/devices/[id]`(design-review M3 統一 hover 規格)
|
||
*/
|
||
|
||
import Link from "next/link";
|
||
|
||
import { RemoteDeviceBadge } from "@/components/cloud/remote-device-badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { useT } from "@/lib/i18n/context";
|
||
import { cn } from "@/lib/utils";
|
||
import type { DeviceSummary } from "@/stores/device-store";
|
||
|
||
interface DeviceCardProps {
|
||
device: DeviceSummary;
|
||
}
|
||
|
||
export function DeviceCard({ device }: DeviceCardProps) {
|
||
const t = useT();
|
||
const displayName = device.alias || device.name;
|
||
const isOnline = device.remoteStatus === "online";
|
||
|
||
return (
|
||
<Card
|
||
data-testid="device-card"
|
||
data-remote-status={device.remoteStatus}
|
||
className={cn(
|
||
"transition-colors",
|
||
// 離線裝置 opacity-75(flow-offline-handling §4.1)
|
||
!isOnline && device.remoteStatus !== "reconnecting" && "opacity-75",
|
||
)}
|
||
>
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div className="min-w-0">
|
||
<CardTitle className="truncate text-base">{displayName}</CardTitle>
|
||
{device.alias && (
|
||
<p className="text-muted-foreground truncate text-xs">{device.name}</p>
|
||
)}
|
||
</div>
|
||
<RemoteDeviceBadge
|
||
status={device.remoteStatus}
|
||
lastSeenAt={device.lastSeenAt ?? null}
|
||
size="sm"
|
||
/>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||
<div>
|
||
<p className="text-muted-foreground">{t("devices.type")}</p>
|
||
<p className="font-medium">{device.type || "—"}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-muted-foreground">{t("devices.firmware")}</p>
|
||
<p className="font-medium">{device.firmwareVersion || t("common.na")}</p>
|
||
</div>
|
||
{device.flashedModel && (
|
||
<div className="col-span-2">
|
||
<p className="text-muted-foreground">{t("devices.flashedModel")}</p>
|
||
<p className="truncate font-medium">{device.flashedModel}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href={`/devices/${device.id}`}>
|
||
<Button size="sm" variant="outline">
|
||
{t("common.manage")}
|
||
</Button>
|
||
</Link>
|
||
{isOnline && device.flashedModel && (
|
||
<Link href={`/workspace/${device.id}`}>
|
||
<Button size="sm">{t("devices.openWorkspace")}</Button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|