feat(project): 添加项目编辑和状态管理功能
Some checks failed
Lint Code / Lint Code (push) Has been cancelled

- 新增 Project 类型定义,完善项目数据结构
- 新增 updateProject、updateProjectStatus 等接口封装
- 添加权限控制,实现基于角色的编辑、删除、状态更新等操作权限判断
- 实现项目编辑模态框,支持项目基本信息、预算、进度等字段的修改
- 实现项目状态更新模态框,支持项目状态的切换操作
- 实现项目经理更换模态框,支持更新项目负责人信息
- 更新项目列表操作菜单,添加编辑、状态更新、项目经理更换等功能入口
- 优化项目状态显示,新增状态中文映射文本显示
- 完善项目编辑和状态更新的保存逻辑,增加操作成功与失败提示信息
- 引入表单校验和操作Loading状态,提升交互体验
This commit is contained in:
2026-03-31 18:22:01 +08:00
parent 2735c57778
commit 4b30c1350d
3 changed files with 837 additions and 7 deletions

View File

@@ -17,6 +17,7 @@ export type ProjectItem = {
projectType?: string;
managerId?: number;
managerName?: string;
managerAvatar?: string;
planStartDate?: string;
planEndDate?: string;
progress?: number;
@@ -60,6 +61,60 @@ export const deleteProject = (id: string) => {
return http.request<Result<void>>("delete", `/api/v1/project/${id}`);
};
// ==================== 项目管理 API ====================
/** 项目实体 - 根据 OpenAPI 定义(用于编辑) */
export type Project = {
id?: string;
projectCode?: string;
projectName?: string;
projectType?: string;
description?: string;
objectives?: string;
managerId?: string;
sponsorId?: string;
planStartDate?: string;
planEndDate?: string;
actualStartDate?: string;
actualEndDate?: string;
budget?: number;
cost?: number;
currency?: string;
progress?: number;
status?: string; // draft-草稿, planning-规划中, ongoing-进行中, paused-暂停, completed-已完成, cancelled-已取消
priority?: string; // critical-关键, high-高, medium-中, low-低
riskLevel?: string; // high-高, medium-中, low-低
visibility?: number; // 1-公开, 2-部门内, 3-项目成员
tags?: string[];
extraData?: Record<string, any>;
};
/** 修改项目 */
export const updateProject = (data: Project) => {
return http.request<Result<void>>("put", "/api/v1/project", { data });
};
/** 更新项目状态 */
export const updateProjectStatus = (id: string, status: string) => {
return http.request<Result<void>>("put", `/api/v1/project/${id}/status`, {
params: { status }
});
};
/** 更新项目进度 */
export const updateProjectProgress = (id: string, progress: number) => {
return http.request<Result<void>>("put", `/api/v1/project/${id}/progress`, {
params: { progress }
});
};
/** 更新项目经理 */
export const updateProjectManager = (id: string, managerName: string) => {
return http.request<Result<void>>("put", `/api/v1/project/${id}/manager`, {
params: { managerName }
});
};
// ==================== 项目统计 ====================
/** 项目统计数据 - 根据 OpenAPI 定义 */

View File

@@ -0,0 +1,396 @@
{
"openapi": "3.0.1",
"info": {
"title": "默认模块",
"description": "",
"version": "1.0.0"
},
"tags": [],
"paths": {
"/api/v1/project": {
"put": {
"summary": "修改项目",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
"schema": {
"type": "string",
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project",
"description": ""
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseVoid"
}
}
}
}
},
"security": []
}
},
"/api/v1/project/{id}": {
"delete": {
"summary": "删除项目",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
"schema": {
"type": "string",
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseVoid"
}
}
}
}
},
"security": []
}
},
"/api/v1/project/{id}/status": {
"put": {
"summary": "更新项目状态",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "status",
"in": "query",
"description": "",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
"schema": {
"type": "string",
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseVoid"
}
}
}
}
},
"security": []
}
},
"/api/v1/project/{id}/progress": {
"put": {
"summary": "更新项目进度",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "progress",
"in": "query",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
"schema": {
"type": "string",
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseVoid"
}
}
}
}
},
"security": []
}
},
"/api/v1/project/{id}/manager": {
"put": {
"summary": "更新项目经理",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "id",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "managerId",
"in": "query",
"description": "",
"required": true,
"schema": {
"type": "integer"
}
},
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95",
"schema": {
"type": "string",
"default": "Bearer b35c6f5b-bc0b-4652-bef2-eca04a5cdd95"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BaseResponseVoid"
}
}
}
}
},
"security": []
}
}
},
"components": {
"schemas": {
"Project": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "",
"format": "int64"
},
"projectCode": {
"type": "string",
"description": "项目编号"
},
"projectName": {
"type": "string",
"description": "项目名称"
},
"projectType": {
"type": "string",
"description": "项目类型"
},
"description": {
"type": "string",
"description": "项目描述"
},
"objectives": {
"type": "string",
"description": "项目目标"
},
"managerId": {
"type": "integer",
"description": "项目经理ID",
"format": "int64"
},
"sponsorId": {
"type": "integer",
"description": "项目发起人ID",
"format": "int64"
},
"planStartDate": {
"type": "string",
"description": "计划开始日期"
},
"planEndDate": {
"type": "string",
"description": "计划结束日期"
},
"actualStartDate": {
"type": "string",
"description": "实际开始日期"
},
"actualEndDate": {
"type": "string",
"description": "实际结束日期"
},
"budget": {
"type": "number",
"description": "项目预算"
},
"cost": {
"type": "number",
"description": "已花费金额"
},
"currency": {
"type": "string",
"description": "币种"
},
"progress": {
"type": "integer",
"description": "进度百分比"
},
"status": {
"type": "string",
"description": "状态: draft-草稿, planning-规划中, ongoing-进行中, paused-暂停, completed-已完成, cancelled-已取消"
},
"priority": {
"type": "string",
"description": "优先级: critical-关键, high-高, medium-中, low-低"
},
"riskLevel": {
"type": "string",
"description": "风险等级: high-高, medium-中, low-低"
},
"visibility": {
"type": "integer",
"description": "可见性: 1-公开, 2-部门内, 3-项目成员"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "标签列表"
},
"extraData": {
"type": "object",
"properties": {},
"description": "扩展数据"
},
"createBy": {
"type": "integer",
"description": "创建人",
"format": "int64"
},
"createTime": {
"type": "string",
"description": "创建时间"
},
"updateBy": {
"type": "integer",
"description": "更新人",
"format": "int64"
},
"updateTime": {
"type": "string",
"description": "更新时间"
},
"deleted": {
"type": "integer",
"description": "删除标记"
}
}
},
"BaseResponseVoid": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"description": ""
},
"data": {
"description": "",
"type": "null"
},
"message": {
"type": "string",
"description": ""
}
}
}
},
"responses": {},
"securitySchemes": {}
},
"servers": [],
"security": []
}

