这是一个完整的企业级项目示例,展示如何使用 Agent Skills 构建生产就绪的应用。
项目概述
项目名称: TaskMaster Pro 技术栈: Vue 3 + TypeScript + Pinia + Vite + Element Plus 功能: 企业任务管理、团队协作、数据可视化
涉及的 Skills
skills/
├── vue3-enterprise/ # 前端框架规范
├── typescript-strict/ # 严格类型检查
├── pinia-store-patterns/ # 状态管理模式
├── api-integration/ # API 集成规范
├── form-validation/ # 表单验证
├── data-visualization/ # 数据可视化
└── performance-optimization/ # 性能优化
第一阶段: 项目初始化
1.1 项目结构 (由 vue3-enterprise skill 生成)
taskmaster-pro/
├── src/
│ ├── api/ # API 调用层
│ │ ├── client.ts # HTTP 客户端
│ │ ├── tasks.ts # 任务相关 API
│ │ ├── users.ts # 用户相关 API
│ │ └── projects.ts # 项目相关 API
│ ├── components/ # 通用组件
│ │ ├── common/
│ │ │ ├── AppButton.vue
│ │ │ ├── AppInput.vue
│ │ │ ├── AppModal.vue
│ │ │ └── AppTable.vue
│ │ ├── task/
│ │ │ ├── TaskCard.vue
│ │ │ ├── TaskList.vue
│ │ │ ├── TaskForm.vue
│ │ │ └── TaskFilter.vue
│ │ └── chart/
│ │ ├── ProgressChart.vue
│ │ ├── BurndownChart.vue
│ │ └── TeamVelocity.vue
│ ├── composables/ # 可复用逻辑
│ │ ├── useTask.ts
│ │ ├── useProject.ts
│ │ ├── useAuth.ts
│ │ ├── usePagination.ts
│ │ └── useWebSocket.ts
│ ├── stores/ # Pinia 状态管理
│ │ ├── auth.ts
│ │ ├── task.ts
│ │ ├── project.ts
│ │ └── notification.ts
│ ├── types/ # TypeScript 类型
│ │ ├── task.ts
│ │ ├── user.ts
│ │ ├── project.ts
│ │ └── api.ts
│ ├── utils/ # 工具函数
│ │ ├── date.ts
│ │ ├── validators.ts
│ │ ├── formatters.ts
│ │ └── storage.ts
│ ├── views/ # 页面组件
│ │ ├── Dashboard.vue
│ │ ├── TaskBoard.vue
│ │ ├── ProjectList.vue
│ │ └── Settings.vue
│ ├── router/
│ │ └── index.ts
│ ├── App.vue
│ └── main.ts
├── tests/
│ ├── unit/
│ └── e2e/
├── .env.development
├── .env.production
├── tsconfig.json
├── vite.config.ts
└── package.json
1.2 核心类型定义 (typescript-strict skill)
// types/task.ts
export interface Task {
id: string
title: string
description: string
status: TaskStatus
priority: TaskPriority
assigneeId: string
projectId: string
dueDate: Date
estimatedHours: number
actualHours: number
tags: string[]
createdAt: Date
updatedAt: Date
createdBy: string
}
export enum TaskStatus {
TODO = 'todo',
IN_PROGRESS = 'in_progress',
IN_REVIEW = 'in_review',
DONE = 'done',
BLOCKED = 'blocked'
}
export enum TaskPriority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
URGENT = 'urgent'
}
export interface TaskFilter {
status?: TaskStatus[]
priority?: TaskPriority[]
assigneeId?: string[]
projectId?: string[]
dueDateRange?: [Date, Date]
searchQuery?: string
}
export interface TaskStatistics {
total: number
byStatus: Record<TaskStatus, number>
byPriority: Record<TaskPriority, number>
completionRate: number
avgCompletionTime: number
}
// types/project.ts
export interface Project {
id: string
name: string
description: string
ownerId: string
memberIds: string[]
startDate: Date
endDate: Date
status: ProjectStatus
progress: number
budget: number
spentBudget: number
}
export enum ProjectStatus {
PLANNING = 'planning',
ACTIVE = 'active',
ON_HOLD = 'on_hold',
COMPLETED = 'completed',
CANCELLED = 'cancelled'
}
// types/user.ts
export interface User {
id: string
name: string
email: string
avatar?: string
role: UserRole
department: string
joinDate: Date
}
export enum UserRole {
ADMIN = 'admin',
MANAGER = 'manager',
MEMBER = 'member',
GUEST = 'guest'
}
// types/api.ts
export interface ApiResponse<T = unknown> {
success: boolean
data: T
message?: string
timestamp: number
}
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface ApiError {
code: string
message: string
details?: unknown
}
第二阶段: API 层实现
2.1 HTTP 客户端 (api-integration skill)
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { ApiResponse, ApiError } from '@/types/api'
import { useAuthStore } from '@/stores/auth'
import { useNotificationStore } from '@/stores/notification'
class ApiClient {
private client: AxiosInstance
private requestQueue: Map<string, AbortController> = new Map()
constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
this.setupInterceptors()
}
private setupInterceptors() {
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
// 添加认证 token
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
// 添加请求 ID 用于取消重复请求
const requestId = this.getRequestId(config)
if (this.requestQueue.has(requestId)) {
// 取消之前的相同请求
this.requestQueue.get(requestId)?.abort()
}
const controller = new AbortController()
config.signal = controller.signal
this.requestQueue.set(requestId, controller)
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
this.client.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// 清理请求队列
const requestId = this.getRequestId(response.config)
this.requestQueue.delete(requestId)
return response
},
async (error) => {
const notificationStore = useNotificationStore()
// 清理请求队列
if (error.config) {
const requestId = this.getRequestId(error.config)
this.requestQueue.delete(requestId)
}
// 处理不同的错误情况
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,跳转到登录
const authStore = useAuthStore()
authStore.logout()
window.location.href = '/login'
break
case 403:
notificationStore.error('没有权限执行此操作')
break
case 404:
notificationStore.error('请求的资源不存在')
break
case 429:
notificationStore.error('请求过于频繁,请稍后再试')
break
case 500:
notificationStore.error('服务器错误,请稍后再试')
break
default:
const apiError: ApiError = error.response.data
notificationStore.error(apiError.message || '请求失败')
}
} else if (error.request) {
notificationStore.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
}
private getRequestId(config: AxiosRequestConfig): string {
return `${config.method}-${config.url}-${JSON.stringify(config.params || {})}`
}
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<ApiResponse<T>>(url, config)
return response.data.data
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<ApiResponse<T>>(url, data, config)
return response.data.data
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<ApiResponse<T>>(url, data, config)
return response.data.data
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<ApiResponse<T>>(url, config)
return response.data.data
}
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.patch<ApiResponse<T>>(url, data, config)
return response.data.data
}
}
export const apiClient = new ApiClient()
2.2 任务 API (api-integration skill)
// api/tasks.ts
import { apiClient } from './client'
import type { Task, TaskFilter, TaskStatistics, PaginatedResponse } from '@/types'
export const taskApi = {
/**
* 获取任务列表
*/
async getTasks(filter: TaskFilter = {}, page = 1, pageSize = 20) {
return apiClient.get<PaginatedResponse<Task>>('/tasks', {
params: {
...filter,
page,
pageSize
}
})
},
/**
* 获取单个任务
*/
async getTask(id: string) {
return apiClient.get<Task>(`/tasks/${id}`)
},
/**
* 创建任务
*/
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'createdBy'>) {
return apiClient.post<Task>('/tasks', task)
},
/**
* 更新任务
*/
async updateTask(id: string, updates: Partial<Task>) {
return apiClient.patch<Task>(`/tasks/${id}`, updates)
},
/**
* 删除任务
*/
async deleteTask(id: string) {
return apiClient.delete<void>(`/tasks/${id}`)
},
/**
* 批量更新任务
*/
async batchUpdateTasks(taskIds: string[], updates: Partial<Task>) {
return apiClient.post<Task[]>('/tasks/batch-update', {
taskIds,
updates
})
},
/**
* 获取任务统计
*/
async getTaskStatistics(projectId?: string) {
return apiClient.get<TaskStatistics>('/tasks/statistics', {
params: { projectId }
})
},
/**
* 分配任务
*/
async assignTask(taskId: string, assigneeId: string) {
return apiClient.post<Task>(`/tasks/${taskId}/assign`, { assigneeId })
},
/**
* 更新任务状态
*/
async updateTaskStatus(taskId: string, status: TaskStatus) {
return apiClient.patch<Task>(`/tasks/${taskId}/status`, { status })
}
}
第三阶段: 状态管理
3.1 任务 Store (pinia-store-patterns skill)
// stores/task.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { taskApi } from '@/api/tasks'
import type { Task, TaskFilter, TaskStatistics } from '@/types'
export const useTaskStore = defineStore('task', () => {
// State
const tasks = ref<Task[]>([])
const selectedTask = ref<Task | null>(null)
const filter = ref<TaskFilter>({})
const statistics = ref<TaskStatistics | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
// Pagination
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
// Getters
const filteredTasks = computed(() => {
let result = tasks.value
// 状态筛选
if (filter.value.status?.length) {
result = result.filter(task => filter.value.status!.includes(task.status))
}
// 优先级筛选
if (filter.value.priority?.length) {
result = result.filter(task => filter.value.priority!.includes(task.priority))
}
// 负责人筛选
if (filter.value.assigneeId?.length) {
result = result.filter(task => filter.value.assigneeId!.includes(task.assigneeId))
}
// 搜索
if (filter.value.searchQuery) {
const query = filter.value.searchQuery.toLowerCase()
result = result.filter(task =>
task.title.toLowerCase().includes(query) ||
task.description.toLowerCase().includes(query)
)
}
return result
})
const tasksByStatus = computed(() => {
return filteredTasks.value.reduce((acc, task) => {
if (!acc[task.status]) {
acc[task.status] = []
}
acc[task.status].push(task)
return acc
}, {} as Record<TaskStatus, Task[]>)
})
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
// Actions
async function fetchTasks() {
loading.value = true
error.value = null
try {
const response = await taskApi.getTasks(
filter.value,
currentPage.value,
pageSize.value
)
tasks.value = response.items
total.value = response.total
} catch (err) {
error.value = err as Error
throw err
} finally {
loading.value = false
}
}
async function fetchTask(id: string) {
loading.value = true
error.value = null
try {
selectedTask.value = await taskApi.getTask(id)
return selectedTask.value
} catch (err) {
error.value = err as Error
throw err
} finally {
loading.value = false
}
}
async function createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'createdBy'>) {
loading.value = true
error.value = null
try {
const newTask = await taskApi.createTask(task)
tasks.value.unshift(newTask)
total.value++
return newTask
} catch (err) {
error.value = err as Error
throw err
} finally {
loading.value = false
}
}
async function updateTask(id: string, updates: Partial<Task>) {
loading.value = true
error.value = null
try {
const updatedTask = await taskApi.updateTask(id, updates)
// 更新列表中的任务
const index = tasks.value.findIndex(t => t.id === id)
if (index !== -1) {
tasks.value[index] = updatedTask
}
// 更新选中的任务
if (selectedTask.value?.id === id) {
selectedTask.value = updatedTask
}
return updatedTask
} catch (err) {
error.value = err as Error
throw err
} finally {
loading.value = false
}
}
async function deleteTask(id: string) {
loading.value = true
error.value = null
try {
await taskApi.deleteTask(id)
tasks.value = tasks.value.filter(t => t.id !== id)
total.value--
if (selectedTask.value?.id === id) {
selectedTask.value = null
}
} catch (err) {
error.value = err as Error
throw err
} finally {
loading.value = false
}
}
async function fetchStatistics(projectId?: string) {
try {
statistics.value = await taskApi.getTaskStatistics(projectId)
} catch (err) {
console.error('Failed to fetch statistics:', err)
}
}
function setFilter(newFilter: TaskFilter) {
filter.value = newFilter
currentPage.value = 1
fetchTasks()
}
function clearFilter() {
filter.value = {}
currentPage.value = 1
fetchTasks()
}
function setPage(page: number) {
currentPage.value = page
fetchTasks()
}
function reset() {
tasks.value = []
selectedTask.value = null
filter.value = {}
statistics.value = null
currentPage.value = 1
total.value = 0
loading.value = false
error.value = null
}
return {
// State
tasks,
selectedTask,
filter,
statistics,
loading,
error,
currentPage,
pageSize,
total,
// Getters
filteredTasks,
tasksByStatus,
totalPages,
// Actions
fetchTasks,
fetchTask,
createTask,
updateTask,
deleteTask,
fetchStatistics,
setFilter,
clearFilter,
setPage,
reset
}
})
第四阶段: 核心组件
4.1 任务看板 (vue3-enterprise skill)
<!-- components/task/TaskBoard.vue -->
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useTaskStore } from '@/stores/task'
import { TaskStatus } from '@/types'
import TaskCard from './TaskCard.vue'
import { useDraggable } from '@/composables/useDraggable'
const taskStore = useTaskStore()
const columns = [
{ status: TaskStatus.TODO, title: '待办', color: '#909399' },
{ status: TaskStatus.IN_PROGRESS, title: '进行中', color: '#409EFF' },
{ status: TaskStatus.IN_REVIEW, title: '审核中', color: '#E6A23C' },
{ status: TaskStatus.DONE, title: '已完成', color: '#67C23A' },
{ status: TaskStatus.BLOCKED, title: '阻塞', color: '#F56C6C' }
]
const tasksByColumn = computed(() => {
return columns.map(col => ({
...col,
tasks: taskStore.tasksByStatus[col.status] || []
}))
})
const { onDragStart, onDragOver, onDrop } = useDraggable()
const handleDrop = async (status: TaskStatus, event: DragEvent) => {
const taskId = onDrop(event)
if (!taskId) return
try {
await taskStore.updateTask(taskId, { status })
} catch (error) {
console.error('Failed to update task status:', error)
}
}
onMounted(() => {
taskStore.fetchTasks()
taskStore.fetchStatistics()
})
</script>
<template>
<div class="task-board">
<!-- 统计面板 -->
<div v-if="taskStore.statistics" class="statistics-panel">
<div class="stat-card">
<div class="stat-value">{{ taskStore.statistics.total }}</div>
<div class="stat-label">总任务数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ Math.round(taskStore.statistics.completionRate * 100) }}%</div>
<div class="stat-label">完成率</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ taskStore.statistics.avgCompletionTime }}h</div>
<div class="stat-label">平均完成时间</div>
</div>
</div>
<!-- 看板列 -->
<div class="board-columns">
<div
v-for="column in tasksByColumn"
:key="column.status"
class="board-column"
@dragover.prevent="onDragOver"
@drop="handleDrop(column.status, $event)"
>
<div class="column-header" :style="{ borderTopColor: column.color }">
<h3>{{ column.title }}</h3>
<span class="task-count">{{ column.tasks.length }}</span>
</div>
<div class="column-content">
<TaskCard
v-for="task in column.tasks"
:key="task.id"
:task="task"
draggable="true"
@dragstart="onDragStart($event, task.id)"
/>
<div v-if="column.tasks.length === 0" class="empty-column">
暂无任务
</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="taskStore.loading" class="loading-overlay">
<el-loading />
</div>
</div>
</template>
<style scoped>
.task-board {
height: 100%;
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
background-color: #f5f7fa;
}
.statistics-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #303133;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.board-columns {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
overflow-x: auto;
}
.board-column {
display: flex;
flex-direction: column;
min-width: 280px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.column-header {
padding: 16px;
border-top: 4px solid;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fafafa;
}
.column-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.task-count {
background-color: #e4e7ed;
color: #606266;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.column-content {
flex: 1;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.empty-column {
text-align: center;
color: #c0c4cc;
padding: 40px 20px;
font-size: 14px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
</style>
4.2 任务卡片组件
<!-- components/task/TaskCard.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import type { Task } from '@/types'
import { formatDate, getRelativeTime } from '@/utils/date'
import { getPriorityColor, getStatusColor } from '@/utils/formatters'
interface Props {
task: Task
}
const props = defineProps<Props>()
const emit = defineEmits<{
click: [task: Task]
edit: [task: Task]
delete: [taskId: string]
}>()
const priorityColor = computed(() => getPriorityColor(props.task.priority))
const statusColor = computed(() => getStatusColor(props.task.status))
const isOverdue = computed(() =>
props.task.dueDate && new Date(props.task.dueDate) < new Date()
)
const progressPercentage = computed(() => {
if (props.task.estimatedHours === 0) return 0
return Math.min((props.task.actualHours / props.task.estimatedHours) * 100, 100)
})
</script>
<template>
<div class="task-card" @click="emit('click', task)">
<!-- 优先级标识 -->
<div class="priority-indicator" :style="{ backgroundColor: priorityColor }" />
<!-- 任务标题 -->
<h4 class="task-title">{{ task.title }}</h4>
<!-- 任务描述 -->
<p v-if="task.description" class="task-description">
{{ task.description }}
</p>
<!-- 标签 -->
<div v-if="task.tags.length" class="task-tags">
<span v-for="tag in task.tags" :key="tag" class="task-tag">
{{ tag }}
</span>
</div>
<!-- 进度条 -->
<div v-if="task.estimatedHours > 0" class="progress-section">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progressPercentage}%` }"
/>
</div>
<div class="progress-text">
{{ task.actualHours }}h / {{ task.estimatedHours }}h
</div>
</div>
<!-- 截止日期 -->
<div class="task-footer">
<div v-if="task.dueDate" class="due-date" :class="{ overdue: isOverdue }">
<el-icon><Calendar /></el-icon>
{{ getRelativeTime(task.dueDate) }}
</div>
<!-- 负责人头像 -->
<div class="assignee-avatar">
<el-avatar :size="24" :src="task.assignee?.avatar" />
</div>
</div>
<!-- 操作按钮 -->
<div class="task-actions">
<el-button
size="small"
type="text"
@click.stop="emit('edit', task)"
>
<el-icon><Edit /></el-icon>
</el-button>
<el-button
size="small"
type="text"
@click.stop="emit('delete', task.id)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<style scoped>
.task-card {
position: relative;
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.task-card:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
transform: translateY(-2px);
}
.priority-indicator {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
border-radius: 8px 0 0 8px;
}
.task-title {
margin: 0 0 8px 0;
font-size: 15px;
font-weight: 600;
color: #303133;
line-height: 1.4;
padding-left: 12px;
}
.task-description {
margin: 0 0 12px 0;
font-size: 13px;
color: #606266;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.task-tag {
padding: 2px 8px;
background-color: #f4f4f5;
color: #606266;
border-radius: 4px;
font-size: 12px;
}
.progress-section {
margin-bottom: 12px;
}
.progress-bar {
height: 6px;
background-color: #e4e7ed;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #409eff, #67c23a);
transition: width 0.3s;
}
.progress-text {
font-size: 11px;
color: #909399;
text-align: right;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.due-date {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
}
.due-date.overdue {
color: #f56c6c;
}
.task-actions {
position: absolute;
top: 12px;
right: 12px;
display: none;
gap: 4px;
}
.task-card:hover .task-actions {
display: flex;
}
</style>
第五阶段: 性能优化
5.1 虚拟滚动 (performance-optimization skill)
// composables/useVirtualScroll.ts
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
interface VirtualScrollOptions {
itemHeight: number
bufferSize?: number
}
export function useVirtualScroll<T>(
items: Ref<T[]>,
options: VirtualScrollOptions
) {
const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)
const containerHeight = ref(0)
const { itemHeight, bufferSize = 5 } = options
// 计算可见区域
const visibleStart = computed(() =>
Math.max(0, Math.floor(scrollTop.value / itemHeight) - bufferSize)
)
const visibleEnd = computed(() =>
Math.min(
items.value.length,
Math.ceil((scrollTop.value + containerHeight.value) / itemHeight) + bufferSize
)
)
// 可见的项目
const visibleItems = computed(() =>
items.value.slice(visibleStart.value, visibleEnd.value).map((item, index) => ({
data: item,
index: visibleStart.value + index
}))
)
// 总高度
const totalHeight = computed(() => items.value.length * itemHeight)
// 偏移量
const offsetY = computed(() => visibleStart.value * itemHeight)
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement
scrollTop.value = target.scrollTop
}
const updateContainerHeight = () => {
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight
}
}
onMounted(() => {
updateContainerHeight()
window.addEventListener('resize', updateContainerHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', updateContainerHeight)
})
return {
containerRef,
visibleItems,
totalHeight,
offsetY,
handleScroll
}
}
这个实战项目展示了:
- 完整的项目结构 - 企业级目录组织
- 严格的类型系统 - TypeScript 类型定义
- API 层设计 - HTTP 客户端和错误处理
- 状态管理 - Pinia store 最佳实践
- 组件化开发 - 可复用组件设计
- 性能优化 - 虚拟滚动、请求去重
所有代码都遵循了对应 Skills 的最佳实践。