十、【核心功能篇】项目与模块管理:前端页面开发与后端 API 联调实战

发布于:2025-05-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

一个测试平台最基础也最核心的功能之一就是对测试项目和项目内模块的管理。用户需要能够方便地创建、查看、修改和删除这些实体。

  • 项目管理: 我们已经在 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-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: 使用 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-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>

代码解释:

  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; // 所属项目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 提供返回功能,使用 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) 的实践。

现在,我们的测试平台已经具备了管理项目和模块的核心能力,用户可以通过界面直观地操作这些数据了。这为后续实现更复杂的测试用例管理、测试执行等功能奠定了坚实的基础。

在下一篇文章中,我们将继续挑战核心功能——测试用例管理。测试用例的表单通常更复杂,可能包含多个步骤、参数化等,我们将学习如何设计和实现一个强大的用例编辑界面。


网站公告

今日签到

点亮在社区的每一天
去签到