View File

@@ -1,8 +1,17 @@
<script setup lang="ts">
import { ref } from "vue";
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import { useProject } from "./utils/hook";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { hasPerms } from "@/utils/auth";
import { message } from "@/utils/message";
import {
updateProject,
updateProjectStatus,
updateProjectManager,
type Project,
type ProjectItem
} from "@/api/project";
import CreateProjectWizard from "./components/CreateProjectWizard.vue";
import dayjs from "dayjs";
@@ -23,6 +32,34 @@ defineOptions({
const router = useRouter();
const wizardVisible = ref(false);
// 权限控制
const canEditProject = computed(() => hasPerms("project:center:update"));
const canDeleteProject = computed(() => hasPerms("project:center:delete"));
const canUpdateProjectStatus = computed(() =>
hasPerms("project:center:update")
);
// 项目编辑模态框
const projectEditModal = ref(false);
const projectEditForm = ref<Project>({});
const projectEditLoading = ref(false);
// 状态更新模态框
const statusUpdateModal = ref(false);
const statusUpdateForm = ref({
id: "",
status: ""
});
const statusUpdateLoading = ref(false);
// 项目经理更新模态框
const managerUpdateModal = ref(false);
const managerUpdateForm = ref({
id: "",
managerName: ""
});
const managerUpdateLoading = ref(false);
const {
form,
formRef,
@@ -59,8 +96,106 @@ function handleView(row: any) {
}
// 编辑项目
function handleEdit(row: any) {
console.log("编辑项目", row);
function handleEdit(row: ProjectItem) {
projectEditForm.value = {
id: row.id,
projectCode: row.projectCode,
projectName: row.projectName,
projectType: row.projectType,
planStartDate: row.planStartDate,
planEndDate: row.planEndDate,
progress: row.progress,
status: row.status,
priority: row.priority,
riskLevel: row.riskLevel,
budget: row.budget,
cost: row.cost
};
projectEditModal.value = true;
}
/** 保存项目编辑 */
async function saveProjectEdit() {
if (!projectEditForm.value.projectName) {
message("请输入项目名称", { type: "warning" });
return;
}
projectEditLoading.value = true;
try {
await updateProject(projectEditForm.value);
message("项目更新成功", { type: "success" });
projectEditModal.value = false;
onSearch();
} catch (error) {
console.error("更新项目失败:", error);
message("更新项目失败", { type: "error" });
} finally {
projectEditLoading.value = false;
}
}
/** 打开状态更新模态框 */
function openStatusModal(row: ProjectItem) {
statusUpdateForm.value = {
id: row.id!,
status: row.status || "ongoing"
};
statusUpdateModal.value = true;
}
/** 保存状态更新 */
async function saveStatusUpdate() {
if (!statusUpdateForm.value.status) {
message("请选择状态", { type: "warning" });
return;
}
statusUpdateLoading.value = true;
try {
await updateProjectStatus(
statusUpdateForm.value.id,
statusUpdateForm.value.status
);
message("状态更新成功", { type: "success" });
statusUpdateModal.value = false;
onSearch();
} catch (error) {
console.error("更新状态失败:", error);
message("更新状态失败", { type: "error" });
} finally {
statusUpdateLoading.value = false;
}
}
/** 打开项目经理更新模态框 */
function openManagerModal(row: ProjectItem) {
managerUpdateForm.value = {
id: row.id!,
managerName: ""
};
managerUpdateModal.value = true;
}
/** 保存项目经理更新 */
async function saveManagerUpdate() {
if (!managerUpdateForm.value.managerName) {
message("请输入项目经理姓名", { type: "warning" });
return;
}
managerUpdateLoading.value = true;
try {
await updateProjectManager(
managerUpdateForm.value.id,
managerUpdateForm.value.managerName
);
message("项目经理更新成功", { type: "success" });
managerUpdateModal.value = false;
onSearch();
} catch (error) {
console.error("更新项目经理失败:", error);
message("更新项目经理失败", { type: "error" });
} finally {
managerUpdateLoading.value = false;
}
}
// 获取状态标签类型
@@ -94,6 +229,19 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
return "success";
}
}
// 获取状态文本
function getStatusText(status?: string): string {
const statusMap: Record<string, string> = {
draft: "草稿",
planning: "规划中",
ongoing: "进行中",
paused: "已暂停",
completed: "已完成",
cancelled: "已取消"
};
return statusMap[status || ""] || status || "未知";
}
</script>
<template>
@@ -344,11 +492,38 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
<component :is="useRenderIcon(ViewIcon)" class="mr-2" />
查看详情
</el-dropdown-item>
<el-dropdown-item @click="handleEdit(item)">
<el-dropdown-item
v-if="canEditProject"
@click="handleEdit(item)"
>
<component :is="useRenderIcon(EditPenIcon)" class="mr-2" />
编辑项目
</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(item)">
<el-dropdown-item
v-if="canUpdateProjectStatus"
@click="openStatusModal(item)"
>
<component
:is="useRenderIcon('ri/settings-3-line')"
class="mr-2"
/>
更新状态
</el-dropdown-item>
<el-dropdown-item
v-if="canEditProject"
@click="openManagerModal(item)"
>
<component
:is="useRenderIcon('ri-user-settings-line')"
class="mr-2"
/>
更换项目经理
</el-dropdown-item>
<el-dropdown-item
v-if="canDeleteProject"
divided
@click="handleDelete(item)"
>
<component :is="useRenderIcon(DeleteIcon)" class="mr-2" />
<span class="text-red-500">删除项目</span>
</el-dropdown-item>
@@ -363,7 +538,7 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
size="small"
class="mr-2"
>
{{ item.status || "未知" }}
{{ getStatusText(item.status) }}
</el-tag>
<el-tag
:type="getRiskType(item.riskLevel)"
@@ -400,7 +575,10 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
<div class="flex-bc">
<div class="flex items-center gap-2">
<el-avatar :size="28">
<el-avatar v-if="item.managerAvatar" :size="28">
<img :src="item.managerAvatar" alt="负责人头像" />
</el-avatar>
<el-avatar v-else :size="28">
<component :is="useRenderIcon(UserIcon)" />
</el-avatar>
<span class="text-sm">{{ item.managerName || "未分配" }}</span>
@@ -436,6 +614,207 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
v-model:visible="wizardVisible"
@success="handleWizardSuccess"
/>
<!-- 项目编辑模态框 -->
<el-dialog
v-model="projectEditModal"
title="编辑项目"
width="600px"
destroy-on-close
>
<el-form
ref="projectFormRef"
:model="projectEditForm"
label-width="100px"
class="project-edit-form"
>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="项目名称" prop="projectName" required>
<el-input
v-model="projectEditForm.projectName"
placeholder="请输入项目名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号">
<el-input
v-model="projectEditForm.projectCode"
placeholder="请输入项目编号"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目类型">
<el-input
v-model="projectEditForm.projectType"
placeholder="请输入项目类型"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划开始日期">
<el-date-picker
v-model="projectEditForm.planStartDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划结束日期">
<el-date-picker
v-model="projectEditForm.planEndDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级">
<el-select
v-model="projectEditForm.priority"
placeholder="请选择优先级"
style="width: 100%"
>
<el-option label="关键" value="critical" />
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="风险等级">
<el-select
v-model="projectEditForm.riskLevel"
placeholder="请选择风险等级"
style="width: 100%"
>
<el-option label="高" value="high" />
<el-option label="中" value="medium" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目预算">
<el-input-number
v-model="projectEditForm.budget"
:min="0"
:precision="2"
placeholder="预算"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="已花费金额">
<el-input-number
v-model="projectEditForm.cost"
:min="0"
:precision="2"
placeholder="已花费"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="项目进度">
<el-slider
v-model="projectEditForm.progress"
:max="100"
show-input
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="projectEditModal = false">取消</el-button>
<el-button
type="primary"
:loading="projectEditLoading"
@click="saveProjectEdit"
>
保存
</el-button>
</template>
</el-dialog>
<!-- 状态更新模态框 -->
<el-dialog
v-model="statusUpdateModal"
title="更新项目状态"
width="400px"
destroy-on-close
>
<el-form :model="statusUpdateForm" label-width="80px">
<el-form-item label="项目状态">
<el-select
v-model="statusUpdateForm.status"
placeholder="请选择状态"
style="width: 100%"
>
<el-option label="草稿" value="draft" />
<el-option label="规划中" value="planning" />
<el-option label="进行中" value="ongoing" />
<el-option label="已暂停" value="paused" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusUpdateModal = false">取消</el-button>
<el-button
type="primary"
:loading="statusUpdateLoading"
@click="saveStatusUpdate"
>
确认
</el-button>
</template>
</el-dialog>
<!-- 项目经理更新模态框 -->
<el-dialog
v-model="managerUpdateModal"
title="更换项目经理"
width="450px"
destroy-on-close
>
<el-form :model="managerUpdateForm" label-width="100px">
<el-form-item label="当前负责人">
<el-input
:model-value="managerUpdateForm.managerName"
disabled
placeholder="暂无"
/>
</el-form-item>
<el-form-item label="新项目经理" required>
<el-input
v-model="managerUpdateForm.managerName"
placeholder="请输入新项目经理的姓名"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="managerUpdateModal = false">取消</el-button>
<el-button
type="primary"
:loading="managerUpdateLoading"
@click="saveManagerUpdate"
>
确认更换
</el-button>
</template>
</el-dialog>
</div>
</template>