十、【核心功能篇】項目與模塊管理:前端頁面開發與后端 API 聯調實戰

【核心功能篇】項目與模塊管理:前端頁面開發與后端 API 聯調實戰

    • 前言
      • 準備工作
      • 第一部分:完善項目管理功能 (Project)
        • 1. 創建/編輯項目的表單對話框組件
      • 第二部分:模塊管理功能 (集成到項目詳情頁)
        • 1. 創建模塊相關的 API 服務 (`src/api/module.ts`)
        • 2. 在 `ProjectDetailView.vue` 中展示和管理模塊
        • 3. 測試項目、模塊管理功能:
    • 總結

前言

一個測試平臺最基礎也最核心的功能之一就是對測試項目和項目內模塊的管理。用戶需要能夠方便地創建、查看、修改和刪除這些實體。

  • 項目管理: 我們已經在 ProjectListView.vue 中展示了項目列表。現在需要添加:
    • 一個“新建項目”的入口,彈出表單對話框。
    • 列表中每行項目有“編輯”和“刪除”操作。
  • 模塊管理: 模塊是項目的一部分。我們考慮在查看項目詳情時,一并展示和管理該項目下的模塊。
    • ProjectDetailView.vue 中顯示模塊列表。
    • 提供在該項目下“新建模塊”、“編輯模塊”、“刪除模塊”的功能。

我們將遵循“數據驅動視圖”和“用戶操作 -> 調用 API -> 更新視圖/狀態”的模式。

這篇文章將帶你

  1. 完善項目列表功能,并實現新建、編輯項目的表單交互和 API 調用。
  2. 在項目詳情頁中展示其關聯的模塊列表。
  3. 實現新建、編輯和刪除模塊的功能。

我們將大量使用 Element Plus 的表單、表格和對話框組件,并結合之前封裝的 Axios 請求和 API 服務模塊。

