Files
effekt-interface-frontend/src/components/TestPlatform/Plan/PlanBuilder.vue

380 lines
13 KiB
Vue

<template>
<div class="page-wrap">
<page-section :title="isEditMode ? '编辑计划' : '计划构建'">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small">
<el-form-item label="产品名称" prop="productId">
<el-select
v-model="form.productId"
filterable
clearable
placeholder="请选择产品"
style="width: 360px;"
@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="项目名称" prop="projectId">
<el-select
v-model="form.projectId"
filterable
clearable
placeholder="请选择项目"
style="width: 360px;"
:disabled="!form.productId"
@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-item label="计划名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input v-model="form.version"></el-input>
</el-form-item>
<el-form-item label="负责人" prop="owner_id">
<el-select
v-model="form.owner_id"
filterable
clearable
placeholder="请选择负责人"
style="width: 360px;"
:disabled="!form.projectId">
<el-option
v-for="item in ownerOptions"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="环境名称" prop="environment_id">
<el-select
v-model="form.environment_id"
filterable
clearable
placeholder="请选择环境"
style="width: 360px;"
:disabled="!form.projectId">
<el-option
v-for="item in environmentOptions"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="start_time">
<el-date-picker
v-model="form.start_time"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择开始时间"
style="width: 360px;">
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="end_time">
<el-date-picker
v-model="form.end_time"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择结束时间"
style="width: 360px;">
</el-date-picker>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="goBack">返回</el-button>
<el-button type="primary" :loading="saving" @click="submitForm">保存计划</el-button>
</el-form-item>
</el-form>
</page-section>
</div>
</template>
<script>
import PageSection from '@/components/TestPlatform/common/PageSection'
import { createPlan, getPlanDetail, updatePlan } from '@/api/planApi'
import { getProductList } from '@/api/productApi'
import { getProjectDetail, getProjectEnvironments, getProjectList, getProjectMembers } from '@/api/projectApi'
export default {
name: 'PlanBuilder',
components: {PageSection},
data() {
return {
saving: false,
planId: this.$route.query.planId || '',
productOptions: [],
projectOptions: [],
ownerOptions: [],
environmentOptions: [],
form: {
productId: '',
projectId: this.$route.query.projectId ? Number(this.$route.query.projectId) : '',
name: '',
version: '',
owner_id: '',
environment_id: '',
start_time: '',
end_time: '',
description: ''
},
rules: {
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
version: [{ required: true, message: '请输入版本', trigger: 'blur' }],
owner_id: [{ required: true, message: '请选择负责人', trigger: 'change' }],
environment_id: [{ required: true, message: '请选择环境', trigger: 'change' }],
start_time: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
end_time: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
}
}
},
computed: {
isEditMode() {
return !!this.planId
}
},
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 = []
})
},
loadProjectMeta(projectId) {
if (!projectId) {
this.ownerOptions = []
this.environmentOptions = []
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 || []
this.ownerOptions = (Array.isArray(list) ? list : []).map(item => ({
id: item.user_id || item.userId || item.id,
name:
item.real_name ||
item.realName ||
item.username ||
item.name ||
item.user_name ||
`用户${item.user_id || item.id}`
})).filter(item => item.id !== undefined && item.id !== null && item.id !== '')
}).catch(() => {
this.ownerOptions = []
})
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.environmentOptions = (Array.isArray(list) ? list : []).map(item => ({
id: item.id,
name: item.name
}))
}).catch(() => {
this.environmentOptions = []
})
return Promise.all([memberReq, envReq])
},
handleProductChange() {
this.form.projectId = ''
this.form.owner_id = ''
this.form.environment_id = ''
this.projectOptions = []
this.ownerOptions = []
this.environmentOptions = []
this.loadProjectOptionsByProduct(this.form.productId)
},
handleProjectChange() {
this.form.owner_id = ''
this.form.environment_id = ''
this.ownerOptions = []
this.environmentOptions = []
this.loadProjectMeta(this.form.projectId)
},
goBack() {
this.$router.push({
path: '/test-platform/plan',
query: {
productId: this.form.productId || undefined,
projectId: this.form.projectId || undefined
}
})
},
submitForm() {
this.$refs.formRef.validate(valid => {
if (!valid) {
return
}
const projectId = this.form.projectId
const payload = {
name: this.form.name,
version: this.form.version,
owner_id: this.form.owner_id,
environment_id: this.form.environment_id,
start_time: this.form.start_time,
end_time: this.form.end_time,
description: this.form.description
}
this.saving = true
const request = this.isEditMode
? updatePlan(projectId, this.planId, payload)
: createPlan(projectId, payload)
request.then(() => {
this.$message({ type: 'success', message: this.isEditMode ? '计划更新成功' : '计划创建成功' })
this.$router.push({
path: '/test-platform/plan',
query: {
productId: this.form.productId || undefined,
projectId: projectId || undefined
}
})
}).finally(() => {
this.saving = false
})
})
},
/** 与 PlanList 列表行一致,兼容详情接口多种时间字段名 */
pickPlanDetailStartRaw(plan) {
if (!plan) return ''
return (
plan.start_date ||
plan.startDate ||
plan.start_time ||
plan.startTime ||
plan.begin_time ||
plan.beginTime ||
plan.planned_start_time ||
plan.plannedStartTime ||
''
)
},
pickPlanDetailEndRaw(plan) {
if (!plan) return ''
return (
plan.end_date ||
plan.endDate ||
plan.end_time ||
plan.endTime ||
plan.finish_time ||
plan.finishTime ||
plan.planned_end_time ||
plan.plannedEndTime ||
''
)
},
/** 将接口返回的时间戳 / ISO 串等转为 date-picker 的 value-format 字符串 */
toDatePickerValue(value) {
if (value === undefined || value === null || value === '') return ''
if (typeof value === 'number' || (typeof value === 'string' && /^\d+$/.test(value.trim()))) {
const raw = Number(value)
if (Number.isNaN(raw) || raw <= 0) return ''
const ms = raw < 1000000000000 ? raw * 1000 : raw
const d = new Date(ms)
if (Number.isNaN(d.getTime())) return ''
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
const s = String(value).trim()
if (!s) return ''
let normalized = s
if (s.includes('T')) {
normalized = s.replace('T', ' ').replace(/\.\d+/, '').replace(/Z$/i, '').trim()
}
if (normalized.length >= 19) {
return normalized.slice(0, 19)
}
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
return `${normalized} 00:00:00`
}
const parsed = new Date(s)
if (!Number.isNaN(parsed.getTime())) {
const pad = n => String(n).padStart(2, '0')
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}:${pad(parsed.getSeconds())}`
}
return normalized
},
unwrapPlanDetailPayload(res) {
const raw = (res && res.data) || res || {}
const inner = raw.plan || raw.detail
if (inner && typeof inner === 'object') {
return Object.assign({}, raw, inner)
}
return raw
},
loadPlanDetail() {
if (!this.isEditMode || !this.form.projectId) {
return Promise.resolve()
}
return getPlanDetail(this.form.projectId, this.planId).then(res => {
const data = this.unwrapPlanDetailPayload(res)
this.form.name = data.name || ''
this.form.version = data.version || ''
this.form.owner_id = data.owner_id || data.ownerId || ''
this.form.environment_id = data.environment_id || data.environmentId || ''
this.form.start_time = this.toDatePickerValue(this.pickPlanDetailStartRaw(data))
this.form.end_time = this.toDatePickerValue(this.pickPlanDetailEndRaw(data))
this.form.description = data.description || ''
}).catch(() => {})
},
initByRouteProject() {
if (!this.form.projectId) {
return Promise.resolve()
}
return getProjectDetail(this.form.projectId).then(res => {
const data = res && res.data ? res.data : res || {}
const productId = data.productId || data.product_id || ''
if (productId) {
this.form.productId = productId
return this.loadProjectOptionsByProduct(productId)
}
}).catch(() => {})
}
},
created() {
this.loadProductOptions().then(() => this.initByRouteProject()).finally(() => {
if (this.form.projectId) {
this.loadProjectMeta(this.form.projectId).then(() => {
this.loadPlanDetail()
})
}
})
}
}
</script>
<style scoped>
.page-wrap {
padding: 20px;
}
</style>