Files
cn-rdms-web/src/views/workbench/modules/workbench-project-grid.vue

533 lines
14 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 { computed, ref, watch } from 'vue';
import { useRouterPush } from '@/hooks/common/router';
import { buildWorkbenchOwnedProjectItems, buildWorkbenchProjectItems } from '../homepage';
import { workbenchOwnedProjectMock, workbenchProjectMock } from '../mock';
import WorkbenchModuleCard from './workbench-module-card.vue';
defineOptions({ name: 'WorkbenchProjectGrid' });
interface Props {
editing?: boolean;
collapsed?: boolean;
}
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
defineEmits<{
(e: 'hide'): void;
(e: 'toggle-collapse'): void;
}>();
const { routerPushByKey } = useRouterPush();
type ProjectViewKey = 'participated' | 'owned';
const activeView = ref<ProjectViewKey>('participated');
const participatedItems = computed(() => buildWorkbenchProjectItems(workbenchProjectMock));
const ownedItems = computed(() => buildWorkbenchOwnedProjectItems(workbenchOwnedProjectMock));
const currentOwnedId = ref<string>(ownedItems.value[0]?.id ?? '');
watch(ownedItems, list => {
if (!list.find(item => item.id === currentOwnedId.value)) {
currentOwnedId.value = list[0]?.id ?? '';
}
});
const currentOwned = computed(() => ownedItems.value.find(item => item.id === currentOwnedId.value) ?? null);
function handleEnterProjectList() {
routerPushByKey('project_list');
}
</script>
<template>
<WorkbenchModuleCard
title="我的项目"
icon="mdi:briefcase-outline"
:editing="editing"
:collapsed="collapsed"
@hide="$emit('hide')"
@toggle-collapse="$emit('toggle-collapse')"
>
<div class="workbench-project__tabs">
<ElRadioGroup v-model="activeView" size="small">
<ElRadioButton value="participated">我参与的</ElRadioButton>
<ElRadioButton value="owned">我负责的</ElRadioButton>
</ElRadioGroup>
<ElButton type="primary" link @click="handleEnterProjectList">
<span>进入项目列表</span>
<SvgIcon icon="mdi:arrow-right-thin" class="workbench-project__more-icon" />
</ElButton>
</div>
<!-- 我参与的网格视图 -->
<template v-if="activeView === 'participated'">
<p class="workbench-project__desc">直接看每个项目的当前进度我的角色与未完成任务</p>
<div v-if="participatedItems.length" class="workbench-project__grid">
<article v-for="item in participatedItems" :key="item.id" class="workbench-project__card">
<div class="workbench-project__card-header">
<div class="workbench-project__card-title-group">
<h4 class="workbench-project__card-title">{{ item.name }}</h4>
<span class="workbench-project__card-code">{{ item.code }}</span>
</div>
<span class="workbench-project__card-status" :class="`workbench-project__card-status--${item.statusTone}`">
{{ item.statusLabel }}
</span>
</div>
<div class="workbench-project__card-role">
<span class="workbench-project__card-role-label">我的角色</span>
<strong class="workbench-project__card-role-value">{{ item.myRole }}</strong>
</div>
<div class="workbench-project__progress">
<div class="workbench-project__progress-header">
<span class="workbench-project__progress-label">进度</span>
<strong class="workbench-project__progress-value">{{ item.progress }}%</strong>
</div>
<div class="workbench-project__progress-bar">
<div
class="workbench-project__progress-bar-inner"
:class="`workbench-project__progress-bar-inner--${item.statusTone}`"
:style="{ width: `${item.progress}%` }"
/>
</div>
</div>
<div class="workbench-project__footer">
<div class="workbench-project__footer-block">
<span class="workbench-project__footer-label">我负责的任务</span>
<strong class="workbench-project__footer-value">
{{ item.myTaskCount }}
<span v-if="item.myPendingTaskCount > 0" class="workbench-project__footer-sub">
待处理 {{ item.myPendingTaskCount }}
</span>
</strong>
</div>
<div class="workbench-project__footer-block workbench-project__footer-block--right">
<span class="workbench-project__footer-label">最近活动</span>
<strong class="workbench-project__footer-value">{{ item.lastActiveLabel }}</strong>
</div>
</div>
</article>
</div>
<ElEmpty v-else description="暂未参与任何项目" :image-size="72" />
</template>
<!-- 我负责的单对象深度详情 -->
<template v-else>
<ElEmpty v-if="!currentOwned" description="您当前没有负责的项目" :image-size="72" />
<template v-else>
<div v-if="ownedItems.length > 1" class="ps-head">
<span class="ps-pin-label">当前项目</span>
<ElSelect v-model="currentOwnedId" size="small" class="ps-pin">
<ElOption v-for="p in ownedItems" :key="p.id" :label="p.name" :value="p.id" />
</ElSelect>
</div>
<div v-else class="ps-head ps-head--single">
<span class="ps-pin-label">当前项目</span>
<strong class="ps-single-name">{{ currentOwned.name }}</strong>
</div>
<div class="ps-overview">
<div class="ps-ring" :style="{ '--p': currentOwned.progress } as any">
<span>{{ currentOwned.progress }}%</span>
</div>
<div class="ps-kpis">
<div class="ps-kpi">
<b>{{ currentOwned.executionCount }}</b>
<span>执行</span>
</div>
<div class="ps-kpi">
<b>{{ currentOwned.taskCount }}</b>
<span>任务</span>
</div>
<div class="ps-kpi">
<b>{{ currentOwned.memberCount }}</b>
<span>成员</span>
</div>
<div class="ps-kpi">
<b :class="{ 'is-danger': currentOwned.overdueCount > 0 }">{{ currentOwned.overdueCount }}</b>
<span>逾期</span>
</div>
</div>
</div>
<div class="ps-sub"> {{ currentOwned.remainingDays }} · 我的角色{{ currentOwned.myRole }}</div>
<div class="ps-section-title">📌 本周关键节点</div>
<ul class="ps-milestones">
<li v-for="m in currentOwned.milestones" :key="m.id">
<span>{{ m.title }}</span>
<span :class="`ps-time tone-${m.tone}`">{{ m.timeLabel }}</span>
</li>
</ul>
<div class="ps-section-title">👥 成员负载</div>
<ul class="ps-members">
<li v-for="m in currentOwned.members" :key="m.name">
<span class="ps-member-name">{{ m.name }}</span>
<div class="ps-bar">
<div class="ps-bar-inner" :class="`is-${m.level}`" :style="{ width: `${m.load}%` }" />
</div>
<span class="ps-member-load">{{ Math.round(m.load / 10) }}</span>
</li>
</ul>
</template>
</template>
</WorkbenchModuleCard>
</template>
<style scoped>
.workbench-project__tabs {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.workbench-project__desc {
margin: 0 0 14px;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.6;
}
.workbench-project__more-icon {
margin-left: 4px;
font-size: 16px;
}
.workbench-project__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.workbench-project__card {
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 18px;
background: linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 96%));
transition:
border-color 160ms ease,
transform 160ms ease;
}
.workbench-project__card:hover {
border-color: rgb(14 116 144 / 60%);
}
.workbench-project__card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.workbench-project__card-title-group {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.workbench-project__card-title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
line-height: 1.4;
}
.workbench-project__card-code {
color: rgb(100 116 139 / 92%);
font-size: 12px;
letter-spacing: 0.04em;
}
.workbench-project__card-status {
flex-shrink: 0;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.workbench-project__card-status--emerald {
background-color: rgb(220 252 231 / 96%);
color: rgb(5 150 105 / 96%);
}
.workbench-project__card-status--sky {
background-color: rgb(224 242 254 / 96%);
color: rgb(14 116 144 / 96%);
}
.workbench-project__card-status--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-project__card-role {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 12px;
background-color: rgb(241 245 249 / 80%);
}
.workbench-project__card-role-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__card-role-value {
color: rgb(15 23 42 / 98%);
font-size: 13px;
font-weight: 600;
}
.workbench-project__progress {
display: flex;
flex-direction: column;
gap: 8px;
}
.workbench-project__progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.workbench-project__progress-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__progress-value {
color: rgb(15 23 42 / 98%);
font-size: 18px;
line-height: 1;
letter-spacing: -0.02em;
}
.workbench-project__progress-bar {
height: 6px;
border-radius: 999px;
background-color: rgb(226 232 240 / 88%);
overflow: hidden;
}
.workbench-project__progress-bar-inner {
height: 100%;
border-radius: 999px;
transition: width 240ms ease;
}
.workbench-project__progress-bar-inner--emerald {
background: linear-gradient(90deg, rgb(5 150 105 / 92%), rgb(16 185 129 / 86%));
}
.workbench-project__progress-bar-inner--sky {
background: linear-gradient(90deg, rgb(14 116 144 / 92%), rgb(14 165 233 / 86%));
}
.workbench-project__progress-bar-inner--amber {
background: linear-gradient(90deg, rgb(217 119 6 / 92%), rgb(245 158 11 / 86%));
}
.workbench-project__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.workbench-project__footer-block {
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-project__footer-block--right {
align-items: flex-end;
}
.workbench-project__footer-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__footer-value {
color: rgb(15 23 42 / 98%);
font-size: 14px;
font-weight: 600;
}
.workbench-project__footer-sub {
color: rgb(190 18 60 / 94%);
font-size: 12px;
font-weight: 600;
}
@media (width <= 1280px) {
.workbench-project__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (width <= 600px) {
.workbench-project__grid {
grid-template-columns: 1fr;
}
}
/* ===== 我负责的:单对象深度详情样式 ===== */
.ps-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.ps-pin-label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.ps-pin {
width: 220px;
}
.ps-head--single .ps-single-name {
font-size: 14px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.ps-overview {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.ps-ring {
width: 50px;
height: 50px;
border-radius: 50%;
background: conic-gradient(
var(--el-color-success) 0 calc(var(--p) * 1%),
var(--el-fill-color) calc(var(--p) * 1%) 100%
);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ps-ring span {
background: #fff;
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
}
.ps-kpis {
flex: 1;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
text-align: center;
}
.ps-kpi b {
display: block;
font-size: 16px;
font-weight: 700;
color: var(--el-text-color-primary);
}
.ps-kpi b.is-danger {
color: var(--el-color-danger);
}
.ps-kpi span {
font-size: 11px;
color: var(--el-text-color-secondary);
}
.ps-sub {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
.ps-section-title {
margin-top: 12px;
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--el-text-color-secondary);
}
.ps-milestones,
.ps-members {
list-style: none;
margin: 0;
padding: 0;
}
.ps-milestones li {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
font-size: 13px;
}
.ps-milestones li:last-child {
border-bottom: none;
}
.ps-time {
font-size: 12px;
font-weight: 600;
}
.ps-time.tone-amber {
color: var(--el-color-warning);
}
.ps-time.tone-slate {
color: var(--el-text-color-secondary);
}
.ps-members li {
display: grid;
grid-template-columns: 60px 1fr 30px;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 12px;
}
.ps-bar {
height: 6px;
border-radius: 3px;
background: var(--el-fill-color);
overflow: hidden;
}
.ps-bar-inner {
height: 100%;
}
.ps-bar-inner.is-ok {
background: var(--el-color-success);
}
.ps-bar-inner.is-warn {
background: var(--el-color-warning);
}
.ps-bar-inner.is-over {
background: var(--el-color-danger);
}
.ps-member-load {
text-align: right;
color: var(--el-text-color-secondary);
font-size: 11px;
}
</style>