【核心功能篇】项目与模块管理:前端页面开发与后端 API 联调实战
前言
一个测试平台最基础也最核心的功能之一就是对测试项目和项目内模块的管理。用户需要能够方便地创建、查看、修改和删除这些实体。
- 项目管理: 我们已经在
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-form
ref="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-input
v-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 = true
try {
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 = false
resetForm()
}
const handleSubmit = async () => {
if (!projectFormRef.value) return
await projectFormRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
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-popconfirm
title="确定要删除这个项目吗?"
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 = true
try {
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 // 设置编辑ID
dialogVisible.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 = true
try {
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; // 所属项目ID
project_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 getModuleListByProjectId(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', // 或 patch
data
})
}
// 5. 删除模块
export function deleteModule(moduleId: number): AxiosPromise<void> {
return request({
url: `/modules/${moduleId}/`,
method: 'delete'
})
}
注意: getModuleListByProjectId
中的 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 = ModuleSerializer
def 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 error
return 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 = Module
fields = ['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-popconfirm
title="确定要删除这个模块吗?"
@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-form
ref="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 {
getModuleListByProjectId,
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)) // 从路由获取项目ID
const 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) return
pageLoading.value = true
try {
const response = await getProjectDetail(projectId.value)
projectDetail.value = response.data
await fetchModuleList() // 获取项目详情成功后,获取其模块列表
} catch (error) {
ElMessage.error('获取项目详情失败')
console.error(error)
} finally {
pageLoading.value = false
}
}
// 获取模块列表
const fetchModuleList = async () => {
if (!projectDetail.value) return // 确保项目详情已加载
moduleLoading.value = true
try {
const response = await getModuleListByProjectId(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 = null
Object.assign(moduleFormData, initialModuleFormData) // 重置表单
moduleFormRef.value?.clearValidate()
moduleDialogVisible.value = true
}
const openEditModuleDialog = (module: Module) => {
editingModuleId.value = module.id
Object.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) return
await moduleFormRef.value.validate(async (valid) => {
if (valid) {
moduleSubmitLoading.value = true
const dataToSubmit = { ...moduleFormData, project: projectDetail.value!.id }
try {
if (editingModuleId.value) {
await updateModule(editingModuleId.value, moduleFormData) // 更新时不传 project ID
ElMessage.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
) 的实践。
现在,我们的测试平台已经具备了管理项目和模块的核心能力,用户可以通过界面直观地操作这些数据了。这为后续实现更复杂的测试用例管理、测试执行等功能奠定了坚实的基础。
在下一篇文章中,我们将继续挑战核心功能——测试用例管理。测试用例的表单通常更复杂,可能包含多个步骤、参数化等,我们将学习如何设计和实现一个强大的用例编辑界面。