jim800121chen 99dea42239 feat(visionA-frontend): Phase 0 → 0.7 雲端前端(Next.js + OIDC redirect 流程)
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>
2026-05-01 11:21:36 +08:00

97 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* DeviceCard — 裝置卡片(雲端版)
*
* 來源:`local-tool/frontend/src/components/devices/device-card.tsx`(雲端版改造)
*
* 對齊:
* - `.autoflow/03-design/components.md` §5Devices 元件)
* - `.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-75flow-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>
);
}