【核心功能篇】項目與模塊管理:前端頁面開發與后端 API 聯調實戰
- 前言
- 準備工作
- 第一部分:完善項目管理功能 (Project)
- 1. 創建/編輯項目的表單對話框組件
- 第二部分:模塊管理功能 (集成到項目詳情頁)
- 1. 創建模塊相關的 API 服務 (`src/api/module.ts`)
- 2. 在 `ProjectDetailView.vue` 中展示和管理模塊
- 3. 測試項目、模塊管理功能:
- 總結
前言
一個測試平臺最基礎也最核心的功能之一就是對測試項目和項目內模塊的管理。用戶需要能夠方便地創建、查看、修改和刪除這些實體。
- 項目管理: 我們已經在
ProjectListView.vue
中展示了項目列表。現在需要添加:- 一個“新建項目”的入口,彈出表單對話框。
- 列表中每行項目有“編輯”和“刪除”操作。
- 模塊管理: 模塊是項目的一部分。我們考慮在查看項目詳情時,一并展示和管理該項目下的模塊。
- 在
ProjectDetailView.vue
中顯示模塊列表。 - 提供在該項目下“新建模塊”、“編輯模塊”、“刪除模塊”的功能。
- 在
我們將遵循“數據驅動視圖”和“用戶操作 -> 調用 API -> 更新視圖/狀態”的模式。
這篇文章將帶你:
- 完善項目列表功能,并實現新建、編輯項目的表單交互和 API 調用。
- 在項目詳情頁中展示其關聯的模塊列表。
- 實現新建、編輯和刪除模塊的功能。
我們將大量使用 Element Plus 的表單、表格和對話框組件,并結合之前封裝的 Axios 請求和 API 服務模塊。
準備工作
- 前端項目就緒:
test-platform/frontend
項目可以正常運行 (npm run dev
)。 - 后端 API 運行中: Django 后端服務運行(
python manage.py runserver
),項目和模塊的 API (/api/projects/
,/api/modules/
) 可用。 - Axios 和 API 服務已封裝:
utils/request.ts
和api/project.ts
已按上一篇文章配置好。 - Pinia 狀態管理可用: 確保用戶登錄狀態能被正確管理。
- Element Plus 集成完畢。
第一部分:完善項目管理功能 (Project)
1. 創建/編輯項目的表單對話框組件
為了代碼的復用性和可維護性,我們將新建/編輯項目的表單邏輯封裝到一個單獨的對話框組件中。
a. 在frontend/src/views/project
目錄下創建 components
目錄和ProjectFormDialog.vue
文件:
b. 編寫 ProjectFormDialog.vue
組件:
<!-- test-platform/frontend/src/views/project/components/ProjectFormDialog.vue -->
<template><el-dialog:title="dialogTitle"v-model="internalVisible"width="500px":close-on-click-modal="false"@close="handleClose"><el-formref="projectFormRef":model="formData":rules="formRules"label-width="100px"v-loading="formLoading"><el-form-item label="項目名稱" prop="name"><el-input v-model="formData.name" placeholder="請輸入項目名稱" /></el-form-item><el-form-item label="項目描述" prop="description"><el-inputv-model="formData.description"type="textarea"placeholder="請輸入項目描述"/></el-form-item><el-form-item label="負責人" prop="owner"><el-input v-model="formData.owner" placeholder="請輸入負責人" /></el-form-item><el-form-item label="項目狀態" prop="status"><el-select v-model="formData.status" placeholder="請選擇項目狀態"><el-option label="規劃中" :value="0" /><el-option label="進行中" :value="1" /><el-option label="已完成" :value="2" /><el-option label="擱置" :value="3" /></el-select></el-form-item></el-form><template #footer><span class="dialog-footer"><el-button @click="handleClose">取 消</el-button><el-button type="primary" @click="handleSubmit" :loading="submitLoading">確 定</el-button></span></template></el-dialog>
</template><script setup lang="ts">
import { ref, watch, reactive, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { createProject, getProjectDetail, updateProject, type Project, type CreateProjectData } from '@/api/project'// Props
const props = defineProps<{visible: boolean; // 控制對話框顯示/隱藏projectId?: number | null; // 項目ID,用于編輯模式
}>()// Emits
const emit = defineEmits<{(e: 'update:visible', value: boolean): void; // 更新 visible(e: 'success'): void; // 操作成功后觸發
}>()// 內部控制對話框的顯示,通過 emit 更新父組件的 visible
const internalVisible = computed({get: () => props.visible,set: (val) => emit('update:visible', val)
})const projectFormRef = ref<FormInstance>()
const formLoading = ref(false)
const submitLoading = ref(false)const initialFormData: CreateProjectData = {name: '',description: '',owner: '',status: 1, // 默認為 "進行中"
}
const formData = reactive<CreateProjectData>({ ...initialFormData })const formRules = reactive<FormRules>({name: [{ required: true, message: '項目名稱不能為空', trigger: 'blur' }],status: [{ required: true, message: '請選擇項目狀態', trigger: 'change' }],
})const dialogTitle = computed(() => (props.projectId ? '編輯項目' : '新建項目'))// 監聽 projectId 的變化,用于編輯模式下加載數據
watch(() => props.projectId,async (newId) => {if (newId && props.visible) { // 確保是編輯模式且對話框可見時加載formLoading.value = truetry {const response = await getProjectDetail(newId)// 將獲取到的數據填充到表單Object.assign(formData, {name: response.data.name,description: response.data.description || '',owner: response.data.owner || '',status: response.data.status,})} catch (error) {ElMessage.error('獲取項目詳情失敗')console.error('Failed to fetch project detail:', error)} finally {formLoading.value = false}} else if (!newId) {// 如果是新建模式 (projectId 為 null 或 undefined),重置表單resetForm()}},{ immediate: true } // 立即執行一次,以便在對話框首次打開時(如果是編輯模式)加載數據
)// 監聽對話框顯示狀態,如果從隱藏變為顯示,且是新建模式,則重置表單
watch(() => props.visible, (newVal) => {if (newVal && !props.projectId) {resetForm();}if (newVal && props.projectId) {// 如果是編輯模式,并且 watch projectId 沒有立即執行(例如 props.visible 先變為 true)// 最好也觸發一次數據加載,但上面的 watch projectId 應該能覆蓋}
});const resetForm = () => {Object.assign(formData, initialFormData)projectFormRef.value?.clearValidate() // 清除校驗狀態
}const handleClose = () => {internalVisible.value = falseresetForm()
}const handleSubmit = async () => {if (!projectFormRef.value) returnawait projectFormRef.value.validate(async (valid) => {if (valid) {submitLoading.value = truetry {if (props.projectId) {// 編輯模式await updateProject(props.projectId, formData)ElMessage.success('項目更新成功!')} else {// 新建模式await createProject(formData)ElMessage.success('項目創建成功!')}emit('success') // 通知父組件操作成功handleClose()} catch (error) {// 錯誤提示已在 request.ts 中統一處理,這里可以不重復提示console.error('項目操作失敗:', error)} finally {submitLoading.value = false}} else {console.log('表單校驗失敗!')return false}})
}
</script><style scoped>
/* 可選:為對話框添加一些樣式 */
</style>
代碼解釋:
- Props & Emits: 定義了
visible
(雙向綁定) 和projectId
(用于區分新建/編輯) 作為 props,以及success
事件用于通知父組件操作成功。 internalVisible
: 使用computed
和emit('update:visible', val)
來實現v-model
的效果,使得父組件可以直接用v-model
控制對話框的顯示。- 表單數據與校驗:
formData
存儲表單數據,formRules
定義校驗規則。initialFormData
用于重置表單。 dialogTitle
: 根據props.projectId
是否存在來動態顯示“新建項目”或“編輯項目”。watch(props.projectId, ...)
: 監聽projectId
的變化。- 如果是編輯模式 (
newId
存在且props.visible
為true),則調用getProjectDetail
API 獲取項目數據并填充到表單中。 - 如果是新建模式 (
!newId
),則調用resetForm
。 { immediate: true }
確保在組件初始化時,如果projectId
有值且visible
為true
,也會嘗試加載數據。
- 如果是編輯模式 (
watch(props.visible, ...)
: 監聽visible
的變化。當對話框從隱藏變為顯示,并且是新建模式時,重置表單。這是為了確保每次打開新建對話框時表單是干凈的。resetForm()
: 將formData
重置為初始值,并清除表單的校驗狀態。handleClose()
: 關閉對話框并重置表單。handleSubmit()
:- 首先進行表單校驗 (
projectFormRef.value.validate
)。 - 校驗通過后,根據是否存在
props.projectId
來判斷是調用createProject
API 還是updateProject
API。 - API 調用成功后,顯示成功消息,觸發
success
事件,并關閉對話框。 - API 調用失敗的錯誤提示已在
request.ts
中統一處理。
- 首先進行表單校驗 (
c. 在 ProjectListView.vue
中使用 ProjectFormDialog
組件:
修改 test-platform/frontend/src/views/project/ProjectListView.vue
文件:
<!-- test-platform/frontend/src/views/project/ProjectListView.vue -->
<template><div class="project-list-view"><div class="page-header"><h2>項目列表</h2><el-button type="primary" @click="openCreateDialog"> <!-- 修改:調用 openCreateDialog --><el-icon><Plus /></el-icon> 新建項目</el-button></div><el-table :data="projects" v-loading="loading" style="width: 100%" empty-text="暫無項目數據"><!-- 表格列定義保持不變 --><el-table-column prop="id" label="ID" width="80" /><el-table-column prop="name" label="項目名稱" min-width="180" /><el-table-column prop="description" label="描述" min-width="250" show-overflow-tooltip /><el-table-column prop="owner" label="負責人" width="120" /><el-table-column prop="status" label="狀態" width="120"><template #default="scope"><el-tag :type="getStatusTagType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag></template></el-table-column><el-table-column prop="create_time" label="創建時間" width="180"><template #default="scope">{{ formatDateTime(scope.row.create_time) }}</template></el-table-column><el-table-column label="操作" width="200" fixed="right"><template #default="scope"><el-button size="small" type="primary" @click="handleViewDetail(scope.row.id)">查看</el-button><el-button size="small" type="warning" @click="openEditDialog(scope.row)"> <!-- 修改:調用 openEditDialog -->編輯</el-button><el-popconfirmtitle="確定要刪除這個項目嗎?"confirm-button-text="確定"cancel-button-text="取消"@confirm="handleDeleteProject(scope.row.id)"><template #reference><el-button size="small" type="danger">刪除</el-button></template></el-popconfirm></template></el-table-column></el-table><!-- 1. 引入并使用項目表單對話框組件 --><project-form-dialog v-model:visible="dialogVisible" :project-id="editingProjectId" @success="onFormSuccess" /></div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getProjectList, deleteProject, type Project } from '@/api/project'
import ProjectFormDialog from './components/ProjectFormDialog.vue' // 2. 導入組件const router = useRouter()
const projects = ref<Project[]>([])
const loading = ref(false)// 3. 控制對話框顯示和編輯的項目ID
const dialogVisible = ref(false)
const editingProjectId = ref<number | null>(null)const fetchProjectList = async () => { /* ...保持不變... */ loading.value = truetry {const response = await getProjectList()projects.value = response.data} catch (error) {console.error('獲取項目列表失敗:', error)} finally {loading.value = false}
}onMounted(() => {fetchProjectList()
})const formatDateTime = (dateTimeStr: string) => { /* ...保持不變... */ if (!dateTimeStr) return ''const date = new Date(dateTimeStr)return date.toLocaleString()
}
const getStatusText = (status: number) => { /* ...保持不變... */ const statusMap: { [key: number]: string } = {0: '規劃中', 1: '進行中', 2: '已完成', 3: '擱置'}return statusMap[status] || '未知狀態'
}
const getStatusTagType = (status: number) => { /* ...保持不變... */ const typeMap: { [key: number]: '' | 'success' | 'warning' | 'info' | 'danger' } = {0: 'info', 1: '', 2: 'success', 3: 'warning'}return typeMap[status] || 'info'
}// 4. 打開新建對話框
const openCreateDialog = () => {editingProjectId.value = null // 清空編輯ID,表示是新建dialogVisible.value = true
}// 5. 打開編輯對話框
const openEditDialog = (project: Project) => {editingProjectId.value = project.id // 設置編輯IDdialogVisible.value = true
}// 6. 表單操作成功后的回調
const onFormSuccess = () => {dialogVisible.value = false // 關閉對話框fetchProjectList() // 刷新列表
}const handleViewDetail = (projectId: number) => { /* ...保持不變... */ router.push(`/project/detail/${projectId}`)
}const handleDeleteProject = async (projectId: number) => { /* ...保持不變... */ loading.value = truetry {await deleteProject(projectId)ElMessage.success('項目刪除成功!')fetchProjectList()} catch (error) {console.error('刪除項目失敗:', error)} finally {loading.value = false}
}
</script><style scoped lang="scss">
/* ...樣式保持不變... */
.project-list-view {padding: 20px;.page-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;}
}
</style>
代碼解釋:
- 在
<template>
中,我們添加了<project-form-dialog ... />
組件。v-model:visible="dialogVisible"
: 雙向綁定對話框的顯示狀態。:project-id="editingProjectId"
: 將當前要編輯的項目 ID (或null
表示新建) 傳遞給子組件。@success="onFormSuccess"
: 監聽子組件觸發的success
事件。
- 在
<script setup>
中,導入ProjectFormDialog
組件。 - 定義
dialogVisible
(控制對話框顯示) 和editingProjectId
(存儲當前編輯的項目 ID)。 openCreateDialog()
: 當點擊“新建項目”按鈕時調用。它將editingProjectId
設置為null
(表示新建模式),并將dialogVisible
設置為true
來打開對話框。openEditDialog(project: Project)
: 當點擊某行項目的“編輯”按鈕時調用。它將editingProjectId
設置為該項目的id
,并將dialogVisible
設置為true
。onFormSuccess()
: 當ProjectFormDialog
組件觸發success
事件時調用 (表示新建或編輯成功)。它會關閉對話框并調用fetchProjectList()
重新加載項目列表。
第二部分:模塊管理功能 (集成到項目詳情頁)
模塊是屬于特定項目的,所以我們將其管理功能放在項目詳情頁 ProjectDetailView.vue
中。
1. 創建模塊相關的 API 服務 (src/api/module.ts
)
// test-platform/frontend/src/api/module.ts
import request from '@/utils/request'
import type { AxiosPromise } from 'axios'export interface Module {id: number;name: string;description: string | null;project: number; // 所屬項目IDproject_name?: string; // 可選,如果API返回create_time: string;update_time: string;
}export type ModuleListResponse = Module[]export interface CreateModuleData {name: string;description?: string;project: number; // 創建時必須指定項目ID
}// 1. 獲取某個項目下的模塊列表
export function getModuleList(projectId: number): AxiosPromise<ModuleListResponse> {return request({url: '/modules/', // DRF ViewSet 通常支持通過查詢參數過濾method: 'get',params: { project_id: projectId } // 假設后端API支持 project_id 參數過濾})
}// 2. 在指定項目下創建模塊
export function createModule(data: CreateModuleData): AxiosPromise<Module> {return request({url: '/modules/',method: 'post',data})
}// 3. 獲取單個模塊詳情 (如果需要)
export function getModuleDetail(moduleId: number): AxiosPromise<Module> {return request({url: `/modules/${moduleId}/`,method: 'get'})
}// 4. 更新模塊
export function updateModule(moduleId: number, data: Partial<Omit<CreateModuleData, 'project'>>): AxiosPromise<Module> {// Omit<CreateModuleData, 'project'> 表示從 CreateModuleData 中排除 project 字段,因為通常不更新模塊的所屬項目return request({url: `/modules/${moduleId}/`,method: 'put', // 或 patchdata})
}// 5. 刪除模塊
export function deleteModule(moduleId: number): AxiosPromise<void> {return request({url: `/modules/${moduleId}/`,method: 'delete'})
}
注意: getModuleList
中的 params: { project_id: projectId }
假設你的后端 /api/modules/
ViewSet 支持通過 project_id
查詢參數進行過濾。這通常需要在 DRF 的 ModuleViewSet
中重寫 get_queryset
方法來實現,我們在【接口開發(下)】中為 TestCaseViewSet
做過類似處理。如果你的 ModuleViewSet
還沒有這個過濾,你需要去后端添加:
# api/views.py -> ModuleViewSet
class ModuleViewSet(viewsets.ModelViewSet):queryset = Module.objects.all()serializer_class = ModuleSerializerdef get_queryset(self):queryset = super().get_queryset()project_id = self.request.query_params.get('project_id')if project_id:try:queryset = queryset.filter(project_id=int(project_id))except ValueError:pass # Or handle errorreturn queryset.order_by('-create_time')
修改 api/serializers.py
中的 ModuleSerializer
:
# api/serializers.py -> ModuleSerializer
class ModuleSerializer(serializers.ModelSerializer):"""模塊序列化器"""project_name = serializers.CharField(source='project.name', read_only=True)class Meta:model = Modulefields = ['id', 'name', 'description', 'project', 'project_name', 'create_time', 'update_time']extra_kwargs = {'project': {'help_text': "關聯的項目ID",'required': False,},'create_time': {'read_only': True},'update_time': {'read_only': True},}# 如果你希望 project 在創建時必需,更新時非必需,可以這樣做:def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)# 如果是更新操作 (instance 存在),則 project 字段不是必需的if self.instance:self.fields['project'].required = False
2. 在 ProjectDetailView.vue
中展示和管理模塊
修改 test-platform/frontend/src/views/project/ProjectDetailView.vue
文件:
<!-- test-platform/frontend/src/views/project/ProjectDetailView.vue -->
<template><div class="project-detail-view" v-loading="pageLoading"><el-page-header @back="goBack" :content="projectDetail?.name || '項目詳情'" class="page-header-custom"><template #extra><el-button type="primary" @click="openCreateModuleDialog" v-if="projectDetail"><el-icon><Plus /></el-icon> 新建模塊</el-button></template></el-page-header><el-card class="box-card project-info-card" v-if="projectDetail"><template #header><div class="card-header"><span>項目基本信息</span><!-- <el-button class="button" text>編輯項目</el-button> --></div></template><el-descriptions :column="2" border><el-descriptions-item label="項目ID">{{ projectDetail.id }}</el-descriptions-item><el-descriptions-item label="項目名稱">{{ projectDetail.name }}</el-descriptions-item><el-descriptions-item label="負責人">{{ projectDetail.owner || '-' }}</el-descriptions-item><el-descriptions-item label="狀態"><el-tag :type="getStatusTagType(projectDetail.status)">{{ getStatusText(projectDetail.status) }}</el-tag></el-descriptions-item><el-descriptions-item label="創建時間" :span="2">{{ formatDateTime(projectDetail.create_time) }}</el-descriptions-item><el-descriptions-item label="描述" :span="2">{{ projectDetail.description || '-' }}</el-descriptions-item></el-descriptions></el-card><el-card class="box-card module-list-card" v-if="projectDetail"><template #header><div class="card-header"><span>模塊列表</span></div></template><el-table :data="modules" v-loading="moduleLoading" style="width: 100%" empty-text="該項目下暫無模塊"><el-table-column prop="id" label="模塊ID" width="100" /><el-table-column prop="name" label="模塊名稱" min-width="200" /><el-table-column prop="description" label="描述" min-width="300" show-overflow-tooltip /><el-table-column prop="create_time" label="創建時間" width="180"><template #default="scope">{{ formatDateTime(scope.row.create_time) }}</template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="scope"><el-button size="small" type="warning" @click="openEditModuleDialog(scope.row)">編輯</el-button><el-popconfirmtitle="確定要刪除這個模塊嗎?"@confirm="handleDeleteModule(scope.row.id)"><template #reference><el-button size="small" type="danger">刪除</el-button></template></el-popconfirm></template></el-table-column></el-table></el-card><!-- 新建/編輯模塊對話框 (為了簡化,先不封裝成獨立組件,直接寫在這里) --><el-dialog:title="moduleDialogTitle"v-model="moduleDialogVisible"width="500px":close-on-click-modal="false"@close="closeModuleDialog"><el-formref="moduleFormRef":model="moduleFormData":rules="moduleFormRules"label-width="100px"v-loading="moduleFormLoading"><el-form-item label="模塊名稱" prop="name"><el-input v-model="moduleFormData.name" placeholder="請輸入模塊名稱" /></el-form-item><el-form-item label="模塊描述" prop="description"><el-input v-model="moduleFormData.description" type="textarea" placeholder="請輸入模塊描述" /></el-form-item></el-form><template #footer><el-button @click="closeModuleDialog">取 消</el-button><el-button type="primary" @click="handleModuleSubmit" :loading="moduleSubmitLoading">確 定</el-button></template></el-dialog></div>
</template><script setup lang="ts">
import { ref, onMounted, computed, reactive, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElPageHeader } from 'element-plus' // 確保導入 ElPageHeader
import type { FormInstance, FormRules } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getProjectDetail, type Project } from '@/api/project'
import { getModuleList, createModule, updateModule, deleteModule, type Module, type CreateModuleData
} from '@/api/module'const route = useRoute()
const router = useRouter()const pageLoading = ref(false)
const projectDetail = ref<Project | null>(null)
const projectId = computed(() => Number(route.params.id)) // 從路由獲取項目IDconst modules = ref<Module[]>([])
const moduleLoading = ref(false)// 模塊表單對話框相關
const moduleDialogVisible = ref(false)
const moduleFormLoading = ref(false)
const moduleSubmitLoading = ref(false)
const moduleFormRef = ref<FormInstance>()
const editingModuleId = ref<number | null>(null)const initialModuleFormData: Omit<CreateModuleData, 'project'> = { // project ID 將在提交時添加name: '',description: '',
}
const moduleFormData = reactive({ ...initialModuleFormData })const moduleFormRules = reactive<FormRules>({name: [{ required: true, message: '模塊名稱不能為空', trigger: 'blur' }],
})const moduleDialogTitle = computed(() => (editingModuleId.value ? '編輯模塊' : '新建模塊'))// 獲取項目詳情
const fetchProjectDetail = async () => {if (!projectId.value) returnpageLoading.value = truetry {const response = await getProjectDetail(projectId.value)projectDetail.value = response.dataawait fetchModuleList() // 獲取項目詳情成功后,獲取其模塊列表} catch (error) {ElMessage.error('獲取項目詳情失敗')console.error(error)} finally {pageLoading.value = false}
}// 獲取模塊列表
const fetchModuleList = async () => {if (!projectDetail.value) return // 確保項目詳情已加載moduleLoading.value = truetry {const response = await getModuleList(projectDetail.value.id)modules.value = response.data} catch (error) {console.error('獲取模塊列表失敗:', error)} finally {moduleLoading.value = false}
}onMounted(() => {fetchProjectDetail()
})// 監聽路由參數變化,如果 projectId 變了,重新加載項目詳情和模塊列表
watch(() => route.params.id, (newId) => {if (newId && Number(newId) !== projectDetail.value?.id) {fetchProjectDetail();}
});const goBack = () => {router.back() // ???? router.push('/project/list')
}// --- 模塊操作 ---
const openCreateModuleDialog = () => {editingModuleId.value = nullObject.assign(moduleFormData, initialModuleFormData) // 重置表單moduleFormRef.value?.clearValidate()moduleDialogVisible.value = true
}const openEditModuleDialog = (module: Module) => {editingModuleId.value = module.idObject.assign(moduleFormData, { name: module.name, description: module.description || '' })moduleFormRef.value?.clearValidate()moduleDialogVisible.value = true
}const closeModuleDialog = () => {moduleDialogVisible.value = false
}const handleModuleSubmit = async () => {if (!moduleFormRef.value || !projectDetail.value) returnawait moduleFormRef.value.validate(async (valid) => {if (valid) {moduleSubmitLoading.value = trueconst dataToSubmit = { ...moduleFormData, project: projectDetail.value!.id }try {if (editingModuleId.value) {await updateModule(editingModuleId.value, moduleFormData) // 更新時不傳 project IDElMessage.success('模塊更新成功!')} else {await createModule(dataToSubmit)ElMessage.success('模塊創建成功!')}fetchModuleList() // 刷新模塊列表closeModuleDialog()} catch (error) {console.error('模塊操作失敗:', error)} finally {moduleSubmitLoading.value = false}}})
}const handleDeleteModule = async (moduleId: number) => {moduleLoading.value = true // 可以用一個更細粒度的刪除中狀態try {await deleteModule(moduleId)ElMessage.success('模塊刪除成功!')fetchModuleList()} catch (error) {console.error('刪除模塊失敗:', error)} finally {moduleLoading.value = false}
}// 輔助函數 (從 ProjectListView 復制或提取到公共 utils)
const formatDateTime = (dateTimeStr: string) => { /* ... */ if (!dateTimeStr) return ''const date = new Date(dateTimeStr)return date.toLocaleString()
}
const getStatusText = (status: number) => { /* ... */ const statusMap: { [key: number]: string } = {0: '規劃中', 1: '進行中', 2: '已完成', 3: '擱置'}return statusMap[status] || '未知狀態'
}
const getStatusTagType = (status: number) => { /* ... */ const typeMap: { [key: number]: '' | 'success' | 'warning' | 'info' | 'danger' } = {0: 'info', 1: '', 2: 'success', 3: 'warning'}return typeMap[status] || 'info'
}</script><style scoped lang="scss">
.project-detail-view {padding: 20px;
}
.page-header-custom {margin-bottom: 20px;background-color: #fff;padding: 16px 24px;border-radius: 4px;box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
}
.project-info-card, .module-list-card {margin-bottom: 20px;
}
.card-header {display: flex;justify-content: space-between;align-items: center;font-weight: bold;
}
</style>
代碼解釋:
- 項目詳情展示: 使用
ElPageHeader
提供返回功能,使用ElCard
和ElDescriptions
展示項目基本信息。 - 模塊列表展示: 在另一個
ElCard
中使用ElTable
展示模塊列表。 - 新建/編輯模塊對話框: 為了簡化本篇文章的篇幅,模塊的表單對話框直接內聯寫在了
ProjectDetailView.vue
中,沒有再封裝成單獨組件。其邏輯與ProjectFormDialog.vue
非常相似。- 表單只包含模塊名稱和描述。所屬項目 ID (
projectDetail.value.id
) 在提交創建模塊時自動添加。 - 編輯模塊時,通常不修改其所屬項目。
- 表單只包含模塊名稱和描述。所屬項目 ID (
- API 調用:
fetchProjectDetail
獲取項目詳情,成功后再調用fetchModuleList
獲取模塊列表。模塊的 CRUD 操作分別調用api/module.ts
中定義的函數。 watch
路由參數: 添加了watch
來監聽路由參數id
的變化。如果用戶在項目詳情頁之間切換 (例如通過瀏覽器歷史記錄或直接修改 URL),可以重新加載對應項目的數據。
3. 測試項目、模塊管理功能:
-
測試新建項目: 點擊“新建項目”,填寫表單,提交。觀察效果。
-
測試編輯項目: 點擊項目的“編輯”按鈕,修改信息,提交。
-
測試刪除項目: 點擊刪除,確認。
-
測試新建模塊: 點擊“新建模塊”,填寫表單,提交。觀察效果。
-
測試編輯模塊: 點擊模塊的“編輯”按鈕,修改信息,提交。
-
測試刪除模塊: 點擊刪除,確認。
總結
我們成功地實現了測試平臺核心功能——項目管理和模塊管理的前端頁面交互及與后端 API 的完整聯調:
- ? 項目管理:
- 將新建/編輯項目的表單邏輯封裝到了可復用的
ProjectFormDialog.vue
組件中。 - 在項目列表頁 (
ProjectListView.vue
) 集成了該對話框,實現了項目的創建和編輯功能,并與后端createProject
和updateProject
API 聯調。 - 完善了刪除項目功能與后端
deleteProject
API 的聯調。
- 將新建/編輯項目的表單邏輯封裝到了可復用的
- ? 模塊管理 (集成在項目詳情頁):
- 創建了
api/module.ts
文件,封裝了模塊相關的 API 調用函數 (獲取列表、創建、更新、刪除) 和 TypeScript 類型。 - 在項目詳情頁 (
ProjectDetailView.vue
) 中:- 調用 API 獲取并展示了當前項目的基本信息。
- 調用 API 獲取并使用表格展示了該項目下的模塊列表。
- 實現了內聯的模塊新建/編輯表單對話框,并與后端
createModule
和updateModule
API 聯調。 - 實現了刪除模塊功能與后端
deleteModule
API 的聯調。
- 創建了
- ? 大量使用了 Element Plus 組件 (如
ElDialog
,ElForm
,ElTable
,ElDescriptions
,ElPageHeader
) 來構建用戶界面。 - ? 強化了異步操作 (
async/await
)、表單校驗、用戶反饋 (ElMessage
) 和組件間通信 (props
,emits
) 的實踐。
現在,我們的測試平臺已經具備了管理項目和模塊的核心能力,用戶可以通過界面直觀地操作這些數據了。這為后續實現更復雜的測試用例管理、測試執行等功能奠定了堅實的基礎。
在下一篇文章中,我們將繼續挑戰核心功能——測試用例管理。測試用例的表單通常更復雜,可能包含多個步驟、參數化等,我們將學習如何設計和實現一個強大的用例編輯界面。