实现流程项目逻辑删除与恢复

This commit is contained in:
wkc
2026-07-02 10:54:36 +08:00
parent 2f53fc4d1e
commit 57a33098c9
27 changed files with 690 additions and 97 deletions

View File

@@ -52,6 +52,14 @@ export function delProject(projectIds) {
})
}
// 恢复已删除初核项目
export function restoreProject(projectId) {
return request({
url: '/ccdi/project/' + projectId + '/restore',
method: 'post'
})
}
// 导出初核项目
export function exportProject(query) {
return request({

View File

@@ -92,23 +92,66 @@
<el-table-column
label="操作"
width="350"
width="390"
align="left"
fixed="right"
>
<template slot-scope="scope">
<el-button
v-if="['0', '3', '4'].includes(scope.row.status)"
size="mini"
type="text"
:icon="canOperate(scope.row) ? 'el-icon-right' : 'el-icon-view'"
@click="handleEnter(scope.row)"
>
{{ canOperate(scope.row) ? "进入项目" : "查看项目" }}
</el-button>
<template v-if="scope.row.status === '1'">
<template v-if="deletedList">
<el-button
size="mini"
type="text"
icon="el-icon-refresh-left"
@click="handleRestore(scope.row)"
>
恢复
</el-button>
</template>
<template v-else>
<el-button
v-if="['0', '3', '4'].includes(scope.row.status)"
size="mini"
type="text"
:icon="canOperate(scope.row) ? 'el-icon-right' : 'el-icon-view'"
@click="handleEnter(scope.row)"
>
{{ canOperate(scope.row) ? "进入项目" : "查看项目" }}
</el-button>
<template v-if="scope.row.status === '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>
查看结果
</el-button>
<el-button
v-if="canOperate(scope.row)"
size="mini"
type="text"
icon="el-icon-refresh"
:loading="reAnalyzeLoadingMap[String(scope.row.projectId)]"
:disabled="reAnalyzeLoadingMap[String(scope.row.projectId)]"
@click="handleReAnalyze(scope.row)"
>
重新分析
</el-button>
<el-button
v-if="canOperate(scope.row)"
size="mini"
type="text"
icon="el-icon-folder"
@click="handleArchive(scope.row)"
>
归档
</el-button>
</template>
<el-button
v-if="scope.row.status === '2'"
size="mini"
type="text"
icon="el-icon-view"
@@ -116,37 +159,18 @@
>
查看结果
</el-button>
<el-button
v-if="canOperate(scope.row)"
v-if="canDelete(scope.row) && scope.row.status !== '5'"
size="mini"
type="text"
icon="el-icon-refresh"
:loading="reAnalyzeLoadingMap[String(scope.row.projectId)]"
:disabled="reAnalyzeLoadingMap[String(scope.row.projectId)]"
@click="handleReAnalyze(scope.row)"
class="delete-button"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>
重新分析
</el-button>
<el-button
v-if="canOperate(scope.row)"
size="mini"
type="text"
icon="el-icon-folder"
@click="handleArchive(scope.row)"
>
归档
删除
</el-button>
</template>
<el-button
v-if="scope.row.status === '2'"
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewResult(scope.row)"
>
查看结果
</el-button>
</template>
</el-table-column>
</el-table>
@@ -193,6 +217,10 @@ export default {
type: Object,
default: () => ({}),
},
deletedList: {
type: Boolean,
default: false,
},
},
methods: {
getStatusColor(status) {
@@ -202,6 +230,7 @@ export default {
2: "#8c8c8c",
3: "#fa8c16",
4: "#f56c6c",
5: "#f56c6c",
};
return colorMap[status] || "#8c8c8c";
},
@@ -230,9 +259,18 @@ export default {
handleArchive(row) {
this.$emit("archive", row);
},
handleDelete(row) {
this.$emit("delete", row);
},
handleRestore(row) {
this.$emit("restore", row);
},
canOperate(row) {
return !row || row.canOperate !== false;
},
canDelete(row) {
return !row || row.canDelete !== false;
},
handleSizeChange(val) {
this.$emit("pagination", {
pageNum: this.pageParams.pageNum,
@@ -367,6 +405,15 @@ export default {
}
}
::v-deep .el-button--text.delete-button {
color: #f56c6c;
&:hover {
color: #dd6161;
background-color: rgba(245, 108, 108, 0.08);
}
}
::v-deep .el-pagination {
margin-top: 24px;
text-align: right;

View File

@@ -14,7 +14,7 @@
</div>
<div class="tab-filters">
<div
v-for="tab in tabs"
v-for="tab in visibleTabs"
:key="tab.value"
:class="['tab-item', { active: activeTab === tab.value }]"
@click="handleTabChange(tab.value)"
@@ -41,8 +41,13 @@ export default {
'1': 0,
'2': 0,
'3': 0,
'4': 0
'4': 0,
'5': 0
})
},
showDeletedTab: {
type: Boolean,
default: false
}
},
data() {
@@ -55,20 +60,32 @@ export default {
{ label: '已完成', value: '1', count: 0 },
{ label: '已归档', value: '2', count: 0 },
{ label: '打标中', value: '3', count: 0 },
{ label: '打标失败', value: '4', count: 0 }
{ label: '打标失败', value: '4', count: 0 },
{ label: '已删除', value: 'deleted', count: 0 }
]
}
},
computed: {
visibleTabs() {
return this.tabs.filter(tab => tab.value !== 'deleted' || this.showDeletedTab)
}
},
watch: {
tabCounts: {
handler(newVal) {
this.tabs = this.tabs.map(tab => ({
...tab,
count: newVal[tab.value] || 0
count: newVal[tab.value === 'deleted' ? '5' : tab.value] || 0
}))
},
immediate: true,
deep: true
},
showDeletedTab(newVal) {
if (!newVal && this.activeTab === 'deleted') {
this.activeTab = 'all'
this.emitQuery()
}
}
},
methods: {
@@ -83,9 +100,11 @@ export default {
},
/** 发送查询 */
emitQuery() {
const includeDeleted = this.activeTab === 'deleted'
this.$emit('query', {
projectName: this.searchKeyword || null,
status: this.activeTab === 'all' ? null : this.activeTab
status: includeDeleted || this.activeTab === 'all' ? null : this.activeTab,
includeDeleted
})
}
}

View File

@@ -10,6 +10,7 @@
<search-bar
:show-search="showSearch"
:tab-counts="tabCounts"
:show-deleted-tab="isProjectAdmin"
@query="handleQuery"
/>
@@ -20,11 +21,14 @@
:total="total"
:page-params="queryParams"
:re-analyze-loading-map="reAnalyzeLoadingMap"
:deleted-list="queryParams.includeDeleted === true"
@pagination="handlePagination"
@enter="handleEnter"
@view-result="handleViewResult"
@re-analyze="handleReAnalyze"
@archive="handleArchive"
@delete="handleDelete"
@restore="handleRestore"
/>
<!-- 快捷入口区 -->
@@ -63,7 +67,7 @@
</template>
<script>
import {archiveProject, getStatusCounts, listProject, rebuildProjectTags} from '@/api/ccdiProject'
import {archiveProject, delProject, getStatusCounts, listProject, rebuildProjectTags, restoreProject} from '@/api/ccdiProject'
import SearchBar from './components/SearchBar'
import ProjectTable from './components/ProjectTable'
import QuickEntry from './components/QuickEntry'
@@ -96,7 +100,8 @@ export default {
pageNum: 1,
pageSize: 10,
projectName: null,
status: null
status: null,
includeDeleted: false
},
// 标签页数量统计
tabCounts: {
@@ -105,7 +110,8 @@ export default {
'1': 0,
'2': 0,
'3': 0,
'4': 0
'4': 0,
'5': 0
},
// 新增/编辑弹窗
addDialogVisible: false,
@@ -123,6 +129,12 @@ export default {
created() {
this.getList();
},
computed: {
isProjectAdmin() {
const roles = this.$store && this.$store.getters ? this.$store.getters.roles || [] : []
return roles.includes("admin") || roles.includes("manager")
}
},
methods: {
/** 查询项目列表 */
getList() {
@@ -145,7 +157,8 @@ export default {
'1': counts.status1 || 0,
'2': counts.status2 || 0,
'3': counts.status3 || 0,
'4': counts.status4 || 0
'4': counts.status4 || 0,
'5': counts.status5 || 0
}
this.loading = false
@@ -157,10 +170,21 @@ export default {
},
/** 搜索按钮操作 */
handleQuery(queryParams) {
const nextQueryParams = { ...this.queryParams }
if (queryParams) {
this.queryParams = { ...this.queryParams, ...queryParams };
Object.assign(nextQueryParams, queryParams)
}
this.queryParams.pageNum = 1;
if (nextQueryParams.includeDeleted && !this.isProjectAdmin) {
nextQueryParams.includeDeleted = false
nextQueryParams.status = null
}
if (nextQueryParams.includeDeleted) {
nextQueryParams.status = null
} else {
nextQueryParams.includeDeleted = false
}
nextQueryParams.pageNum = 1
this.queryParams = nextQueryParams
this.getList();
},
/** 分页事件处理 */
@@ -312,9 +336,60 @@ export default {
this.$modal.msgError(message)
}
},
/** 删除项目 */
async handleDelete(row) {
if (!this.canDelete(row)) {
this.$modal.msgWarning("当前项目不能删除")
return
}
try {
await this.$modal.confirm(
`确认删除项目“${row.projectName}”吗?删除后项目进入已删除列表,项目内数据不会删除。`
)
} catch (confirmError) {
if (confirmError === "cancel" || confirmError === "close") {
return
}
throw confirmError
}
try {
await delProject(row.projectId)
this.$modal.msgSuccess("项目删除成功")
this.getList()
} catch (error) {
const message = error && error.message ? error.message : "项目删除失败,请稍后重试"
this.$modal.msgError(message)
}
},
/** 恢复已删除项目 */
async handleRestore(row) {
if (!this.isProjectAdmin) {
this.$modal.msgWarning("当前用户不能恢复项目")
return
}
try {
await this.$modal.confirm(`确认恢复项目“${row.projectName}”吗?项目将恢复为已完成状态。`)
} catch (confirmError) {
if (confirmError === "cancel" || confirmError === "close") {
return
}
throw confirmError
}
try {
await restoreProject(row.projectId)
this.$modal.msgSuccess("项目恢复成功")
this.getList()
} catch (error) {
const message = error && error.message ? error.message : "项目恢复失败,请稍后重试"
this.$modal.msgError(message)
}
},
canOperate(row) {
return !row || row.canOperate !== false
},
canDelete(row) {
return !row || row.canDelete !== false
},
},
};
</script>

View File

@@ -10,6 +10,46 @@ const dialogPath = path.resolve(
const pageSource = fs.readFileSync(pagePath, "utf8");
const dialogSource = fs.readFileSync(dialogPath, "utf8");
assert(
pageSource.includes("delProject") && pageSource.includes("restoreProject"),
"项目列表页应引入删除和恢复接口"
);
assert(
pageSource.includes("includeDeleted: false"),
"默认查询参数应不包含已删除项目"
);
assert(
pageSource.includes(':show-deleted-tab="isProjectAdmin"'),
"已删除入口应仅对项目管理员角色展示"
);
assert(
pageSource.includes("'5': counts.status5 || 0"),
"状态统计应接入已删除数量"
);
assert(
pageSource.includes('await delProject(row.projectId)'),
"删除确认后应调用项目删除接口"
);
assert(
pageSource.includes('await restoreProject(row.projectId)'),
"恢复确认后应调用项目恢复接口"
);
assert(
pageSource.includes("项目内数据不会删除"),
"删除确认文案应明确项目内数据不会删除"
);
assert(
pageSource.includes("项目将恢复为已完成状态"),
"恢复确认文案应明确恢复到已完成状态"
);
assert(
pageSource.includes("await archiveProject(data.projectId)"),
"确认归档后应调用真实归档接口"

View File

@@ -6,6 +6,48 @@ const pagePath = path.resolve(__dirname, "../../src/views/ccdiProject/index.vue"
const tablePath = path.resolve(__dirname, "../../src/views/ccdiProject/components/ProjectTable.vue");
const pageSource = fs.readFileSync(pagePath, "utf8");
const tableSource = fs.readFileSync(tablePath, "utf8");
const searchBarPath = path.resolve(__dirname, "../../src/views/ccdiProject/components/SearchBar.vue");
const searchBarSource = fs.readFileSync(searchBarPath, "utf8");
assert(
searchBarSource.includes("{ label: '已删除', value: 'deleted'"),
"搜索条应提供独立的已删除列表入口"
);
assert(
searchBarSource.includes("tab.value !== 'deleted' || this.showDeletedTab"),
"已删除入口应由 showDeletedTab 控制可见性"
);
assert(
searchBarSource.includes("const includeDeleted = this.activeTab === 'deleted'"),
"切换已删除入口时应生成 includeDeleted 查询态"
);
assert(
searchBarSource.includes("status: includeDeleted || this.activeTab === 'all' ? null : this.activeTab"),
"已删除列表不应和普通状态 tab 混用"
);
assert(
tableSource.includes('v-if="deletedList"'),
"删除列表应使用独立操作区"
);
assert(
tableSource.includes('@click="handleRestore(scope.row)"'),
"删除列表应提供恢复按钮"
);
assert(
tableSource.includes('v-if="canDelete(scope.row) && scope.row.status !== \'5\'"'),
"普通列表应按 canDelete 展示删除按钮且排除已删除状态"
);
assert(
/<template v-if="deletedList">[\s\S]*?handleRestore\(scope\.row\)[\s\S]*?<\/template>\s*<template v-else>[\s\S]*?handleViewResult/.test(tableSource),
"删除列表只展示恢复动作,进入项目和查看结果应留在普通列表分支"
);
assert(
pageSource.includes("rebuildProjectTags({ projectId: row.projectId })"),

View File

@@ -23,5 +23,14 @@ assert(
!source.includes("::v-deep .el-table"),
"项目列表不应继续保留自定义深度表格皮肤"
);
assert(
source.includes('class="delete-button"'),
"项目列表删除按钮应使用独立红色样式类"
);
assert(
source.includes("::v-deep .el-button--text.delete-button") &&
source.includes("color: #f56c6c"),
"项目列表删除按钮应渲染为红色"
);
console.log("project-table-style test passed");