From 413d5f0b101a09bf83b1c12ee0a631c0b430ddfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Tue, 23 Jun 2026 18:10:22 +0800 Subject: [PATCH] fix dockerfile unhealth problem --- haixun-backend/AGENTS.md | 83 +- haixun-backend/web/src/components/AcIcon.tsx | 81 ++ .../web/src/components/AuthDecor.tsx | 82 ++ .../web/src/components/AuthShell.tsx | 47 +- haixun-backend/web/src/components/Layout.tsx | 169 ++-- .../web/src/components/MobileBottomNav.tsx | 88 +- .../web/src/components/ThemeToggle.tsx | 39 +- haixun-backend/web/src/components/ui.tsx | 79 +- haixun-backend/web/src/index.css | 844 ++++++++++++++++-- haixun-backend/web/src/lib/acAssets.ts | 21 + .../web/src/pages/DashboardPage.tsx | 60 +- haixun-backend/web/src/pages/LoginPage.tsx | 19 +- haixun-backend/web/src/pages/RegisterPage.tsx | 18 +- 13 files changed, 1279 insertions(+), 351 deletions(-) create mode 100644 haixun-backend/web/src/components/AcIcon.tsx create mode 100644 haixun-backend/web/src/components/AuthDecor.tsx create mode 100644 haixun-backend/web/src/lib/acAssets.ts diff --git a/haixun-backend/AGENTS.md b/haixun-backend/AGENTS.md index 74d581e..311f67b 100644 --- a/haixun-backend/AGENTS.md +++ b/haixun-backend/AGENTS.md @@ -183,7 +183,36 @@ runner.RegisterStepHandler("analyze_8d", func(ctx context.Context, step job.Step ## 前端設計規則(`web/`) -巡樓 Console 前端在 `haixun-backend/web/`,風格參考 [simular.co](https://simular.co/):**明亮、年輕、圓角多、配色克制**。不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。 +巡樓 Console 前端在 `haixun-backend/web/`,視覺為**沉穩田園巡檢台**(動森感:天空、雲朵、奶油卡片、青綠 brand;**不是**任天堂 UI 複製)。 + +**字體固定** Inter + Taipei Sans TC;圖示僅 `AcIcon` / `AuthDecor` 內原創 SVG 線條圖。**禁止** emoji、貼圖 JPG、咖啡色木質頂欄、Nook / 任天堂命名。樣式集中在 `index.css`(`--hx-*` token + `hx-*` / `ac-*` / `auth-*` class)。 + +不要把舊 Next.js / `template-monorepo` UI 搬進來,也不要引入重型 UI 框架。 + +### 視覺架構(登入前後共用) + +```text +全頁背景 .hx-scene 灰藍天空 → 淡草地單一漸層(淺/深各一套,見 index.css) +裝飾層 SceneDecor 雲朵(多朵緩動)+ 淡光暈 + 小葉子;登入與 Layout 共用 +奶油卡片 .auth-ticket 2px line 邊框、圓角 2rem、surface 底、可選 .ac-dialog-texture 點陣 +頂部品牌列 圖示 .auth-ticket-icon(brand-soft 底)+ ink 標題;不用獨立色塊 ribbon +主內容 表單或 Outlet;內文頁用 PageTitle / Card(綠色 .ac-title-bar 僅內容區小標) +桌面側欄 .ac-pocket-device 掌上終端外框;PATROL PAD 狀態列;固定尺寸 + .ac-pocket-scroll 內捲 +手機 .ac-dock 底部最多 4 格 +「更多」sheet +``` + +| 區域 | 元件 | 關鍵 class | +|------|------|------------| +| 未登入 | `AuthShell` + `LoginPage` / `RegisterPage` | `hx-scene` `auth-scene` `auth-ticket` `auth-welcome` `auth-shell-form` | +| 已登入外殼 | `Layout` | `hx-scene` `ac-app-shell` `ac-app-header` `auth-ticket` `ac-app-main-inner` | +| 背景裝飾 | `AuthDecor.tsx` → `SceneDecor` | `hx-scene-deco` `auth-cloud--*` | +| 品牌小圖 | `AuthTicketIcon` | `auth-ticket-icon`(小屋+樹,原創 SVG) | +| 側欄導覽 | `Layout` + `navApps` | `ac-pocket-device` `ac-app-tile` | +| 手機導覽 | `MobileBottomNav` | `ac-dock` | + +**登入頁刻意不做的事**:上方不要獨立大色塊 header;表單上方**不要**再放 `ac-title-bar`「登入」大牌(品牌已在 `auth-welcome`)。註冊頁同理可省略重複大標。 + +**已淘汰、勿加回**:`ac-island`(改用 `hx-scene`)、`ac-wood-bar` / 咖啡色木質頂欄、`public/ac/` 貼圖、Nook Phone 文案。 ### 技術棧與指令 @@ -192,11 +221,11 @@ web/ src/ api/ # API client(envelope、JWT refresh) auth/ # AuthContext - components/ # Layout、ui、ThemeToggle、AuthShell + components/ # Layout、AuthShell、AuthDecor、ui、ThemeToggle、MobileBottomNav、AcIcon theme/ # ThemeContext(淺色 / 深色) pages/ # 路由頁面 - lib/ # jobStatus 等共用工具 - index.css # 設計 token 唯一來源 + lib/ # acAssets(導覽 icon key)、jobStatus 等 + index.css # 設計 token 與場景樣式唯一來源 ``` ```bash @@ -208,7 +237,7 @@ make web-build # tsc + vite build | 語言 | 字型 | 載入方式 | |------|------|----------| -| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`,在 `index.css` `@import` Light / Regular / Bold | +| 繁體中文 | **台北黑體 Taipei Sans TC** | npm `taipei-sans-tc`,在 `index.css` `@import` Regular + Bold | | 英文 | **Inter**(與 simular.co 相同,Google Fonts 免費) | `web/index.html` link | 規則: @@ -231,10 +260,26 @@ make web-build # tsc + vite build - `ThemeProvider`(`src/theme/ThemeContext.tsx`)包住 App;偏好存 `localStorage` key:`haixun.theme`(`light` | `dark`)。 - `index.html` 內嵌 script 在 React 載入前設定 `data-theme`,避免閃爍。 - 所有顏色必須走 CSS 變數 `--hx-*`,再映射到 Tailwind `@theme`(`bg-canvas`、`text-brand` 等)。 -- 切換按鈕用 `ThemeToggle`;Layout 頂欄與 `AuthShell` 都要有。 +- 切換按鈕用 `ThemeToggle`(`ac-btn-secondary` 樣式);`Layout` 頂欄與 `AuthShell` 右上角都要有。 - **禁止**在元件裡寫死 `bg-slate-*`、`text-emerald-*`、`bg-amber-*` 等 Tailwind 預設色;語意狀態用 `text-success` / `text-warning` / `text-danger` 或 `jobStatus.ts` 的 badge class。 -淺色預設明亮藍白底;深色為深藍黑底。兩套都只允許 **一個主色 brand(靛藍)** + success / warning / danger 語意色,不要再加褐色、墨綠、多種 accent 亂配。 +淺色:低飽和灰藍天空 + 灰綠草地 + 奶油 `surface` + **brand 青綠**;深色:黃昏低對比、同一套 token 自動切換。頂欄與卡片內品牌區都用 **surface / ink / brand**,不要再用木色 `#c4a882` 當 header 底。 + +### 場景與卡片 class(維護時對照) + +| Class | 用途 | +|-------|------| +| `.hx-scene` | 全頁天空→草地漸層(登入 + 已登入根節點) | +| `.hx-scene-deco` / `SceneDecor` | 背景雲、光暈、葉子(`pointer-events: none`) | +| `.auth-ticket` | 奶油主卡片外框(登入卡、已登入主內容區) | +| `.auth-welcome` | 卡片內品牌列:圖示 + 標題 + 一句 tagline,底部分隔線 | +| `.ac-app-header` | 已登入 sticky 頂欄:半透明 surface + blur,**非**木色 | +| `.ac-title-bar` | 內容區綠色小標題(裝置色漸層);用於 `PageTitle` 等,**不**用於登入頁表單上方大牌 | +| `.ac-pocket-device` | 側欄掌上終端;`--pocket-width`(28rem)、`--pocket-screen-height` 固定,內容在 `.ac-pocket-scroll` 捲動 | +| `.ac-app-tile` / `.ac-dock` | App 格導覽、手機底欄 | +| `.auth-shell-form` | 登入/註冊表單放大字級(僅 auth 頁) | + +側欄標示用 **PATROL PAD** 等中性英文裝飾字(`display-en`);圖示僅 `AcIcon` SVG。 ### 色彩 token(語意命名) @@ -262,7 +307,7 @@ make web-build # tsc + vite build --radius-pill 9999px Button、Badge、導覽 pill ``` -陰影用 utility:`shadow-card`(一般卡片)、`shadow-soft`(主按鈕、Hero)。Hero 背景用 class `hero-panel`;裝飾 blob 用 `glow-blob` / `glow-blob-alt`。 +陰影用 utility:`shadow-card`(一般卡片)、`shadow-soft`(主按鈕、Hero、`.auth-ticket`)。內容 Hero 可用 `ac-bulletin` + `ac-hero-gradient` token;全頁裝飾雲朵走 `SceneDecor`,不要另加會打架的強色 blob。 ### 共用元件(優先復用) @@ -289,12 +334,12 @@ make web-build # tsc + vite build ### 版面與導覽 -- 已登入(桌面):`Layout` = 左側欄 + 頂部 sticky bar(UID + `ThemeToggle`)+ `Outlet`。 -- 已登入(手機):頂欄品牌 + 主題切換;導覽走 `MobileBottomNav`。 -- 側欄分組:**工作區**(總覽、背景任務、排程、AI)、**管理**(模板、設定、會員、權限)。 -- Active 導覽:`bg-brand text-white shadow-soft`;hover:`bg-brand-soft text-brand`。 -- 未登入:`AuthShell` 置中卡片 + 右上主題切換;背景用柔和 blob,不要花俏插圖牆。 -- 語氣:年輕、直接、短句;Hero 可有一句主標 + brand 色強調詞,避免長篇企業八股。 +- 已登入(桌面):`Layout` = `hx-scene` 背景 + `SceneDecor` + `ac-app-header`(品牌 + 角色 chip + `ThemeToggle`)+ 左 `ac-pocket-device` + 右 `auth-ticket` 主內容 `Outlet`。 +- 已登入(手機):同上頂欄;導覽走 `MobileBottomNav`(總覽/任務/排程/更多)。 +- 側欄 App 來源:`src/lib/acAssets.ts` 的 `navApps`;圖示 key 對應 `AcIcon`。 +- Active 導覽:`ac-app-tile--active`(brand-soft 底 + brand 字色);hover:`bg-brand-soft text-brand`。 +- 未登入:`AuthShell` 置中 `auth-ticket` + 右上 `ThemeToggle`;`auth-welcome` 內品牌,表單緊接說明文字。 +- 語氣:年輕、直接、短句;可帶「島民」「巡樓」等原創文案,避免企業八股與任天堂用語。 ### API 與狀態 @@ -305,15 +350,17 @@ make web-build # tsc + vite build ### 新增頁面流程 -1. 在 `App.tsx` 掛路由(需登入的放在 `Layout` 底下)。 -2. 頁面用 `PageTitle` + `Card` + 既有元件;色票只引用 semantic token。 +1. 在 `App.tsx` 掛路由(需登入的放在 `Layout` 底下,自動享有 `hx-scene` + 頂欄 + 主內容 `auth-ticket`)。 +2. 頁面內用 `PageTitle`(含 `.ac-title-bar` 小標)+ `Card` / `ac-bulletin` + `ui.tsx` 元件;色票只引用 semantic token。 3. 若需新語意色,**先**改 `index.css` 的 `--hx-*` 與 `@theme`,再改元件;不要頁面內硬編色碼。 -4. 完成後執行 `make web-build`。 +4. 新導覽項:改 `acAssets.ts` 的 `navApps`,並在 `AcIcon` 補 SVG path。 +5. 完成後執行 `make web-build`。 ### 前端禁忌 - 不要引入 MUI / Ant Design / Chakra 等大型 UI 庫。 -- 不要為單頁新增第三套配色或漸層彩虹按鈕。 +- 不要為單頁新增第三套配色、木質頂欄、或漸層彩虹按鈕。 +- 不要在登入/註冊頁加回獨立大牌 `ac-title-bar` 或咖啡色 header ribbon。 - 不要讓 SSE / AI 直接吃 provider 原始 chunk(後端已 normalize)。 - 不要用 `offset/limit` 呼叫列表 API;用 `page` / `pageSize`。 diff --git a/haixun-backend/web/src/components/AcIcon.tsx b/haixun-backend/web/src/components/AcIcon.tsx new file mode 100644 index 0000000..d84c7a5 --- /dev/null +++ b/haixun-backend/web/src/components/AcIcon.tsx @@ -0,0 +1,81 @@ +import type { AcAppKey } from '../lib/acAssets' + +const stroke = { + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.75, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, +} + +const paths: Record = { + home: , + jobs: , + schedule: ( + <> + + + + ), + ai: ( + <> + + + + + ), + template: ( + <> + + + + ), + settings: ( + <> + + + + ), + profile: ( + <> + + + + ), + permissions: ( + <> + + + + ), + more: ( + <> + + + + + ), +} + +export function AcIcon({ + app, + size = 'md', + className = '', +}: { + app: AcAppKey + size?: 'sm' | 'md' | 'lg' + className?: string +}) { + const px = size === 'sm' ? 'h-9 w-9' : size === 'lg' ? 'h-12 w-12' : 'h-10 w-10' + const iconPx = size === 'sm' ? 'h-4 w-4' : size === 'lg' ? 'h-6 w-6' : 'h-5 w-5' + return ( + + + {paths[app]} + + + ) +} \ No newline at end of file diff --git a/haixun-backend/web/src/components/AuthDecor.tsx b/haixun-backend/web/src/components/AuthDecor.tsx new file mode 100644 index 0000000..de8e13b --- /dev/null +++ b/haixun-backend/web/src/components/AuthDecor.tsx @@ -0,0 +1,82 @@ +function CloudShape({ path }: { path: string }) { + return +} + +const cloudLg = + 'M24 36c-9.941 0-18-6.268-18-14S14.059 8 24 8c2.2 0 4.28.45 6.16 1.26C33.1 4.54 38.78 2 45 2c11.046 0 20 7.82 20 17.5 0 .84-.06 1.66-.17 2.46C68.9 20.62 74.6 18 81 18c13.255 0 24 9.402 24 21S94.255 60 81 60H24Z' + +const cloudMd = + 'M18 30C8.059 30 0 24.627 0 18S8.059 6 18 6c1.65 0 3.21.34 4.62.95C25.58 3.4 30.22 1 35 1c9.389 0 17 6.82 17 15.25 0 .7-.05 1.38-.14 2.05C55.12 16.35 59.68 14 65 14c11.046 0 20 8.402 20 18.5S76.046 51 65 51H18Z' + +const cloudSm = + 'M14 22C6.82 22 1 17.523 1 12S6.82 2 14 2c1.28 0 2.5.26 3.6.72C19.98 1.13 23.52 0 27 0c7.18 0 13 5.82 13 13s-5.82 13-13 13H14Z' + +const clouds = [ + { id: 1, path: cloudLg, viewBox: '0 0 120 48', className: 'auth-cloud--1' }, + { id: 2, path: cloudMd, viewBox: '0 0 96 40', className: 'auth-cloud--2' }, + { id: 3, path: cloudSm, viewBox: '0 0 54 26', className: 'auth-cloud--3' }, + { id: 4, path: cloudMd, viewBox: '0 0 96 40', className: 'auth-cloud--4' }, + { id: 5, path: cloudLg, viewBox: '0 0 120 48', className: 'auth-cloud--5' }, + { id: 6, path: cloudSm, viewBox: '0 0 54 26', className: 'auth-cloud--6' }, + { id: 7, path: cloudMd, viewBox: '0 0 96 40', className: 'auth-cloud--7' }, +] as const + +export function SceneDecor() { + return ( +
+ + + + {clouds.map((cloud) => ( + + + + ))} + + + + + + + + +
+ ) +} + +/** @deprecated 使用 SceneDecor */ +export const AuthSceneDecor = SceneDecor + +export function AuthTicketIcon({ className = '' }: { className?: string }) { + return ( + + + + + + + + ) +} \ No newline at end of file diff --git a/haixun-backend/web/src/components/AuthShell.tsx b/haixun-backend/web/src/components/AuthShell.tsx index ce7f2fb..2f5416b 100644 --- a/haixun-backend/web/src/components/AuthShell.tsx +++ b/haixun-backend/web/src/components/AuthShell.tsx @@ -1,27 +1,38 @@ import type { ReactNode } from 'react' +import { AuthTicketIcon, SceneDecor } from './AuthDecor' import { ThemeToggle } from './ThemeToggle' -export function AuthShell({ children }: { children: ReactNode }) { +export function AuthShell({ + children, + tagline = '準備好巡樓了嗎?', +}: { + children: ReactNode + tagline?: string +}) { return ( -
-
- +
+ + +
+
-
-
-
-
-

Haixun Patrol

-

巡樓管理系統

-

智慧巡檢 · 任務追蹤 · 即時協作

+ +
+
+
+
+ +
+

+ Haixun Patrol +

+

巡樓管理台

+

{tagline}

+
+
+ {children} +
- {children}
) diff --git a/haixun-backend/web/src/components/Layout.tsx b/haixun-backend/web/src/components/Layout.tsx index 15f11ed..3c2d269 100644 --- a/haixun-backend/web/src/components/Layout.tsx +++ b/haixun-backend/web/src/components/Layout.tsx @@ -1,36 +1,38 @@ -import { NavLink, Outlet } from 'react-router-dom' +import { NavLink, Outlet, useLocation } from 'react-router-dom' +import { navApps } from '../lib/acAssets' +import type { AcAppKey } from '../lib/acAssets' import { useAuth } from '../auth/AuthContext' +import { AuthTicketIcon, SceneDecor } from './AuthDecor' +import { AcIcon } from './AcIcon' import { MobileBottomNav } from './MobileBottomNav' import { ThemeToggle } from './ThemeToggle' -const navMain = [ - { to: '/', label: '總覽' }, - { to: '/jobs', label: '背景任務' }, - { to: '/job-schedules', label: '排程' }, - { to: '/ai', label: 'AI' }, -] - -const navMore = [ - { to: '/job-templates', label: '模板' }, - { to: '/settings', label: '設定' }, - { to: '/profile', label: '會員' }, - { to: '/permissions', label: '權限' }, -] - -function NavItem({ to, label, end }: { to: string; label: string; end?: boolean }) { +function AppTile({ + to, + label, + icon, + end, + matchPrefix, +}: { + to: string + label: string + icon: AcAppKey + end?: boolean + matchPrefix?: string +}) { + const { pathname } = useLocation() return ( - `rounded-[var(--radius-pill)] px-4 py-2 text-sm font-semibold transition ${ - isActive - ? 'bg-brand text-white shadow-soft' - : 'text-ink hover:bg-brand-soft hover:text-brand' - }` - } + className={({ isActive }) => { + const prefixActive = matchPrefix ? pathname.startsWith(matchPrefix) : false + const active = isActive || prefixActive + return `ac-app-tile ${active ? 'ac-app-tile--active' : ''}` + }} > - {label} + + {label} ) } @@ -39,72 +41,77 @@ export function Layout() { const { member, uid, logout } = useAuth() return ( -
- -
-
{member?.email}
-
- {member?.roles?.join(', ') || 'user'} +
+
+
- -
- - -
-
-
-

巡樓

-

{uid || member?.uid}

-
-

- {uid || member?.uid} -

- -
-
-
) -} - -function BadgePill({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) } \ No newline at end of file diff --git a/haixun-backend/web/src/components/MobileBottomNav.tsx b/haixun-backend/web/src/components/MobileBottomNav.tsx index f6d0f3b..476a7aa 100644 --- a/haixun-backend/web/src/components/MobileBottomNav.tsx +++ b/haixun-backend/web/src/components/MobileBottomNav.tsx @@ -1,21 +1,17 @@ import { useEffect, useState } from 'react' import { NavLink, useLocation, useNavigate } from 'react-router-dom' +import { navApps } from '../lib/acAssets' import { useAuth } from '../auth/AuthContext' +import { AcIcon } from './AcIcon' import { ThemeToggle } from './ThemeToggle' const mobileTabs = [ - { to: '/', label: '總覽', end: true }, - { to: '/jobs', label: '任務', matchPrefix: '/jobs' }, - { to: '/job-schedules', label: '排程' }, + { to: '/', label: '總覽', icon: 'home' as const, end: true }, + { to: '/jobs', label: '任務', icon: 'jobs' as const, matchPrefix: '/jobs' }, + { to: '/job-schedules', label: '排程', icon: 'schedule' as const }, ] as const -const moreRoutes = [ - { to: '/ai', label: 'AI' }, - { to: '/job-templates', label: '模板' }, - { to: '/settings', label: '設定' }, - { to: '/profile', label: '會員' }, - { to: '/permissions', label: '權限' }, -] +const moreRoutes = navApps.filter((n) => !mobileTabs.some((t) => t.to === n.to)) function isMoreActive(pathname: string) { return moreRoutes.some((r) => pathname === r.to || pathname.startsWith(`${r.to}/`)) @@ -51,11 +47,8 @@ export function MobileBottomNav() { return ( <> -