From 9ae91418e0706fe1cdd57b2c5f2325c0368a9dd7 Mon Sep 17 00:00:00 2001 From: skytek_xinliang Date: Tue, 19 May 2026 11:35:01 +0800 Subject: [PATCH 1/9] feat(shell): add app shell and maintenance page driver Introduce reusable shell components for layout, tabs, and global overlays. Add maintenance page model, wrapper component, and composable driver to standardize maintenance page state, search, and pagination handling.feat(shell): add app shell and maintenance page driver Introduce reusable shell components for layout, tabs, and global overlays. Add maintenance page model, wrapper component, and composable driver to standardize maintenance page state, search, and pagination handling. --- src/components/pages/PageMaintenance.vue | 34 +++ .../page-drivers/useMaintenancePage.ts | 46 ++++ src/models/page.ts | 12 ++ src/models/student.ts | 13 ++ src/shell/AppShell.vue | 35 +++ src/shell/AppTabs.vue | 114 ++++++++++ src/shell/GlobalOverlays.vue | 199 ++++++++++++++++++ src/stores/menu.ts | 1 + src/stores/students.ts | 15 +- 9 files changed, 456 insertions(+), 13 deletions(-) create mode 100644 src/components/pages/PageMaintenance.vue create mode 100644 src/composables/page-drivers/useMaintenancePage.ts create mode 100644 src/models/page.ts create mode 100644 src/models/student.ts create mode 100644 src/shell/AppShell.vue create mode 100644 src/shell/AppTabs.vue create mode 100644 src/shell/GlobalOverlays.vue diff --git a/src/components/pages/PageMaintenance.vue b/src/components/pages/PageMaintenance.vue new file mode 100644 index 0000000..83c2072 --- /dev/null +++ b/src/components/pages/PageMaintenance.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/composables/page-drivers/useMaintenancePage.ts b/src/composables/page-drivers/useMaintenancePage.ts new file mode 100644 index 0000000..23a920f --- /dev/null +++ b/src/composables/page-drivers/useMaintenancePage.ts @@ -0,0 +1,46 @@ +import { computed, ref } from 'vue' +import type { MaintenancePageModel } from '@/models/page' + +export interface UseMaintenancePageOptions { + title: string + records: unknown[] + itemsPerPage?: number +} + +export function useMaintenancePage(options: UseMaintenancePageOptions) { + const search = ref>({}) + const searchPanelOpen = ref(false) + const currentPage = ref(1) + const itemsPerPage = options.itemsPerPage ?? 10 + + const pageCount = computed(() => + Math.max(1, Math.ceil(options.records.length / itemsPerPage)) + ) + + const pageModel = computed(() => ({ + type: 'maintenance', + title: options.title, + records: options.records, + loading: false, + error: null, + })) + + function load() { + // 由呼叫方在 load 中觸發資料載入;未來可擴充為非同步 + } + + function resetSearch() { + search.value = {} + } + + return { + pageModel, + search, + searchPanelOpen, + currentPage, + itemsPerPage, + pageCount, + load, + resetSearch, + } +} diff --git a/src/models/page.ts b/src/models/page.ts new file mode 100644 index 0000000..0a32be8 --- /dev/null +++ b/src/models/page.ts @@ -0,0 +1,12 @@ +export interface BasePageModel { + title: string + loading?: boolean + error?: string | null +} + +export interface MaintenancePageModel extends BasePageModel { + type: 'maintenance' + records: unknown[] +} + +export type PageModel = MaintenancePageModel diff --git a/src/models/student.ts b/src/models/student.ts new file mode 100644 index 0000000..2e32a29 --- /dev/null +++ b/src/models/student.ts @@ -0,0 +1,13 @@ +export interface StudentRecord { + id: number + studentId: string + name: string + department: string + grade: number + enrollYear: number + credits: number + advisor: string + email: string + phone: string + status: string +} diff --git a/src/shell/AppShell.vue b/src/shell/AppShell.vue new file mode 100644 index 0000000..376d7a0 --- /dev/null +++ b/src/shell/AppShell.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/shell/AppTabs.vue b/src/shell/AppTabs.vue new file mode 100644 index 0000000..e958e66 --- /dev/null +++ b/src/shell/AppTabs.vue @@ -0,0 +1,114 @@ + + + diff --git a/src/shell/GlobalOverlays.vue b/src/shell/GlobalOverlays.vue new file mode 100644 index 0000000..d8a7e65 --- /dev/null +++ b/src/shell/GlobalOverlays.vue @@ -0,0 +1,199 @@ + + + diff --git a/src/stores/menu.ts b/src/stores/menu.ts index 4b01f08..14ed286 100644 --- a/src/stores/menu.ts +++ b/src/stores/menu.ts @@ -6,6 +6,7 @@ import { menuApi, type MenuNode } from '@/services/modules/menu' export interface LayoutMenuItem { title: string path?: string + icon?: string navigable?: boolean subItems?: LayoutMenuItem[] } diff --git a/src/stores/students.ts b/src/stores/students.ts index 8dcda7a..3d89d35 100644 --- a/src/stores/students.ts +++ b/src/stores/students.ts @@ -1,19 +1,8 @@ +import type { StudentRecord } from '@/models/student' import { defineStore } from 'pinia' import { ref } from 'vue' -export interface StudentRecord { - id: number - studentId: string - name: string - department: string - grade: number - enrollYear: number - credits: number - advisor: string - email: string - phone: string - status: string -} +export type { StudentRecord } from '@/models/student' const seedStudents: StudentRecord[] = [ { From 96b96bcaaa15ea7269ae7bbcf6b5727154507f7b Mon Sep 17 00:00:00 2001 From: skytek_xinliang Date: Tue, 19 May 2026 14:13:10 +0800 Subject: [PATCH 2/9] docs: reorganize architecture strategy documentation Split current project diagnostics into a dedicated analysis document and trim the main architecture strategy to focus on core guidance. This makes the documentation easier to navigate and separates observed issues from recommended architectural principles.docs: reorganize architecture strategy documentation Split current project diagnostics into a dedicated analysis document and trim the main architecture strategy to focus on core guidance. This makes the documentation easier to navigate and separates observed issues from recommended architectural principles. --- docs/analyse-now.md | 47 + docs/architecture-strategy.md | 218 ++-- docs/llm-development-guide.md | 45 +- docs/what-apple-do.md | 81 ++ src/components/items/ItemFormFieldGroup.vue | 172 ++++ src/components/sections/SectionDataTable.vue | 169 ++++ src/components/sections/SectionFormPanel.vue | 282 ++++++ .../sections/SectionSearchPanel.vue | 102 ++ src/composables/commands/useCrudCommands.ts | 125 +++ .../maint/useMaintenanceCrudFlow.ts | 2 +- .../useSingleRecordMaintenancePage.ts | 258 +++++ src/views/maint/SingleRecord.vue | 949 +----------------- 12 files changed, 1373 insertions(+), 1077 deletions(-) create mode 100644 docs/analyse-now.md create mode 100644 docs/what-apple-do.md create mode 100644 src/components/items/ItemFormFieldGroup.vue create mode 100644 src/components/sections/SectionDataTable.vue create mode 100644 src/components/sections/SectionFormPanel.vue create mode 100644 src/components/sections/SectionSearchPanel.vue create mode 100644 src/composables/commands/useCrudCommands.ts create mode 100644 src/composables/page-drivers/useSingleRecordMaintenancePage.ts diff --git a/docs/analyse-now.md b/docs/analyse-now.md new file mode 100644 index 0000000..879c923 --- /dev/null +++ b/docs/analyse-now.md @@ -0,0 +1,47 @@ +## 二、我們專案的現況診斷 + +### 2.1 App.vue 過度臃腫(~590 行) + +| 職責 | 行數 | 應屬層級 | +|------|------|----------| +| Layout 切換 | ~20 | App Shell | +| Tabs 管理 | ~80 | Page Driver | +| Breadcrumb 組裝 | ~40 | Layout | +| Favorites 管理 | ~60 | Store | +| Search Dialog | ~80 | App Shell / Widget | +| Message Dialog | ~60 | App Shell / Widget | +| Snackbar | ~10 | Global Overlay | +| Logout / Force logout | ~30 | Auth Flow | +| HTTP Toast | ~20 | Service Layer | + +- **問題**:App.vue 同時承擔 App Shell、Page Driver、Global Widget、Auth Flow 四種責任。 +- **對比**:App Store 的 `App.svelte` 只有 161 行,只負責 `Navigation + PageResolver + Footer`。 + +### 2.2 Views 過厚(SingleRecord.vue ~830 行) + +- 混雜:表格呈現、搜尋表單、dialog 模板、表單狀態、CRUD 流程、驗證邏輯、分頁、snackbar。 +- **對比**:App Store 的 `ProductPage.svelte` 只有 77 行,只負責「把 page 轉成 DefaultPageRequirements + 一個 slot override」。 + +### 2.3 缺乏統一的頁面資料門面 + +``` +現況: + view → store → service(直接鏈式呼叫) + view 自己管理 loading / error / dialog visible + +App Store: + UI → jet.dispatch(intent) → runtime → controller → page model + UI 只接收 page model,不管理載入狀態 +``` + +### 2.4 Dialog 狀態與模板內嵌於 View + +- `SingleRecord.vue` 內含 5 個 `ConfirmDialog` 實例 + 1 個大 form overlay。 +- 任何 dialog 更動都需要修改 view 檔案。 + +### 2.5 沒有容器/內容分離的 Section 層 + +- 表格、表單、搜尋區塊都是直接寫在 view 或 page component 中。 +- 缺乏類似 `ShelfItemLayout` 的通用佈局抽象:「這一區是水平捲軸還是網格」應該由容器決定,裡面的內容元件不應該知道。 + +--- diff --git a/docs/architecture-strategy.md b/docs/architecture-strategy.md index d2ab4c7..667e03a 100644 --- a/docs/architecture-strategy.md +++ b/docs/architecture-strategy.md @@ -1,139 +1,19 @@ # 資料流與元件分層優化策略 -> 分析標的:`~/code/apps.apple.com-main`(Apple App Store Web — Svelte + Jet 架構) -> 適用目標:`/home/carl/git/skt-vuetify-templates`(Vue 3 + Vuetify + Pinia) -> 原則:不強制遷移既有程式碼,但所有新增功能與重構以本文件為最高準則。 - ---- - ## 一、Apple App Store 專案的核心架構特徵 -### 1.1 單一業務邏輯門面(Jet Facade) +1 單一業務邏輯門面 +2 Intent / Action 分離(查詢與命令) +3 Page Model 驅動 UI(資料驅動) +4 Shelf / Item 分層(容器與內容分離) +5 Svelte Context 作為跨層依賴注入 +6 命令式外殼 + 聲明式 UI -``` -browser.ts → bootstrap → Jet.load → runtime + objectGraph - ↓ - UI 層僅透過 jet.dispatch(intent) / jet.perform(action) 溝通 -``` - -- **Jet** 封裝所有業務邏輯:路由、資料取得、動作分發、metrics。 -- UI 層**不直接**呼叫 API、不直接操作 storage、不直接操作 history。 -- 所有外部依賴(fetch、storage、locale、user)統一注入 `Dependencies`,再組裝成 `ObjectGraph`。 - -### 1.2 Intent / Action 分離(查詢與命令) - -| 類型 | 職責 | 回傳值 | 例子 | -|------|------|--------|------| -| **Intent** | 取得頁面資料(Query) | `Promise` | `RouteUrlIntent` → 回傳 `ProductPage` | -| **Action** | 執行副作用(Command) | `'performed' \| 'unsupported'` | `FlowAction` → 導航到新頁面 | - -- `FlowAction` 是主要導航機制:內含 `destination Intent` + `pageUrl` + `presentationContext`。 -- Action handler 註冊採用**型別註冊制**:`jet.onAction('flowAction', handler)`。 - -### 1.3 Page Model 驅動 UI(資料驅動) - -```ts -// App.svelte 的介面極簡 -export let page: Promise | Page -``` - -- 整個應用由單一 `page` prop 驅動。 -- `PageResolver` 處理 `Promise` 的 loading / error 狀態。 -- `Page.svelte` 用 type guard 分發到對應的 page component:`isProductPage(page)` → ``。 -- **Page 是 union type**,不是 route-based 的硬編碼映射。 - -### 1.4 Shelf / Item 分層(容器與內容分離) - -``` -Page (TodayPage / ProductPage / ...) - └── Shelf[] (水平捲軸 / 網格) - └── ShelfItemLayout (佈局抽象:HorizontalShelf or Grid) - └── Item (BrickItem / LargeLockupItem / ...) -``` - -- **Shelf** = 容器邏輯:決定是水平捲軸還是網格、rowsPerColumn、邊框。 -- **ShelfItemLayout** = 佈局中介:根據 `isHorizontal` 選擇 `HorizontalShelf` 或 `Grid`。 -- **Item** = 純粹內容渲染:只關心單一資料單位的呈現,不知道自己是水平還是網格。 -- **FallbackShelf** = 優雅的降級策略:遇到未實作的 shelf 類型顯示 placeholder,不 crash。 - -### 1.5 Svelte Context 作為跨層依賴注入 - -```ts -// bootstrap.ts -context.set('jet', jet) -context.set('i18n', i18nStore) - -// 深層元件 -const jet = getJet() // 從 Svelte Context 取得 -const i18n = getI18n() // 從 Svelte Context 取得 -``` - -- 避免 props drilling:Jet、i18n、accessibility layout、today-card layout 都透過 context 傳遞。 -- Context 在啟動時注入,生命周期與應用一致,不是用來傳遞 UI 狀態的。 - -### 1.6 命令式外殼 + 聲明式 UI - -```ts -// browser.ts(命令式啟動層) -const app = new App({ target: container, context, hydrate: true }) -registerActionHandlers({ - jet, - updateApp: (props) => app.$set(props), // 橋接命令式 → 聲明式 -}) -``` - -- 導航、歷史管理、scroll 復原由命令式的 action handler 處理。 -- UI 渲染完全聲明式,只接收 `page` 與 `isFirstPage` 兩個 prop。 - ---- +Read only when needed: [what apple do](./what-apple-do.md) ## 二、我們專案的現況診斷 -### 2.1 App.vue 過度臃腫(~590 行) - -| 職責 | 行數 | 應屬層級 | -|------|------|----------| -| Layout 切換 | ~20 | App Shell | -| Tabs 管理 | ~80 | Page Driver | -| Breadcrumb 組裝 | ~40 | Layout | -| Favorites 管理 | ~60 | Store | -| Search Dialog | ~80 | App Shell / Widget | -| Message Dialog | ~60 | App Shell / Widget | -| Snackbar | ~10 | Global Overlay | -| Logout / Force logout | ~30 | Auth Flow | -| HTTP Toast | ~20 | Service Layer | - -- **問題**:App.vue 同時承擔 App Shell、Page Driver、Global Widget、Auth Flow 四種責任。 -- **對比**:App Store 的 `App.svelte` 只有 161 行,只負責 `Navigation + PageResolver + Footer`。 - -### 2.2 Views 過厚(SingleRecord.vue ~830 行) - -- 混雜:表格呈現、搜尋表單、dialog 模板、表單狀態、CRUD 流程、驗證邏輯、分頁、snackbar。 -- **對比**:App Store 的 `ProductPage.svelte` 只有 77 行,只負責「把 page 轉成 DefaultPageRequirements + 一個 slot override」。 - -### 2.3 缺乏統一的頁面資料門面 - -``` -現況: - view → store → service(直接鏈式呼叫) - view 自己管理 loading / error / dialog visible - -App Store: - UI → jet.dispatch(intent) → runtime → controller → page model - UI 只接收 page model,不管理載入狀態 -``` - -### 2.4 Dialog 狀態與模板內嵌於 View - -- `SingleRecord.vue` 內含 5 個 `ConfirmDialog` 實例 + 1 個大 form overlay。 -- 任何 dialog 更動都需要修改 view 檔案。 - -### 2.5 沒有容器/內容分離的 Section 層 - -- 表格、表單、搜尋區塊都是直接寫在 view 或 page component 中。 -- 缺乏類似 `ShelfItemLayout` 的通用佈局抽象:「這一區是水平捲軸還是網格」應該由容器決定,裡面的內容元件不應該知道。 - ---- +Read only when needed: [analyse now](./analyse-now.md) ## 三、優化後的資料流策略 @@ -188,7 +68,7 @@ App Store: ```ts // src/composables/usePageDriver.ts -export function useStudentMaintenancePage() { +export function useMaintenancePage() { const studentStore = useStudentStore() const { records, loading, error, load } = useCrudDriver({ store: studentStore, @@ -264,7 +144,7 @@ src/ │ ├── components/ │ ├── pages/ ← 新增:Page Component 層 -│ │ ├── PageStudentMaintenance.vue +│ │ ├── PageMaintenance.vue │ │ └── PageReport.vue │ │ │ ├── sections/ ← 新增:Section / Shelf 層 @@ -273,7 +153,7 @@ src/ │ │ └── SectionFormPanel.vue │ │ │ ├── items/ ← 新增:Item / Atom 層(領域獨立) -│ │ ├── ItemStudentRow.vue +│ │ ├── ItemDataRow.vue │ │ └── ItemFormField.vue │ │ │ ├── layouts/ ← 維持:App Shell Layout @@ -285,11 +165,11 @@ src/ │ ├── composables/ │ ├── page-drivers/ ← 新增:頁面資料協調 -│ │ └── useStudentMaintenancePage.ts +│ │ └── useMaintenancePage.ts │ ├── commands/ ← 新增:命令流程(對齊 Jet Action) -│ │ └── useStudentCrudCommands.ts +│ │ └── useCrudCommands.ts │ ├── forms/ ← 維持/重組:表單狀態機 -│ │ └── useStudentForm.ts +│ │ └── useForm.ts │ └── layout/ ← 維持 │ ├── models/ ← 新增:領域模型與 Page Union @@ -321,15 +201,18 @@ src/ ```vue ``` @@ -340,7 +223,7 @@ load() - **對齊**:App Store 的 `ProductPage.svelte`、`TodayPage.svelte`、`DefaultPage.svelte`。 ```vue - +