0cd1047b10
Run Tests / test (pull_request) Successful in 30s
repo 內 35/38 份既有 spec 都是舊版歸檔流程留下的原始 delta 內容(`## ADDED Requirements`,缺標題與 Purpose),openspec CLI 現版本嚴格驗證 (--strict)會報錯或警告。逐一補上: - 缺標題/Purpose 的 30 份:加上 `# <name> Specification` 標題 + 依內容 撰寫的 Purpose 段落,`## ADDED Requirements` 改回 `## Requirements` - compose-cloud-baseline、scheduler-container:已有標題與 Requirements,只補 Purpose 包裝 - provider-verification:已有標題與 Purpose,只需把 `## ADDED Requirements` 改回 `## Requirements` - admin-user-management、notification-email:各有一條 requirement 缺 SHALL/MUST 關鍵字(純敘述句或表格),補上規範用語,內容不變 `openspec validate --specs --strict`:38/38 全過(原本 4/38)。純文件 補齊,未變更任何 requirement 的實質內容或行為描述。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LpcY9b7y9x4fusHBTXciQ
155 lines
6.4 KiB
Markdown
155 lines
6.4 KiB
Markdown
# member-portal-ui Specification
|
||
|
||
## Purpose
|
||
|
||
定義 Member Portal 前端的完整頁面行為:首頁、課程列表/詳情、登入註冊(含 Google OAuth)、個人資料,以及以 sessionStorage 管理的認證狀態。
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 專案基礎建設
|
||
前端 SHALL 建立於本 repo 的 `frontend/` 目錄(原規劃獨立 repo,後併入主 repo 以簡化版控與部署),使用 Vue 3 + Vite + Tailwind CSS + Vue Router 4 + Pinia + Axios,並設定 `.env` 指定後端 API base URL。
|
||
|
||
#### Scenario: 開發環境啟動
|
||
- **WHEN** 開發者執行 `npm run dev`
|
||
- **THEN** 應用在 `http://localhost:5173` 啟動,無編譯錯誤
|
||
|
||
#### Scenario: API base URL 設定
|
||
- **WHEN** `.env` 中設定 `VITE_API_URL=http://localhost:80`
|
||
- **THEN** 所有 Axios 請求以此為 base URL
|
||
|
||
---
|
||
|
||
### Requirement: 首頁 Landing Page
|
||
前端 SHALL 提供靜態首頁,展示平台品牌、簡介,以及引導至課程列表的 CTA(Call to Action)按鈕。
|
||
|
||
#### Scenario: 訪客瀏覽首頁
|
||
- **WHEN** 使用者訪問 `/`
|
||
- **THEN** 看到平台名稱、簡介文字、「探索課程」按鈕
|
||
|
||
#### Scenario: 點擊 CTA 跳轉
|
||
- **WHEN** 使用者點擊「探索課程」按鈕
|
||
- **THEN** 導航至 `/courses`(課程列表頁)
|
||
|
||
---
|
||
|
||
### Requirement: 課程列表頁
|
||
前端 SHALL 提供 `/courses` 頁面,顯示從後端取得的潛水課程卡片列表,並支援搜尋與篩選。
|
||
|
||
#### Scenario: 載入課程列表
|
||
- **WHEN** 使用者訪問 `/courses`
|
||
- **THEN** 頁面呼叫 `GET /api/diving-offers` 並渲染課程卡片(含標題、地點、價格、評分、標籤)
|
||
|
||
#### Scenario: 搜尋課程
|
||
- **WHEN** 使用者在搜尋框輸入關鍵字後按 Enter 或點搜尋
|
||
- **THEN** 以 `?q=<keyword>` 重新呼叫 API,列表更新
|
||
|
||
#### Scenario: 地區篩選
|
||
- **WHEN** 使用者從地區下拉選單選擇某地區
|
||
- **THEN** 以 `?region=<region>` 重新呼叫 API,列表更新
|
||
|
||
#### Scenario: 無結果
|
||
- **WHEN** 搜尋/篩選後後端回傳空陣列
|
||
- **THEN** 頁面顯示「找不到符合的課程」提示訊息
|
||
|
||
---
|
||
|
||
### Requirement: 課程詳情頁
|
||
前端 SHALL 提供 `/courses/:id` 頁面,顯示單一課程的完整資訊。
|
||
|
||
#### Scenario: 載入課程詳情
|
||
- **WHEN** 使用者訪問 `/courses/1`
|
||
- **THEN** 頁面呼叫 `GET /api/diving-offers/1` 並顯示標題、地點、景點、價格、評分、評論數、描述、徽章、標籤
|
||
|
||
#### Scenario: 課程不存在
|
||
- **WHEN** 使用者訪問不存在的課程 id
|
||
- **THEN** 頁面顯示「課程不存在」並提供返回列表按鈕
|
||
|
||
---
|
||
|
||
### Requirement: 登入頁
|
||
前端 SHALL 提供 `/login` 頁面,供會員以 email/password 登入,以及 Google OAuth 登入入口。
|
||
|
||
#### Scenario: Email/Password 登入成功
|
||
- **WHEN** 使用者填入正確的 email 與 password 並送出
|
||
- **THEN** 呼叫 `POST /api/member/login`,儲存回傳的 token 至 localStorage,導航至 `/courses`
|
||
|
||
#### Scenario: 登入失敗
|
||
- **WHEN** 使用者填入錯誤的 email 或 password
|
||
- **THEN** 頁面顯示錯誤訊息,不跳轉
|
||
|
||
#### Scenario: Google OAuth 登入
|
||
- **WHEN** 使用者點擊「以 Google 登入」按鈕
|
||
- **THEN** 瀏覽器導航至後端 `GET /api/auth/google/redirect`,開始 OAuth 流程
|
||
|
||
#### Scenario: 超過登入頻率限制
|
||
- **WHEN** 同一 IP 在 1 分鐘內送出超過 5 次登入請求
|
||
- **THEN** 前端顯示適當的錯誤訊息(對應後端回傳的 HTTP 429)
|
||
|
||
---
|
||
|
||
### Requirement: Google OAuth Callback 處理
|
||
前端 SHALL 在 `/auth/callback` 路由讀取 URL fragment(`#token=<value>`)取得 Sanctum token,完成 OAuth 登入後將 token 存入 localStorage 並導航至 `/courses`。token 不得透過 URL query string 傳遞。
|
||
|
||
#### Scenario: OAuth callback 成功取得 token
|
||
- **WHEN** 後端 OAuth callback redirect 到 `/auth/callback#token=<token>`
|
||
- **THEN** 前端從 `window.location.hash` 解析 token,呼叫 `/api/member/profile` 取得使用者資料,呼叫 `auth.setAuth()` 儲存認證狀態,並導航至 `/courses`
|
||
|
||
#### Scenario: OAuth callback 缺少 token
|
||
- **WHEN** redirect 到 `/auth/callback` 但 hash 中無 `token` 參數
|
||
- **THEN** 前端導航至 `/login?error=oauth_failed`,顯示錯誤訊息
|
||
|
||
#### Scenario: URL 中不留存 token
|
||
- **WHEN** callback 頁面成功處理 token 後
|
||
- **THEN** 瀏覽器網址列不顯示 token(使用 `history.replaceState` 清除 hash)
|
||
|
||
---
|
||
|
||
### Requirement: 註冊頁
|
||
前端 SHALL 提供 `/register` 頁面,供訪客建立會員帳號。
|
||
|
||
#### Scenario: 註冊成功
|
||
- **WHEN** 使用者填入 name、email、password 並送出
|
||
- **THEN** 呼叫 `POST /api/member/register`,成功後導航至 `/login`,顯示「註冊成功,請登入」
|
||
|
||
#### Scenario: Email 已被使用
|
||
- **WHEN** 使用者填入已存在的 email 送出
|
||
- **THEN** 頁面顯示「此 Email 已被使用」錯誤訊息
|
||
|
||
---
|
||
|
||
### Requirement: 會員個人資料頁
|
||
前端 SHALL 提供 `/profile` 頁面,已登入會員可查看並更新個人資料。此頁面需登入後才能訪問。
|
||
|
||
#### Scenario: 已登入會員訪問個人資料
|
||
- **WHEN** 已登入使用者訪問 `/profile`
|
||
- **THEN** 頁面呼叫 `GET /api/member/profile` 並顯示姓名、email、生日、性別、地址、緊急聯絡人
|
||
|
||
#### Scenario: 未登入訪問個人資料
|
||
- **WHEN** 未登入使用者訪問 `/profile`
|
||
- **THEN** 自動導向 `/login`
|
||
|
||
#### Scenario: 更新個人資料成功
|
||
- **WHEN** 已登入使用者修改欄位後點擊儲存
|
||
- **THEN** 呼叫 `PUT /api/member/profile`,成功後顯示「資料已更新」提示
|
||
|
||
---
|
||
|
||
### Requirement: 認證狀態管理
|
||
前端 SHALL 使用 Pinia store 管理認證狀態,token 存於 sessionStorage(非 localStorage),並在所有需認證的 API 請求自動附加 Bearer token。收到 401 時先嘗試 refresh,成功後 retry;refresh 失敗才清除 session 並導向登入頁。
|
||
|
||
#### Scenario: 頁面刷新後保持登入狀態
|
||
- **WHEN** 已登入使用者在同一分頁重新整理頁面
|
||
- **THEN** 從 sessionStorage 還原 token,使用者仍為登入狀態
|
||
|
||
#### Scenario: 分頁關閉後 token 自動清除
|
||
- **WHEN** 使用者關閉瀏覽器分頁或瀏覽器
|
||
- **THEN** sessionStorage 自動清除,重新開啟需重新登入
|
||
|
||
#### Scenario: Token 過期後自動 refresh
|
||
- **WHEN** API 請求因 token 過期回傳 401
|
||
- **THEN** 自動呼叫 `POST /api/member/refresh`,成功後 retry 原始請求,使用者無感知
|
||
|
||
#### Scenario: 登出
|
||
- **WHEN** 使用者點擊登出
|
||
- **THEN** 呼叫 `POST /api/member/logout`,清除 sessionStorage token,導向 `/login`
|