準備工作

  1. 前端項目就緒: test-platform/frontend 項目可以正常運行 (npm run dev)。
  2. 后端 API 運行中: Django 后端服務運行(python manage.py runserver),項目和模塊的 API (/api/projects/, /api/modules/) 可用。
  3. Axios 和 API 服務已封裝: utils/request.tsapi/project.ts 已按上一篇文章配置好。
  4. Pinia 狀態管理可用: 確保用戶登錄狀態能被正確管理。
  5. 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: 使用 computedemit('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 有值且 visibletrue,也會嘗試加載數據。
  • 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>

代碼解釋:

  1. <template> 中,我們添加了 <project-form-dialog ... /> 組件。
    • v-model:visible="dialogVisible": 雙向綁定對話框的顯示狀態。
    • :project-id="editingProjectId": 將當前要編輯的項目 ID (或 null 表示新建) 傳遞給子組件。
    • @success="onFormSuccess": 監聽子組件觸發的 success 事件。
  2. <script setup> 中,導入 ProjectFormDialog 組件。
  3. 定義 dialogVisible (控制對話框顯示) 和 editingProjectId (存儲當前編輯的項目 ID)。
  4. openCreateDialog(): 當點擊“新建項目”按鈕時調用。它將 editingProjectId 設置為 null (表示新建模式),并將 dialogVisible 設置為 true 來打開對話框。
  5. openEditDialog(project: Project): 當點擊某行項目的“編輯”按鈕時調用。它將 editingProjectId 設置為該項目的 id,并將 dialogVisible 設置為 true
  6. 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 提供返回功能,使用 ElCardElDescriptions 展示項目基本信息。
  • 模塊列表展示: 在另一個 ElCard 中使用 ElTable 展示模塊列表。
  • 新建/編輯模塊對話框: 為了簡化本篇文章的篇幅,模塊的表單對話框直接內聯寫在了 ProjectDetailView.vue 中,沒有再封裝成單獨組件。其邏輯與 ProjectFormDialog.vue 非常相似。
    • 表單只包含模塊名稱和描述。所屬項目 ID (projectDetail.value.id) 在提交創建模塊時自動添加。
    • 編輯模塊時,通常不修改其所屬項目。
  • API 調用: fetchProjectDetail 獲取項目詳情,成功后再調用 fetchModuleList 獲取模塊列表。模塊的 CRUD 操作分別調用 api/module.ts 中定義的函數。
  • watch 路由參數: 添加了 watch 來監聽路由參數 id 的變化。如果用戶在項目詳情頁之間切換 (例如通過瀏覽器歷史記錄或直接修改 URL),可以重新加載對應項目的數據。
3. 測試項目、模塊管理功能:
  • 測試新建項目: 點擊“新建項目”,填寫表單,提交。觀察效果。
    在這里插入圖片描述
    在這里插入圖片描述

  • 測試編輯項目: 點擊項目的“編輯”按鈕,修改信息,提交。
    在這里插入圖片描述
    在這里插入圖片描述

  • 測試刪除項目: 點擊刪除,確認。
    在這里插入圖片描述
    在這里插入圖片描述

  • 測試新建模塊: 點擊“新建模塊”,填寫表單,提交。觀察效果。
    在這里插入圖片描述

  • 測試編輯模塊: 點擊模塊的“編輯”按鈕,修改信息,提交。
    在這里插入圖片描述

  • 測試刪除模塊: 點擊刪除,確認。
    在這里插入圖片描述

總結

我們成功地實現了測試平臺核心功能——項目管理和模塊管理的前端頁面交互及與后端 API 的完整聯調:

  • ? 項目管理:
    • 將新建/編輯項目的表單邏輯封裝到了可復用的 ProjectFormDialog.vue 組件中。
    • 在項目列表頁 (ProjectListView.vue) 集成了該對話框,實現了項目的創建和編輯功能,并與后端 createProjectupdateProject API 聯調。
    • 完善了刪除項目功能與后端 deleteProject API 的聯調。
  • ? 模塊管理 (集成在項目詳情頁):
    • 創建了 api/module.ts 文件,封裝了模塊相關的 API 調用函數 (獲取列表、創建、更新、刪除) 和 TypeScript 類型。
    • 在項目詳情頁 (ProjectDetailView.vue) 中:
      • 調用 API 獲取并展示了當前項目的基本信息。
      • 調用 API 獲取并使用表格展示了該項目下的模塊列表。
      • 實現了內聯的模塊新建/編輯表單對話框,并與后端 createModuleupdateModule API 聯調。
      • 實現了刪除模塊功能與后端 deleteModule API 的聯調。
  • ? 大量使用了 Element Plus 組件 (如 ElDialog, ElForm, ElTable, ElDescriptions, ElPageHeader) 來構建用戶界面。
  • ? 強化了異步操作 (async/await)、表單校驗、用戶反饋 (ElMessage) 和組件間通信 (props, emits) 的實踐。

現在,我們的測試平臺已經具備了管理項目和模塊的核心能力,用戶可以通過界面直觀地操作這些數據了。這為后續實現更復雜的測試用例管理、測試執行等功能奠定了堅實的基礎。

在下一篇文章中,我們將繼續挑戰核心功能——測試用例管理。測試用例的表單通常更復雜,可能包含多個步驟、參數化等,我們將學習如何設計和實現一個強大的用例編輯界面。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/82997.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/82997.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/82997.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ES分詞搜索

ES的使用 前言作者使用的版本作者需求 簡介ES簡略介紹ik分詞器簡介 使用es的直接簡單使用es的查詢 es在java中使用備注說明 前言 作者使用的版本 es: 7.17.27spring-boot-starter-data-elasticsearch: 7.14.2 作者需求 作者接到一個業務需求&#xff0c;我們系統有份數據被…

Axure設計案例——科技感立體柱狀圖

想讓你的數據展示告別平淡無奇&#xff0c;成為吸引全場目光的焦點嗎&#xff1f;快來瞧瞧這個Axure設計的科技感立體柱狀圖案例&#xff01;科技感設計風格借助逼真的立體效果打破傳統柱狀圖的平面感&#xff0c;營造出一種令人眼前一亮的視覺震撼。每一個柱狀體都仿佛是真實存…

惡意npm與VS Code包竊取數據及加密貨幣資產

60個npm包竊取系統敏感信息 安全研究人員在npm軟件包注冊表中發現60個惡意組件&#xff0c;這些組件能夠收集主機名、IP地址、DNS服務器和用戶目錄信息&#xff0c;并將其發送至Discord平臺控制的終端節點。據Socket安全研究員Kirill Boychenko上周發布的報告顯示&#xff0c;…

leetcode 2359. 找到離給定兩個節點最近的節點

給你一個 n 個節點的 有向圖 &#xff0c;節點編號為 0 到 n - 1 &#xff0c;每個節點 至多 有一條出邊。 有向圖用大小為 n 下標從 0 開始的數組 edges 表示&#xff0c;表示節點 i 有一條有向邊指向 edges[i] 。如果節點 i 沒有出邊&#xff0c;那么 edges[i] -1 。 同時…

1. pytorch手寫數字預測

1. pytorch手寫數字預測 1.背景2.準備數據集2.定義模型3.dataloader和訓練4.訓練模型5.測試模型6.保存模型 1.背景 因為自身的研究方向是多模態目標跟蹤&#xff0c;突然對其他的視覺方向產生了興趣&#xff0c;所以心血來潮的回到最經典的視覺任務手寫數字預測上來&#xff0…

AWS WebRTC:獲取ICE服務地址(part 2): ICE Agent的作用

上一篇&#xff0c;已經獲取到了ICE服務地址&#xff0c;從返回結果中看&#xff0c;是兩組TURN服務地址。 拿到這些地址有什么用呢&#xff1f;接下來就要說到WebRTC中ICE Agent的作用了&#xff0c;返回的服務地址會傳給WebRTC最終給到ICE Agent。 ICE Agent的作用&#xf…

大數據時代的利劍:Bright Data網頁抓取與自動化工具共建高效數據采集新生態

目錄 一、為何要選用Bright Data網頁自動化抓取——幫助我們高效高質解決以下問題&#xff01; 二、Bright Data網頁抓取工具 - 網頁爬蟲工具實測 2.1 首先注冊用戶 2.2 首先點擊 Proxies & Scraping &#xff0c;再點擊瀏覽器API的開始使用 2.3 填寫通道名稱&#xff…

指紋識別+精準化POC攻擊

開發目的 解決漏洞掃描器的痛點 第一就是掃描量太大&#xff0c;對一個站點掃描了大量的無用 POC&#xff0c;浪費時間 指紋識別后還需要根據對應的指紋去進行 payload 掃描&#xff0c;非常的麻煩 開發思路 我們的思路分為大體分為指紋POC掃描 所以思路大概從這幾個方面…

【Day40】

DAY 40 訓練和測試的規范寫法 知識點回顧&#xff1a; 彩色和灰度圖片測試和訓練的規范寫法&#xff1a;封裝在函數中展平操作&#xff1a;除第一個維度batchsize外全部展平dropout操作&#xff1a;訓練階段隨機丟棄神經元&#xff0c;測試階段eval模式關閉dropout 作業&#x…

【HTML-13】HTML表格合并技術詳解:打造專業數據展示

表格是HTML中展示結構化數據的重要元素&#xff0c;而表格合并則是提升表格表現力的關鍵技術。本文將全面介紹HTML中的表格合并方法&#xff0c;幫助您創建更專業、更靈活的數據展示界面。 1. 表格合并基礎概念 在HTML中&#xff0c;表格合并主要通過兩個屬性實現&#xff1a…

<uniapp><threejs>在uniapp中,怎么使用threejs來顯示3D圖形?

前言 本專欄是基于uniapp實現手機端各種小功能的程序,并且基于各種通訊協議如http、websocekt等,實現手機端作為客戶端(或者是手持機、PDA等),與服務端進行數據通訊的實例開發。 發文平臺 CSDN 環境配置 系統:windows 平臺:visual studio code、HBuilderX(uniapp開…

如何制作全景VR圖?

全景VR圖&#xff0c;特別是720度全景VR&#xff0c;為觀眾提供一種沉浸式體驗。 全景VR圖能夠捕捉場景的全貌&#xff0c;還能將多個角度的圖片或視頻無縫拼接成一個完整的全景視角&#xff0c;讓觀眾在虛擬環境中自由探索。隨著虛擬現實&#xff08;VR&#xff09;技術的飛速…

前端使用qrcode來生成二維碼的時候中間添加logo圖標

這個開源倉庫可以讓你在前端頁面中生成二維碼圖片&#xff0c;并且支持調整前景色和背景色&#xff0c;但是有個問題&#xff0c;就是不能添加logo圖片。issue&#xff1a; GitHub Where software is built 但是已經有解決方案了&#xff1a; add a logo picture Issue #21…

【C語言】函數指針及其應用

目錄 1.1 函數指針的概念和應用 1.2 賦值與內存模型 1.3 調用方式與注意事項 二、函數指針的使用 2.1 函數指針的定義和訪問 2.2 動態調度&#xff1a;用戶輸入驅動函數執行 2.3 函數指針數組進階應用 2.4 函數作為參數的高階抽象 三、回調函數 3.1 指針函數…

安裝flash-attention失敗的終極解決方案(WINDOWS環境)

想要看linux版本下安裝問題的請走這里&#xff1a;安裝flash-attention失敗的終極解決方案&#xff08;LINUX環境&#xff09; 其實&#xff0c;現在的flash-attention不像 v2.3.2之前的版本&#xff0c;基本上不兼容WINDOWS環境。但是在WINDOWS環境安裝總還是有那么一點不順暢…

[C]基礎16.數據在內存中的存儲

博客主頁&#xff1a;向不悔本篇專欄&#xff1a;[C]您的支持&#xff0c;是我的創作動力。 文章目錄 0、總結1、整數在內存中的存儲1.1 整數的二進制表示方法1.2 不同整數的表示方法1.3 內存中存儲的是補碼 2、大小端字節序和字節序判斷2.1 什么是大小端2.2 為什么有大小端2.3…

Python 基于卷積神經網絡手寫數字識別

Ubuntu系統&#xff1a;22.04 python版本&#xff1a;3.9 安裝依賴庫&#xff1a; pip install tensorflow2.13 matplotlib numpy -i https://mirrors.aliyun.com/pypi/simple 代碼實現&#xff1a; import tensorflow as tf from tensorflow.keras.models import Sequent…

ElectronBot復刻-電路測試篇

typec-16p 接口部分 USB1&#xff08;Type - C 接口&#xff09;&#xff1a;這是通用的 USB Type - C 接口&#xff0c;具備供電和數據傳輸功能。 GND 引腳&#xff08;如 A1、A12、B1、B12 等&#xff09;&#xff1a;接地引腳&#xff0c;用于提供電路的參考電位&#xff0…

ESP8266+STM32 AT驅動程序,心知天氣API 記錄時間: 2025年5月26日13:24:11

接線為 串口2 接入ESP8266 esp8266.c #include "stm32f10x.h"//8266預處理文件 #include "esp8266.h"//硬件驅動 #include "delay.h" #include "usart.h"//用得到的庫 #include <string.h> #include <stdio.h> #include …

CDN安全加速:HTTPS加密最佳配置方案

CDN安全加速的HTTPS加密最佳配置方案需從證書管理、協議優化、安全策略到性能調優進行全鏈路設計&#xff0c;以下是核心實施步驟與注意事項&#xff1a; ??一、證書配置與管理?? ??證書選擇與格式?? ??證書類型??&#xff1a;優先使用受信任CA機構頒發的DV/OV/EV證…