Files
ylhp-ai-project-manager-fro…/src/views/project/index.vue
JiaoTianBo c7abf48c6a
Some checks failed
Lint Code / Lint Code (push) Failing after 1m40s
refactor(project): 移除角色信息展示代码
- 删除了项目列表中显示用户角色的文本块
- 简化了项目项的UI布局
- 减少了冗余的DOM元素和样式声明
- 保持UI整体风格一致性
2026-03-31 18:23:12 +08:00

859 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
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";
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 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,
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) {
router.push({
name: "ProjectDetail",
params: { id: row.id.toString() }
});
}
// 编辑项目
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;
}
}
// 获取状态标签类型
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";
}
}
// 获取状态文本
function getStatusText(status?: string): string {
const statusMap: Record<string, string> = {
draft: "草稿",
planning: "规划中",
ongoing: "进行中",
paused: "已暂停",
completed: "已完成",
cancelled: "已取消"
};
return statusMap[status || ""] || status || "未知";
}
</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.totalCount }}
</p>
<p class="text-xs text-blue-500 mt-1">
<el-icon
><component :is="useRenderIcon('ri/folder-line')"
/></el-icon>
包含各状态项目
</p>
</div>
<div class="stat-icon bg-blue-100">
<el-icon :size="24" color="#409eff">
<component :is="useRenderIcon('ri/folders-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.ongoingCount }}
</p>
<p class="text-xs text-green-500 mt-1">
<el-icon
><component :is="useRenderIcon('ri/play-circle-line')"
/></el-icon>
{{ statistics.planningCount }} 个规划中
</p>
</div>
<div class="stat-icon bg-green-100">
<el-icon :size="24" color="#67c23a">
<component :is="useRenderIcon('ri/play-circle-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">
{{ statistics.pausedCount }} 个已暂停 |
{{ statistics.cancelledCount }} 个已取消
</p>
</div>
<div class="stat-icon bg-purple-100">
<el-icon :size="24" color="#9b59b6">
<component :is="useRenderIcon('ri/check-double-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">
{{ Math.round(statistics.averageProgress || 0) }}%
</p>
<p class="text-xs text-orange-400 mt-1">
<el-icon
><component :is="useRenderIcon('ri/alert-line')"
/></el-icon>
{{ statistics.highRiskCount }} 个高风险项目
</p>
</div>
<div class="stat-icon bg-orange-100">
<el-icon :size="24" color="#e6a23c">
<component :is="useRenderIcon('ri/bar-chart-line')" />
</el-icon>
</div>
</div>
<el-progress
:percentage="Math.round(statistics.averageProgress || 0)"
:show-text="false"
class="mt-2"
style="width: 100%"
/>
</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" @click="handleView(item)">
<div class="flex justify-between items-start mb-3" @click.stop>
<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 @click.stop>
<el-button link @click.stop>
<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
v-if="canEditProject"
@click="handleEdit(item)"
>
<component :is="useRenderIcon(EditPenIcon)" class="mr-2" />
编辑项目
</el-dropdown-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>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="mb-3">
<el-tag
:type="getStatusType(item.status)"
size="small"
class="mr-2"
>
{{ getStatusText(item.status) }}
</el-tag>
<el-tag
:type="getRiskType(item.riskLevel)"
size="small"
effect="plain"
>
{{ item.riskLevel || "未知" }}风险
</el-tag>
</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 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>
</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"
/>
<!-- 项目编辑模态框 -->
<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>
<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>