|
|
@@ -9,7 +9,16 @@
|
|
|
v-model="queryForm.taskTitle"
|
|
|
class="query-input query-input--title"
|
|
|
clearable
|
|
|
- placeholder="请输入" />
|
|
|
+ placeholder="请输入"
|
|
|
+ @keyup.enter.native="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="任务编号">
|
|
|
+ <el-input
|
|
|
+ v-model="queryForm.taskNo"
|
|
|
+ class="query-input query-input--title"
|
|
|
+ clearable
|
|
|
+ placeholder="请输入"
|
|
|
+ @keyup.enter.native="handleSearch" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="任务类型">
|
|
|
<el-select
|
|
|
@@ -68,6 +77,9 @@
|
|
|
</el-button>
|
|
|
<el-divider direction="vertical" />
|
|
|
<el-button icon="el-icon-plus" type="success" @click="handleAdd">新增</el-button>
|
|
|
+ <el-button icon="el-icon-download" size="small" style="margin-left: 8px" type="warning" @click="handleExport">
|
|
|
+ 导出
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -258,6 +270,16 @@
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ v-if="isColumnVisible('projectName')"
|
|
|
+ label="项目名称"
|
|
|
+ :render-header="renderSortableHeader('项目名称', 'projectName')"
|
|
|
+ show-overflow-tooltip
|
|
|
+ width="180">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.projectName || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<el-table-column
|
|
|
v-if="isColumnVisible('functionName')"
|
|
|
label="功能模块"
|
|
|
@@ -379,11 +401,6 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column v-if="isColumnVisible('projectName')" label="项目名称" show-overflow-tooltip width="180">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- {{ row.projectName || '-' }}
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
<el-table-column
|
|
|
v-if="isColumnVisible('createdTime')"
|
|
|
label="创建时间"
|
|
|
@@ -402,23 +419,48 @@
|
|
|
<template slot="header">
|
|
|
<div class="table-operation-header">
|
|
|
<span>操作</span>
|
|
|
- <el-popover placement="bottom-end" trigger="click" width="320">
|
|
|
+ <el-popover placement="bottom-end" trigger="click" width="360">
|
|
|
<div class="column-setting-panel">
|
|
|
- <div class="column-setting-header">
|
|
|
- <span class="column-setting-title">列设置</span>
|
|
|
- <div class="column-setting-actions">
|
|
|
- <el-button size="mini" type="text" @click="selectAllColumns">全选</el-button>
|
|
|
- <el-button size="mini" type="text" @click="resetVisibleColumns">重置</el-button>
|
|
|
+ <!-- 字段排序区域 -->
|
|
|
+ <div v-if="sortFields.length > 0" class="column-sort-section">
|
|
|
+ <div class="column-setting-header">
|
|
|
+ <span class="column-setting-title">当前排序</span>
|
|
|
+ <el-button size="mini" type="text" @click="clearAllSorts">清除全部</el-button>
|
|
|
</div>
|
|
|
+ <div class="sort-list">
|
|
|
+ <div v-for="(sf, idx) in sortFields" :key="sf.field" class="sort-item">
|
|
|
+ <span class="sort-item-index">{{ idx + 1 }}</span>
|
|
|
+ <span class="sort-item-label">{{ getColumnSortLabel(sf.field) }}</span>
|
|
|
+ <span :class="['sort-item-order', sf.order]">
|
|
|
+ {{ sf.order === 'asc' ? '升序' : '降序' }}
|
|
|
+ </span>
|
|
|
+ <el-button
|
|
|
+ class="sort-item-remove"
|
|
|
+ icon="el-icon-close"
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ @click="removeSortField(sf.field)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 列显示设置区域 -->
|
|
|
+ <div class="column-display-section">
|
|
|
+ <div class="column-setting-header">
|
|
|
+ <span class="column-setting-title">列设置</span>
|
|
|
+ <div class="column-setting-actions">
|
|
|
+ <el-button size="mini" type="text" @click="selectAllColumns">全选</el-button>
|
|
|
+ <el-button size="mini" type="text" @click="resetVisibleColumns">重置</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-checkbox-group
|
|
|
+ v-model="visibleColumnKeys"
|
|
|
+ class="column-setting-list"
|
|
|
+ @change="handleVisibleColumnsChange">
|
|
|
+ <el-checkbox v-for="column in columnOptions" :key="column.key" :label="column.key">
|
|
|
+ {{ column.label }}
|
|
|
+ </el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
</div>
|
|
|
- <el-checkbox-group
|
|
|
- v-model="visibleColumnKeys"
|
|
|
- class="column-setting-list"
|
|
|
- @change="handleVisibleColumnsChange">
|
|
|
- <el-checkbox v-for="column in columnOptions" :key="column.key" :label="column.key">
|
|
|
- {{ column.label }}
|
|
|
- </el-checkbox>
|
|
|
- </el-checkbox-group>
|
|
|
</div>
|
|
|
<el-tooltip slot="reference" content="列设置" placement="top">
|
|
|
<el-button class="table-column-setting-btn" icon="el-icon-setting" type="text" @click.stop />
|
|
|
@@ -565,6 +607,7 @@
|
|
|
const TABLE_COLUMN_OPTIONS = [
|
|
|
{ key: 'taskNo', label: '任务编号' },
|
|
|
{ key: 'taskTitle', label: '任务标题' },
|
|
|
+ { key: 'projectName', label: '项目名称' },
|
|
|
{ key: 'functionName', label: '功能模块' },
|
|
|
{ key: 'taskType', label: '任务类型' },
|
|
|
{ key: 'taskStatus', label: '任务状态' },
|
|
|
@@ -577,7 +620,6 @@
|
|
|
{ key: 'defectType', label: '缺陷类型' },
|
|
|
{ key: 'attribute2', label: '历史遗留' },
|
|
|
{ key: 'releaseVersion', label: '发布版本' },
|
|
|
- { key: 'projectName', label: '项目名称' },
|
|
|
{ key: 'createdTime', label: '创建时间' },
|
|
|
{ key: 'createdName', label: '创建人' },
|
|
|
]
|
|
|
@@ -601,6 +643,7 @@
|
|
|
pageNum: 1,
|
|
|
pageSize: 20,
|
|
|
projectId: '',
|
|
|
+ taskNo: '',
|
|
|
taskTitle: '',
|
|
|
taskType: [],
|
|
|
taskStatus: [],
|
|
|
@@ -934,6 +977,7 @@
|
|
|
pageNum: this.queryForm.pageNum,
|
|
|
pageSize: this.queryForm.pageSize,
|
|
|
projectId: this.selectedProject ? parseInt(this.selectedProject) : 0,
|
|
|
+ taskNo: this.queryForm.taskNo,
|
|
|
taskTitle: this.queryForm.taskTitle,
|
|
|
taskType: this.queryForm.taskType,
|
|
|
taskStatus: this.queryForm.taskStatus,
|
|
|
@@ -1010,12 +1054,67 @@
|
|
|
this.queryForm.pageNum = 1
|
|
|
this.fetchData()
|
|
|
},
|
|
|
+ // 导出数据
|
|
|
+ async handleExport() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ projectId: this.selectedProject ? parseInt(this.selectedProject) : 0,
|
|
|
+ taskNo: this.queryForm.taskNo,
|
|
|
+ taskTitle: this.queryForm.taskTitle,
|
|
|
+ taskType: this.queryForm.taskType,
|
|
|
+ taskStatus: this.queryForm.taskStatus,
|
|
|
+ opsUserName: this.queryForm.opsUserName,
|
|
|
+ planStartDateStart: this.queryForm.planStartDateRange?.[0] || '',
|
|
|
+ planStartDateEnd: this.queryForm.planStartDateRange?.[1] || '',
|
|
|
+ planEndDateStart: this.queryForm.planEndDateRange?.[0] || '',
|
|
|
+ planEndDateEnd: this.queryForm.planEndDateRange?.[1] || '',
|
|
|
+ createdTimeStart: this.queryForm.createdTimeRange?.[0] || '',
|
|
|
+ createdTimeEnd: this.queryForm.createdTimeRange?.[1] || '',
|
|
|
+ completeTimeStart: this.queryForm.completeTimeRange?.[0] || '',
|
|
|
+ completeTimeEnd: this.queryForm.completeTimeRange?.[1] || '',
|
|
|
+ scheduleStatus: this.queryForm.scheduleStatus || '',
|
|
|
+ }
|
|
|
+ if (this.queryForm.productLine && !this.selectedProject) {
|
|
|
+ params.productLine = this.queryForm.productLine
|
|
|
+ }
|
|
|
+ const res = await opsEventTaskApi.export(params)
|
|
|
+ if (res.data?.content) {
|
|
|
+ const binaryString = window.atob(res.data.content)
|
|
|
+ const len = binaryString.length
|
|
|
+ const bytes = new Uint8Array(len)
|
|
|
+ for (let i = 0; i < len; i++) {
|
|
|
+ bytes[i] = binaryString.charCodeAt(i)
|
|
|
+ }
|
|
|
+ const blob = new Blob([bytes], {
|
|
|
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
+ })
|
|
|
+ const link = document.createElement('a')
|
|
|
+ const url = window.URL.createObjectURL(blob)
|
|
|
+ link.href = url
|
|
|
+ link.download = `软件交付任务_${new Date().getTime()}.xlsx`
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ window.URL.revokeObjectURL(url)
|
|
|
+ this.$message.success('导出成功')
|
|
|
+ } else {
|
|
|
+ this.$message.warning('没有可导出的数据')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('导出失败')
|
|
|
+ console.error(error)
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
// 重置
|
|
|
handleReset() {
|
|
|
this.queryForm = {
|
|
|
pageNum: 1,
|
|
|
pageSize: 20,
|
|
|
projectId: '',
|
|
|
+ taskNo: '',
|
|
|
taskTitle: '',
|
|
|
taskType: [],
|
|
|
taskStatus: [],
|
|
|
@@ -1066,6 +1165,22 @@
|
|
|
const option = this.columnOptions.find((c) => c.key === field)
|
|
|
return option ? option.label : field
|
|
|
},
|
|
|
+ // 清除所有排序
|
|
|
+ clearAllSorts() {
|
|
|
+ this.sortFields = []
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$refs.taskTable && this.$refs.taskTable.clearSort()
|
|
|
+ })
|
|
|
+ this.fetchData()
|
|
|
+ },
|
|
|
+ // 移除单个排序字段
|
|
|
+ removeSortField(field) {
|
|
|
+ const idx = this.sortFields.findIndex((s) => s.field === field)
|
|
|
+ if (idx !== -1) {
|
|
|
+ this.sortFields.splice(idx, 1)
|
|
|
+ this.fetchData()
|
|
|
+ }
|
|
|
+ },
|
|
|
renderSortableHeader(label, field) {
|
|
|
const vm = this
|
|
|
return function (h) {
|
|
|
@@ -1265,9 +1380,7 @@
|
|
|
// 优先使用 res.data.data(某些接口包装在data中),否则直接使用res.data
|
|
|
const taskData = res.data.data || res.data
|
|
|
this.detailData = this.normalizeTaskDetail(taskData)
|
|
|
- this.detailDialogMode = ['30', '90'].includes(String(taskData.taskStatus || taskData.task_status))
|
|
|
- ? 'view'
|
|
|
- : 'edit'
|
|
|
+ this.detailDialogMode = 'view'
|
|
|
this.detailDialogVisible = true
|
|
|
} else {
|
|
|
this.$message.error('获取任务详情失败')
|
|
|
@@ -1424,10 +1537,9 @@
|
|
|
try {
|
|
|
const res = await opsEventTaskApi.getById(row.id)
|
|
|
if (res.code === 200) {
|
|
|
- this.detailData = this.normalizeTaskDetail({
|
|
|
- ...row,
|
|
|
- ...(res.data || {}),
|
|
|
- })
|
|
|
+ // 优先使用 res.data.data(某些接口包装在data中),否则直接使用res.data
|
|
|
+ const taskData = res.data.data || res.data
|
|
|
+ this.detailData = this.normalizeTaskDetail(taskData)
|
|
|
this.detailDialogMode = 'schedule'
|
|
|
this.detailDialogVisible = true
|
|
|
}
|
|
|
@@ -1535,14 +1647,42 @@
|
|
|
// 复制文本到剪贴板
|
|
|
handleCopy(text) {
|
|
|
if (!text) return
|
|
|
- navigator.clipboard
|
|
|
- .writeText(text)
|
|
|
- .then(() => {
|
|
|
+ // 优先使用 navigator.clipboard API(需要 HTTPS 或 localhost)
|
|
|
+ if (navigator.clipboard && window.isSecureContext) {
|
|
|
+ navigator.clipboard
|
|
|
+ .writeText(text)
|
|
|
+ .then(() => {
|
|
|
+ this.$message.success('已复制: ' + text)
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.copyWithFallback(text)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 非 HTTPS 环境使用 fallback 方案
|
|
|
+ this.copyWithFallback(text)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // Fallback 复制方法(兼容非 HTTPS 环境)
|
|
|
+ copyWithFallback(text) {
|
|
|
+ const textarea = document.createElement('textarea')
|
|
|
+ textarea.value = text
|
|
|
+ textarea.style.position = 'fixed'
|
|
|
+ textarea.style.left = '-9999px'
|
|
|
+ textarea.style.top = '-9999px'
|
|
|
+ document.body.appendChild(textarea)
|
|
|
+ textarea.focus()
|
|
|
+ textarea.select()
|
|
|
+ try {
|
|
|
+ const successful = document.execCommand('copy')
|
|
|
+ if (successful) {
|
|
|
this.$message.success('已复制: ' + text)
|
|
|
- })
|
|
|
- .catch(() => {
|
|
|
+ } else {
|
|
|
this.$message.error('复制失败')
|
|
|
- })
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ this.$message.error('复制失败')
|
|
|
+ }
|
|
|
+ document.body.removeChild(textarea)
|
|
|
},
|
|
|
},
|
|
|
}
|
|
|
@@ -1641,6 +1781,16 @@
|
|
|
}
|
|
|
|
|
|
.column-setting-panel {
|
|
|
+ .column-sort-section {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .column-display-section {
|
|
|
+ // 列显示区域
|
|
|
+ }
|
|
|
+
|
|
|
.column-setting-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -1659,6 +1809,70 @@
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
+
|
|
|
+ .sort-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 6px 10px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-item-index {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #409eff;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-item-label {
|
|
|
+ flex: 1;
|
|
|
+ color: #303133;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-item-order {
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-size: 11px;
|
|
|
+
|
|
|
+ &.asc {
|
|
|
+ background: #e1f3d8;
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.desc {
|
|
|
+ background: #fef0f0;
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-item-remove {
|
|
|
+ padding: 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.column-setting-list {
|