feat(projects): 工作台部分组件调成真实数据
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
import type { WorkbenchColumnId, WorkbenchModuleKey } from '../composables/use-workbench-modules';
|
||||
import { useWorkbenchModules } from '../composables/use-workbench-modules';
|
||||
|
||||
interface Props {
|
||||
columnId: WorkbenchColumnId;
|
||||
modules: WorkbenchModuleKey[];
|
||||
editing: boolean;
|
||||
collapsed: WorkbenchModuleKey[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modules', modules: WorkbenchModuleKey[]): void;
|
||||
(e: 'hide', key: WorkbenchModuleKey): void;
|
||||
(e: 'toggle-collapse', key: WorkbenchModuleKey): void;
|
||||
(e: 'open-settings', key: WorkbenchModuleKey): void;
|
||||
}>();
|
||||
|
||||
const { getModuleMeta } = useWorkbenchModules();
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.modules,
|
||||
set: (val: WorkbenchModuleKey[]) => emit('update:modules', val)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueDraggable
|
||||
v-model="modelValue"
|
||||
group="workbench-modules"
|
||||
:animation="180"
|
||||
handle=".module-drag-handle"
|
||||
:disabled="!editing"
|
||||
class="workbench-column"
|
||||
>
|
||||
<template v-for="key in modelValue" :key="key">
|
||||
<component
|
||||
:is="getModuleMeta(key)?.component"
|
||||
:module-key="key"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed.includes(key)"
|
||||
@hide="emit('hide', key)"
|
||||
@toggle-collapse="emit('toggle-collapse', key)"
|
||||
@open-settings="emit('open-settings', key)"
|
||||
/>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workbench-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchModuleCard' });
|
||||
|
||||
@@ -12,31 +12,25 @@ interface Props {
|
||||
icon?: string;
|
||||
badgeCount?: number;
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
hasSettings?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
icon: undefined,
|
||||
badgeCount: undefined,
|
||||
editing: false,
|
||||
collapsed: false,
|
||||
hasSettings: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle-collapse'): void;
|
||||
(e: 'hide'): void;
|
||||
(e: 'open-settings'): void;
|
||||
(e: 'refresh'): void;
|
||||
(e: 'navigate'): void;
|
||||
}>();
|
||||
|
||||
const showBody = computed(() => !props.collapsed);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="module-card" :class="{ 'is-editing': editing, 'is-collapsed': collapsed }">
|
||||
<section class="module-card" :class="{ 'is-editing': editing }">
|
||||
<header class="module-card__head">
|
||||
<span v-if="editing" class="module-drag-handle" title="拖动调整位置">
|
||||
<SvgIcon icon="mdi:drag-vertical" />
|
||||
@@ -49,21 +43,9 @@ const showBody = computed(() => !props.collapsed);
|
||||
<ElButton v-if="editing && hasSettings" link size="small" title="模块设置" @click="emit('open-settings')">
|
||||
<SvgIcon icon="mdi:cog-outline" />
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="!editing"
|
||||
link
|
||||
size="small"
|
||||
:title="collapsed ? '展开' : '折叠'"
|
||||
@click="emit('toggle-collapse')"
|
||||
>
|
||||
<SvgIcon :icon="collapsed ? 'mdi:chevron-down' : 'mdi:chevron-up'" />
|
||||
</ElButton>
|
||||
<ElButton v-if="!editing" link size="small" title="刷新" @click="emit('refresh')">
|
||||
<SvgIcon icon="mdi:refresh" />
|
||||
</ElButton>
|
||||
<ElButton v-if="!editing" link size="small" title="跳详情" @click="emit('navigate')">
|
||||
<SvgIcon icon="mdi:open-in-new" />
|
||||
</ElButton>
|
||||
<ElButton v-if="!editing && enterEditing" link size="small" title="编辑工作台布局" @click="enterEditing()">
|
||||
<SvgIcon icon="mdi:view-dashboard-edit-outline" />
|
||||
</ElButton>
|
||||
@@ -73,7 +55,7 @@ const showBody = computed(() => !props.collapsed);
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-show="showBody" class="module-card__body">
|
||||
<div class="module-card__body">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
@@ -84,7 +66,7 @@ const showBody = computed(() => !props.collapsed);
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
min-height: 180px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -98,10 +80,6 @@ const showBody = computed(() => !props.collapsed);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.module-card.is-collapsed {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.module-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -111,10 +89,6 @@ const showBody = computed(() => !props.collapsed);
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.module-card.is-collapsed .module-card__head {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.module-drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--el-text-color-secondary);
|
||||
@@ -153,7 +127,10 @@ const showBody = computed(() => !props.collapsed);
|
||||
|
||||
.module-card__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type {
|
||||
WorkbenchColumnId,
|
||||
WorkbenchModuleCategory,
|
||||
WorkbenchModuleMeta
|
||||
} from '../composables/use-workbench-modules';
|
||||
import type { WorkbenchModuleCategory, WorkbenchModuleMeta } from '../composables/use-workbench-modules';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
@@ -13,7 +9,7 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
(e: 'add-module', key: WorkbenchModuleMeta['key'], column: WorkbenchColumnId): void;
|
||||
(e: 'add-module', key: WorkbenchModuleMeta['key']): void;
|
||||
}>();
|
||||
|
||||
// 模块库展示分三段:个人(含动作 / 快照)/ 管理 / 工具
|
||||
@@ -57,18 +53,13 @@ const groups = computed<Array<{ key: LibraryGroupKey; label: string; items: Work
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #default>
|
||||
<p class="hint">点击下方模块加入工作台(默认进左栏)。</p>
|
||||
<p class="hint">点击下方模块加入工作台(落到网格底部,可在编辑态拖动调整)。</p>
|
||||
<div v-if="hiddenMetas.length === 0" class="empty">所有模块都已显示</div>
|
||||
<div v-else class="library">
|
||||
<section v-for="group in groups" :key="group.key" class="library-group">
|
||||
<h4 class="library-group__title">{{ group.label }}</h4>
|
||||
<ul class="library-group__list">
|
||||
<li
|
||||
v-for="meta in group.items"
|
||||
:key="meta.key"
|
||||
class="library-item"
|
||||
@click="emit('add-module', meta.key, 'left')"
|
||||
>
|
||||
<li v-for="meta in group.items" :key="meta.key" class="library-item" @click="emit('add-module', meta.key)">
|
||||
<SvgIcon :icon="meta.icon" />
|
||||
<span class="library-item__name">{{ meta.displayName }}</span>
|
||||
</li>
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetMyExecutionPage } from '@/service/api';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
import { formatDateRange, getExecutionStatusTagType } from '@/views/project/project/execution/shared';
|
||||
import { type WorkbenchMyExecutionItem, buildWorkbenchMyExecutionItems } from '../homepage';
|
||||
import { workbenchMyExecutionMock } from '../mock';
|
||||
import { buildWorkbenchMyExecutionItems } from '../homepage';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchMyExecution' });
|
||||
|
||||
type MyExecutionItem = Api.Project.MyExecutionItem;
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const items = computed(() => buildWorkbenchMyExecutionItems(workbenchMyExecutionMock));
|
||||
const items = ref<MyExecutionItem[]>([]);
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||||
// pageSize=-1 一次拉取全部当前用户负责的进行中执行;状态/进度过滤由后端完成
|
||||
const { data, error } = await fetchGetMyExecutionPage({ pageNo: 1, pageSize: -1 });
|
||||
if (error) return;
|
||||
items.value = buildWorkbenchMyExecutionItems(data?.list ?? []);
|
||||
});
|
||||
|
||||
onMounted(refresh);
|
||||
|
||||
// 按项目归类:未完成执行数多的项目在前;项目内按计划结束日升序(更紧的在前)
|
||||
const groups = computed<Array<{ projectId: string; projectName: string; items: WorkbenchMyExecutionItem[] }>>(() => {
|
||||
const map = new Map<string, { projectId: string; projectName: string; items: WorkbenchMyExecutionItem[] }>();
|
||||
const groups = computed<Array<{ projectId: string; projectName: string; items: MyExecutionItem[] }>>(() => {
|
||||
const map = new Map<string, { projectId: string; projectName: string; items: MyExecutionItem[] }>();
|
||||
items.value.forEach(item => {
|
||||
if (!map.has(item.projectId)) {
|
||||
map.set(item.projectId, { projectId: item.projectId, projectName: item.projectName, items: [] });
|
||||
@@ -42,6 +53,26 @@ const groups = computed<Array<{ projectId: string; projectName: string; items: W
|
||||
return groupsArr.sort((a, b) => b.items.length - a.items.length);
|
||||
});
|
||||
|
||||
// 手风琴:单开,默认展开第一个项目(执行最多);展开项消失时回退到第一个
|
||||
const expandedProjectId = ref<string>('');
|
||||
watch(
|
||||
groups,
|
||||
list => {
|
||||
if (!list.length) {
|
||||
expandedProjectId.value = '';
|
||||
return;
|
||||
}
|
||||
if (!list.some(g => g.projectId === expandedProjectId.value)) {
|
||||
expandedProjectId.value = list[0].projectId;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function toggleProject(projectId: string) {
|
||||
expandedProjectId.value = expandedProjectId.value === projectId ? '' : projectId;
|
||||
}
|
||||
|
||||
function goProjectExecutionPool(projectId: string) {
|
||||
router.push({
|
||||
path: '/project/project/execution',
|
||||
@@ -49,7 +80,7 @@ function goProjectExecutionPool(projectId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
function goRequirementDetail(item: MyExecutionItem) {
|
||||
if (!item.projectRequirementId) return;
|
||||
router.push({
|
||||
path: '/project/project/requirement',
|
||||
@@ -67,27 +98,43 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
icon="mdi:flag-checkered"
|
||||
:badge-count="items.length"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div v-if="items.length" class="exec-groups">
|
||||
<section v-for="group in groups" :key="group.projectId" class="exec-group">
|
||||
<header class="exec-group__head">
|
||||
<div v-if="items.length" v-loading="loading" class="exec-groups">
|
||||
<section
|
||||
v-for="group in groups"
|
||||
:key="group.projectId"
|
||||
class="exec-group"
|
||||
:class="{ 'is-open': expandedProjectId === group.projectId }"
|
||||
>
|
||||
<header
|
||||
class="exec-group__head"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-expanded="expandedProjectId === group.projectId"
|
||||
@click="toggleProject(group.projectId)"
|
||||
@keydown.enter.prevent="toggleProject(group.projectId)"
|
||||
>
|
||||
<SvgIcon
|
||||
icon="mdi:chevron-right"
|
||||
class="exec-group__chevron"
|
||||
:class="{ 'is-open': expandedProjectId === group.projectId }"
|
||||
/>
|
||||
<SvgIcon icon="mdi:briefcase-outline" class="exec-group__icon" />
|
||||
<span
|
||||
class="exec-group__name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:title="`进入「${group.projectName}」执行池`"
|
||||
@click="goProjectExecutionPool(group.projectId)"
|
||||
@keydown.enter.prevent="goProjectExecutionPool(group.projectId)"
|
||||
>
|
||||
{{ group.projectName }}
|
||||
</span>
|
||||
<span class="exec-group__name" :title="group.projectName">{{ group.projectName }}</span>
|
||||
<span class="exec-group__count">{{ group.items.length }}</span>
|
||||
<ElButton
|
||||
link
|
||||
size="small"
|
||||
class="exec-group__go"
|
||||
:title="`进入「${group.projectName}」执行池`"
|
||||
@click.stop="goProjectExecutionPool(group.projectId)"
|
||||
>
|
||||
<SvgIcon icon="mdi:open-in-new" />
|
||||
</ElButton>
|
||||
</header>
|
||||
<ul class="exec-list">
|
||||
<ul v-show="expandedProjectId === group.projectId" class="exec-list">
|
||||
<li v-for="item in group.items" :key="item.id" class="exec-item">
|
||||
<div class="exec-head">
|
||||
<span class="exec-name" :title="item.executionName">{{ item.executionName }}</span>
|
||||
@@ -122,6 +169,7 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
</div>
|
||||
<div v-if="item.projectRequirementId && item.projectRequirementName" class="exec-meta__row">
|
||||
<SvgIcon icon="mdi:link-variant" class="exec-meta__icon" />
|
||||
<span class="exec-meta__label">需求</span>
|
||||
<span
|
||||
class="exec-meta__text exec-meta__link"
|
||||
role="button"
|
||||
@@ -139,28 +187,66 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无进行中的执行" :image-size="60" />
|
||||
<div v-else v-loading="loading" class="exec-empty">
|
||||
<ElEmpty description="暂无进行中的执行" :image-size="60" />
|
||||
</div>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exec-groups {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 6px;
|
||||
}
|
||||
.exec-empty {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.exec-group__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 2px 6px;
|
||||
border-bottom: 1px dashed var(--el-border-color-lighter);
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
/* 常驻分组底色,明显区别于下方执行项卡片 */
|
||||
background: var(--el-fill-color-light);
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition:
|
||||
background 0.16s ease,
|
||||
border-color 0.16s ease;
|
||||
}
|
||||
.exec-group__head:hover,
|
||||
.exec-group__head:focus-visible {
|
||||
background: var(--el-fill-color);
|
||||
outline: none;
|
||||
}
|
||||
.exec-group.is-open > .exec-group__head {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-left-color: var(--el-color-primary);
|
||||
}
|
||||
.exec-group__chevron {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
.exec-group__chevron.is-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.exec-group__icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 15px;
|
||||
/* 项目 icon 用主色,与项目管理业务域图标一致且更醒目 */
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.exec-group__name {
|
||||
flex: 1;
|
||||
@@ -168,17 +254,16 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-regular);
|
||||
color: var(--el-text-color-primary);
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
transition: color 0.16s ease;
|
||||
}
|
||||
.exec-group__name:hover,
|
||||
.exec-group__name:focus-visible {
|
||||
.exec-group.is-open .exec-group__name {
|
||||
color: var(--el-color-primary);
|
||||
outline: none;
|
||||
}
|
||||
.exec-group__go {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.exec-group__count {
|
||||
flex-shrink: 0;
|
||||
@@ -195,8 +280,8 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
}
|
||||
.exec-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 4px 0 2px;
|
||||
padding: 0 2px 0 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -252,6 +337,10 @@ function goRequirementDetail(item: WorkbenchMyExecutionItem) {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.exec-meta__label {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.exec-meta__text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -13,19 +13,21 @@ import {
|
||||
buildWorkbenchWeekWorklogView
|
||||
} from '../homepage';
|
||||
import { workbenchMyWeekWorklogMock, workbenchTeamWorklogMock } from '../mock';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchMyWeekWorklog' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
// EP type='week' 默认 firstDayOfWeek=7,从日历点选时返回当周"周日"。
|
||||
// 我们按 ISO 周(周一-周日)存储;遇到周日 +1 天再 startOf('isoWeek'),避免回退到上一周。
|
||||
function resolveIsoWeekStart(weekDate: Date | null) {
|
||||
@@ -302,12 +304,12 @@ watch(activeTab, async tab => {
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="工时"
|
||||
icon="mdi:timer-outline"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="ww-tabbar">
|
||||
<ElTabs v-model="activeTab" class="ww-tabs">
|
||||
@@ -327,7 +329,7 @@ watch(activeTab, async tab => {
|
||||
</div>
|
||||
|
||||
<!-- ============ 我的工时 tab ============ -->
|
||||
<div v-show="activeTab === 'my'">
|
||||
<div v-show="activeTab === 'my'" class="ww-tab-content">
|
||||
<template v-if="myView">
|
||||
<div class="ww-headline">
|
||||
<div class="ww-section-title">
|
||||
@@ -375,7 +377,7 @@ watch(activeTab, async tab => {
|
||||
</div>
|
||||
|
||||
<!-- ============ 团队工时 tab ============ -->
|
||||
<div v-show="activeTab === 'team'">
|
||||
<div v-show="activeTab === 'team'" class="ww-tab-content">
|
||||
<template v-if="teamView">
|
||||
<div class="tw-kpis">
|
||||
<div class="tw-kpi">
|
||||
@@ -444,6 +446,19 @@ watch(activeTab, async tab => {
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* tab 内容区填充剩余高度:flex 列布局,图表区自适应撑满,不写死高度、不内部滚动 */
|
||||
.ww-tab-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ww-tab-content :deep(.el-empty) {
|
||||
margin: auto;
|
||||
}
|
||||
.ww-tabs {
|
||||
flex: 1;
|
||||
@@ -468,12 +483,15 @@ watch(activeTab, async tab => {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ww-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@media (width <= 520px) {
|
||||
.ww-grid {
|
||||
@@ -488,6 +506,7 @@ watch(activeTab, async tab => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ww-section-title {
|
||||
@@ -508,7 +527,8 @@ watch(activeTab, async tab => {
|
||||
.ww-pie-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.ww-pie {
|
||||
width: 100%;
|
||||
@@ -517,7 +537,8 @@ watch(activeTab, async tab => {
|
||||
|
||||
.ww-bar {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.ww-bar-legend {
|
||||
display: flex;
|
||||
@@ -526,6 +547,7 @@ watch(activeTab, async tab => {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ww-bar-legend__item {
|
||||
display: inline-flex;
|
||||
@@ -547,6 +569,7 @@ watch(activeTab, async tab => {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ww-footer b {
|
||||
font-weight: 700;
|
||||
@@ -570,6 +593,7 @@ watch(activeTab, async tab => {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tw-kpi {
|
||||
display: flex;
|
||||
@@ -609,7 +633,8 @@ watch(activeTab, async tab => {
|
||||
|
||||
.tw-bar {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tw-footer {
|
||||
@@ -619,6 +644,7 @@ watch(activeTab, async tab => {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tw-footer b {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchNoticeNotification' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
|
||||
interface NoticeRow {
|
||||
id: string;
|
||||
title: string;
|
||||
timeLabel: string;
|
||||
}
|
||||
|
||||
interface NotificationRow {
|
||||
id: string;
|
||||
title: string;
|
||||
timeLabel: string;
|
||||
unread: boolean;
|
||||
}
|
||||
|
||||
const notices: NoticeRow[] = [
|
||||
{ id: 'n1', title: '【运维】本周六 02:00-04:00 数据库主从切换', timeLabel: '2 天前' },
|
||||
{ id: 'n2', title: '【HR】Q2 OKR 复盘截止 06-05', timeLabel: '3 天前' },
|
||||
{ id: 'n3', title: '【流程】工单 SLA 新规则即将上线', timeLabel: '1 周前' }
|
||||
];
|
||||
|
||||
const notifications: NotificationRow[] = [
|
||||
{ id: 'm1', title: '你被指派为执行「迭代 24.06」负责人', timeLabel: '10min 前', unread: true },
|
||||
{ id: 'm2', title: '任务「SSO 改造」状态变更:开发中 → 待验收', timeLabel: '2h 前', unread: true },
|
||||
{ id: 'm3', title: '需求「多币种支持」评审通过', timeLabel: '昨日', unread: false }
|
||||
];
|
||||
|
||||
const unreadCount = computed(() => notifications.filter(n => n.unread).length);
|
||||
|
||||
// mock 阶段:交互函数留占位,等后端接口落地后接通
|
||||
function handleOpenNotification(row: NotificationRow) {
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO: 跳对应业务对象详情
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[notification] open', row.id);
|
||||
}
|
||||
|
||||
function handleMarkRead(row: NotificationRow) {
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO: 调标已读接口
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[notification] mark-read', row.id);
|
||||
}
|
||||
|
||||
function handleMarkAllRead() {
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO: 调一键全部已读接口
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[notification] mark-all-read');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
title="公告与通知"
|
||||
icon="mdi:bullhorn-outline"
|
||||
:badge-count="unreadCount || undefined"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
>
|
||||
<div class="nn-grid">
|
||||
<!-- 左 1/3:公告(只读,露头扫一眼) -->
|
||||
<section class="nn-col nn-col--notice">
|
||||
<header class="nn-h">
|
||||
<SvgIcon icon="mdi:bullhorn-outline" class="nn-h__icon" />
|
||||
<span class="nn-h__title">公告</span>
|
||||
<span class="nn-h__count">{{ notices.length }}</span>
|
||||
</header>
|
||||
<ul class="nn-list">
|
||||
<li v-for="row in notices" :key="row.id" class="nn-notice">
|
||||
<div class="nn-notice__title">{{ row.title }}</div>
|
||||
<div class="nn-notice__time">{{ row.timeLabel }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 右 2/3:通知(可操作,按行跳详情/标已读) -->
|
||||
<section class="nn-col nn-col--notify">
|
||||
<header class="nn-h">
|
||||
<SvgIcon icon="mdi:bell-outline" class="nn-h__icon" />
|
||||
<span class="nn-h__title">通知</span>
|
||||
<span v-if="unreadCount > 0" class="nn-h__count is-unread">未读 {{ unreadCount }}</span>
|
||||
<span v-else class="nn-h__count">{{ notifications.length }}</span>
|
||||
<ElButton v-if="unreadCount > 0" link size="small" class="nn-h__action" @click="handleMarkAllRead">
|
||||
全部已读
|
||||
</ElButton>
|
||||
</header>
|
||||
<ul class="nn-list">
|
||||
<li
|
||||
v-for="row in notifications"
|
||||
:key="row.id"
|
||||
class="nn-notify"
|
||||
:class="{ 'is-unread': row.unread }"
|
||||
@click="handleOpenNotification(row)"
|
||||
>
|
||||
<span v-if="row.unread" class="nn-notify__dot" />
|
||||
<span class="nn-notify__title">{{ row.title }}</span>
|
||||
<span class="nn-notify__time">{{ row.timeLabel }}</span>
|
||||
<span class="nn-notify__actions">
|
||||
<ElTooltip v-if="row.unread" content="标为已读" placement="top">
|
||||
<button class="nn-notify__act" @click.stop="handleMarkRead(row)">
|
||||
<SvgIcon icon="mdi:check" />
|
||||
</button>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="跳详情" placement="top">
|
||||
<button class="nn-notify__act" @click.stop="handleOpenNotification(row)">
|
||||
<SvgIcon icon="mdi:open-in-new" />
|
||||
</button>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nn-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.nn-col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nn-h {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.nn-h__icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.nn-h__title {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.nn-h__count {
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--el-fill-color);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.nn-h__count.is-unread {
|
||||
background: var(--el-color-danger);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nn-h__action {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.nn-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.nn-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.nn-list::-webkit-scrollbar-thumb {
|
||||
background: var(--el-fill-color-darker);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.nn-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--el-border-color);
|
||||
}
|
||||
|
||||
/* 公告行:纯阅读 + 标题 2 行 clamp */
|
||||
.nn-notice {
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px dashed var(--el-border-color-lighter);
|
||||
}
|
||||
.nn-notice:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.nn-notice__title {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
.nn-notice__time {
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 通知行:可操作 + hover 浮出动作按钮 */
|
||||
.nn-notify {
|
||||
display: grid;
|
||||
grid-template-columns: 8px 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px;
|
||||
margin: 0 -8px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms;
|
||||
}
|
||||
.nn-notify + .nn-notify {
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
}
|
||||
.nn-notify:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.nn-notify__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
.nn-notify:not(.is-unread) .nn-notify__dot {
|
||||
background: transparent;
|
||||
}
|
||||
.nn-notify__title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.nn-notify.is-unread .nn-notify__title {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.nn-notify__time {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nn-notify__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms;
|
||||
}
|
||||
.nn-notify:hover .nn-notify__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.nn-notify__act {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background-color 120ms;
|
||||
}
|
||||
.nn-notify__act:hover {
|
||||
background: var(--el-fill-color);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchProductSnapshot' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
interface ProductOption {
|
||||
id: string;
|
||||
@@ -69,12 +71,12 @@ function onChange(id: string) {
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="产品深度快照"
|
||||
icon="mdi:image-area-close"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="ps-head">
|
||||
<span class="ps-pin-label">当前产品:</span>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { fetchGetMyOwnedProjectPage, fetchGetMyParticipatedProjectPage } from '@/service/api';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { buildWorkbenchOwnedProjectItems, buildWorkbenchProjectItems } from '../homepage';
|
||||
import { workbenchOwnedProjectMock, workbenchProjectMock } from '../mock';
|
||||
import {
|
||||
type WorkbenchOwnedProjectView,
|
||||
type WorkbenchParticipatedProjectView,
|
||||
buildWorkbenchOwnedProjects,
|
||||
buildWorkbenchParticipatedProjects
|
||||
} from '../homepage';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchProjectGrid' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
|
||||
defineEmits<{
|
||||
(e: 'hide'): void;
|
||||
(e: 'toggle-collapse'): void;
|
||||
}>();
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
@@ -25,10 +29,26 @@ type ProjectViewKey = 'participated' | 'owned';
|
||||
|
||||
const activeView = ref<ProjectViewKey>('participated');
|
||||
|
||||
const participatedItems = computed(() => buildWorkbenchProjectItems(workbenchProjectMock));
|
||||
const ownedItems = computed(() => buildWorkbenchOwnedProjectItems(workbenchOwnedProjectMock));
|
||||
const participatedItems = ref<WorkbenchParticipatedProjectView[]>([]);
|
||||
const ownedItems = ref<WorkbenchOwnedProjectView[]>([]);
|
||||
|
||||
const currentOwnedId = ref<string>(ownedItems.value[0]?.id ?? '');
|
||||
const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||||
// pageSize=-1 一次拉全部;列表已由后端按"进行中 + 创建时间升序"过滤排序
|
||||
const [participated, owned] = await Promise.all([
|
||||
fetchGetMyParticipatedProjectPage({ pageNo: 1, pageSize: -1 }),
|
||||
fetchGetMyOwnedProjectPage({ pageNo: 1, pageSize: -1 })
|
||||
]);
|
||||
if (!participated.error) {
|
||||
participatedItems.value = buildWorkbenchParticipatedProjects(participated.data?.list ?? []);
|
||||
}
|
||||
if (!owned.error) {
|
||||
ownedItems.value = buildWorkbenchOwnedProjects(owned.data?.list ?? []);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(refresh);
|
||||
|
||||
const currentOwnedId = ref<string>('');
|
||||
watch(ownedItems, list => {
|
||||
if (!list.find(item => item.id === currentOwnedId.value)) {
|
||||
currentOwnedId.value = list[0]?.id ?? '';
|
||||
@@ -36,6 +56,24 @@ watch(ownedItems, list => {
|
||||
});
|
||||
const currentOwned = computed(() => ownedItems.value.find(item => item.id === currentOwnedId.value) ?? null);
|
||||
|
||||
// 成员负载:柱长按组内最高任务数归一(相对负载),颜色按绝对任务数分档(与团队负载 6/4 阈值一致)
|
||||
function resolveMemberLoadLevel(activeTaskCount: number) {
|
||||
if (activeTaskCount >= 6) return 'over';
|
||||
if (activeTaskCount >= 4) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
const ownedMembersView = computed(() => {
|
||||
const members = currentOwned.value?.members ?? [];
|
||||
const maxTaskCount = members.reduce((max, member) => Math.max(max, member.activeTaskCount), 0);
|
||||
return members.map(member => ({
|
||||
userId: member.userId,
|
||||
userName: member.userName,
|
||||
activeTaskCount: member.activeTaskCount,
|
||||
barPercent: maxTaskCount > 0 ? Math.round((member.activeTaskCount / maxTaskCount) * 100) : 0,
|
||||
level: resolveMemberLoadLevel(member.activeTaskCount)
|
||||
}));
|
||||
});
|
||||
|
||||
function handleEnterProjectList() {
|
||||
routerPushByKey('project_list');
|
||||
}
|
||||
@@ -43,12 +81,12 @@ function handleEnterProjectList() {
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="我的项目"
|
||||
icon="mdi:briefcase-outline"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="workbench-project__tabs">
|
||||
<ElRadioGroup v-model="activeView" size="small">
|
||||
@@ -60,122 +98,125 @@ function handleEnterProjectList() {
|
||||
<SvgIcon icon="mdi:arrow-right-thin" class="workbench-project__more-icon" />
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="workbench-project__scroll">
|
||||
<!-- 我参与的:网格视图 -->
|
||||
<template v-if="activeView === 'participated'">
|
||||
<p class="workbench-project__desc">直接看每个项目的当前进度、我的角色与未完成任务</p>
|
||||
|
||||
<!-- 我参与的:网格视图 -->
|
||||
<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 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" :title="item.name">{{ item.name }}</h4>
|
||||
<span v-if="item.code" class="workbench-project__card-code">{{ item.code }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="workbench-project__card-status"
|
||||
:class="`workbench-project__card-status--${item.statusTone}`"
|
||||
>
|
||||
{{ item.statusName || '进行中' }}
|
||||
</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__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 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__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 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>
|
||||
<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>
|
||||
</article>
|
||||
</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>
|
||||
<ElEmpty v-else description="暂未参与任何项目" :image-size="72" />
|
||||
</template>
|
||||
</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 v-if="currentOwned.plannedEndDate" class="ps-sub">
|
||||
计划结束 {{ currentOwned.plannedEndDate }}
|
||||
<template v-if="currentOwned.remainingDays !== null">
|
||||
·
|
||||
{{
|
||||
currentOwned.remainingDays >= 0
|
||||
? `剩 ${currentOwned.remainingDays} 天`
|
||||
: `已逾期 ${-currentOwned.remainingDays} 天`
|
||||
}}
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="ps-sub">未设置计划结束日期</div>
|
||||
|
||||
<div class="ps-section-title">👥 成员负载</div>
|
||||
<ul class="ps-members">
|
||||
<li v-for="m in ownedMembersView" :key="m.userId">
|
||||
<span class="ps-member-name" :title="m.userName || ''">{{ m.userName || '—' }}</span>
|
||||
<div class="ps-bar">
|
||||
<div class="ps-bar-inner" :class="`is-${m.level}`" :style="{ width: `${m.barPercent}%` }" />
|
||||
</div>
|
||||
<span class="ps-member-tasks">{{ m.activeTaskCount }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
@@ -186,6 +227,13 @@ function handleEnterProjectList() {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workbench-project__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.workbench-project__desc {
|
||||
@@ -202,7 +250,8 @@ function handleEnterProjectList() {
|
||||
|
||||
.workbench-project__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
/* 按容器宽度自适应列数(而非视口):minmax 180 让 w7≈588px 容器排 3 列,auto-fit 平分不留白 */
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@@ -239,6 +288,11 @@ function handleEnterProjectList() {
|
||||
|
||||
.workbench-project__card-title {
|
||||
margin: 0;
|
||||
/* 标题最长等效 10 个汉字宽度(10em≈160px),超出省略号,hover 看完整名 */
|
||||
max-width: 10em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: rgb(15 23 42 / 98%);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
@@ -356,10 +410,6 @@ function handleEnterProjectList() {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.workbench-project__footer-block--right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.workbench-project__footer-label {
|
||||
color: rgb(100 116 139 / 92%);
|
||||
font-size: 12px;
|
||||
@@ -377,18 +427,6 @@ function handleEnterProjectList() {
|
||||
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;
|
||||
@@ -472,32 +510,11 @@ function handleEnterProjectList() {
|
||||
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;
|
||||
@@ -506,6 +523,12 @@ function handleEnterProjectList() {
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ps-member-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.ps-bar {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
@@ -514,6 +537,7 @@ function handleEnterProjectList() {
|
||||
}
|
||||
.ps-bar-inner {
|
||||
height: 100%;
|
||||
transition: width 240ms ease;
|
||||
}
|
||||
.ps-bar-inner.is-ok {
|
||||
background: var(--el-color-success);
|
||||
@@ -524,7 +548,7 @@ function handleEnterProjectList() {
|
||||
.ps-bar-inner.is-over {
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
.ps-member-load {
|
||||
.ps-member-tasks {
|
||||
text-align: right;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
import { computed } from 'vue';
|
||||
import { buildWorkbenchProjectHealthCards } from '../homepage';
|
||||
import { workbenchProjectHealthMock } from '../mock';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
const projectCards = computed(() => buildWorkbenchProjectHealthCards(workbenchProjectHealthMock));
|
||||
|
||||
@@ -32,13 +34,13 @@ const productCards: ProductHealth[] = [
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="产品 / 项目健康度"
|
||||
icon="mdi:heart-pulse"
|
||||
:badge-count="projectCards.length + productCards.length"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="demo-banner">
|
||||
<SvgIcon icon="mdi:alert-circle-outline" class="demo-banner__icon" />
|
||||
|
||||
@@ -78,6 +78,7 @@ function handleConfirm() {
|
||||
direction="rtl"
|
||||
size="380px"
|
||||
title="选择快捷入口菜单"
|
||||
append-to-body
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #default>
|
||||
|
||||
@@ -5,28 +5,28 @@ import { objectContextDomainConfigs } from '@/constants/object-context';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useWorkbenchStore } from '@/store/modules/workbench';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
import WorkbenchShortcutPicker from './workbench-shortcut-picker.vue';
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
editing: false,
|
||||
collapsed: false
|
||||
editing: false
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
(e: 'hide'): void;
|
||||
(e: 'toggle-collapse'): void;
|
||||
}>();
|
||||
|
||||
const routeStore = useRouteStore();
|
||||
const workbench = useWorkbenchStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
interface FlatMenu {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -78,22 +78,26 @@ function handleClick(key: string) {
|
||||
function handleConfirm(keys: string[]) {
|
||||
workbench.updateModuleSettings('shortcut', { menuKeys: keys });
|
||||
}
|
||||
|
||||
function handleRemove(key: string) {
|
||||
workbench.updateModuleSettings('shortcut', { menuKeys: selectedKeys.value.filter(k => k !== key) });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="快捷入口"
|
||||
icon="mdi:rocket-launch-outline"
|
||||
:badge-count="selected.length || undefined"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
has-settings
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@open-settings="openPicker"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div v-if="selected.length === 0" class="shortcut-empty">
|
||||
<ElEmpty description="还未选择菜单" :image-size="60">
|
||||
<ElEmpty description="还未选择菜单" :image-size="48">
|
||||
<ElButton type="primary" size="small" @click="openPicker">+ 选择菜单</ElButton>
|
||||
</ElEmpty>
|
||||
</div>
|
||||
@@ -104,6 +108,9 @@ function handleConfirm(keys: string[]) {
|
||||
</ElIcon>
|
||||
<SvgIcon v-else icon="mdi:link-variant" class="shortcut-item__icon" />
|
||||
<span class="shortcut-item__label">{{ item.label }}</span>
|
||||
<span class="shortcut-item__remove" title="移除此快捷入口" @click.stop="handleRemove(item.key)">
|
||||
<SvgIcon icon="mdi:close" />
|
||||
</span>
|
||||
</button>
|
||||
<button class="shortcut-item shortcut-item--add" title="添加快捷入口" @click="openPicker">
|
||||
<SvgIcon icon="mdi:plus" />
|
||||
@@ -120,9 +127,14 @@ function handleConfirm(keys: string[]) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -137,6 +149,34 @@ function handleConfirm(keys: string[]) {
|
||||
transition: all 120ms;
|
||||
}
|
||||
|
||||
.shortcut-item__remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 120ms,
|
||||
color 120ms,
|
||||
background-color 120ms;
|
||||
}
|
||||
|
||||
.shortcut-item:hover .shortcut-item__remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shortcut-item__remove:hover {
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.shortcut-item__icon {
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
@@ -169,6 +209,15 @@ function handleConfirm(keys: string[]) {
|
||||
}
|
||||
|
||||
.shortcut-empty {
|
||||
padding: 20px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 压缩 ElEmpty 默认大 padding,空态在最小高度下也不溢出 */
|
||||
.shortcut-empty :deep(.el-empty) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,18 @@ import { computed } from 'vue';
|
||||
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
||||
import { type WorkbenchTeamLoadLevel, buildWorkbenchTeamLoadView } from '../homepage';
|
||||
import { workbenchTeamLoadMock } from '../mock';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
defineOptions({ name: 'WorkbenchTeamLoad' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>();
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
const view = computed(() => buildWorkbenchTeamLoadView(workbenchTeamLoadMock));
|
||||
|
||||
@@ -31,12 +33,12 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="团队负载"
|
||||
icon="mdi:scale-balance"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="tl-kpis">
|
||||
<div class="tl-kpi">
|
||||
@@ -150,8 +152,9 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.tl-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
sortWorkbenchTodoItemsByPriority
|
||||
} from '../homepage';
|
||||
import { workbenchTodoMock } from '../mock';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
@@ -36,18 +37,18 @@ defineOptions({ name: 'WorkbenchTodoPanel' });
|
||||
|
||||
interface Props {
|
||||
editing?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { editing: false, collapsed: false });
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
|
||||
defineEmits<{
|
||||
(e: 'hide'): void;
|
||||
(e: 'toggle-collapse'): void;
|
||||
}>();
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
const activeTab = ref<WorkbenchTodoMainTab>('all');
|
||||
@@ -333,12 +334,12 @@ onMounted(loadOvertimeApprovalItems);
|
||||
|
||||
<template>
|
||||
<WorkbenchModuleCard
|
||||
v-loading="loading"
|
||||
title="我的待办"
|
||||
icon="mdi:clipboard-text-clock-outline"
|
||||
:editing="editing"
|
||||
:collapsed="collapsed"
|
||||
@hide="$emit('hide')"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<div class="workbench-todo__tabs">
|
||||
<div class="workbench-todo__tabs-group">
|
||||
@@ -493,10 +494,12 @@ onMounted(loadOvertimeApprovalItems);
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- append-to-body:脱离 grid item 的 transform 容器,弹窗才能正常全屏居中 -->
|
||||
<PersonalItemOperateDialog
|
||||
v-model:visible="addDialogVisible"
|
||||
operate-type="add"
|
||||
:row-data="null"
|
||||
append-to-body
|
||||
@submitted="handleAddSubmitted"
|
||||
/>
|
||||
|
||||
@@ -691,9 +694,11 @@ onMounted(loadOvertimeApprovalItems);
|
||||
}
|
||||
|
||||
.workbench-todo__content {
|
||||
min-height: 400px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.workbench-todo__content :deep(.el-empty) {
|
||||
|
||||
Reference in New Issue
Block a user