-
这里汇总各项目常用环境地址和文档链接,方便快速进入。
-
-
{{ project.name }}
-
-
-
{{ item.name }}:
-
- {{ item.url }}
-
+
+
+
+ {{ greetingPrefix }}{{ greetingTime }}
+ {{ todayText }}
+
+ 待处理进度
+
+ 已完成 100%
+
+
+ 登录后查看个人工作台
+
+
+
+
+
+ 今天剩余工作总计
+
+
+
{{ formatCount(workCountOpportunity) }}
+
我的机会
+
+
{{ formatCount(workCountBug) }}
+
我的 BUG
+
点击查看指派给我
+
+
+
{{ formatCount(workCountPlan) }}
+
我的计划
+
点击查看我负责的
+
+
+
+
+
+
+
+ 环境与文档
+ 这里汇总各项目常用环境地址和文档链接,方便快速进入。
+
+
{{ project.name }}
+
+
+ {{ item.name }}:
+
+ {{ item.url }}
+
@@ -27,10 +64,17 @@
@@ -62,6 +211,117 @@ export default {
diff --git a/src/components/System/UserManage.vue b/src/components/System/UserManage.vue
index 9c5acbb..28a312e 100644
--- a/src/components/System/UserManage.vue
+++ b/src/components/System/UserManage.vue
@@ -125,7 +125,7 @@
@@ -107,4 +1204,263 @@ export default {
.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;
+}
diff --git a/src/components/TestPlatform/DataFactory/BuilderEditor.vue b/src/components/TestPlatform/DataFactory/BuilderEditor.vue
index c41ecfd..239c288 100644
--- a/src/components/TestPlatform/DataFactory/BuilderEditor.vue
+++ b/src/components/TestPlatform/DataFactory/BuilderEditor.vue
@@ -66,7 +66,7 @@ export default {
this.saving = true
createBuilder(this.projectId, Object.assign({}, this.form, { definition, input_schema })).then(() => {
this.$message({ type: 'success', message: '造数器保存成功' })
- this.$router.push({ path: '/test-platform/data-factory/builders', query: { projectId: this.projectId } })
+ this.$router.push({ path: '/data-tools/factory/builders', query: { projectId: this.projectId } })
}).finally(() => {
this.saving = false
})
diff --git a/src/components/TestPlatform/DataFactory/BuilderList.vue b/src/components/TestPlatform/DataFactory/BuilderList.vue
index ec8b05d..4268ba0 100644
--- a/src/components/TestPlatform/DataFactory/BuilderList.vue
+++ b/src/components/TestPlatform/DataFactory/BuilderList.vue
@@ -72,13 +72,13 @@ export default {
})
},
goEditor(row) {
- this.$router.push({ path: '/test-platform/data-factory/editor', query: { projectId: this.projectId, builderId: row && row.id } })
+ this.$router.push({ path: '/data-tools/factory/editor', query: { projectId: this.projectId, builderId: row && row.id } })
},
goTasks() {
- this.$router.push({ path: '/test-platform/data-factory/tasks', query: { projectId: this.projectId } })
+ this.$router.push({ path: '/data-tools/factory/task', query: { projectId: this.projectId } })
},
goMock() {
- this.$router.push({ path: '/test-platform/data-factory/mock', query: { projectId: this.projectId } })
+ this.$router.push({ path: '/data-tools/factory/mock', query: { projectId: this.projectId } })
},
execute(row) {
executeBuilder(this.projectId, row.id, { params: { count: 1 }, async: true }).then(() => {
diff --git a/src/components/TestPlatform/Plan/PlanBuilder.vue b/src/components/TestPlatform/Plan/PlanBuilder.vue
index d5d5852..385e132 100644
--- a/src/components/TestPlatform/Plan/PlanBuilder.vue
+++ b/src/components/TestPlatform/Plan/PlanBuilder.vue
@@ -1,26 +1,100 @@
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
+
-
-
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 返回
保存计划
@@ -30,34 +104,270 @@
diff --git a/src/components/TestPlatform/Plan/PlanCaseAdd.vue b/src/components/TestPlatform/Plan/PlanCaseAdd.vue
new file mode 100644
index 0000000..cc318fd
--- /dev/null
+++ b/src/components/TestPlatform/Plan/PlanCaseAdd.vue
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.module_name || '-' }}
+
+
+
+
+ {{ formatPriority(scope.row.priority) }}
+
+
+
+
+ {{ isCaseAssociated(scope.row.id) ? '已关联' : '未关联' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/TestPlatform/Plan/PlanExecute.vue b/src/components/TestPlatform/Plan/PlanExecute.vue
index 2278c95..0d68254 100644
--- a/src/components/TestPlatform/Plan/PlanExecute.vue
+++ b/src/components/TestPlatform/Plan/PlanExecute.vue
@@ -1,108 +1,245 @@
-
-
-
-
-
-
-
-
- 刷新
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 提交执行
-
-
+
+
+
+
+
+
+
+
+ {{ scope.row.statusLabel }}
+
+
+
+
+ 执行
+
+
+
+
+
+
+
+
+ {{ selectedPlanCase ? (selectedPlanCase.caseTitle || selectedPlanCase.title || selectedPlanCase.caseKey || selectedPlanCase.caseId) : '' }}
+
+
前置条件
+
{{ caseDetail.preconditions || '-' }}
+
+
+
执行步骤
+
{{ formatSteps(caseDetail.steps) || '-' }}
+
+
+
预期结果
+
{{ caseDetail.expected_results || caseDetail.expectedResults || '-' }}
+
+
+
+ 取消
+ 通过
+ 失败
+ 阻塞
+
+
@@ -111,4 +248,54 @@ export default {
.page-wrap {
padding: 20px;
}
+
+.filter-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.filter-toolbar-form {
+ flex: 1;
+ min-width: 0;
+}
+
+.filter-toolbar-actions {
+ flex-shrink: 0;
+ padding-top: 4px;
+}
+
+.detail-panel {
+ margin-top: 16px;
+}
+
+.detail-title {
+ margin-bottom: 8px;
+ font-weight: 600;
+ color: #303133;
+ font-size: 18px;
+ line-height: 1.4;
+}
+
+.detail-text {
+ white-space: pre-wrap;
+ color: #606266;
+ line-height: 1.5;
+}
+
+.detail-section {
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ padding: 10px 12px;
+ margin-bottom: 10px;
+}
+
+.detail-section-title {
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 8px;
+}
+
diff --git a/src/components/TestPlatform/Plan/PlanList.vue b/src/components/TestPlatform/Plan/PlanList.vue
index 91290dd..4b9b8fe 100644
--- a/src/components/TestPlatform/Plan/PlanList.vue
+++ b/src/components/TestPlatform/Plan/PlanList.vue
@@ -4,37 +4,165 @@
新建计划
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
- 查询
+ 查询
+
+
+ 重置
-
-
-
+
+ {{ formatDateTime(getStartTimeValue(scope.row)) }}
+
+
+ {{ formatDateTime(getEndTimeValue(scope.row)) }}
+
+
+ {{ formatOwner(scope.row) }}
+
+
+ {{ formatEnvironment(scope.row) }}
+
+
+ {{ formatStatus(scope.row.status) }}
+
+
+ 编辑
+ 关联用例
执行
进度
+ 机器人消息
+ 执行自动化用例
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 发送
+
+
import PageSection from '@/components/TestPlatform/common/PageSection'
import { getPlanList } from '@/api/planApi'
+import { getProductList } from '@/api/productApi'
+import {
+ getProjectDetail,
+ getProjectEnvironments,
+ getProjectHookList,
+ getProjectList,
+ getProjectMembers,
+ sendProjectHookMessage
+} from '@/api/projectApi'
+import {
+ readLastProductProjectCache,
+ saveLastProductProjectCache,
+ pickIdFromOptions
+} from '@/utils/lastProductProjectCache'
export default {
name: 'PlanList',
components: { PageSection },
+ computed: {
+ hookSendPreviewTitle() {
+ const row = this.hookSendPlanRow
+ if (!row) return ''
+ return (row.name || '').trim() || this.hookSendPlanName || '测试计划'
+ }
+ },
data() {
return {
loading: false,
- projectId: this.$route.query.projectId || 1,
+ projectId: this.$route.query.projectId || '',
+ selectedProductId: '',
+ selectedProjectId: this.$route.query.projectId ? Number(this.$route.query.projectId) : '',
+ productOptions: [],
+ projectOptions: [],
+ ownerMap: {},
+ /** 负责人筛选下拉(label 优先 real_name,与 ownerMap 同源) */
+ ownerMemberOptions: [],
+ environmentMap: {},
queryForm: {
- keyword: '',
- status: ''
+ planName: '',
+ status: '',
+ version: '',
+ owner: ''
},
pageNo: 1,
pageSize: 10,
total: 0,
- tableData: []
+ tableData: [],
+ hookSendDialogVisible: false,
+ hookSendSubmitting: false,
+ hookSendPlanName: '',
+ /** 当前要推送机器人消息的计划行 */
+ hookSendPlanRow: null,
+ hookSendHookOptions: [],
+ hookSendHookListLoading: false,
+ hookSendForm: {
+ hookType: '',
+ hookId: '',
+ content: '',
+ realName: ''
+ },
+ hookSendResultLines: []
}
},
+ beforeRouteLeave(to, from, next) {
+ this.savePageCache()
+ next()
+ },
methods: {
+ getCacheKey() {
+ return 'test-platform-plan-list-cache'
+ },
+ savePageCache() {
+ const cache = {
+ projectId: this.projectId,
+ selectedProductId: this.selectedProductId,
+ selectedProjectId: this.selectedProjectId,
+ productOptions: this.productOptions,
+ projectOptions: this.projectOptions,
+ ownerMap: this.ownerMap,
+ ownerMemberOptions: this.ownerMemberOptions,
+ environmentMap: this.environmentMap,
+ queryForm: this.queryForm,
+ pageNo: this.pageNo,
+ pageSize: this.pageSize,
+ total: this.total,
+ tableData: this.tableData
+ }
+ window.sessionStorage.setItem(this.getCacheKey(), JSON.stringify(cache))
+ },
+ restorePageCache() {
+ const raw = window.sessionStorage.getItem(this.getCacheKey())
+ if (!raw) return false
+ try {
+ const cache = JSON.parse(raw)
+ this.projectId = cache.projectId || ''
+ this.selectedProductId = cache.selectedProductId || ''
+ this.selectedProjectId = cache.selectedProjectId || ''
+ this.productOptions = cache.productOptions || []
+ this.projectOptions = cache.projectOptions || []
+ this.ownerMap = cache.ownerMap || {}
+ this.ownerMemberOptions = Array.isArray(cache.ownerMemberOptions) ? cache.ownerMemberOptions : []
+ if (!this.ownerMemberOptions.length && this.ownerMap && Object.keys(this.ownerMap).length) {
+ this.ownerMemberOptions = Object.keys(this.ownerMap).map(k => ({
+ id: /^\d+$/.test(String(k)) ? Number(k) : k,
+ name: this.ownerMap[k]
+ }))
+ }
+ this.environmentMap = cache.environmentMap || {}
+ this.queryForm = cache.queryForm || {
+ planName: '',
+ status: '',
+ version: '',
+ owner: ''
+ }
+ this.syncOwnerFilterWithMemberOptions()
+ this.pageNo = Number(cache.pageNo || 1)
+ this.pageSize = Number(cache.pageSize || 10)
+ this.total = Number(cache.total || 0)
+ this.tableData = Array.isArray(cache.tableData) ? cache.tableData : []
+ return true
+ } catch (e) {
+ return false
+ }
+ },
+ 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 = []
+ })
+ },
+ handleProductChange(val) {
+ this.selectedProjectId = ''
+ this.projectId = ''
+ this.queryForm.owner = ''
+ this.ownerMap = {}
+ this.ownerMemberOptions = []
+ this.tableData = []
+ this.total = 0
+ this.loadProjectOptionsByProduct(val)
+ },
+ handleProjectChange(val) {
+ this.selectedProjectId = val || ''
+ this.projectId = val || ''
+ this.pageNo = 1
+ this.queryForm.owner = ''
+ this.ownerMap = {}
+ this.ownerMemberOptions = []
+ this.environmentMap = {}
+ if (!val) {
+ this.tableData = []
+ this.total = 0
+ return
+ }
+ saveLastProductProjectCache(this.selectedProductId, val)
+ this.loadProjectMetaMaps(val).finally(() => {
+ this.fetchList()
+ })
+ },
+ restoreSharedProductProjectCache() {
+ const cached = readLastProductProjectCache()
+ const q = this.$route.query || {}
+ const fromPlanSelf = q.planOwnerSelf === '1' || q.planOwnerSelf === 'true'
+ let pid = cached && cached.productId
+ let projId = cached && cached.projectId
+ if (fromPlanSelf) {
+ if (q.productId !== undefined && q.productId !== null && String(q.productId).trim() !== '') {
+ pid = q.productId
+ }
+ if (q.projectId !== undefined && q.projectId !== null && String(q.projectId).trim() !== '') {
+ projId = q.projectId
+ }
+ }
+ 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
+ })
+ },
+ mergeCurrentUserIntoOwnerMemberOptionsIfNeeded() {
+ const u = this.$store.state.currentUser
+ if (!u || u.id == null || u.id === '') return
+ const id = u.id
+ if ((this.ownerMemberOptions || []).some(m => String(m.id) === String(id))) return
+ const name = u.realName || u.username || '当前用户'
+ this.ownerMemberOptions = [{ id, name }, ...(this.ownerMemberOptions || [])]
+ this.ownerMap = Object.assign({}, this.ownerMap, { [id]: name })
+ },
+ applyPlanOwnerSelfFromRoute() {
+ const q = this.$route.query || {}
+ if (q.planOwnerSelf !== '1' && q.planOwnerSelf !== 'true') return
+ const u = this.$store.state.currentUser
+ const uid = u && u.id != null && u.id !== '' ? u.id : null
+ if (uid == null) {
+ this.$message.warning('请先登录')
+ return
+ }
+ if (!this.selectedProjectId) {
+ this.$message.warning('请先选择项目,或从首页在已选过产品/项目时进入')
+ return
+ }
+ this.mergeCurrentUserIntoOwnerMemberOptionsIfNeeded()
+ this.queryForm.owner = uid
+ this.pageNo = 1
+ },
+ /** 负责人筛选值为成员 id;若不在当前项目成员列表中则清空(避免缓存里旧姓名或无效 id) */
+ syncOwnerFilterWithMemberOptions() {
+ const cur = this.queryForm.owner
+ if (cur === '' || cur === undefined || cur === null) return
+ const idSet = new Set(this.ownerMemberOptions.map(u => String(u.id)))
+ if (!idSet.has(String(cur))) {
+ this.queryForm.owner = ''
+ }
+ },
+ loadProjectMetaMaps(projectId) {
+ if (!projectId) {
+ this.ownerMap = {}
+ this.ownerMemberOptions = []
+ this.environmentMap = {}
+ return Promise.resolve()
+ }
+ const memberReq = getProjectMembers(projectId, { pageNo: 1, pageSize: 1000 }).then(res => {
+ const data = (res && res.data) || res || {}
+ const list = data.items || data.list || data.data || data || []
+ const arr = Array.isArray(list) ? list : []
+ this.ownerMemberOptions = arr
+ .map(item => {
+ const id = item.user_id || item.userId || item.id
+ const name =
+ item.real_name ||
+ item.realName ||
+ item.username ||
+ item.name ||
+ item.user_name ||
+ (id !== undefined && id !== null ? String(id) : '')
+ return { id, name }
+ })
+ .filter(u => u.id !== undefined && u.id !== null && u.id !== '')
+ this.ownerMap = this.ownerMemberOptions.reduce((map, u) => {
+ map[u.id] = u.name || String(u.id)
+ return map
+ }, {})
+ this.syncOwnerFilterWithMemberOptions()
+ }).catch(() => {
+ this.ownerMap = {}
+ this.ownerMemberOptions = []
+ })
+ const envReq = getProjectEnvironments(projectId, { pageNo: 1, pageSize: 1000 }).then(res => {
+ const data = (res && res.data) || res || {}
+ const list = data.items || data.list || data.data || data || []
+ this.environmentMap = (Array.isArray(list) ? list : []).reduce((map, item) => {
+ if (item.id !== undefined && item.id !== null && item.id !== '') {
+ map[item.id] = item.name || String(item.id)
+ }
+ return map
+ }, {})
+ }).catch(() => {
+ this.environmentMap = {}
+ })
+ return Promise.all([memberReq, envReq])
+ },
fetchList() {
+ if (!this.projectId) {
+ this.tableData = []
+ this.total = 0
+ return
+ }
this.loading = true
- getPlanList(this.projectId, this.queryForm).then(res => {
+ const params = this.cleanParams({
+ planName: this.queryForm.planName,
+ keyword: this.queryForm.planName,
+ status: this.queryForm.status,
+ version: this.queryForm.version,
+ owner: this.queryForm.owner,
+ owner_id: this.queryForm.owner,
+ pageNo: this.pageNo,
+ pageSize: this.pageSize
+ })
+ getPlanList(this.projectId, params).then(res => {
const data = (res && res.data) || res || {}
this.tableData = data.items || data.list || []
+ this.total = Number(data.total || this.tableData.length || 0)
+ this.savePageCache()
}).catch(() => {
this.tableData = []
+ this.total = 0
+ this.savePageCache()
}).finally(() => {
this.loading = false
})
},
+ cleanParams(params) {
+ return Object.keys(params).reduce((result, key) => {
+ if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
+ result[key] = params[key]
+ }
+ return result
+ }, {})
+ },
+ resetQuery() {
+ this.queryForm = {
+ planName: '',
+ status: '',
+ version: '',
+ owner: ''
+ }
+ this.pageNo = 1
+ this.fetchList()
+ },
+ handleSizeChange(size) {
+ this.pageSize = size
+ this.pageNo = 1
+ this.fetchList()
+ },
+ handleCurrentChange(page) {
+ this.pageNo = page
+ this.fetchList()
+ },
goBuilder() {
- this.$router.push({ path: '/test-platform/plans/builder', query: { projectId: this.projectId } })
+ this.$router.push({ path: '/test-platform/plan/builder', query: { projectId: this.projectId } })
+ },
+ goEdit(row) {
+ this.$router.push({
+ path: '/test-platform/plan/builder',
+ query: {
+ projectId: this.projectId,
+ planId: row.id
+ }
+ })
+ },
+ goAssociateCases(row) {
+ const project = (this.projectOptions || []).find(item => String(item.id) === String(this.projectId))
+ const product = (this.productOptions || []).find(item => String(item.id) === String(this.selectedProductId))
+ this.$router.push({
+ path: '/test-platform/plan/case/add',
+ query: {
+ productId: this.selectedProductId || undefined,
+ productName: (product && product.name) || '',
+ projectId: this.projectId || undefined,
+ projectName: (project && project.name) || '',
+ planId: row.id,
+ planName: row.name || '',
+ ownerId: row.owner_id || row.ownerId || undefined
+ }
+ })
},
goExecute(row) {
- this.$router.push({ path: '/test-platform/plans/execute', query: { projectId: this.projectId, planId: row.id } })
+ const project = (this.projectOptions || []).find(item => String(item.id) === String(this.projectId))
+ const product = (this.productOptions || []).find(item => String(item.id) === String(this.selectedProductId))
+ this.$router.push({
+ path: '/test-platform/plan/execute',
+ query: {
+ productId: this.selectedProductId || undefined,
+ productName: (product && product.name) || '',
+ projectId: this.projectId,
+ projectName: (project && project.name) || '',
+ planId: row.id,
+ planName: row.name || ''
+ }
+ })
+ },
+ openHookSendDialog(row) {
+ if (!this.projectId) {
+ this.$message.warning('请先选择项目')
+ return
+ }
+ const name = (row && row.name) || ''
+ this.hookSendPlanName = name || `计划 #${row && row.id != null ? row.id : ''}`
+ this.hookSendPlanRow = row || null
+ const defaultContent = this.buildPlanExecuteMessageBody(row)
+ const defaultReal = this.getPlanOwnerRealName(row)
+ this.hookSendForm = {
+ hookType: '',
+ hookId: '',
+ content: defaultContent,
+ realName: defaultReal
+ }
+ this.hookSendHookOptions = []
+ this.hookSendResultLines = []
+ this.hookSendDialogVisible = true
+ this.loadHookSendOptions()
+ this.$nextTick(() => {
+ if (this.$refs.hookSendFormRef) {
+ this.$refs.hookSendFormRef.clearValidate()
+ }
+ })
+ },
+ resetHookSendForm() {
+ this.hookSendSubmitting = false
+ this.hookSendPlanName = ''
+ this.hookSendPlanRow = null
+ this.hookSendHookOptions = []
+ this.hookSendForm = {
+ hookType: '',
+ hookId: '',
+ content: '',
+ realName: ''
+ }
+ this.hookSendResultLines = []
+ this.$nextTick(() => {
+ if (this.$refs.hookSendFormRef) {
+ this.$refs.hookSendFormRef.resetFields()
+ }
+ })
+ },
+ /** 与「执行」入口一致的计划执行页链接(绝对地址,便于 IM 中点击) */
+ buildPlanExecuteUrl(row) {
+ if (!row || row.id == null) return ''
+ const project = (this.projectOptions || []).find(item => String(item.id) === String(this.projectId))
+ const product = (this.productOptions || []).find(item => String(item.id) === String(this.selectedProductId))
+ const loc = this.$router.resolve({
+ path: '/test-platform/plan/execute',
+ query: {
+ productId: this.selectedProductId || undefined,
+ productName: (product && product.name) || '',
+ projectId: this.projectId,
+ projectName: (project && project.name) || '',
+ planId: row.id,
+ planName: row.name || ''
+ }
+ })
+ const href = (loc && loc.href) || ''
+ if (!href) return ''
+ if (/^https?:\/\//i.test(href)) return href
+ const origin = typeof window !== 'undefined' && window.location ? window.location.origin : ''
+ return origin + (href.charAt(0) === '/' ? href : `/${href}`)
+ },
+ buildPlanExecuteMessageBody(row) {
+ const url = this.buildPlanExecuteUrl(row)
+ if (!url) return '你有一条测试计划待执行:'
+ return `你有一条测试计划待执行:\n${url}`
+ },
+ /** 计划负责人展示名(优先真实姓名类字段,用于 @) */
+ getPlanOwnerRealName(row) {
+ if (!row) return ''
+ const oid = row.owner_id != null ? row.owner_id : row.ownerId
+ const fromMap = oid != null && oid !== '' ? this.ownerMap[oid] : ''
+ const s = (
+ row.owner_real_name ||
+ row.ownerRealName ||
+ row.owner_name ||
+ row.ownerName ||
+ fromMap ||
+ ''
+ ).trim()
+ return s
+ },
+ onHookSendTypeChange() {
+ this.hookSendForm.hookId = ''
+ this.loadHookSendOptions()
+ },
+ loadHookSendOptions() {
+ const pid = Number(this.projectId)
+ if (!pid || Number.isNaN(pid)) {
+ this.hookSendHookOptions = []
+ return
+ }
+ const params = { projectId: pid, pageNo: 1, pageSize: 500 }
+ const ht = this.hookSendForm.hookType
+ if (ht === 1 || ht === 2 || ht === 3) {
+ params.hookType = ht
+ }
+ this.hookSendHookListLoading = true
+ getProjectHookList(params)
+ .then(res => {
+ const data = (res && res.data) || res || {}
+ const list = data.list || data.items || []
+ this.hookSendHookOptions = Array.isArray(list) ? list : []
+ })
+ .catch(() => {
+ this.hookSendHookOptions = []
+ })
+ .finally(() => {
+ this.hookSendHookListLoading = false
+ })
+ },
+ formatHookSendOptionLabel(item) {
+ if (!item) return ''
+ const typeName = item.hook_type_name || this.hookTypeSendLabel(item.hook_type != null ? item.hook_type : item.hookType)
+ const desc = (item.description || '').trim()
+ const url = String(item.webhook_url || item.webhookUrl || '').trim()
+ const shortUrl = url.length > 42 ? `${url.slice(0, 42)}…` : url
+ if (desc) return `${desc}(${typeName} #${item.id})`
+ if (shortUrl) return `${shortUrl}(${typeName} #${item.id})`
+ return `${typeName} #${item.id}`
+ },
+ hookTypeSendLabel(type) {
+ const map = { 1: '飞书', 2: '钉钉', 3: '企微' }
+ return map[Number(type)] || String(type || '-')
+ },
+ submitHookSend() {
+ const row = this.hookSendPlanRow
+ if (!row || row.id == null) {
+ this.$message.warning('缺少计划信息')
+ return
+ }
+ const pid = Number(this.projectId)
+ if (!pid || Number.isNaN(pid)) {
+ this.$message.warning('项目 ID 无效')
+ return
+ }
+ const title = (this.hookSendPreviewTitle || '').trim() || '测试计划'
+ const content = (this.hookSendForm.content || '').trim()
+ if (!content) {
+ this.$message.warning('消息正文不能为空')
+ return
+ }
+ if (content === '你有一条测试计划待执行:') {
+ this.$message.warning('无法生成计划执行链接,请检查项目与计划')
+ return
+ }
+ const payload = {
+ projectId: pid,
+ title,
+ content
+ }
+ const rn = (this.hookSendForm.realName || '').trim()
+ if (rn) payload.realName = rn
+ const hid = this.hookSendForm.hookId
+ if (hid !== '' && hid !== null && hid !== undefined) {
+ const n = Number(hid)
+ if (!Number.isNaN(n)) {
+ payload.hookId = n
+ const found = (this.hookSendHookOptions || []).find(h => String(h.id) === String(hid))
+ const fht = found && (found.hook_type != null ? found.hook_type : found.hookType)
+ if (fht === 1 || fht === 2 || fht === 3) {
+ payload.hookType = fht
+ }
+ }
+ } else {
+ const ht = this.hookSendForm.hookType
+ if (ht === 1 || ht === 2 || ht === 3) {
+ payload.hookType = ht
+ }
+ }
+ this.hookSendSubmitting = true
+ this.hookSendResultLines = []
+ sendProjectHookMessage(payload)
+ .then(res => {
+ const msg = (res && res.message) || ''
+ if (res && res.code === 20000) {
+ const list = res.data
+ const lines = []
+ if (Array.isArray(list)) {
+ list.forEach(item => {
+ const hid = item.hook_id != null ? item.hook_id : item.hookId
+ const htype = item.hook_type != null ? item.hook_type : item.hookType
+ const ok = item.success === true || item.success === 1
+ lines.push(
+ `Hook #${hid}(${this.hookTypeSendLabel(htype)}):${ok ? '成功' : '失败'}`
+ )
+ })
+ }
+ this.hookSendResultLines = lines
+ this.$message.success(msg || '已提交发送')
+ return
+ }
+ this.$message.error(msg || '发送失败')
+ })
+ .catch(() => {})
+ .finally(() => {
+ this.hookSendSubmitting = false
+ })
+ },
+ runAutoCases() {
+ this.$message.info('执行自动化用例功能开发中')
},
goProgress(row) {
- this.$router.push({ path: '/test-platform/plans/progress', query: { projectId: this.projectId, planId: row.id } })
+ const project = (this.projectOptions || []).find(item => String(item.id) === String(this.projectId))
+ const product = (this.productOptions || []).find(item => String(item.id) === String(this.selectedProductId))
+ this.$router.push({
+ path: '/test-platform/plan/progress',
+ query: {
+ productId: this.selectedProductId || undefined,
+ productName: (product && product.name) || '',
+ projectId: this.projectId || undefined,
+ projectName: (project && project.name) || '',
+ planId: row.id,
+ planName: row.name || ''
+ }
+ })
+ },
+ formatStatus(value) {
+ const map = { 0: '草稿', 1: '进行中', 2: '已完成', 3: '已归档', 4: '已通过' }
+ return map[value] || value
+ },
+ formatOwner(row) {
+ const ownerId = row.owner_id || row.ownerId
+ return row.owner_name || row.ownerName || row.username || row.owner || this.ownerMap[ownerId] || ownerId || '-'
+ },
+ formatEnvironment(row) {
+ const envId = row.environment_id || row.environmentId
+ return row.environment_name || row.environmentName || row.env_name || row.environment || this.environmentMap[envId] || envId || '-'
+ },
+ getStartTimeValue(row) {
+ if (!row) return ''
+ return row.start_date || row.startDate || row.start_time || row.startTime || row.begin_time || row.beginTime || row.planned_start_time || row.plannedStartTime || ''
+ },
+ getEndTimeValue(row) {
+ if (!row) return ''
+ return row.end_date || row.endDate || row.end_time || row.endTime || row.finish_time || row.finishTime || row.planned_end_time || row.plannedEndTime || ''
+ },
+ formatDateTime(value) {
+ if (!value) return '-'
+ if (typeof value === 'number' || /^\d+$/.test(String(value))) {
+ const raw = Number(value)
+ if (!Number.isNaN(raw) && raw > 0) {
+ const ms = raw < 1000000000000 ? raw * 1000 : raw
+ const date = new Date(ms)
+ if (!Number.isNaN(date.getTime())) {
+ const pad = n => String(n).padStart(2, '0')
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+ }
+ }
+ }
+ return String(value).replace('T', ' ').slice(0, 19)
}
},
created() {
- this.fetchList()
+ const planOwnerSelf = this.$route.query.planOwnerSelf === '1' || this.$route.query.planOwnerSelf === 'true'
+ if (planOwnerSelf) {
+ try {
+ window.sessionStorage.removeItem(this.getCacheKey())
+ } catch (e) {}
+ }
+ // 若存在缓存(通常是从二级页返回),直接恢复,不自动刷新数据
+ if (this.restorePageCache()) {
+ if (planOwnerSelf && this.selectedProjectId) {
+ this.$nextTick(() => {
+ this.loadProjectMetaMaps(this.selectedProjectId).finally(() => {
+ this.applyPlanOwnerSelfFromRoute()
+ this.fetchList()
+ })
+ })
+ }
+ return
+ }
+ this.loadProductOptions().then(() => {
+ 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.loadProjectMetaMaps(this.selectedProjectId).finally(() => {
+ this.applyPlanOwnerSelfFromRoute()
+ this.fetchList()
+ })
+ }
+ })
}
}
@@ -103,4 +871,23 @@ export default {
.page-wrap {
padding: 20px;
}
+
+.hook-send-result {
+ margin-top: 12px;
+ padding: 10px 12px;
+ background: #f5f7fa;
+ border-radius: 4px;
+ font-size: 13px;
+ color: #606266;
+}
+
+.hook-send-result-title {
+ font-weight: 600;
+ margin-bottom: 6px;
+ color: #303133;
+}
+
+.hook-send-result-line {
+ line-height: 1.6;
+}
diff --git a/src/components/TestPlatform/Plan/PlanProgress.vue b/src/components/TestPlatform/Plan/PlanProgress.vue
index 47ae1b2..30122fb 100644
--- a/src/components/TestPlatform/Plan/PlanProgress.vue
+++ b/src/components/TestPlatform/Plan/PlanProgress.vue
@@ -1,67 +1,293 @@
-
-
-
+
+
+
-
-
+
+
+
+
+
- 刷新
+ 刷新看板
+
+
+ 返回
-
-
-
-
-
+
+
+
+
+
总用例数
+
{{ summary.total }}
+
-
-
-
-
+
+
+
待执行
+
{{ summary.pending }}
+
-
-
-
-
+
+
+
已执行
+
{{ summary.executed }}
+
+
+
+
+
完成率
+
{{ summary.progressPercent }}%
+
+
+
+
+
+
执行完成度
+
+
+
+
已执行 {{ summary.executed }} / {{ summary.total }}
+
+
+
+
+
+
状态分布
+
+
+ {{ item.label }}
+
+
+
{{ item.count }}({{ item.percent }}%)
+
+
+
+
+
+
+
用例状态看板明细
+
+
+ {{ formatModuleName(scope.row) }}
+
+
+
+
+
+ {{ statusLabel(scope.row.statusCode) }}
+
+
+
+
+
+
+
@@ -69,5 +295,99 @@ export default {
diff --git a/src/components/TestPlatform/Project/ProjectList.vue b/src/components/TestPlatform/Project/ProjectList.vue
index 0c3a4cb..1024e5c 100644
--- a/src/components/TestPlatform/Project/ProjectList.vue
+++ b/src/components/TestPlatform/Project/ProjectList.vue
@@ -148,7 +148,7 @@ export default {
this.fetchList()
},
goSettings(row) {
- this.$router.push({ path: '/test-platform/projects/settings', query: { projectId: row.id } })
+ this.$router.push({ path: '/test-platform/project/setting', query: { projectId: row.id } })
},
fetchProductOptions() {
return getProductList({
diff --git a/src/components/TestPlatform/Project/ProjectSettings.vue b/src/components/TestPlatform/Project/ProjectSettings.vue
index 16d8399..e24b07e 100644
--- a/src/components/TestPlatform/Project/ProjectSettings.vue
+++ b/src/components/TestPlatform/Project/ProjectSettings.vue
@@ -1,15 +1,19 @@
-
+
+ 返回
+
+
新增成员
-
-
-
+
+
+
+
+
+
+
+
+ {{ scope.row.hook_type_name || hookTypeLabel(scope.row.hook_type) }}
+
+
+ {{ scope.row.webhook_url || scope.row.webhookUrl || '-' }}
+
+
+
+
+ {{ (scope.row.enabled === 1 || scope.row.enabled === true) ? '启用' : '禁用' }}
+
+
+
+
+
+ {{ scope.row.created_time || scope.row.createdTime || '-' }}
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+ 加载中...
+ 加载更多
+
+
@@ -79,17 +151,62 @@
确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
@@ -278,4 +671,16 @@ export default {
margin-bottom: 16px;
text-align: right;
}
+
+.hook-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.hook-toolbar-left {
+ text-align: left;
+}
diff --git a/src/components/TestPlatform/Report/ReportList.vue b/src/components/TestPlatform/Report/ReportList.vue
index 279bc24..9041977 100644
--- a/src/components/TestPlatform/Report/ReportList.vue
+++ b/src/components/TestPlatform/Report/ReportList.vue
@@ -4,24 +4,76 @@
生成报告
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
- 查询
+ 查询
+
+
+ 重置
-
-
+
+ {{ formatPlanName(scope.row) }}
+
+
+ {{ formatReportType(scope.row.report_type || scope.row.type) }}
+
+
+ {{ formatDateTime(scope.row.generated_time || scope.row.generated_at || scope.row.created_at || scope.row.create_time) }}
+
- 查看
+ 查看链接
@@ -42,7 +94,15 @@
diff --git a/src/components/TestPlatform/Report/ReportViewer.vue b/src/components/TestPlatform/Report/ReportViewer.vue
index 0adb7c5..dda0e8e 100644
--- a/src/components/TestPlatform/Report/ReportViewer.vue
+++ b/src/components/TestPlatform/Report/ReportViewer.vue
@@ -1,6 +1,9 @@
+
+ 返回
+
{
+ getReportDetail(this.reportId, this.projectId).then(res => {
this.report = (res && res.data) || res || {}
}).catch(() => {
this.report = {}
})
+ },
+ goBack() {
+ this.$router.push({
+ path: '/test-platform/report',
+ query: {
+ projectId: this.projectId || undefined
+ }
+ })
}
},
created() {
diff --git a/src/components/User/Login.vue b/src/components/User/Login.vue
index e7d49a2..26c11cc 100644
--- a/src/components/User/Login.vue
+++ b/src/components/User/Login.vue
@@ -49,7 +49,7 @@