feat(project): 新增项目管理功能模块
Some checks failed
Lint Code / Lint Code (push) Failing after 6m48s

- 新增项目菜单项及路由配置,支持项目管理入口
- 实现项目相关API接口,包括项目列表、统计、甘特图及项目初始化接口
- 添加项目新建向导组件,支持上传文件预览及确认保存
- 实现项目管理页面,包含项目列表展示、筛选、统计卡片及新建项目操作
- 支持项目基本信息、里程碑、任务、成员及风险等多维度管理数据录入
- 优化页面交互体验,支持上传文件格式校验及数据编辑预览
- 提供状态及风险等级标签显示,辅助项目状态快速识别
This commit is contained in:
2026-03-28 15:25:03 +08:00
parent ce2f4767f1
commit 87bdef6416
11 changed files with 2125 additions and 104 deletions

467
src/views/project/index.vue Normal file
View File

@@ -0,0 +1,467 @@
<script setup lang="ts">
import { ref } from "vue";
import { useProject } from "./utils/hook";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import CreateProjectWizard from "./components/CreateProjectWizard.vue";
import dayjs from "dayjs";
import AddIcon from "~icons/ri/add-line";
import SearchIcon from "~icons/ri/search-line";
import RefreshIcon from "~icons/ri/refresh-line";
import MoreIcon from "~icons/ep/more-filled";
import DeleteIcon from "~icons/ep/delete";
import EditPenIcon from "~icons/ep/edit-pen";
import ViewIcon from "~icons/ri/eye-line";
import CalendarIcon from "~icons/ri/calendar-line";
import UserIcon from "~icons/ri/user-line";
defineOptions({
name: "Project"
});
const wizardVisible = ref(false);
const {
form,
formRef,
loading,
dataList,
pagination,
statistics,
activeFilter,
statusFilterButtons,
onSearch,
resetForm,
handleDelete,
handleSizeChange,
handleCurrentChange,
setFilter
} = useProject();
// 打开新建项目向导
function openWizard() {
wizardVisible.value = true;
}
// 向导成功回调
function handleWizardSuccess() {
onSearch();
}
// 查看项目详情
function handleView(row: any) {
console.log("查看项目", row);
}
// 编辑项目
function handleEdit(row: any) {
console.log("编辑项目", row);
}
// 获取状态标签类型
function getStatusType(
status?: string
): "success" | "warning" | "info" | "primary" | "danger" {
switch (status) {
case "completed":
return "success";
case "ongoing":
return "primary";
case "paused":
return "warning";
case "cancelled":
return "danger";
default:
return "info";
}
}
// 获取风险标签类型
function getRiskType(risk?: string): "success" | "warning" | "danger" {
switch (risk) {
case "low":
return "success";
case "medium":
return "warning";
case "high":
return "danger";
default:
return "success";
}
}
</script>
<template>
<div class="project-management w-full">
<!-- 页面标题 -->
<div class="flex-bc mb-4">
<div>
<h2 class="text-xl font-bold">项目管理</h2>
<p class="text-gray-500 text-sm mt-1">
管理所有项目的进度资源分配和风险管控
</p>
</div>
<div class="flex gap-2">
<el-button>
<template #icon>
<component :is="useRenderIcon('ri/download-line')" />
</template>
导出报表
</el-button>
<el-button type="primary" @click="openWizard">
<template #icon>
<component :is="useRenderIcon(AddIcon)" />
</template>
新建项目
</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" class="mb-4">
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="flex-bc">
<div>
<p class="text-gray-500 text-sm">进行中项目</p>
<p class="text-2xl font-bold mt-1">
{{ statistics.ongoingCount }}
</p>
<p class="text-xs text-green-500 mt-1">
<el-icon
><component :is="useRenderIcon('ri/arrow-up-line')"
/></el-icon>
较上月增加2个
</p>
</div>
<div class="stat-icon bg-blue-100">
<el-icon :size="24" color="#409eff">
<component :is="useRenderIcon('ri/folder-line')" />
</el-icon>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="flex-bc">
<div>
<p class="text-gray-500 text-sm">已完成项目</p>
<p class="text-2xl font-bold mt-1">
{{ statistics.completedCount }}
</p>
<p class="text-xs text-gray-400 mt-1">本年度累计完成</p>
</div>
<div class="stat-icon bg-green-100">
<el-icon :size="24" color="#67c23a">
<component :is="useRenderIcon('ri/check-line')" />
</el-icon>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="flex-bc">
<div>
<p class="text-gray-500 text-sm">高风险项目</p>
<p class="text-2xl font-bold mt-1 text-orange-500">
{{ statistics.highRiskCount }}
</p>
<p class="text-xs text-orange-400 mt-1">需要重点关注</p>
</div>
<div class="stat-icon bg-orange-100">
<el-icon :size="24" color="#e6a23c">
<component :is="useRenderIcon('ri/alert-line')" />
</el-icon>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card shadow="hover" class="stat-card">
<div class="flex-bc">
<div>
<p class="text-gray-500 text-sm">平均完成率</p>
<p class="text-2xl font-bold mt-1">
{{ statistics.averageProgress }}%
</p>
<el-progress
:percentage="statistics.averageProgress"
:show-text="false"
class="mt-2"
style="width: 100px"
/>
</div>
<div class="stat-icon bg-purple-100">
<el-icon :size="24" color="#9b59b6">
<component :is="useRenderIcon('ri/bar-chart-line')" />
</el-icon>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选区域 -->
<el-card shadow="never" class="mb-4 filter-card">
<div class="flex-bc flex-wrap gap-4">
<div class="flex items-center gap-2">
<el-button
v-for="btn in statusFilterButtons"
:key="btn.value"
:type="activeFilter === btn.value ? 'primary' : ''"
@click="setFilter(btn.value)"
>
{{ btn.label }}
</el-button>
</div>
<div class="flex items-center gap-2">
<el-input
v-model="form.keyword"
placeholder="搜索项目名称..."
clearable
style="width: 200px"
@keyup.enter="onSearch"
>
<template #prefix>
<component :is="useRenderIcon(SearchIcon)" />
</template>
</el-input>
<el-select
v-model="form.status"
placeholder="状态"
clearable
style="width: 120px"
@change="onSearch"
>
<el-option label="未开始" :value="0" />
<el-option label="进行中" :value="1" />
<el-option label="已完成" :value="2" />
<el-option label="已延期" :value="3" />
</el-select>
<el-button
:icon="useRenderIcon(RefreshIcon)"
@click="resetForm(formRef)"
>
重置
</el-button>
</div>
</div>
</el-card>
<!-- 项目列表卡片 -->
<div class="flex-bc mb-4">
<h3 class="text-lg font-medium">项目列表</h3>
<el-button type="primary" @click="openWizard">
<template #icon>
<component :is="useRenderIcon(AddIcon)" />
</template>
新建项目
</el-button>
</div>
<!-- 空状态 -->
<el-empty
v-if="!loading && dataList.length === 0"
description="暂无参与的项目"
class="py-12"
>
<template #image>
<div class="empty-icon">
<component
:is="useRenderIcon('ri/folder-open-line')"
style="font-size: 64px; color: var(--el-text-color-secondary)"
/>
</div>
</template>
<template #description>
<div class="text-center">
<p class="text-gray-500 mb-2">暂无参与的项目</p>
<p class="text-xs text-gray-400">
您还没有参与任何项目可以创建一个新项目开始
</p>
</div>
</template>
<el-button type="primary" @click="openWizard">
<template #icon>
<component :is="useRenderIcon(AddIcon)" />
</template>
创建项目
</el-button>
</el-empty>
<el-row v-else v-loading="loading" :gutter="16">
<el-col
v-for="item in dataList"
:key="item.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
class="mb-4"
>
<el-card shadow="hover" class="project-card">
<div class="flex justify-between items-start mb-3">
<div class="flex-1 min-w-0">
<h4
class="font-medium text-base truncate"
:title="item.projectName"
>
{{ item.projectName }}
</h4>
<p class="text-xs text-gray-400 mt-1 truncate">
{{ item.projectCode || "暂无项目编号" }}
</p>
</div>
<el-dropdown>
<el-button link>
<component :is="useRenderIcon(MoreIcon)" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleView(item)">
<component :is="useRenderIcon(ViewIcon)" class="mr-2" />
查看详情
</el-dropdown-item>
<el-dropdown-item @click="handleEdit(item)">
<component :is="useRenderIcon(EditPenIcon)" class="mr-2" />
编辑项目
</el-dropdown-item>
<el-dropdown-item divided @click="handleDelete(item)">
<component :is="useRenderIcon(DeleteIcon)" class="mr-2" />
<span class="text-red-500">删除项目</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="mb-3">
<el-tag
:type="getStatusType(item.status)"
size="small"
class="mr-2"
>
{{ item.status || "未知" }}
</el-tag>
<el-tag
:type="getRiskType(item.riskLevel)"
size="small"
effect="plain"
>
{{ item.riskLevel || "未知" }}风险
</el-tag>
</div>
<div
class="text-sm text-gray-500 mb-3 line-clamp-2"
style="min-height: 40px"
>
{{ item.myRole ? `我的角色: ${item.myRole}` : "暂无角色信息" }}
</div>
<div class="flex items-center gap-4 text-xs text-gray-400 mb-3">
<span class="flex items-center gap-1">
<component :is="useRenderIcon(CalendarIcon)" />
{{
item.planStartDate
? dayjs(item.planStartDate).format("MM-DD")
: "--"
}}
~
{{
item.planEndDate
? dayjs(item.planEndDate).format("MM-DD")
: "--"
}}
</span>
</div>
<div class="flex-bc">
<div class="flex items-center gap-2">
<el-avatar :size="28">
<component :is="useRenderIcon(UserIcon)" />
</el-avatar>
<span class="text-sm">{{ item.managerName || "未分配" }}</span>
</div>
<div class="flex items-center gap-2">
<el-progress
:percentage="item.progress || 0"
:status="item.progress === 100 ? 'success' : ''"
style="width: 80px"
/>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 分页 -->
<div class="flex justify-end mt-4">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[8, 12, 16, 20]"
layout="total, sizes, prev, pager, next"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 新建项目向导 -->
<CreateProjectWizard
v-model:visible="wizardVisible"
@success="handleWizardSuccess"
/>
</div>
</template>
<style scoped lang="scss">
.project-management {
padding: 16px 80px 16px 16px;
.stat-card {
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 8px;
}
}
.filter-card {
:deep(.el-card__body) {
padding: 12px 16px;
}
}
}
.project-card {
cursor: pointer;
border-radius: 12px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 8px 24px rgb(0 0 0 / 10%);
transform: translateY(-4px);
}
:deep(.el-card__body) {
padding: 16px;
}
}
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
:deep(.el-button:focus-visible) {
outline: none;
}
</style>