1467 lines
52 KiB
Vue
1467 lines
52 KiB
Vue
<template>
|
||
<div class="page-wrap">
|
||
<page-section title="用例管理">
|
||
<el-form :inline="true" size="small" @submit.native.prevent>
|
||
<el-form-item label="产品">
|
||
<el-select
|
||
v-model="selectedProductId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择产品"
|
||
style="width: 220px;"
|
||
@change="handleProductChange"
|
||
@focus="loadProductOptions">
|
||
<el-option
|
||
v-for="item in productOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目名称">
|
||
<el-select
|
||
v-model="selectedProjectId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择项目"
|
||
style="width: 240px;"
|
||
:disabled="!selectedProductId"
|
||
@change="handleProjectChange">
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-tabs v-model="activeTab" style="margin-top: 8px;">
|
||
<el-tab-pane label="模块列表" name="modules">
|
||
<div class="toolbar-wrap">
|
||
<el-button type="primary" size="small" :disabled="!selectedProjectId" @click="openModuleCreate">新增模块</el-button>
|
||
</div>
|
||
<el-form :inline="true" :model="moduleSearchForm" size="small" style="margin-top: 8px;" @submit.native.prevent>
|
||
<el-form-item label="模块名称">
|
||
<el-input v-model="moduleSearchForm.keyword" clearable style="width: 180px;" @keyup.enter.native="handleModuleSearch"></el-input>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :disabled="!selectedProjectId" @click="handleModuleSearch">查询</el-button>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button size="small" @click="resetModuleSearch">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table v-loading="moduleLoading" :data="pagedModuleData" border style="margin-top: 8px;">
|
||
<el-table-column prop="id" label="模块ID" width="100"></el-table-column>
|
||
<el-table-column prop="name" label="模块名称" min-width="160"></el-table-column>
|
||
<el-table-column prop="parent_name" label="父模块名称" min-width="160"></el-table-column>
|
||
<el-table-column prop="sort_order" label="排序" width="90"></el-table-column>
|
||
<el-table-column prop="path" label="路径" min-width="180"></el-table-column>
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" @click="openModuleEdit(scope.row)">编辑</el-button>
|
||
<el-button type="text" style="color: #F56C6C;" @click="removeModule(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="margin-top: 16px; text-align: right;">
|
||
<el-pagination
|
||
:current-page="modulePageNo"
|
||
:page-size="modulePageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="moduleTotal"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleModuleSizeChange"
|
||
@current-change="handleModuleCurrentChange">
|
||
</el-pagination>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="用例列表" name="cases">
|
||
<el-form :inline="true" :model="queryForm" size="small" @submit.native.prevent>
|
||
<el-form-item label="用例标题">
|
||
<el-input v-model="queryForm.keyword" clearable style="width: 180px;" @keyup.enter.native="fetchList"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="优先级">
|
||
<el-select v-model="queryForm.priority" clearable style="width: 100px;">
|
||
<el-option label="P0" :value="0"></el-option>
|
||
<el-option label="P1" :value="1"></el-option>
|
||
<el-option label="P2" :value="2"></el-option>
|
||
<el-option label="P3" :value="3"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="标签">
|
||
<el-input v-model="queryForm.tag" clearable style="width: 140px;" @keyup.enter.native="fetchList"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="创建人">
|
||
<el-input v-model="queryForm.creator" clearable style="width: 140px;" @keyup.enter.native="fetchList"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker
|
||
v-model="createdTimeRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="yyyy-MM-dd"
|
||
style="width: 260px;"
|
||
@change="handleCreatedTimeChange">
|
||
</el-date-picker>
|
||
</el-form-item>
|
||
<el-form-item class="more-filter-item">
|
||
<el-popover
|
||
v-model="moreFilterVisible"
|
||
placement="bottom-start"
|
||
width="560"
|
||
trigger="click">
|
||
<div class="more-filter-wrap">
|
||
<el-form :inline="true" :model="queryForm" size="small" @submit.native.prevent>
|
||
<el-form-item label="模块">
|
||
<el-select v-model="queryForm.moduleId" clearable filterable style="width: 180px;">
|
||
<el-option
|
||
v-for="item in flatModuleOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id">
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="类型">
|
||
<el-select v-model="queryForm.caseType" clearable style="width: 180px;">
|
||
<el-option label="功能" :value="1"></el-option>
|
||
<el-option label="性能" :value="2"></el-option>
|
||
<el-option label="安全" :value="3"></el-option>
|
||
<el-option label="接口" :value="4"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="queryForm.status" clearable style="width: 180px;">
|
||
<el-option label="正常" :value="1"></el-option>
|
||
<el-option label="已废弃" :value="2"></el-option>
|
||
<el-option label="评审中" :value="3"></el-option>
|
||
<el-option label="评审通过" :value="4"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否自动化">
|
||
<el-select v-model="queryForm.isAuto" clearable style="width: 180px;">
|
||
<el-option label="未实现" :value="0"></el-option>
|
||
<el-option label="已实现" :value="1"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="more-filter-footer">
|
||
<el-button size="small" @click="moreFilterVisible = false">取消</el-button>
|
||
<el-button type="primary" size="small" :disabled="!selectedProjectId" @click="applyMoreFilters">搜索</el-button>
|
||
</div>
|
||
</div>
|
||
<el-button slot="reference" size="small">更多筛选</el-button>
|
||
</el-popover>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :disabled="!selectedProjectId" @click="fetchList">查询</el-button>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button size="small" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
<el-form-item class="case-action-buttons-item">
|
||
<el-button type="primary" size="small" :disabled="!selectedProjectId" @click="goEditor()">新建用例</el-button>
|
||
<el-button size="small" :disabled="!selectedProjectId" @click="openCaseImportDialog">导入Excel</el-button>
|
||
<el-popover
|
||
v-model="columnSettingVisible"
|
||
placement="bottom-end"
|
||
width="300"
|
||
trigger="click">
|
||
<div class="column-setting-wrap">
|
||
<div class="column-setting-title">自定义列表展示字段</div>
|
||
<el-checkbox-group v-model="selectedCaseColumnKeys" @change="handleColumnSelectionChange">
|
||
<el-checkbox v-for="item in allCaseColumns" :key="item.key" :label="item.key">{{ item.label }}</el-checkbox>
|
||
</el-checkbox-group>
|
||
</div>
|
||
<el-button slot="reference" size="small">自定义列表展示字段</el-button>
|
||
</el-popover>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table v-loading="loading" :data="tableData" border style="margin-top: 8px;">
|
||
<el-table-column
|
||
v-for="column in visibleCaseColumns"
|
||
:key="column.key"
|
||
:label="column.label"
|
||
:prop="column.prop"
|
||
:min-width="column.minWidth"
|
||
:width="column.width">
|
||
<template slot-scope="scope">
|
||
<template v-if="column.key === 'tags'">
|
||
<template v-if="Array.isArray(getCaseColumnValue(scope.row, 'tags'))">
|
||
<el-tag v-for="tag in getCaseColumnValue(scope.row, 'tags')" :key="tag" size="mini" style="margin-right: 4px;">{{ tag }}</el-tag>
|
||
</template>
|
||
<span v-else>{{ formatTags(getCaseColumnValue(scope.row, 'tags')) }}</span>
|
||
</template>
|
||
<template v-else>
|
||
{{ formatCaseColumnValue(column.key, getCaseColumnValue(scope.row, column.key)) }}
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="140" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button type="text" @click="goEditor(scope.row)">编辑</el-button>
|
||
<el-button type="text" style="color: #F56C6C;" @click="remove(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="margin-top: 16px; text-align: right;">
|
||
<el-pagination
|
||
:current-page="pageNo"
|
||
:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange">
|
||
</el-pagination>
|
||
</div>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="用例脑图" name="case-mindmap">
|
||
<div class="toolbar-wrap">
|
||
<el-button size="small" :disabled="!selectedProjectId" @click="fetchCaseMindmapData">刷新结构</el-button>
|
||
<el-button size="small" :disabled="!selectedProjectId" @click="toggleMindmapCollapsed">{{ mindmapCollapsed ? '展开展示' : '合并展示' }}</el-button>
|
||
</div>
|
||
<div v-loading="caseMindmapLoading" class="mindmap-wrap">
|
||
<el-tree
|
||
v-if="caseMindmapTreeData && caseMindmapTreeData.length > 0"
|
||
ref="mindmapTreeRef"
|
||
:key="mindmapRenderKey"
|
||
:data="caseMindmapTreeData"
|
||
node-key="id"
|
||
:default-expand-all="!mindmapCollapsed"
|
||
:expand-on-click-node="false"
|
||
:props="{ children: 'children', label: 'name' }"
|
||
class="xmind-tree">
|
||
<div slot-scope="{ data }" class="mindmap-node-wrap">
|
||
<span class="mindmap-node" :class="{
|
||
'mindmap-node-project': data.nodeTypeLabel === '项目',
|
||
'mindmap-node-module': data.nodeTypeLabel === '模块',
|
||
'mindmap-node-case': data.nodeTypeLabel === '用例',
|
||
'mindmap-node-active': selectedMindmapCase && selectedMindmapCase.id === data.id
|
||
}" @click.stop="handleMindmapNodeClick(data)">
|
||
<span class="mindmap-node-title">{{ data.name }}</span>
|
||
<span class="mindmap-node-meta">
|
||
<el-tag size="mini" :type="data.nodeTypeLabel === '项目' ? 'success' : (data.nodeTypeLabel === '模块' ? 'warning' : 'info')">{{ data.nodeTypeLabel }}</el-tag>
|
||
<span v-if="data.creator" class="mindmap-meta-text">创建人:{{ data.creator }}</span>
|
||
<span v-if="data.updatedTime" class="mindmap-meta-text">更新时间:{{ data.updatedTime }}</span>
|
||
</span>
|
||
</span>
|
||
<div v-if="data.nodeTypeLabel === '用例' && selectedMindmapCase && selectedMindmapCase.id === data.id" class="mindmap-inline-detail">
|
||
<div class="mindmap-inline-detail-line"></div>
|
||
<div class="mindmap-inline-detail-card">
|
||
<div class="mindmap-inline-detail-header">
|
||
<div class="mindmap-inline-detail-title">用例详情</div>
|
||
<el-tag size="mini" :type="formatStatusTagType(data.status)">{{ formatStatus(data.status) || '未知状态' }}</el-tag>
|
||
</div>
|
||
<template v-if="mindmapCaseEditing">
|
||
<div class="mindmap-inline-detail-item">
|
||
<b>前置条件:</b>
|
||
<el-input v-model="mindmapCaseEditForm.preconditions" type="textarea" :rows="2"></el-input>
|
||
</div>
|
||
<div class="mindmap-inline-detail-item">
|
||
<b>测试步骤:</b>
|
||
<el-input v-model="mindmapCaseEditForm.steps" type="textarea" :rows="4"></el-input>
|
||
</div>
|
||
<div class="mindmap-inline-detail-item">
|
||
<b>预期结果:</b>
|
||
<el-input v-model="mindmapCaseEditForm.expectedResults" type="textarea" :rows="2"></el-input>
|
||
</div>
|
||
<div class="mindmap-inline-actions">
|
||
<el-button size="mini" @click="cancelMindmapCaseEdit">取消</el-button>
|
||
<el-button type="primary" size="mini" :loading="mindmapCaseSaving" @click="saveMindmapCaseEdit">保存</el-button>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="mindmap-inline-detail-item"><b>前置条件:</b>{{ data.preconditions || '无' }}</div>
|
||
<div class="mindmap-inline-detail-item"><b>测试步骤:</b>{{ formatMindmapSteps(data.steps) || '无' }}</div>
|
||
<div class="mindmap-inline-detail-item"><b>预期结果:</b>{{ data.expectedResults || '无' }}</div>
|
||
<div class="mindmap-inline-actions">
|
||
<el-button size="mini" :loading="mindmapCaseReviewing" @click="startMindmapCaseReview">评审</el-button>
|
||
<el-button type="primary" plain size="mini" @click="startMindmapCaseEdit">编辑</el-button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tree>
|
||
<div v-else class="mindmap-empty">暂无结构数据</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</page-section>
|
||
|
||
<el-dialog :title="moduleDialogMode === 'edit' ? '编辑模块' : '新增模块'" :visible.sync="moduleDialogVisible" width="520px" @close="resetModuleForm">
|
||
<el-form ref="moduleForm" :model="moduleForm" :rules="moduleRules" label-width="100px" size="small">
|
||
<el-form-item label="产品" prop="productId">
|
||
<el-select v-model="moduleForm.productId" filterable clearable placeholder="请选择产品" style="width: 100%;" @change="handleModuleProductChange" @focus="loadProductOptions">
|
||
<el-option v-for="item in productOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目" prop="projectId">
|
||
<el-select v-model="moduleForm.projectId" filterable clearable placeholder="请选择项目" style="width: 100%;" :disabled="!moduleForm.productId" @change="handleModuleProjectChange">
|
||
<el-option v-for="item in moduleProjectOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="模块名称" prop="name">
|
||
<el-input v-model="moduleForm.name"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="父模块">
|
||
<el-select v-model="moduleForm.parentId" filterable placeholder="不选则为根模块" style="width: 100%;" :disabled="!moduleForm.projectId">
|
||
<el-option label="无(根模块)" value="0"></el-option>
|
||
<el-option v-for="item in moduleParentOptions" :key="item.id" :label="item.name" :value="String(item.id)"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="排序">
|
||
<el-input v-model="moduleForm.sortOrder"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="路径">
|
||
<el-input v-model="moduleForm.path"></el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button size="small" @click="moduleDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" size="small" :loading="moduleSubmitting" @click="submitModule">{{ moduleDialogMode === 'edit' ? '保存' : '确定' }}</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<el-dialog title="批量上传" :visible.sync="caseImportDialogVisible" width="780px" @close="resetImportDialog">
|
||
<div class="case-import-panel">
|
||
<div class="case-import-icon">X</div>
|
||
<div class="case-import-title">上传用例</div>
|
||
<div class="case-import-subtitle">仅支持 xlsx、xls 文件,系统将自动解析用例数据</div>
|
||
<div
|
||
class="case-import-dropzone"
|
||
@dragover.prevent
|
||
@drop.prevent="onImportFileDrop">
|
||
<div class="case-import-drop-text">
|
||
拖拽文件到此处,或<span class="link-text" @click="triggerImportFileSelect">点击选择</span>
|
||
</div>
|
||
<div class="case-import-file-tip">仅支持 .xlsx 和 .xls 格式,最大文件大小 20MB</div>
|
||
<div v-if="importFile" class="case-import-file-name">当前文件:{{ importFile.name }}</div>
|
||
<input
|
||
ref="importFileInput"
|
||
class="hidden-file-input"
|
||
type="file"
|
||
accept=".xls,.xlsx"
|
||
@change="onImportFileChange">
|
||
</div>
|
||
<div class="case-import-actions">
|
||
<el-button type="text" @click="downloadImportTemplate">下载标准模板</el-button>
|
||
<el-button type="primary" :disabled="!importFile || !selectedProjectId" :loading="importSubmitting" @click="submitCaseImport">开始上传并解析</el-button>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import PageSection from '@/components/TestPlatform/common/PageSection'
|
||
import { createModule, deleteCase, deleteModule, downloadCaseImportTemplate, getCaseList, getModuleTree, importCaseExcel, updateCase, updateModule } from '@/api/caseApi'
|
||
import { getProductList } from '@/api/productApi'
|
||
import { getProjectDetail, getProjectList } from '@/api/projectApi'
|
||
import {
|
||
readLastProductProjectCache,
|
||
saveLastProductProjectCache,
|
||
pickIdFromOptions
|
||
} from '@/utils/lastProductProjectCache'
|
||
|
||
export default {
|
||
name: 'CaseList',
|
||
components: { PageSection },
|
||
data() {
|
||
const routeTab = this.$route.query.tab
|
||
return {
|
||
activeTab: routeTab === 'cases' ? 'cases' : 'modules',
|
||
loading: false,
|
||
moduleLoading: false,
|
||
moduleSubmitting: false,
|
||
moduleDialogVisible: false,
|
||
moduleDialogMode: 'create',
|
||
editingModuleId: '',
|
||
caseMindmapLoading: false,
|
||
caseMindmapTreeData: [],
|
||
mindmapCollapsed: false,
|
||
mindmapRenderKey: 0,
|
||
selectedMindmapCase: null,
|
||
mindmapCaseEditing: false,
|
||
mindmapCaseSaving: false,
|
||
mindmapCaseReviewing: false,
|
||
mindmapCaseEditForm: {
|
||
preconditions: '',
|
||
steps: '',
|
||
expectedResults: ''
|
||
},
|
||
projectId: this.$route.query.projectId || '',
|
||
selectedProductId: '',
|
||
selectedProjectId: this.$route.query.projectId ? Number(this.$route.query.projectId) : '',
|
||
productOptions: [],
|
||
projectOptions: [],
|
||
moduleProjectOptions: [],
|
||
moduleParentOptions: [],
|
||
moduleSearchForm: {
|
||
keyword: ''
|
||
},
|
||
modulePageNo: 1,
|
||
modulePageSize: 20,
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
moreFilterVisible: false,
|
||
columnSettingVisible: false,
|
||
caseImportDialogVisible: false,
|
||
importSubmitting: false,
|
||
createdTimeRange: [],
|
||
importFile: null,
|
||
queryForm: {
|
||
keyword: '',
|
||
priority: '',
|
||
creator: '',
|
||
createdStartTime: '',
|
||
createdEndTime: '',
|
||
moduleId: '',
|
||
caseType: '',
|
||
status: '',
|
||
isAuto: '',
|
||
tag: ''
|
||
},
|
||
allCaseColumns: [
|
||
{ key: 'title', label: '用例标题', prop: 'title', minWidth: 220 },
|
||
{ key: 'moduleName', label: '模块名称', prop: 'module_name', minWidth: 140 },
|
||
{ key: 'priority', label: '优先级', prop: 'priority', width: 90 },
|
||
{ key: 'tags', label: '标签', prop: 'tags', minWidth: 180 },
|
||
{ key: 'creator', label: '创建人', prop: 'creator', minWidth: 120 },
|
||
{ key: 'createdTime', label: '创建时间', prop: 'created_time', minWidth: 160 },
|
||
{ key: 'caseType', label: '类型', prop: 'case_type', width: 90 },
|
||
{ key: 'status', label: '状态', prop: 'status', width: 100 },
|
||
{ key: 'isAuto', label: '是否自动化', prop: 'is_auto', width: 110 },
|
||
{ key: 'caseKey', label: '用例编号', prop: 'case_key', minWidth: 120 },
|
||
{ key: 'projectName', label: '项目名称', prop: 'project_name', minWidth: 140 }
|
||
],
|
||
selectedCaseColumnKeys: ['title', 'moduleName', 'priority', 'tags', 'creator', 'createdTime'],
|
||
moduleQueryForm: {
|
||
projectId: this.$route.query.projectId || ''
|
||
},
|
||
moduleForm: {
|
||
productId: '',
|
||
projectId: this.$route.query.projectId || '',
|
||
name: '',
|
||
parentId: '0',
|
||
sortOrder: '0',
|
||
path: ''
|
||
},
|
||
moduleRules: {
|
||
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
|
||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||
name: [{ required: true, message: '请输入模块名称', trigger: 'blur' }]
|
||
},
|
||
tableData: [],
|
||
moduleData: []
|
||
}
|
||
},
|
||
computed: {
|
||
visibleCaseColumns() {
|
||
return this.allCaseColumns.filter(item => this.selectedCaseColumnKeys.includes(item.key))
|
||
},
|
||
filteredModuleData() {
|
||
const keyword = (this.moduleSearchForm.keyword || '').trim().toLowerCase()
|
||
if (!keyword) {
|
||
return this.moduleData
|
||
}
|
||
return (this.moduleData || []).filter(item => {
|
||
const source = [
|
||
item.id,
|
||
item.name,
|
||
item.parent_name,
|
||
item.path
|
||
].map(v => String(v || '').toLowerCase()).join(' ')
|
||
return source.includes(keyword)
|
||
})
|
||
},
|
||
moduleTotal() {
|
||
return (this.filteredModuleData || []).length
|
||
},
|
||
pagedModuleData() {
|
||
const start = (this.modulePageNo - 1) * this.modulePageSize
|
||
const end = start + this.modulePageSize
|
||
return (this.filteredModuleData || []).slice(start, end)
|
||
},
|
||
flatModuleOptions() {
|
||
const result = []
|
||
const walk = (list, prefix) => {
|
||
;(list || []).forEach(item => {
|
||
const name = prefix ? `${prefix} / ${item.name}` : item.name
|
||
result.push({
|
||
id: item.id,
|
||
name
|
||
})
|
||
const children = item.children || item.child_list || item.childList || []
|
||
if (Array.isArray(children) && children.length > 0) {
|
||
walk(children, name)
|
||
}
|
||
})
|
||
}
|
||
walk(this.moduleData, '')
|
||
return result
|
||
}
|
||
},
|
||
watch: {
|
||
activeTab(val) {
|
||
if (val === 'case-mindmap') {
|
||
this.fetchCaseMindmapData()
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
loadProductOptions() {
|
||
if (this.productOptions && this.productOptions.length > 0) {
|
||
return Promise.resolve()
|
||
}
|
||
return getProductList({ pageNo: 1, pageSize: 1000, status: 1 }).then(res => {
|
||
const data = res && res.data ? res.data : res || {}
|
||
this.productOptions = data.items || data.list || data.data || []
|
||
}).catch(() => {
|
||
this.productOptions = []
|
||
})
|
||
},
|
||
loadProjectOptionsByProduct(productId) {
|
||
if (!productId) {
|
||
this.projectOptions = []
|
||
return Promise.resolve()
|
||
}
|
||
return getProjectList({ pageNo: 1, pageSize: 1000, status: 1, productId }).then(res => {
|
||
const data = res && res.data ? res.data : res || {}
|
||
this.projectOptions = data.items || data.list || data.data || []
|
||
}).catch(() => {
|
||
this.projectOptions = []
|
||
})
|
||
},
|
||
loadModuleProjectOptionsByProduct(productId) {
|
||
if (!productId) {
|
||
this.moduleProjectOptions = []
|
||
return Promise.resolve()
|
||
}
|
||
return getProjectList({ pageNo: 1, pageSize: 1000, status: 1, productId }).then(res => {
|
||
const data = res && res.data ? res.data : res || {}
|
||
this.moduleProjectOptions = data.items || data.list || data.data || []
|
||
}).catch(() => {
|
||
this.moduleProjectOptions = []
|
||
})
|
||
},
|
||
loadModuleParentOptionsByProject(projectId) {
|
||
if (!projectId) {
|
||
this.moduleParentOptions = []
|
||
return Promise.resolve()
|
||
}
|
||
return getModuleTree({ projectId }).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const list = data.list || data.items || []
|
||
this.moduleParentOptions = Array.isArray(list) ? list : []
|
||
}).catch(() => {
|
||
this.moduleParentOptions = []
|
||
})
|
||
},
|
||
handleProductChange(val) {
|
||
this.selectedProjectId = ''
|
||
this.projectId = ''
|
||
this.moduleQueryForm.projectId = ''
|
||
this.tableData = []
|
||
this.total = 0
|
||
this.moduleData = []
|
||
this.loadProjectOptionsByProduct(val)
|
||
},
|
||
handleProjectChange(val) {
|
||
this.projectId = val || ''
|
||
this.moduleQueryForm.projectId = val || ''
|
||
this.pageNo = 1
|
||
this.modulePageNo = 1
|
||
if (!val) {
|
||
this.tableData = []
|
||
this.total = 0
|
||
this.moduleData = []
|
||
return
|
||
}
|
||
saveLastProductProjectCache(this.selectedProductId, val)
|
||
this.fetchModuleList()
|
||
this.fetchList()
|
||
this.fetchCaseMindmapData()
|
||
},
|
||
restoreSharedProductProjectCache() {
|
||
const cached = readLastProductProjectCache()
|
||
if (!cached) return Promise.resolve()
|
||
const { productId: pid, projectId: projId } = cached
|
||
if (pid === '' || pid === undefined || pid === null || projId === '' || projId === undefined || projId === null) {
|
||
return Promise.resolve()
|
||
}
|
||
const hasProduct = (this.productOptions || []).some(p => String(p.id) === String(pid))
|
||
if (!hasProduct) return Promise.resolve()
|
||
this.selectedProductId = pickIdFromOptions(this.productOptions, pid)
|
||
return this.loadProjectOptionsByProduct(this.selectedProductId).then(() => {
|
||
const hasProject = (this.projectOptions || []).some(p => String(p.id) === String(projId))
|
||
if (!hasProject) return
|
||
const picked = pickIdFromOptions(this.projectOptions, projId)
|
||
this.selectedProjectId = picked
|
||
this.projectId = picked
|
||
this.moduleQueryForm.projectId = picked
|
||
})
|
||
},
|
||
fetchModuleList() {
|
||
if (!this.moduleQueryForm.projectId) {
|
||
this.moduleData = []
|
||
return
|
||
}
|
||
this.moduleLoading = true
|
||
getModuleTree(this.cleanParams(this.moduleQueryForm)).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const list = data.list || data.items || []
|
||
this.moduleData = Array.isArray(list) ? list : []
|
||
this.modulePageNo = 1
|
||
}).catch(() => {
|
||
this.moduleData = []
|
||
}).finally(() => {
|
||
this.moduleLoading = false
|
||
})
|
||
},
|
||
fetchCaseMindmapData() {
|
||
if (!this.projectId) {
|
||
this.caseMindmapTreeData = []
|
||
this.selectedMindmapCase = null
|
||
this.mindmapCaseEditing = false
|
||
return
|
||
}
|
||
this.caseMindmapLoading = true
|
||
getModuleTree({
|
||
projectId: this.projectId,
|
||
parentId: 0,
|
||
pageNo: 1,
|
||
pageSize: 200
|
||
}).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const rootModules = data.list || data.items || []
|
||
this.caseMindmapTreeData = this.buildCaseMindmapTree(Array.isArray(rootModules) ? rootModules : [])
|
||
this.mindmapRenderKey += 1
|
||
this.selectedMindmapCase = null
|
||
this.mindmapCaseEditing = false
|
||
}).catch(() => {
|
||
this.caseMindmapTreeData = []
|
||
this.selectedMindmapCase = null
|
||
this.mindmapCaseEditing = false
|
||
}).finally(() => {
|
||
this.caseMindmapLoading = false
|
||
})
|
||
},
|
||
toggleMindmapCollapsed() {
|
||
this.mindmapCollapsed = !this.mindmapCollapsed
|
||
this.mindmapRenderKey += 1
|
||
},
|
||
handleMindmapNodeClick(node) {
|
||
if (!node) {
|
||
return
|
||
}
|
||
if (node.nodeTypeLabel === '模块') {
|
||
this.expandMindmapModuleCases(node)
|
||
return
|
||
}
|
||
if (node.nodeTypeLabel === '用例') {
|
||
this.selectedMindmapCase = node
|
||
this.mindmapCaseEditing = false
|
||
}
|
||
},
|
||
expandMindmapModuleCases(moduleNode) {
|
||
if (!moduleNode || !moduleNode.rawId) {
|
||
return
|
||
}
|
||
const moduleId = moduleNode.rawId
|
||
if (moduleNode._childrenChecked && moduleNode._casesLoaded) {
|
||
return
|
||
}
|
||
if (moduleNode._childrenLoading || moduleNode._casesLoading) {
|
||
return
|
||
}
|
||
moduleNode._childrenLoading = true
|
||
getModuleTree({
|
||
projectId: this.projectId,
|
||
parentId: moduleId,
|
||
pageNo: 1,
|
||
pageSize: 200
|
||
}).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const children = data.list || data.items || []
|
||
const childList = Array.isArray(children) ? children : []
|
||
moduleNode._childrenChecked = true
|
||
if (childList.length > 0) {
|
||
const childNodes = childList.map(item => this.buildMindmapModuleNode(item))
|
||
moduleNode.children = (moduleNode.children || []).concat(childNodes)
|
||
this.mindmapRenderKey += 1
|
||
return
|
||
}
|
||
moduleNode._casesLoading = true
|
||
const query = Object.assign({}, this.queryForm, {
|
||
moduleId,
|
||
created_by_name: this.queryForm.creator
|
||
})
|
||
delete query.creator
|
||
const params = this.cleanParams(Object.assign({}, query, {
|
||
pageNo: 1,
|
||
pageSize: 20
|
||
}))
|
||
return getCaseList(this.projectId, params).then(caseRes => {
|
||
const caseData = (caseRes && caseRes.data) || caseRes || {}
|
||
const list = caseData.list || caseData.items || []
|
||
const caseNodes = (Array.isArray(list) ? list : []).map(item => this.buildMindmapCaseNode(item))
|
||
moduleNode.children = (moduleNode.children || []).concat(caseNodes)
|
||
moduleNode._casesLoaded = true
|
||
this.mindmapRenderKey += 1
|
||
}).finally(() => {
|
||
moduleNode._casesLoading = false
|
||
})
|
||
}).finally(() => {
|
||
moduleNode._childrenLoading = false
|
||
})
|
||
},
|
||
startMindmapCaseEdit() {
|
||
if (!this.selectedMindmapCase) return
|
||
this.mindmapCaseEditForm = {
|
||
preconditions: this.selectedMindmapCase.preconditions || '',
|
||
steps: this.formatMindmapSteps(this.selectedMindmapCase.steps) || '',
|
||
expectedResults: this.selectedMindmapCase.expectedResults || ''
|
||
}
|
||
this.mindmapCaseEditing = true
|
||
},
|
||
cancelMindmapCaseEdit() {
|
||
this.mindmapCaseEditing = false
|
||
},
|
||
saveMindmapCaseEdit() {
|
||
if (!this.selectedMindmapCase || !this.selectedMindmapCase.rawId) {
|
||
return
|
||
}
|
||
const caseId = this.selectedMindmapCase.rawId
|
||
const payload = this.cleanParams({
|
||
preconditions: this.mindmapCaseEditForm.preconditions,
|
||
steps: this.mindmapCaseEditForm.steps,
|
||
expectedResults: this.mindmapCaseEditForm.expectedResults
|
||
})
|
||
this.mindmapCaseSaving = true
|
||
updateCase(this.projectId, caseId, payload).then(() => {
|
||
this.$message.success('保存成功')
|
||
this.selectedMindmapCase.preconditions = this.mindmapCaseEditForm.preconditions
|
||
this.selectedMindmapCase.steps = this.mindmapCaseEditForm.steps
|
||
this.selectedMindmapCase.expectedResults = this.mindmapCaseEditForm.expectedResults
|
||
this.mindmapCaseEditing = false
|
||
this.fetchList()
|
||
}).finally(() => {
|
||
this.mindmapCaseSaving = false
|
||
})
|
||
},
|
||
startMindmapCaseReview() {
|
||
if (!this.selectedMindmapCase || !this.selectedMindmapCase.rawId) {
|
||
return
|
||
}
|
||
const caseId = this.selectedMindmapCase.rawId
|
||
this.mindmapCaseReviewing = true
|
||
// 先进入评审中
|
||
updateCase(this.projectId, caseId, { status: 3 }).then(() => {
|
||
this.selectedMindmapCase.status = 3
|
||
return this.$confirm('是否评审通过?', '评审确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
// 确定后评审通过,状态置为 4
|
||
return updateCase(this.projectId, caseId, { status: 4 }).then(() => {
|
||
this.selectedMindmapCase.status = 4
|
||
this.$message.success('评审通过')
|
||
})
|
||
}).catch(() => {
|
||
// 取消仅关闭弹框,保持评审中
|
||
this.selectedMindmapCase.status = 3
|
||
})
|
||
}).finally(() => {
|
||
this.mindmapCaseReviewing = false
|
||
this.fetchList()
|
||
})
|
||
},
|
||
formatMindmapSteps(steps) {
|
||
if (!steps) return ''
|
||
if (typeof steps === 'string') return steps
|
||
if (Array.isArray(steps)) {
|
||
return steps.map(item => {
|
||
if (typeof item === 'string') return item
|
||
return item.action || item.step || item.text || item.content || ''
|
||
}).filter(Boolean).join('\n')
|
||
}
|
||
return String(steps)
|
||
},
|
||
formatStatusTagType(status) {
|
||
if (status === 1) return 'success'
|
||
if (status === 3) return 'warning'
|
||
if (status === 2) return 'info'
|
||
if (status === 4) return 'success'
|
||
return ''
|
||
},
|
||
buildCaseMindmapTree(rootModules) {
|
||
const moduleChildren = (rootModules || []).map(item => this.buildMindmapModuleNode(item))
|
||
const currentProject = (this.projectOptions || []).find(item => String(item.id) === String(this.projectId))
|
||
return [{
|
||
id: `project-${this.projectId}`,
|
||
rawId: this.projectId,
|
||
nodeTypeLabel: '项目',
|
||
name: (currentProject && currentProject.name) || `项目-${this.projectId}`,
|
||
creator: '',
|
||
updatedTime: '',
|
||
children: moduleChildren
|
||
}]
|
||
},
|
||
buildMindmapModuleNode(item) {
|
||
return {
|
||
id: `module-${item.id}`,
|
||
rawId: item.id,
|
||
nodeTypeLabel: '模块',
|
||
name: item.name || `模块-${item.id}`,
|
||
creator: '',
|
||
updatedTime: item.updated_time || item.updatedTime || '',
|
||
parentId: item.parent_id || item.parentId || 0,
|
||
_childrenChecked: false,
|
||
_childrenLoading: false,
|
||
_casesLoaded: false,
|
||
_casesLoading: false,
|
||
children: []
|
||
}
|
||
},
|
||
buildMindmapCaseNode(item) {
|
||
return {
|
||
id: `case-${item.id}`,
|
||
rawId: item.id,
|
||
nodeTypeLabel: '用例',
|
||
name: item.title || item.case_key || `用例-${item.id}`,
|
||
creator: item.created_by_name || '',
|
||
updatedTime: item.updated_time || item.updatedTime || '',
|
||
status: item.status,
|
||
preconditions: item.preconditions || '',
|
||
steps: item.steps || '',
|
||
expectedResults: item.expected_results || item.expectedResults || ''
|
||
}
|
||
},
|
||
handleModuleSearch() {
|
||
this.modulePageNo = 1
|
||
},
|
||
resetModuleSearch() {
|
||
this.moduleSearchForm.keyword = ''
|
||
this.modulePageNo = 1
|
||
},
|
||
handleModuleSizeChange(size) {
|
||
this.modulePageSize = size
|
||
this.modulePageNo = 1
|
||
},
|
||
handleModuleCurrentChange(page) {
|
||
this.modulePageNo = page
|
||
},
|
||
openModuleCreate() {
|
||
this.moduleDialogMode = 'create'
|
||
this.editingModuleId = ''
|
||
this.moduleDialogVisible = true
|
||
this.moduleForm.productId = this.selectedProductId || ''
|
||
this.moduleForm.projectId = this.selectedProjectId || this.moduleQueryForm.projectId || this.projectId || ''
|
||
this.moduleForm.parentId = '0'
|
||
this.loadModuleProjectOptionsByProduct(this.moduleForm.productId)
|
||
this.loadModuleParentOptionsByProject(this.moduleForm.projectId)
|
||
},
|
||
openModuleEdit(row) {
|
||
this.moduleDialogMode = 'edit'
|
||
this.editingModuleId = row.id
|
||
this.moduleDialogVisible = true
|
||
this.moduleForm.productId = this.selectedProductId || ''
|
||
this.moduleForm.projectId = row.project_id || row.projectId || this.selectedProjectId || this.moduleQueryForm.projectId || this.projectId || ''
|
||
this.moduleForm.name = row.name || ''
|
||
this.moduleForm.parentId = String(row.parent_id || row.parentId || 0)
|
||
this.moduleForm.sortOrder = String(row.sort_order || row.sortOrder || 0)
|
||
this.moduleForm.path = row.path || ''
|
||
this.loadModuleProjectOptionsByProduct(this.moduleForm.productId)
|
||
this.loadModuleParentOptionsByProject(this.moduleForm.projectId).then(() => {
|
||
this.moduleParentOptions = (this.moduleParentOptions || []).filter(item => item.id !== row.id)
|
||
})
|
||
},
|
||
handleModuleProductChange(val) {
|
||
this.moduleForm.projectId = ''
|
||
this.moduleForm.parentId = '0'
|
||
this.moduleParentOptions = []
|
||
this.loadModuleProjectOptionsByProduct(val)
|
||
},
|
||
handleModuleProjectChange(val) {
|
||
this.moduleForm.parentId = '0'
|
||
this.loadModuleParentOptionsByProject(val)
|
||
},
|
||
resetModuleForm() {
|
||
this.moduleDialogMode = 'create'
|
||
this.editingModuleId = ''
|
||
this.moduleForm = {
|
||
productId: this.selectedProductId || '',
|
||
projectId: this.selectedProjectId || this.moduleQueryForm.projectId || this.projectId || '',
|
||
name: '',
|
||
parentId: '0',
|
||
sortOrder: '0',
|
||
path: ''
|
||
}
|
||
this.loadModuleParentOptionsByProject(this.moduleForm.projectId)
|
||
this.$nextTick(() => {
|
||
this.$refs.moduleForm && this.$refs.moduleForm.clearValidate()
|
||
})
|
||
},
|
||
submitModule() {
|
||
this.$refs.moduleForm.validate(valid => {
|
||
if (!valid) {
|
||
return
|
||
}
|
||
this.moduleSubmitting = true
|
||
const payload = this.cleanParams({
|
||
projectId: this.moduleForm.projectId,
|
||
name: this.moduleForm.name,
|
||
parentId: this.moduleForm.parentId,
|
||
sortOrder: this.moduleForm.sortOrder,
|
||
path: this.moduleForm.path
|
||
})
|
||
const request = this.moduleDialogMode === 'edit'
|
||
? updateModule(Object.assign({ moduleId: this.editingModuleId }, payload))
|
||
: createModule(payload)
|
||
request.then(() => {
|
||
this.$message({ type: 'success', message: this.moduleDialogMode === 'edit' ? '保存成功' : '新增成功' })
|
||
this.moduleDialogVisible = false
|
||
if (this.selectedProjectId !== this.moduleForm.projectId) {
|
||
this.selectedProjectId = this.moduleForm.projectId
|
||
this.projectId = this.moduleForm.projectId
|
||
this.moduleQueryForm.projectId = this.moduleForm.projectId
|
||
}
|
||
this.fetchModuleList()
|
||
}).finally(() => {
|
||
this.moduleSubmitting = false
|
||
})
|
||
})
|
||
},
|
||
removeModule(row) {
|
||
this.$confirm('确认删除该模块吗?', '提示', { type: 'warning' }).then(() => {
|
||
deleteModule({ moduleId: row.id }).then(() => {
|
||
this.$message({ type: 'success', message: '删除成功' })
|
||
this.fetchModuleList()
|
||
})
|
||
}).catch(() => {})
|
||
},
|
||
fetchList() {
|
||
if (!this.projectId) {
|
||
this.tableData = []
|
||
this.total = 0
|
||
return
|
||
}
|
||
this.loading = true
|
||
const query = Object.assign({}, this.queryForm, {
|
||
// 后端创建人字段为 created_by_name
|
||
created_by_name: this.queryForm.creator
|
||
})
|
||
delete query.creator
|
||
const params = this.cleanParams(Object.assign({}, query, {
|
||
pageNo: this.pageNo,
|
||
pageSize: this.pageSize
|
||
}))
|
||
getCaseList(this.projectId, params).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const list = data.list || data.items || []
|
||
this.tableData = Array.isArray(list) ? list : []
|
||
this.total = Number(data.total || this.tableData.length || 0)
|
||
}).catch(() => {
|
||
this.tableData = []
|
||
this.total = 0
|
||
}).finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
handleSizeChange(size) {
|
||
this.pageSize = size
|
||
this.pageNo = 1
|
||
this.fetchList()
|
||
},
|
||
handleCurrentChange(page) {
|
||
this.pageNo = page
|
||
this.fetchList()
|
||
},
|
||
handleCreatedTimeChange(value) {
|
||
const range = Array.isArray(value) ? value : []
|
||
this.queryForm.createdStartTime = range[0] || ''
|
||
this.queryForm.createdEndTime = range[1] || ''
|
||
},
|
||
applyMoreFilters() {
|
||
this.moreFilterVisible = false
|
||
this.pageNo = 1
|
||
this.fetchList()
|
||
if (this.activeTab === 'case-mindmap') {
|
||
this.fetchCaseMindmapData()
|
||
}
|
||
},
|
||
resetQuery() {
|
||
this.queryForm = {
|
||
keyword: '',
|
||
priority: '',
|
||
creator: '',
|
||
createdStartTime: '',
|
||
createdEndTime: '',
|
||
moduleId: '',
|
||
caseType: '',
|
||
status: '',
|
||
isAuto: '',
|
||
tag: ''
|
||
}
|
||
this.createdTimeRange = []
|
||
this.pageNo = 1
|
||
this.fetchList()
|
||
if (this.activeTab === 'case-mindmap') {
|
||
this.fetchCaseMindmapData()
|
||
}
|
||
},
|
||
handleColumnSelectionChange(value) {
|
||
if (value.length === 0) {
|
||
this.$message.warning('至少保留一个展示字段')
|
||
this.selectedCaseColumnKeys = ['title']
|
||
}
|
||
},
|
||
getCaseColumnValue(row, key) {
|
||
const map = {
|
||
title: row.title,
|
||
moduleName: row.module_name,
|
||
priority: row.priority,
|
||
tags: row.tags,
|
||
creator: row.created_by_name || row.creator || row.creator_name || row.create_user || row.createUser || row.created_by || '',
|
||
createdTime: row.created_time || row.create_time || row.createdTime || row.createTime || '',
|
||
caseType: row.case_type,
|
||
status: row.status,
|
||
isAuto: row.is_auto,
|
||
caseKey: row.case_key,
|
||
projectName: row.project_name
|
||
}
|
||
return map[key]
|
||
},
|
||
formatCaseColumnValue(key, value) {
|
||
if (key === 'priority') return this.formatPriority(value)
|
||
if (key === 'caseType') return this.formatCaseType(value)
|
||
if (key === 'status') return this.formatStatus(value)
|
||
if (key === 'isAuto') return this.formatIsAuto(value)
|
||
return value
|
||
},
|
||
openCaseImportDialog() {
|
||
this.caseImportDialogVisible = true
|
||
},
|
||
resetImportDialog() {
|
||
this.importFile = null
|
||
if (this.$refs.importFileInput) {
|
||
this.$refs.importFileInput.value = ''
|
||
}
|
||
},
|
||
triggerImportFileSelect() {
|
||
this.$refs.importFileInput && this.$refs.importFileInput.click()
|
||
},
|
||
onImportFileChange(event) {
|
||
const files = event && event.target && event.target.files ? event.target.files : []
|
||
const file = files[0]
|
||
this.setImportFile(file)
|
||
},
|
||
onImportFileDrop(event) {
|
||
const files = event && event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files : []
|
||
const file = files[0]
|
||
this.setImportFile(file)
|
||
},
|
||
setImportFile(file) {
|
||
if (!file) {
|
||
return
|
||
}
|
||
const lowerName = String(file.name || '').toLowerCase()
|
||
const isExcel = lowerName.endsWith('.xls') || lowerName.endsWith('.xlsx')
|
||
if (!isExcel) {
|
||
this.$message.warning('仅支持 .xls 或 .xlsx 文件')
|
||
return
|
||
}
|
||
const maxSize = 20 * 1024 * 1024
|
||
if (file.size > maxSize) {
|
||
this.$message.warning('文件大小不能超过 20MB')
|
||
return
|
||
}
|
||
this.importFile = file
|
||
},
|
||
downloadImportTemplate() {
|
||
if (!this.selectedProjectId) {
|
||
this.$message.warning('请先选择项目')
|
||
return
|
||
}
|
||
downloadCaseImportTemplate(this.selectedProjectId).then(res => {
|
||
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = '用例导入模板.xlsx'
|
||
link.click()
|
||
window.URL.revokeObjectURL(url)
|
||
})
|
||
},
|
||
submitCaseImport() {
|
||
if (!this.selectedProjectId) {
|
||
this.$message.warning('请先选择项目')
|
||
return
|
||
}
|
||
if (!this.importFile) {
|
||
this.$message.warning('请先选择导入文件')
|
||
return
|
||
}
|
||
this.importSubmitting = true
|
||
importCaseExcel(this.selectedProjectId, this.importFile).then(() => {
|
||
this.$message.success('导入并解析成功')
|
||
this.caseImportDialogVisible = false
|
||
this.fetchList()
|
||
}).finally(() => {
|
||
this.importSubmitting = false
|
||
})
|
||
},
|
||
goEditor(row) {
|
||
this.$router.push({
|
||
path: '/test-platform/case/editor',
|
||
query: {
|
||
productId: this.selectedProductId || undefined,
|
||
projectId: this.projectId,
|
||
caseId: row && row.id
|
||
}
|
||
})
|
||
},
|
||
goReview(row) {
|
||
this.$router.push({ path: '/test-platform/case/review', query: { projectId: this.projectId, caseId: row.id } })
|
||
},
|
||
remove(row) {
|
||
this.$confirm('确认删除该用例吗?', '提示', { type: 'warning' }).then(() => {
|
||
deleteCase(this.projectId, row.id).then(() => {
|
||
this.$message({ type: 'success', message: '删除成功' })
|
||
this.fetchList()
|
||
})
|
||
}).catch(() => {})
|
||
},
|
||
cleanParams(params) {
|
||
return Object.keys(params).reduce((result, key) => {
|
||
if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
|
||
result[key] = params[key]
|
||
}
|
||
return result
|
||
}, {})
|
||
},
|
||
formatPriority(value) {
|
||
const map = { 0: 'P0', 1: 'P1', 2: 'P2', 3: 'P3' }
|
||
return map[value] || value
|
||
},
|
||
formatCaseType(value) {
|
||
const map = { 1: '功能', 2: '性能', 3: '安全', 4: '接口' }
|
||
return map[value] || value
|
||
},
|
||
formatStatus(value) {
|
||
const map = { 1: '正常', 2: '已废弃', 3: '评审中', 4: '评审通过' }
|
||
return map[value] || value
|
||
},
|
||
formatIsAuto(value) {
|
||
const map = { 0: '未实现', 1: '已实现' }
|
||
return map[value] || value
|
||
},
|
||
formatTags(tags) {
|
||
return Array.isArray(tags) ? tags.join('、') : (tags || '')
|
||
}
|
||
},
|
||
created() {
|
||
this.loadProductOptions().then(() => {
|
||
const routeProductId = this.$route.query.productId ? Number(this.$route.query.productId) : ''
|
||
if (routeProductId) {
|
||
this.selectedProductId = routeProductId
|
||
return this.loadProjectOptionsByProduct(routeProductId)
|
||
}
|
||
if (this.selectedProjectId) {
|
||
return getProjectDetail(this.selectedProjectId).then(res => {
|
||
const data = res && res.data ? res.data : res || {}
|
||
const productId = data.productId || data.product_id || ''
|
||
if (productId) {
|
||
this.selectedProductId = productId
|
||
return this.loadProjectOptionsByProduct(productId)
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
return this.restoreSharedProductProjectCache()
|
||
}).finally(() => {
|
||
if (this.selectedProjectId) {
|
||
this.projectId = this.selectedProjectId
|
||
this.moduleQueryForm.projectId = this.selectedProjectId
|
||
this.fetchModuleList()
|
||
this.fetchList()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-wrap {
|
||
padding: 20px;
|
||
}
|
||
|
||
.toolbar-wrap {
|
||
text-align: right;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.case-action-buttons-item {
|
||
float: right;
|
||
margin-right: 0 !important;
|
||
}
|
||
|
||
.more-filter-wrap {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.more-filter-footer {
|
||
border-top: 1px solid #ebeef5;
|
||
text-align: right;
|
||
padding-top: 10px;
|
||
}
|
||
|
||
.column-setting-wrap {
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.column-setting-title {
|
||
color: #606266;
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.case-import-panel {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
padding: 28px 24px;
|
||
text-align: center;
|
||
}
|
||
|
||
.case-import-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
background: #409eff;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.case-import-title {
|
||
font-size: 28px;
|
||
color: #303133;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.case-import-subtitle {
|
||
color: #909399;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.case-import-dropzone {
|
||
border: 1px dashed #dcdfe6;
|
||
border-radius: 4px;
|
||
padding: 28px 20px;
|
||
margin: 0 auto;
|
||
max-width: 520px;
|
||
}
|
||
|
||
.case-import-drop-text {
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.link-text {
|
||
color: #409eff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.case-import-file-tip {
|
||
color: #909399;
|
||
}
|
||
|
||
.case-import-file-name {
|
||
margin-top: 8px;
|
||
color: #303133;
|
||
}
|
||
|
||
.case-import-actions {
|
||
margin-top: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.hidden-file-input {
|
||
display: none;
|
||
}
|
||
|
||
.mindmap-wrap {
|
||
margin-top: 8px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
background: #fafcff;
|
||
padding: 14px 12px;
|
||
min-height: 280px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.xmind-tree {
|
||
min-width: 980px;
|
||
background: transparent;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree-node {
|
||
position: relative;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree-node__content {
|
||
height: auto;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree-node__children {
|
||
position: relative;
|
||
margin-left: 12px;
|
||
padding-left: 22px;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree-node__children:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 8px;
|
||
top: 0;
|
||
bottom: 10px;
|
||
border-left: 1px solid #b7d0e8;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree-node__content:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: -14px;
|
||
top: 50%;
|
||
width: 14px;
|
||
border-top: 1px solid #8fb8de;
|
||
}
|
||
|
||
.xmind-tree /deep/ .el-tree > .el-tree-node > .el-tree-node__content:before {
|
||
display: none;
|
||
}
|
||
|
||
.mindmap-node {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 220px;
|
||
max-width: 760px;
|
||
background: #fff;
|
||
border: 1px solid #dce6ff;
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.08);
|
||
}
|
||
|
||
.mindmap-node-wrap {
|
||
display: inline-flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.mindmap-node-title {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.mindmap-node-project {
|
||
border-color: #67c23a;
|
||
box-shadow: 0 1px 6px rgba(103, 194, 58, 0.16);
|
||
}
|
||
|
||
.mindmap-node-module {
|
||
border-color: #e6a23c;
|
||
box-shadow: 0 1px 6px rgba(230, 162, 60, 0.14);
|
||
}
|
||
|
||
.mindmap-node-case {
|
||
border-color: #7fb3e3;
|
||
}
|
||
|
||
.mindmap-inline-detail {
|
||
display: inline-flex;
|
||
align-items: stretch;
|
||
margin-left: 14px;
|
||
}
|
||
|
||
.mindmap-inline-detail-line {
|
||
width: 18px;
|
||
margin-top: 22px;
|
||
border-top: 1px solid #8fb8de;
|
||
}
|
||
|
||
.mindmap-inline-detail-card {
|
||
width: 420px;
|
||
max-width: 620px;
|
||
background: #fff;
|
||
border: 1px solid #d9e8f6;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
box-shadow: 0 1px 6px rgba(64, 158, 255, 0.12);
|
||
}
|
||
|
||
.mindmap-inline-detail-title {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mindmap-inline-detail-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.mindmap-inline-detail-item {
|
||
color: #606266;
|
||
line-height: 1.6;
|
||
margin-bottom: 6px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.mindmap-inline-detail-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.mindmap-inline-actions {
|
||
text-align: right;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.mindmap-node-meta {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mindmap-meta-text {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.mindmap-empty {
|
||
color: #909399;
|
||
text-align: center;
|
||
line-height: 220px;
|
||
}
|
||
</style>
|