Files
SPDM/src/views/task/projectList/index.vue
2025-11-07 11:49:35 +08:00

551 lines
16 KiB
Vue

<template>
<div class="gl-page-content" v-if="!showProjectDetailVisible">
<div class="display-mode">
<el-radio-group v-model="viewType">
<!-- <el-radio-button label="甘特图" value="gantt" /> -->
<el-radio-button label="卡片" value="card" />
<el-radio-button label="列表" value="list" />
</el-radio-group>
</div>
<!-- <div class="project-car-list" v-show="viewType === 'card'"> -->
<div class="project-table-list">
<BaseTable
showIndex
ref="baseTableRef"
tableName="NODE_LIST_LEVEL1"
:api="nodeListApi"
:searchLimitNum="3"
:actionsWidth="tableActionsLength['2-2-2']"
:listMode="viewType === 'list'?'default':'card'"
>
<template #cardTemplate="{tableData}">
<div class="project-card-box" >
<div class="projects-grid">
<div v-for="project in tableData" :key="project.id" class="project-card">
<div class="card-header">
<img class="project-icon" src="@/assets/imgs/projectTree/project-icon.png" alt="">
<div class="title-section">
<div class="project-title">{{ project.nodeName }}</div>
<div class="project-manager">{{ disposeMemberList(project) }}</div>
</div>
</div>
<div class="project-info">
<div class="info-row">
<span class="info-label">状态</span>
<span class="info-value">
<span :class="['status-badge', 'status-' + (project.exeStatus?'':'no-start')]">
{{ PROJECT_EXE_STATUS.O[project.exeStatus ] }}
</span>
</span>
</div>
<div class="date-row">
<span>{{ project.beginTime }}</span>
<span></span>
<span>{{ project.endTime }}</span>
</div>
</div>
<div class="progress-section">
<div class="progress-header">
<span class="progress-label">时间进度</span>
<span class="progress-value">{{ project.progress }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: project.progress + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<template #leftOptions>
<el-button
icon="plus"
@click="openProjectInfoDiaFun('create')"
type="primary"
>
{{ $t('项目列表.新增') }}
</el-button>
</template>
<template #nodeSubType="{row}">
{{ PROJECT_TYPE.O[row.nodeSubType] }}
</template>
<template #exeStatus="{row}">
{{ PROJECT_EXE_STATUS.O[row.exeStatus] }}
</template>
<template #memberList="{row}">
{{ disposeMemberList(row) }}
</template>
<template #tableActions="{ row }">
<div class="gl-table-actions">
<el-link type="primary" @click="goProjectDetailFun(row.uuid,row.nodeName)">查看</el-link>
<el-link type="primary" @click="openProjectInfoDiaFun('edit',row)">编辑</el-link>
<el-popconfirm
title="确认取消关注吗?"
@confirm="deleteNodeFun(row)"
>
<template #reference>
<el-link type="danger">删除</el-link>
</template>
</el-popconfirm>
</div>
</template>
</BaseTable>
</div>
</div>
<projectInfoDialog
ref="basePageRef"
v-model="showProjectInfoDialog"
:projectId="currentProjectBaseInfo.uuid"
:nodeLevel1List="baseTableRef?.tableData||[]"
@update:currentProjectBaseInfo="updateCurrentProjectBaseInfoFun"
@nextPageFun="nextPageFun"
@completeFun="completeFun"
/>
<projectDetail
v-if="showProjectDetailVisible"
:projectName="currentProject.nodeName"
:projectUuid="currentProject.nodeId"
@goBack="showProjectDetailVisible = false"
></projectDetail>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import BaseTable from '@/components/common/table/baseTable.vue';
import projectInfoDialog from '@/components/project/projectInfoDialog.vue';
// import { useRouter } from 'vue-router';
import { deleteNodeApi, queryNodeListApi } from '@/api/project/node';
import { ElMessage } from 'element-plus';
import { NODE_TYPE } from '@/utils/enum/node';
import { disposeMemberList } from '../projectDetail/components/project';
import { useDict } from '@/utils/useDict';
import { tableActionsLength } from '@/utils/common';
import dayjs from 'dayjs';
import projectDetail from '../projectDetail/index.vue';
export interface IUserInfo {
id: number;
company: string | null;
creator: string;
createTime: string;
updater: string | null;
updateTime: string | null;
tenantId: string | null;
nodeId: string;
identity: string;
name: string;
}
interface IProjectInfo {
[key:string]: string;
id: string;
uuid: string;
nodeName: string;
nodeCode: string;
nodeType: string;
nodeSubType: string;
memberList: any;
progressStatus: string;
currentNode: string;
beginTime: string;
endTime: string;
actualStartTime: string;
actualEndTime: string;
description: string;
}
// const router = useRouter();
const basePageRef = ref();
const nodePageRef = ref();
const { PROJECT_TYPE, PROJECT_EXE_STATUS } = useDict('PROJECT_TYPE', 'PROJECT_EXE_STATUS');
const showProjectInfoDialog = ref(false);
const showNodeInfoDialog = ref(false);
const showTaskDialog = ref(false);
const showProjectDetailVisible = ref(false);
const dialogType = ref('create'); // create 编辑 edit
const currentProjectBaseInfo = reactive<any>({
id: '',
exeStatus: '',
uuid: '',
nodeName: '',
nodeCode: '',
nodeType: '',
nodeSubType: '',
progressStatus: '',
currentNode: '',
beginTime: '',
endTime: '',
actualStartTime: '',
actualEndTime: '',
description: '',
memberList: [],
});
const viewType = ref('list');
const currentRow = ref();
const openProjectInfoDiaFun = (tag:string, row?:IProjectInfo) => {
dialogType.value = tag;
showProjectInfoDialog.value = true;
currentRow.value = row;
if (tag === 'edit' && row) {
Object.keys(currentProjectBaseInfo).forEach(key => {
(currentProjectBaseInfo as any)[key] = (row as any)[key];
});
} else {
Object.keys(currentProjectBaseInfo).forEach(key => {
(currentProjectBaseInfo as any)[key] = '';
});
}
};
const currentProject = reactive({
nodeName: '',
nodeId: '',
});
const goProjectDetailFun = (uuid:string, nodeName:string) => {
showProjectDetailVisible.value = true;
currentProject.nodeName = nodeName;
currentProject.nodeId = uuid;
// router.push({ path: '/task/projectDetail', query: { nodeId: uuid, nodeName } });
};
// NODE_LIST_LEVEL1
// const headData = ref<any[]>( [
// { title: '项目名称', key: 'nodeName', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 200 },
// { title: '项目代号', key: 'nodeCode', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 120 },
// { title: '项目类型', key: 'nodeSubType', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 120 },
// { title: '进度状态', key: 'progressStatusValue', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 120 },
// { title: '计划开始时间', key: 'beginTime', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 180 },
// { title: '计划结束时间', key: 'endTime', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 180 },
// { title: '实际完成时间', key: 'finishTime', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 180 },
// { title: '项目经理', key: 'memberList', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 120 },
// { title: '创建人', key: 'creator', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 120 },
// { title: '创建时间', key: 'createTime', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 180 },
// { title: '描述', key: 'description', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 200 },
// { title: '操作', key: 'tableActions', isShow: true, inputMode: 'input', type: 1, inForm: false, required: true, width: 150, fixed: 'right' },
// ]);
const nodeListApi = async(params:any) => {
const res:any = await queryNodeListApi({ ...params, nodeType: NODE_TYPE.PROJECT });
if (res && res.code === 200) {
res.data.data = res.data.data.map((item:any) => {
if (item.beginTime && item.endTime) {
const now = new Date();
if (dayjs(now).isBefore(item.beginTime)) {
item.progress = 0;
}
else if (dayjs(item.endTime).isBefore(now)) {
item.progress = 100;
} else {
const progress = dayjs(now).diff(item.beginTime) / dayjs(item.endTime).diff(item.beginTime);
// console.log('progress', progress);
item.progress = Number(progress.toFixed(4)) * 100;
// console.log('item.progress', item.progress);
}
} else {
item.progress = 0;
}
return item;
});
// console.log('res', res);
return res;
}
// else {
// ElMessage.error('不能提交空数据');
// }
};
const updateCurrentProjectBaseInfoFun = (info:any) => {
for (const key in currentProjectBaseInfo) {
currentProjectBaseInfo[key] = info[key];
}
console.log('currentProjectBaseInfo', currentProjectBaseInfo);
};
const nextPageFun = (step:string) => {
console.log('nodePageRef?.nodeTableList', nodePageRef.value?.nodeTableList);
if (step === 'basePage') {
showProjectInfoDialog.value = false;
showNodeInfoDialog.value = true;
}
if (step === 'nodePage') {
showNodeInfoDialog.value = false;
showTaskDialog.value = true;
}
};
// const prePageFun = (step:string) => {
// if (step === 'nodePage') {
// showNodeInfoDialog.value = false;
// showProjectInfoDialog.value = true;
// }
// if (step === 'taskPage') {
// showTaskDialog.value = false;
// showNodeInfoDialog.value = true;
// }
// };
const baseTableRef = ref();
const completeFun = async (page:string) => {
if (page === 'nodePage') {
await basePageRef.value.createProject();
console.log('currentProjectBaseInfo', currentProjectBaseInfo);
nodePageRef.value.addNodeDisposeFun(nodePageRef.value.nodeTableList, currentProjectBaseInfo.id);
}
baseTableRef.value.resetFun({});
};
const deleteNodeFun = async(row:IProjectInfo) => {
console.log('删除节点', row);
// 调用删除接口
const res:any = await deleteNodeApi({ deleteNodeIdList: [ row.uuid ] });
if (res && res.code === 200) {
ElMessage.success('删除项目成功');
} else {
ElMessage.error(res.msg || '删除项目失败');
}
baseTableRef.value.resetFun({});
};
</script>
<style lang="scss" scoped>
.gl-page-content {
.display-mode {
text-align: right;
}
.project-car-list {
width: 100%;
// // overflow-y: auto;
// // height: calc(100% - 16px);
// // margin-top: 16px;
// display: flex;
// flex-wrap: wrap;
// gap: @DEFAULT_PADDING;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
// padding-right: 5px;
.filter-btn {
text-align: right;
margin: 10px 0;
}
.project {
// width: calc(25% - 9px);
height: 156px;
display: flex;
justify-content: space-between;
padding: 12px;
border-radius: 6px;
position: relative;
overflow: hidden;
border: 1px solid red;
.progress {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
}
}
.activeProject {
// background-color: @DEFAULT_THEME_BACKGROUND_COLOR;
border: 2px solid red;
// color: #fff;
box-shadow: 0 0 5px 2px #d3d0f0;
.projectCenter {
.projectName {
font-size: 14px;
// color: #fff;
}
.phase {
// font-size: 12px;
// color: #fff;
}
}
.name {
font-weight: 700;
}
}
.circle {
display: inline-block;
width: 10px;
height: 10px;
position: absolute;
top: 6px;
right: 6px;
margin-right: 0;
}
}
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.project-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 18px;
transition: all 0.3s ease;
border: 1px solid #e2e8f0;
}
.project-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
border-color: #cbd5e1;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.project-icon {
width: 36px;
height: 36px;
background-color: #ef4444;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: bold;
margin-right: 12px;
flex-shrink: 0;
}
.title-section {
flex-grow: 1;
}
.project-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-manager {
font-size: 12px;
color: #64748b;
}
.project-info {
margin-bottom: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 13px;
}
.info-label {
color: #64748b;
font-weight: 500;
}
.info-value {
color: #1e293b;
font-weight: 500;
}
.status-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.status-planning {
background-color: #eff6ff;
color: #3b82f6;
}
.status-progress {
background-color: #ecfdf5;
color: #10b981;
}
.status-delayed {
background-color: #fef2f2;
color: #ef4444;
}
.status-no-start {
background-color: #f8fafc;
color: #64748b;
}
.progress-section {
margin-top: 18px;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 13px;
}
.progress-label {
color: #64748b;
}
.progress-value {
color: #3b82f6;
font-weight: 600;
}
.progress-bar {
height: 6px;
background-color: #e2e8f0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
border-radius: 3px;
transition: width 0.5s ease;
}
.date-row {
display: flex;
justify-content: space-between;
margin-top: 4px;
font-size: 12px;
color: #64748b;
}
</style>