feat(projects): 工作台小组件设计
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { buildWorkbenchProjectItems } from '../homepage';
|
||||
import { workbenchProjectMock } from '../mock';
|
||||
import { buildWorkbenchOwnedProjectItems, buildWorkbenchProjectItems } from '../homepage';
|
||||
import { workbenchOwnedProjectMock, workbenchProjectMock } from '../mock';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchProjectGrid' });
|
||||
@@ -21,7 +21,20 @@ defineEmits<{
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const items = computed(() => buildWorkbenchProjectItems(workbenchProjectMock));
|
||||
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');
|
||||
@@ -30,84 +43,153 @@ function handleEnterProjectList() {
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
title="我参与的项目"
|
||||
title="我的项目"
|
||||
icon="mdi:briefcase-outline"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
>
|
||||
<div class="workbench-project__subheader">
|
||||
<p class="workbench-project__desc">直接看每个项目的当前进度、我的角色与未完成任务</p>
|
||||
<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>
|
||||
|
||||
<div v-if="items.length" class="workbench-project__grid">
|
||||
<article v-for="item in items" :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>
|
||||
<!-- 我参与的:网格视图 -->
|
||||
<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>
|
||||
<span class="workbench-project__card-status" :class="`workbench-project__card-status--${item.statusTone}`">
|
||||
{{ item.statusLabel }}
|
||||
</span>
|
||||
|
||||
<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="workbench-project__card-role">
|
||||
<span class="workbench-project__card-role-label">我的角色</span>
|
||||
<strong class="workbench-project__card-role-value">{{ item.myRole }}</strong>
|
||||
<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="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="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="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" />
|
||||
<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__subheader {
|
||||
.workbench-project__tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.workbench-project__desc {
|
||||
margin: 0;
|
||||
margin: 0 0 14px;
|
||||
color: rgb(100 116 139 / 92%);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
@@ -306,4 +388,145 @@ function handleEnterProjectList() {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user