ad00f5c195
Update documentation to show that simple pages can define page models directly in views without creating a page driver. Adjust examples, section numbering, and naming guidance to better distinguish simple view state from reusable page-driver patterns.docs: clarify optional page drivers in page guide Update documentation to show that simple pages can define page models directly in views without creating a page driver. Adjust examples, section numbering, and naming guidance to better distinguish simple view state from reusable page-driver patterns.
198 lines
5.4 KiB
Markdown
198 lines
5.4 KiB
Markdown
# 新增頁面範例
|
||
|
||
這份文件示範如何用目前 `src/` 慣例新增一個被 `MainLayout` 包住的一般功能頁。
|
||
|
||
範例功能:`reports`
|
||
|
||
目前新增一般頁面的預設資料流:
|
||
|
||
```txt
|
||
router -> view -> (page driver) -> page component -> sections/items
|
||
↓
|
||
store/composable -> service
|
||
```
|
||
|
||
Page driver 不是必備層:若頁面邏輯只有簡單的 `computed` page model(無搜尋、無 dialog、無複雜事件協調),直接在 view 裡寫即可,不需要建立 page driver。
|
||
|
||
## 1. 新增 view(含 page model)
|
||
|
||
簡單頁面的 page model 直接在 view 裡用 `computed` 組裝,不需要額外建立 page driver。
|
||
|
||
```vue
|
||
<!-- src/views/reports/Reports.vue -->
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue'
|
||
import PageReports from '@/components/pages/PageReports.vue'
|
||
import { useSnackbarStore } from '@/stores/snackbar'
|
||
|
||
export interface ReportSummary {
|
||
id: number
|
||
title: string
|
||
owner: string
|
||
}
|
||
|
||
const initialRows: ReportSummary[] = [
|
||
{ id: 1, title: '學生統計', owner: '教務處' },
|
||
{ id: 2, title: '課程統計', owner: '課務組' },
|
||
]
|
||
|
||
const snackbar = useSnackbarStore()
|
||
const rows = ref<ReportSummary[]>(initialRows)
|
||
const pageModel = computed(() => ({
|
||
title: '報表清單',
|
||
rows: rows.value,
|
||
}))
|
||
|
||
function openReport(row: ReportSummary) {
|
||
snackbar.show({ message: `開啟:${row.title}`, color: 'info' })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<PageReports :page="pageModel" @open="openReport" />
|
||
</template>
|
||
```
|
||
|
||
若頁面需要協調多個 composable(搜尋、表單、CRUD flow、dialog 狀態),才建立 page driver。page driver 的慣例見 `src/composables/GUIDE.md`。
|
||
|
||
## 2. 新增 page component
|
||
|
||
完整頁面主畫面放在 `src/components/pages`,檔名使用 `Page` 前綴。component 以 props 接收資料,以 emit 回報使用者事件,不直接處理 route 或底層 HTTP。
|
||
|
||
```vue
|
||
<!-- src/components/pages/PageReports.vue -->
|
||
<script setup lang="ts">
|
||
import type { ReportSummary } from '@/views/reports/Reports.vue'
|
||
|
||
defineProps<{
|
||
page: { title: string; rows: ReportSummary[] }
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
open: [row: ReportSummary]
|
||
}>()
|
||
</script>
|
||
|
||
<template>
|
||
<v-card flat>
|
||
<v-card-title class="text-h6">{{ page.title }}</v-card-title>
|
||
<v-table>
|
||
<thead>
|
||
<tr>
|
||
<th>名稱</th>
|
||
<th>負責單位</th>
|
||
<th class="text-right">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in page.rows" :key="row.id">
|
||
<td>{{ row.title }}</td>
|
||
<td>{{ row.owner }}</td>
|
||
<td class="text-right">
|
||
<v-btn color="primary" size="small" variant="text" @click="emit('open', row)">
|
||
開啟
|
||
</v-btn>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</v-card>
|
||
</template>
|
||
```
|
||
|
||
若畫面是固定的「篩選條件 + 查詢按鈕 + 結果表格」,優先使用 `components/sections/SectionQueryPage.vue`。若是「表單欄位 + 送出/存檔按鈕」,優先使用 `components/sections/SectionFormPage.vue`。
|
||
|
||
## 3. 加入 route
|
||
|
||
route 加在 `src/router/routes.ts` 的 `routes` 陣列中,並放在 `/:pathMatch(.*)*` catch-all route 前面。
|
||
|
||
```ts
|
||
// src/router/routes.ts
|
||
{
|
||
path: '/reports',
|
||
name: 'reports',
|
||
component: () => import('@/views/reports/Reports.vue'),
|
||
meta: { layout: 'default', requiresAuth: true },
|
||
}
|
||
```
|
||
|
||
`layout: 'default'` 會讓頁面被 `MainLayout` 包住。登入頁、錯誤頁、維護中頁才使用 `layout: 'none'`。
|
||
|
||
若頁面需要出現在 drawer menu、favorites 或 breadcrumb:
|
||
|
||
- menu 來源目前由 `src/stores/menu.ts` 轉換後端選單資料。
|
||
- breadcrumb 會依 route path、menu/favorite items 與 fallback title 產生。
|
||
- 新功能若使用後端選單,優先調整後端選單資料或對應 API mock,不要把頁面專屬選單邏輯塞進 layout。
|
||
- 若只是新增 route,通常不需要修改 `MainLayout.vue` 或 `src/shell/*`。
|
||
|
||
## 4. 需要 API 時新增 service module
|
||
|
||
```ts
|
||
// src/services/modules/reports.ts
|
||
import { httpClient } from '../client'
|
||
|
||
export interface ReportSummary {
|
||
id: number
|
||
title: string
|
||
owner: string
|
||
}
|
||
|
||
export const reportsApi = {
|
||
list: async () => ({
|
||
data: await httpClient.get('Reports').json<ReportSummary[]>(),
|
||
}),
|
||
}
|
||
```
|
||
|
||
service 只封裝 HTTP 細節,不持有 UI 狀態。
|
||
|
||
`httpClient` 的 `baseURL` 來自 `VITE_API_BASE_URL`,沒有設定時預設 `/service/api`。開發模式下,Vite proxy 會將 `/service/*` 轉送到 `VITE_PROXY_TARGET`。
|
||
|
||
## 5. 需要共享狀態時新增 store
|
||
|
||
只有跨頁共享、需要快取、或全域狀態才新增 store。單頁暫時狀態留在 view、component 或 composable。
|
||
|
||
```ts
|
||
// src/stores/reports.ts
|
||
import { defineStore } from 'pinia'
|
||
import { ref } from 'vue'
|
||
import { reportsApi, type ReportSummary } from '@/services/modules/reports'
|
||
|
||
export const useReportsStore = defineStore('reports', () => {
|
||
const items = ref<ReportSummary[]>([])
|
||
const loading = ref(false)
|
||
|
||
const load = async () => {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await reportsApi.list()
|
||
items.value = data
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
return {
|
||
items,
|
||
loading,
|
||
load,
|
||
}
|
||
})
|
||
```
|
||
|
||
## 6. 驗證
|
||
|
||
至少執行:
|
||
|
||
```bash
|
||
pnpm -s type-check
|
||
```
|
||
|
||
需要確認建置產物時再執行:
|
||
|
||
```bash
|
||
pnpm -s build
|
||
```
|
||
|
||
若有 route、layout 或主要互動流程變更,再啟動 dev server 並用瀏覽器確認。
|