十、【核心功能篇】项目与模块管理:前端页面开发与后端 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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证…