diff --git a/frontend/README_dev.md b/frontend/README_dev.md index 59351e2..891dcbc 100644 --- a/frontend/README_dev.md +++ b/frontend/README_dev.md @@ -36,22 +36,58 @@ src/ ├── components/ │ ├── ui/ # shadcn/ui基础组件 +│ │ ├── Avatar.vue +│ │ ├── Button.vue +│ │ ├── Card.vue +│ │ ├── CardContent.vue +│ │ ├── CardHeader.vue +│ │ ├── DropdownMenu.vue +│ │ └── DropdownMenuItem.vue │ ├── layout/ # 布局组件 +│ │ └── Navbar.vue │ └── features/ # 功能组件 +│ ├── BoardCard.vue # 看板卡片组件 +│ ├── CreateBoardDialog.vue # 创建看板对话框 +│ ├── KanbanColumn.vue # 看板列组件 +│ └── TaskCard.vue # 任务卡片组件 ├── views/ # 页面组件 │ ├── HomeView.vue # 首页(独立页面,唯一) │ ├── dashboard/ # 仪表板相关页面 -│ ├── user/ # 用户相关页面 -│ └── auth/ # 认证相关页面 +│ │ ├── BoardListView.vue # 看板列表页面 +│ │ └── KanbanView.vue # 看板详情页面 +│ └── user/ # 用户相关页面 +│ ├── ProfileView.vue # 用户资料页面 +│ └── SettingsView.vue # 用户设置页面 ├── stores/ # Pinia状态管理 -├── composables/ # 可复用逻辑 -└── lib/ # 工具函数 +│ ├── board.ts # 看板状态管理 +│ ├── kanban.ts # 看板任务状态管理 +│ ├── user.ts # 用户状态管理 +│ └── index.ts # Store统一导出 +├── services/ # API服务层 +│ ├── base-api.ts # 基础API服务类 +│ ├── board-api.ts # 看板API服务 +│ └── index.ts # 服务统一导出 +├── config/ # 配置文件 +│ └── api.ts # API配置 +├── router/ # 路由配置 +│ └── index.ts +├── lib/ # 工具函数 +│ └── utils.ts +└── assets/ # 静态资源 + └── vue.svg ``` #### 组件命名规范 -- UI组件:`Button.vue`, `Card.vue` -- 功能组件:`TaskCard.vue`, `KanbanColumn.vue` -- 页面组件:`HomeView.vue`, `ProfileView.vue` +- **UI组件**:`Button.vue`, `Card.vue`, `Avatar.vue` - 基础UI组件,可复用 +- **功能组件**:`BoardCard.vue`, `TaskCard.vue`, `KanbanColumn.vue`, `CreateBoardDialog.vue` - 业务功能组件 +- **布局组件**:`Navbar.vue` - 页面布局相关组件 +- **页面组件**:`HomeView.vue`, `BoardListView.vue`, `KanbanView.vue`, `ProfileView.vue` - 路由页面组件 + +#### 新增组件说明 +- **BoardCard.vue**: 看板卡片组件,支持卡片/列表两种显示模式 +- **CreateBoardDialog.vue**: 创建看板对话框,包含颜色选择器功能 +- **KanbanColumn.vue**: 看板列组件,用于显示任务列 +- **TaskCard.vue**: 任务卡片组件,显示单个任务信息 ### 🔄 状态管理 @@ -61,6 +97,46 @@ src/ - 状态更新通过actions,避免直接修改state - 复杂状态考虑使用computed +#### 当前Store结构 +- **board.ts**: 看板状态管理,包含看板列表、创建、更新、删除等功能 +- **kanban.ts**: 看板任务状态管理,包含任务CRUD操作 +- **user.ts**: 用户状态管理,用户信息和设置 +- **index.ts**: Store统一导出文件 + +### 🌐 API服务层 + +#### 服务架构 +- **base-api.ts**: 基础API服务类,提供通用的HTTP请求方法 +- **board-api.ts**: 看板相关API服务,继承自BaseApiService +- **index.ts**: 服务统一导出文件 + +#### API配置 +- **config/api.ts**: API配置文件,包含端点配置和HTTP方法 +- 支持同时定义请求路径和HTTP方法 +- 提供工具函数构建API URL + +#### 使用示例 +```typescript +// 在组件中使用API服务 +import { boardApiService } from '@/services/board-api' + +const response = await boardApiService.getKanbanList() +``` + +### 🛣️ 路由配置 + +#### 当前路由结构 +- **/** - HomeView.vue (首页) +- **/boards** - BoardListView.vue (看板列表页面) +- **/kanban/:boardId?** - KanbanView.vue (看板详情页面,支持可选参数) +- **/profile** - ProfileView.vue (用户资料页面) +- **/settings** - SettingsView.vue (用户设置页面) + +#### 路由特点 +- 使用动态路由参数支持看板ID +- 支持可选参数,提供向后兼容性 +- 采用懒加载方式导入组件 + #### 示例Store结构 ```typescript export const useFeatureStore = defineStore('feature', () => { diff --git a/frontend/src/components/features/BoardCard.vue b/frontend/src/components/features/BoardCard.vue new file mode 100644 index 0000000..5901788 --- /dev/null +++ b/frontend/src/components/features/BoardCard.vue @@ -0,0 +1,161 @@ + + + diff --git a/frontend/src/components/features/CreateBoardDialog.vue b/frontend/src/components/features/CreateBoardDialog.vue new file mode 100644 index 0000000..e0bd2bb --- /dev/null +++ b/frontend/src/components/features/CreateBoardDialog.vue @@ -0,0 +1,181 @@ + + + diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index a63d370..893adbb 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -20,11 +20,11 @@ 首页 - 看板 + 看板列表 diff --git a/frontend/src/components/ui/Avatar.vue b/frontend/src/components/ui/Avatar.vue index 6d616f2..792d259 100644 --- a/frontend/src/components/ui/Avatar.vue +++ b/frontend/src/components/ui/Avatar.vue @@ -2,9 +2,9 @@ { + return `${API_CONFIG.BASE_URL}/${API_CONFIG.VERSION}${endpoint}` +} + +// 获取端点的完整 URL +export const getEndpointUrl = (endpointKey: keyof typeof API_CONFIG.ENDPOINTS): string => { + return buildApiUrl(API_CONFIG.ENDPOINTS[endpointKey].path) +} + +// 获取端点的 HTTP 方法 +export const getEndpointMethod = (endpointKey: keyof typeof API_CONFIG.ENDPOINTS): string => { + return API_CONFIG.ENDPOINTS[endpointKey].method +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index cbafad6..8277482 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -8,7 +8,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/HomeView.vue') }, { - path: '/kanban', + path: '/boards', + name: 'BoardList', + component: () => import('@/views/dashboard/BoardListView.vue') + }, + { + path: '/kanban/:boardId?', name: 'Kanban', component: () => import('@/views/dashboard/KanbanView.vue') }, diff --git a/frontend/src/services/base-api.ts b/frontend/src/services/base-api.ts new file mode 100644 index 0000000..b6be049 --- /dev/null +++ b/frontend/src/services/base-api.ts @@ -0,0 +1,116 @@ +// 基础 API 服务类 +export interface ApiResponse { + msg: string + data: T +} + +// 错误类型定义 +export interface ApiError { + message: string + status?: number + code?: string +} + +// 请求超时配置 +const REQUEST_TIMEOUT = 10000 // 10秒超时 + +export class BaseApiService { + /** + * 创建带超时的 AbortController + */ + private createTimeoutController(timeout: number = REQUEST_TIMEOUT): AbortController { + const controller = new AbortController() + setTimeout(() => controller.abort(), timeout) + return controller + } + + /** + * 统一错误处理 + */ + private handleError(error: unknown): ApiError { + if (error instanceof Error) { + if (error.name === 'AbortError') { + return { + message: '请求超时,请检查网络连接', + code: 'TIMEOUT' + } + } + return { + message: error.message, + code: 'NETWORK_ERROR' + } + } + return { + message: '未知错误', + code: 'UNKNOWN_ERROR' + } + } + + /** + * 统一请求方法 + */ + protected async request( + url: string, + options: RequestInit = {} + ): Promise> { + const controller = this.createTimeoutController() + + const defaultOptions: RequestInit = { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + signal: controller.signal, + } + + try { + const response = await fetch(url, { ...defaultOptions, ...options }) + + // 处理HTTP状态码错误 + if (!response.ok) { + const errorMessage = this.getHttpErrorMessage(response.status) + throw new Error(errorMessage) + } + + const result = await response.json() + + // 验证响应格式 + if (typeof result !== 'object' || result === null) { + throw new Error('无效的响应格式') + } + + return { + msg: result.msg || '', + data: result.data, + } + } catch (error) { + const apiError = this.handleError(error) + console.error('API request failed:', apiError) + + return { + msg: apiError.message, + data: null as T, + } + } + } + + /** + * 根据HTTP状态码返回错误信息 + */ + private getHttpErrorMessage(status: number): string { + const errorMessages: Record = { + 400: '请求参数错误', + 401: '未授权,请重新登录', + 403: '禁止访问', + 404: '请求的资源不存在', + 408: '请求超时', + 429: '请求过于频繁,请稍后再试', + 500: '服务器内部错误', + 502: '网关错误', + 503: '服务不可用', + 504: '网关超时' + } + + return errorMessages[status] || `HTTP错误: ${status}` + } +} diff --git a/frontend/src/services/board-api.ts b/frontend/src/services/board-api.ts new file mode 100644 index 0000000..a86cd50 --- /dev/null +++ b/frontend/src/services/board-api.ts @@ -0,0 +1,49 @@ +import { BaseApiService, type ApiResponse } from './base-api' +import { getEndpointUrl, getEndpointMethod } from '@/config/api' +import type { Board } from '@/stores/board' + +// 看板 API 响应接口 +export interface KanbanListResponse extends Array {} + +// 看板 API 服务 +export class BoardApiService extends BaseApiService { + // 获取看板列表 + async getKanbanList(): Promise> { + return this.request(getEndpointUrl('KANBAN_LIST'), { + method: getEndpointMethod('KANBAN_LIST') + }) + } + + // 获取单个看板详情 + async getKanbanDetail(boardId: string): Promise> { + return this.request(`${getEndpointUrl('KANBAN_DETAIL')}/${boardId}`, { + method: getEndpointMethod('KANBAN_DETAIL') + }) + } + + // 创建看板 + async createKanban(board: Omit): Promise> { + return this.request(getEndpointUrl('KANBAN_CREATE'), { + method: getEndpointMethod('KANBAN_CREATE'), + body: JSON.stringify(board), + }) + } + + // 更新看板 + async updateKanban(boardId: string, updates: Partial): Promise> { + return this.request(`${getEndpointUrl('KANBAN_UPDATE')}/${boardId}`, { + method: getEndpointMethod('KANBAN_UPDATE'), + body: JSON.stringify(updates), + }) + } + + // 删除看板 + async deleteKanban(boardId: string): Promise> { + return this.request(`${getEndpointUrl('KANBAN_DELETE')}/${boardId}`, { + method: getEndpointMethod('KANBAN_DELETE'), + }) + } +} + +// 导出单例实例 +export const boardApiService = new BoardApiService() diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts new file mode 100644 index 0000000..b3e53d7 --- /dev/null +++ b/frontend/src/services/index.ts @@ -0,0 +1,3 @@ +// API 服务统一导出 +export { BaseApiService, type ApiResponse } from './base-api' +export { BoardApiService, boardApiService, type KanbanListResponse } from './board-api' \ No newline at end of file diff --git a/frontend/src/stores/board.ts b/frontend/src/stores/board.ts new file mode 100644 index 0000000..56cfc5d --- /dev/null +++ b/frontend/src/stores/board.ts @@ -0,0 +1,163 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { boardApiService } from '@/services/board-api' + +export interface Board { + id: string + name: string + description?: string + color: string + createdAt: Date + updatedAt: Date +} + +export const useBoardStore = defineStore('board', () => { + const boards = ref([]) + const loading = ref(false) + const error = ref(null) + + /** + * 转换API数据为本地格式 + * 避免重复的对象转换,提高性能 + */ + const transformBoardData = (apiBoard: any): Board => { + return { + ...apiBoard, + id: String(apiBoard.id), // 确保 id 是字符串 + createdAt: new Date(apiBoard.createdAt), + updatedAt: new Date(apiBoard.updatedAt) + } + } + + /** + * 从 API 获取看板列表 + * 优化数据处理,减少不必要的对象转换 + */ + const fetchBoards = async () => { + loading.value = true + error.value = null + + try { + const response = await boardApiService.getKanbanList() + + // 验证响应数据 + if (response.data && Array.isArray(response.data)) { + // 批量转换数据,避免多次对象创建 + boards.value = response.data.map(transformBoardData) + } else { + error.value = response.msg || '获取看板列表失败' + boards.value = [] // 确保空状态处理 + } + } catch (err) { + error.value = err instanceof Error ? err.message : '获取看板列表失败' + boards.value = [] // 错误时清空列表 + } finally { + loading.value = false + } + } + + /** + * 创建看板 + * 使用统一的数据转换函数,提高代码复用性 + */ + const addBoard = async (board: Omit) => { + loading.value = true + error.value = null + + try { + const response = await boardApiService.createKanban(board) + + if (response.data) { + const newBoard = transformBoardData(response.data) + boards.value.push(newBoard) + return newBoard + } else { + error.value = response.msg || '创建看板失败' + return null + } + } catch (err) { + error.value = err instanceof Error ? err.message : '创建看板失败' + return null + } finally { + loading.value = false + } + } + + /** + * 更新看板 + * 使用统一的数据转换函数,提高代码复用性 + */ + const updateBoard = async (boardId: string, updates: Partial) => { + loading.value = true + error.value = null + + try { + const response = await boardApiService.updateKanban(boardId, updates) + + if (response.data) { + const updatedBoard = transformBoardData(response.data) + const index = boards.value.findIndex(b => b.id === boardId) + + if (index !== -1) { + boards.value[index] = updatedBoard + } + return updatedBoard + } else { + error.value = response.msg || '更新看板失败' + return null + } + } catch (err) { + error.value = err instanceof Error ? err.message : '更新看板失败' + return null + } finally { + loading.value = false + } + } + + // 删除看板 + const deleteBoard = async (boardId: string) => { + loading.value = true + error.value = null + + try { + const response = await boardApiService.deleteKanban(boardId) + if (response.data !== null) { + const index = boards.value.findIndex(b => b.id === boardId) + if (index !== -1) { + boards.value.splice(index, 1) + } + return true + } else { + error.value = response.msg || '删除看板失败' + return false + } + } catch (err) { + error.value = err instanceof Error ? err.message : '删除看板失败' + return false + } finally { + loading.value = false + } + } + + // 根据 ID 获取看板 + const getBoardById = (boardId: string) => { + return boards.value.find(b => b.id === boardId) + } + + // 清除错误状态 + const clearError = () => { + error.value = null + } + + return { + boards, + loading, + error, + fetchBoards, + addBoard, + updateBoard, + deleteBoard, + getBoardById, + clearError + } +}) diff --git a/frontend/src/views/dashboard/BoardListView.vue b/frontend/src/views/dashboard/BoardListView.vue new file mode 100644 index 0000000..00efd7b --- /dev/null +++ b/frontend/src/views/dashboard/BoardListView.vue @@ -0,0 +1,195 @@ + + +