feat(projects): 工作台接口切换为真实数据
This commit is contained in:
@@ -1,19 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onActivated, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import dayjs from 'dayjs';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { fetchGetMyWorklogWeek, fetchGetTeamWorklogWeek } from '@/service/api';
|
||||
import { type ECOption, useEcharts } from '@/hooks/common/echarts';
|
||||
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
||||
import {
|
||||
type WorkbenchTeamWorklogView,
|
||||
type WorkbenchWeekWorklogView,
|
||||
type WorkbenchWorklogDistributionItem,
|
||||
buildWorkbenchTeamWorklogView,
|
||||
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' });
|
||||
@@ -26,7 +23,8 @@ defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
const myWeekData = ref<Api.Project.MyWorklogWeekResult | null>(null);
|
||||
const teamWeekData = ref<Api.Project.TeamWorklogWeekResult | null>(null);
|
||||
|
||||
// EP type='week' 默认 firstDayOfWeek=7,从日历点选时返回当周"周日"。
|
||||
// 我们按 ISO 周(周一-周日)存储;遇到周日 +1 天再 startOf('isoWeek'),避免回退到上一周。
|
||||
@@ -50,25 +48,68 @@ const selectedWeekStart = computed(() => {
|
||||
return aligned ? aligned.format('YYYY-MM-DD') : '';
|
||||
});
|
||||
|
||||
// 周切换必须重拉,不能被"并发守卫"拦掉,故不走 useWorkbenchRefresh:
|
||||
// 自管 loading + 请求序号,旧响应(慢请求落后于新一次切周)直接丢弃
|
||||
const loading = ref(false);
|
||||
let requestSeq = 0;
|
||||
|
||||
async function loadWorklogWeek() {
|
||||
const weekStart = selectedWeekStart.value;
|
||||
if (!weekStart) return;
|
||||
|
||||
requestSeq += 1;
|
||||
const seq = requestSeq;
|
||||
loading.value = true;
|
||||
try {
|
||||
const [myResult, teamResult] = await Promise.all([
|
||||
fetchGetMyWorklogWeek({ weekStart }),
|
||||
fetchGetTeamWorklogWeek({ weekStart })
|
||||
]);
|
||||
|
||||
if (seq !== requestSeq) return;
|
||||
myWeekData.value = myResult.error || !myResult.data ? null : myResult.data;
|
||||
teamWeekData.value = teamResult.error || !teamResult.data ? null : teamResult.data;
|
||||
} finally {
|
||||
if (seq === requestSeq) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
loadWorklogWeek();
|
||||
}
|
||||
|
||||
type TabKey = 'my' | 'team';
|
||||
const activeTab = ref<TabKey>('my');
|
||||
|
||||
// ============ 我的工时 ============
|
||||
// 周切换(含初始)拉取两 tab 数据;竞态由 loadWorklogWeek 内请求序号兜底
|
||||
watch(selectedWeekStart, loadWorklogWeek, { immediate: true });
|
||||
|
||||
const myView = computed<WorkbenchWeekWorklogView | null>(() => {
|
||||
if (!selectedWeekStart.value) return null;
|
||||
if (selectedWeekStart.value === workbenchMyWeekWorklogMock.current.weekStart) {
|
||||
return buildWorkbenchWeekWorklogView(workbenchMyWeekWorklogMock.current);
|
||||
// 工作台路由 keepAlive:切回时组件不重挂载,immediate watch 不再触发。
|
||||
// 每次激活归位到当前周并重拉;首次激活与挂载同拍(上面 immediate 已拉过),跳过避免双发
|
||||
let activatedOnce = false;
|
||||
onActivated(() => {
|
||||
if (!activatedOnce) {
|
||||
activatedOnce = true;
|
||||
return;
|
||||
}
|
||||
if (selectedWeekStart.value === workbenchMyWeekWorklogMock.previous.weekStart) {
|
||||
return buildWorkbenchWeekWorklogView(workbenchMyWeekWorklogMock.previous);
|
||||
const currentWeekDate = dayjs().startOf('isoWeek');
|
||||
if (selectedWeekStart.value === currentWeekDate.format('YYYY-MM-DD')) {
|
||||
// 周未变时 watch 不会触发,手动重拉取最新填报
|
||||
loadWorklogWeek();
|
||||
} else {
|
||||
selectedWeekDate.value = currentWeekDate.toDate();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const isCurrentWeek = computed(() => selectedWeekStart.value === workbenchMyWeekWorklogMock.current.weekStart);
|
||||
// ============ 我的工时 ============
|
||||
|
||||
const totalLabel = computed(() => (isCurrentWeek.value ? '累计' : '上周累计'));
|
||||
const myView = computed(() => (myWeekData.value ? buildWorkbenchWeekWorklogView(myWeekData.value) : null));
|
||||
|
||||
const isCurrentWeek = computed(() => selectedWeekStart.value === dayjs().startOf('isoWeek').format('YYYY-MM-DD'));
|
||||
|
||||
const totalLabel = computed(() => (isCurrentWeek.value ? '累计' : '该周累计'));
|
||||
|
||||
const deltaInfo = computed(() => {
|
||||
if (!myView.value) return null;
|
||||
@@ -80,9 +121,7 @@ const deltaInfo = computed(() => {
|
||||
return { text: `达成 ${completionRate}%`, tone: completionRate >= 100 ? ('success' as const) : ('muted' as const) };
|
||||
});
|
||||
|
||||
// 每日柱图的"按天/按周"分色(与项目色无关,保留本地常量)
|
||||
const DAY_BAR_COLOR = '#409EFF';
|
||||
const WEEK_BAR_COLOR = '#A0CFFF';
|
||||
|
||||
interface DistributionRow extends WorkbenchWorklogDistributionItem {
|
||||
color: string;
|
||||
@@ -155,10 +194,8 @@ function buildMyBarOption(): ECOption {
|
||||
formatter: (rawParams: any) => {
|
||||
const params = Array.isArray(rawParams) ? rawParams : [rawParams];
|
||||
const dayName = params[0]?.axisValue ?? '';
|
||||
const dayPart = params.find((p: any) => p.seriesName === '按天填')?.value ?? 0;
|
||||
const weekPart = params.find((p: any) => p.seriesName === '按周均分')?.value ?? 0;
|
||||
const total = Number(dayPart) + Number(weekPart);
|
||||
return `周${dayName}:${total}h<br/>按天填 ${dayPart}h<br/>按周均分 ${weekPart}h`;
|
||||
const total = Number(params[0]?.value ?? 0);
|
||||
return `周${dayName}:${total}h`;
|
||||
}
|
||||
},
|
||||
grid: { left: 28, right: 8, top: 16, bottom: 24, containLabel: false },
|
||||
@@ -176,20 +213,11 @@ function buildMyBarOption(): ECOption {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '按天填',
|
||||
name: '每日工时',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barWidth: 18,
|
||||
data: v?.dailyByDay ?? [],
|
||||
itemStyle: { color: DAY_BAR_COLOR, borderRadius: [0, 0, 2, 2] }
|
||||
},
|
||||
{
|
||||
name: '按周均分',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barWidth: 18,
|
||||
data: v?.dailyByWeekAvg ?? [],
|
||||
itemStyle: { color: WEEK_BAR_COLOR, borderRadius: [2, 2, 0, 0] }
|
||||
data: v?.dailyHours ?? [],
|
||||
itemStyle: { color: DAY_BAR_COLOR, borderRadius: [2, 2, 0, 0] }
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -208,16 +236,7 @@ const { domRef: myBarRef, updateOptions: updateMyBar } = useEcharts(buildMyBarOp
|
||||
|
||||
// ============ 团队工时 ============
|
||||
|
||||
const teamView = computed<WorkbenchTeamWorklogView | null>(() => {
|
||||
if (!selectedWeekStart.value) return null;
|
||||
if (selectedWeekStart.value === workbenchTeamWorklogMock.current.weekStart) {
|
||||
return buildWorkbenchTeamWorklogView(workbenchTeamWorklogMock.current);
|
||||
}
|
||||
if (selectedWeekStart.value === workbenchTeamWorklogMock.previous.weekStart) {
|
||||
return buildWorkbenchTeamWorklogView(workbenchTeamWorklogMock.previous);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const teamView = computed(() => (teamWeekData.value ? buildWorkbenchTeamWorklogView(teamWeekData.value) : null));
|
||||
|
||||
const teamSeriesWithColor = computed(() =>
|
||||
(teamView.value?.seriesMatrix ?? []).map(s => ({ ...s, color: getWorkbenchItemColor(s.key, s.kind) }))
|
||||
@@ -339,6 +358,9 @@ watch(activeTab, async tab => {
|
||||
<div class="ww-section-title">
|
||||
<SvgIcon icon="mdi:calendar-week" class="ww-section-icon" />
|
||||
<span>每日工时</span>
|
||||
<ElTooltip content="系统按填报日期段均摊到工作日的推算值(周末份额计入周五),非逐日实填" placement="top">
|
||||
<SvgIcon icon="mdi:information-outline" class="ww-section-info" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -351,16 +373,6 @@ watch(activeTab, async tab => {
|
||||
|
||||
<div class="ww-block">
|
||||
<div ref="myBarRef" class="ww-bar" />
|
||||
<div class="ww-bar-legend">
|
||||
<span class="ww-bar-legend__item">
|
||||
<span class="ww-bar-legend__swatch" :style="{ background: DAY_BAR_COLOR }" />
|
||||
按天填
|
||||
</span>
|
||||
<span class="ww-bar-legend__item">
|
||||
<span class="ww-bar-legend__swatch" :style="{ background: WEEK_BAR_COLOR }" />
|
||||
按周均分
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +422,7 @@ watch(activeTab, async tab => {
|
||||
{{ teamView.highCount }}
|
||||
<span class="tw-kpi__unit">人</span>
|
||||
</span>
|
||||
<span class="tw-kpi__sub">超 45h</span>
|
||||
<span class="tw-kpi__sub">超 {{ teamView.overtimeThreshold }}h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -523,6 +535,12 @@ watch(activeTab, async tab => {
|
||||
color: var(--el-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ww-section-info {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.ww-pie-wrap {
|
||||
position: relative;
|
||||
@@ -540,26 +558,6 @@ watch(activeTab, async tab => {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.ww-bar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ww-bar-legend__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.ww-bar-legend__swatch {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ww-footer {
|
||||
display: flex;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { fetchGetMyTeamLoad } from '@/service/api';
|
||||
import { getWorkbenchItemColor } from '../composables/use-workbench-colors';
|
||||
import { type WorkbenchTeamLoadLevel, buildWorkbenchTeamLoadView } from '../homepage';
|
||||
import { workbenchTeamLoadMock } from '../mock';
|
||||
import {
|
||||
type WorkbenchTeamLoadLevel,
|
||||
type WorkbenchTeamLoadMemberSource,
|
||||
buildWorkbenchTeamLoadView
|
||||
} from '../homepage';
|
||||
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
|
||||
import WorkbenchModuleCard from './workbench-module-card.vue';
|
||||
|
||||
@@ -14,21 +18,40 @@ interface Props {
|
||||
withDefaults(defineProps<Props>(), { editing: false });
|
||||
defineEmits<{ (e: 'hide'): void }>();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
const teamLoadMembers = ref<Api.Project.TeamLoadMember[]>([]);
|
||||
|
||||
const view = computed(() => buildWorkbenchTeamLoadView(workbenchTeamLoadMock));
|
||||
async function loadTeamLoad() {
|
||||
const { error, data } = await fetchGetMyTeamLoad();
|
||||
teamLoadMembers.value = error || !data ? [] : data.members;
|
||||
}
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh(loadTeamLoad);
|
||||
|
||||
// 契约:members[0] 恒为当前用户,展示加「(我)」后缀(builder 内部会按负载等级重排,标记跟名字走)
|
||||
function toTeamLoadSource(member: Api.Project.TeamLoadMember, index: number): WorkbenchTeamLoadMemberSource {
|
||||
return {
|
||||
memberId: member.userId,
|
||||
memberName: index === 0 ? `${member.userNickname}(我)` : member.userNickname,
|
||||
items: member.items.map(item => ({
|
||||
key: item.kind === 'project' && item.projectId ? item.projectId : item.kind,
|
||||
label: item.projectName ?? (item.kind === 'personal' ? '个人事项' : '其他'),
|
||||
kind: item.kind,
|
||||
count: item.count
|
||||
})),
|
||||
dueSoon: member.dueSoonCount,
|
||||
overdue: member.overdueCount
|
||||
};
|
||||
}
|
||||
|
||||
const view = computed(() => buildWorkbenchTeamLoadView({ members: teamLoadMembers.value.map(toTeamLoadSource) }));
|
||||
|
||||
onMounted(loadTeamLoad);
|
||||
|
||||
const LEVEL_LABEL: Record<WorkbenchTeamLoadLevel, string> = {
|
||||
high: '高负载',
|
||||
mid: '中负载',
|
||||
normal: '正常'
|
||||
};
|
||||
|
||||
function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
if (dueSoon > 0 && overdue > 0) return `临期 ${dueSoon} · 逾期 ${overdue}`;
|
||||
if (overdue > 0) return `逾期 ${overdue}`;
|
||||
return `临期 ${dueSoon}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,12 +87,12 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="tl-list">
|
||||
<ul v-if="view.members.length" class="tl-list">
|
||||
<li v-for="m in view.members" :key="m.memberId" class="tl-row">
|
||||
<span class="tl-row__dot" :class="`is-${m.level}`" :title="LEVEL_LABEL[m.level]" />
|
||||
<span class="tl-row__name">{{ m.memberName }}</span>
|
||||
<div class="tl-row__bar-wrap">
|
||||
<div class="tl-row__bar" :style="{ width: `${m.barWidthPercent}%` }">
|
||||
<div class="tl-row__bar">
|
||||
<ElTooltip
|
||||
v-for="seg in m.segments"
|
||||
:key="seg.key"
|
||||
@@ -85,27 +108,29 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
/>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<span v-if="m.overflowExtra > 0" class="tl-row__overflow">+{{ m.overflowExtra }}</span>
|
||||
</div>
|
||||
<span class="tl-row__metrics">
|
||||
<span class="tl-row__metric" :class="`is-${m.level}`">
|
||||
<span class="tl-row__metric">
|
||||
<b>{{ m.inProgress }}</b>
|
||||
进行
|
||||
未完成
|
||||
</span>
|
||||
<span v-if="m.urgent > 0" class="tl-row__metric is-urgent">
|
||||
<ElTooltip :content="urgentTooltip(m.dueSoon, m.overdue)" placement="top">
|
||||
<span>
|
||||
<b>{{ m.urgent }}</b>
|
||||
临期
|
||||
<SvgIcon v-if="m.overdue > 0" icon="mdi:alert" class="tl-row__warn-icon" />
|
||||
</span>
|
||||
</ElTooltip>
|
||||
<span v-if="m.dueSoon > 0" class="tl-row__metric is-due-soon">
|
||||
<b>{{ m.dueSoon }}</b>
|
||||
临期
|
||||
</span>
|
||||
<span v-if="m.overdue > 0" class="tl-row__metric is-overdue">
|
||||
<b>{{ m.overdue }}</b>
|
||||
逾期
|
||||
<SvgIcon icon="mdi:alert" class="tl-row__warn-icon" />
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ElEmpty v-else description="暂无团队负载数据" :image-size="72" class="tl-empty" />
|
||||
|
||||
<div class="tl-hint">高 = 进行中 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 进行中 ≥ 4 或 临期+逾期 ≥ 1</div>
|
||||
<div class="tl-hint">
|
||||
口径 = 未完成(含待开始/已暂停)· 高 = 未完成 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 未完成 ≥ 4 或 临期+逾期 ≥ 1
|
||||
</div>
|
||||
</WorkbenchModuleCard>
|
||||
</template>
|
||||
|
||||
@@ -168,7 +193,8 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
}
|
||||
.tl-row {
|
||||
display: grid;
|
||||
grid-template-columns: 10px 64px 1fr auto;
|
||||
/* 指标列固定宽:每行右侧文字宽度不同(有无临期/逾期)会让 1fr 柱子列长短不一,固定后所有柱子等长对齐 */
|
||||
grid-template-columns: 10px 64px 1fr 200px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 0;
|
||||
@@ -206,14 +232,15 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 柱子恒撑满整行:长度不再编码数量(数量看右侧数字),段宽表示该成员内部构成占比;无数据时为空灰槽 */
|
||||
.tl-row__bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.tl-row__seg {
|
||||
height: 100%;
|
||||
@@ -226,49 +253,36 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
.tl-row__seg + .tl-row__seg {
|
||||
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.tl-row__overflow {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
background: var(--el-color-danger);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tl-row__metrics {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 数字统一黑色,负载等级只靠行首圆点表达;仅临期(橙)/ 逾期(红)带警示色 */
|
||||
.tl-row__metric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tl-row__metric b {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tl-row__metric.is-high b {
|
||||
color: var(--el-color-danger);
|
||||
/* 临期用纯橙(el-color-warning 偏黄,与"逾期红"区分度不够) */
|
||||
.tl-row__metric.is-due-soon,
|
||||
.tl-row__metric.is-due-soon b {
|
||||
color: rgb(234 88 12);
|
||||
}
|
||||
.tl-row__metric.is-mid b {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
.tl-row__metric.is-normal b {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.tl-row__metric.is-urgent {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.tl-row__metric.is-urgent b {
|
||||
.tl-row__metric.is-overdue,
|
||||
.tl-row__metric.is-overdue b {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.tl-row__warn-icon {
|
||||
vertical-align: -2px;
|
||||
margin-left: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
@@ -280,4 +294,11 @@ function urgentTooltip(dueSoon: number, overdue: number) {
|
||||
color: var(--el-text-color-placeholder);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tl-empty {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onMounted, ref, watch } from 'vue';
|
||||
import { type Component, computed, markRaw, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { RouteKey } from '@elegant-router/types';
|
||||
import { OBJECT_CONTEXT_QUERY_KEY } from '@/constants/object-context';
|
||||
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
|
||||
import {
|
||||
fetchApproveOvertimeApplication,
|
||||
fetchChangePersonalItemStatus,
|
||||
fetchChangeProjectTaskStatus,
|
||||
fetchGetMonthlyReportApprovalPage,
|
||||
fetchGetMyTaskPage,
|
||||
fetchGetOvertimeApplicationApprovalPage,
|
||||
fetchGetPersonalItemDetail,
|
||||
fetchGetPersonalItemPage,
|
||||
fetchGetProjectReportApprovalPage,
|
||||
fetchGetProjectTask,
|
||||
fetchGetWeeklyReportApprovalPage,
|
||||
fetchRejectOvertimeApplication
|
||||
} from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import type { WorklogChangedPayload } from '@/views/project/project/execution/shared';
|
||||
import TaskStatusActionDialog from '@/views/project/project/execution/modules/status-action-dialog.vue';
|
||||
import TaskWorklogDialog from '@/views/project/project/execution/modules/task-worklog-dialog.vue';
|
||||
import PersonalItemDetailDialog from '@/views/personal-center/my-item/modules/personal-item-detail-dialog.vue';
|
||||
import PersonalItemOperateDialog from '@/views/personal-center/my-item/modules/personal-item-operate-dialog.vue';
|
||||
import PersonalItemStatusActionDialog from '@/views/personal-center/my-item/modules/personal-item-status-action-dialog.vue';
|
||||
import OvertimeApplicationActionDialog from '@/views/personal-center/overtime-application/modules/overtime-application-action-dialog.vue';
|
||||
import OvertimeApplicationDetailDialog from '@/views/personal-center/overtime-application/modules/overtime-application-detail-dialog.vue';
|
||||
import WorkReportPrototypePageDialog from '@/views/personal-center/work-report/shared/components/prototype-page-dialog.vue';
|
||||
@@ -30,10 +46,14 @@ import {
|
||||
isWorkbenchTodoOverdue,
|
||||
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 IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline';
|
||||
import IconMdiCloseCircleOutline from '~icons/mdi/close-circle-outline';
|
||||
import IconMdiEyeOutline from '~icons/mdi/eye-outline';
|
||||
import IconMdiPauseCircleOutline from '~icons/mdi/pause-circle-outline';
|
||||
import IconMdiPlayCircleOutline from '~icons/mdi/play-circle-outline';
|
||||
|
||||
type SortKey = 'created' | 'priority' | 'deadline';
|
||||
type OvertimeApprovalActionType = 'approve' | 'reject';
|
||||
@@ -51,9 +71,20 @@ defineEmits<{
|
||||
(e: 'hide'): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh();
|
||||
const authStore = useAuthStore();
|
||||
const currentUserId = computed(() => authStore.userInfo.userId || '');
|
||||
|
||||
const { loading, refresh } = useWorkbenchRefresh(async () => {
|
||||
await Promise.all([
|
||||
loadMyTaskItems(),
|
||||
loadPersonalTodoItems(),
|
||||
loadOvertimeApprovalItems(),
|
||||
loadWorkReportApprovalItems()
|
||||
]);
|
||||
});
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
@@ -95,20 +126,44 @@ const approvalBizTabs: Array<{ key: ApprovalBizType; label: string }> = [
|
||||
{ key: 'overtime_application', label: '加班申请' }
|
||||
];
|
||||
|
||||
const allItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
|
||||
const myTaskItems = ref<WorkbenchTodoItem[]>([]);
|
||||
// 保留任务原始行,供操作图标按 availableActions 渲染并取 projectId / executionId 调状态变更接口
|
||||
const myTaskRows = ref<Api.Project.MyTaskItem[]>([]);
|
||||
const personalTodoItems = ref<WorkbenchTodoItem[]>([]);
|
||||
// 保留个人事项原始行,供操作图标(详情/填报/完成)按 availableActions 渲染并回传给弹层
|
||||
const personalItemRows = ref<Api.PersonalItem.PersonalItem[]>([]);
|
||||
const overtimeApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const overtimeApprovalRows = ref<Api.OvertimeApplication.OvertimeApplication[]>([]);
|
||||
const workReportApprovalItems = ref<WorkbenchTodoItem[]>([]);
|
||||
const weeklyApprovalRows = ref<Api.WorkReport.Weekly.WeeklyReport[]>([]);
|
||||
const monthlyApprovalRows = ref<Api.WorkReport.Monthly.MonthlyReport[]>([]);
|
||||
const projectApprovalRows = ref<Api.WorkReport.Project.ProjectReport[]>([]);
|
||||
const mergedItems = computed(() => {
|
||||
const mockItems = allItems.value.filter(item => item.category !== 'approval');
|
||||
// 工单 tab 等工单业务上线,当前为空态
|
||||
const mergedItems = computed(() => [
|
||||
...myTaskItems.value,
|
||||
...personalTodoItems.value,
|
||||
...overtimeApprovalItems.value,
|
||||
...workReportApprovalItems.value
|
||||
]);
|
||||
|
||||
return [...mockItems, ...overtimeApprovalItems.value, ...workReportApprovalItems.value];
|
||||
});
|
||||
|
||||
const addDialogVisible = ref(false);
|
||||
// 个人事项操作弹层:详情用 view 模式、新增用 add 模式,共用同一实例
|
||||
const personalOperateVisible = ref(false);
|
||||
const personalOperateType = ref<'add' | 'view'>('add');
|
||||
const personalOperateRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
// 个人事项填报:复用详情弹层的 worklog tab
|
||||
const personalDetailVisible = ref(false);
|
||||
const personalDetailRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
// 个人事项完成:复用状态动作弹层收集原因 + 二次确认
|
||||
const personalStatusVisible = ref(false);
|
||||
const personalStatusRow = ref<Api.PersonalItem.PersonalItem | null>(null);
|
||||
const personalStatusAction = ref<Api.PersonalItem.PersonalItemLifecycleAction | null>(null);
|
||||
// 任务生命周期动作:复用任务工作区的状态动作弹层收集原因
|
||||
const taskStatusVisible = ref(false);
|
||||
const taskStatusRow = ref<Api.Project.MyTaskItem | null>(null);
|
||||
const taskStatusAction = ref<Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode> | null>(null);
|
||||
// 任务填报工时:复用任务工作区的工时弹层;弹层需要完整任务详情,点击时按需拉取
|
||||
const taskWorklogVisible = ref(false);
|
||||
const taskWorklogTask = ref<Api.Project.ProjectTask | null>(null);
|
||||
const overtimeDetailVisible = ref(false);
|
||||
const overtimeActionVisible = ref(false);
|
||||
const overtimeActionSubmitting = ref(false);
|
||||
@@ -122,13 +177,228 @@ const OVERTIME_APPROVAL_ACTION_ICONS = {
|
||||
detail: markRaw(IconMdiEyeOutline)
|
||||
};
|
||||
|
||||
function handleOpenAdd() {
|
||||
addDialogVisible.value = true;
|
||||
const PERSONAL_ACTION_ICONS = {
|
||||
detail: markRaw(IconMdiEyeOutline),
|
||||
worklog: markRaw(IconMdiClipboardEditOutline),
|
||||
complete: markRaw(IconMdiCheckCircleOutline)
|
||||
};
|
||||
|
||||
// auto_start 是系统自动动作,接口不返回;无图标的未知动作不渲染按钮
|
||||
const TASK_ACTION_ICONS: Partial<Record<Api.Project.ProjectTaskActionCode, Component>> = {
|
||||
pause: markRaw(IconMdiPauseCircleOutline),
|
||||
resume: markRaw(IconMdiPlayCircleOutline),
|
||||
complete: markRaw(IconMdiCheckCircleOutline),
|
||||
cancel: markRaw(IconMdiCloseCircleOutline)
|
||||
};
|
||||
|
||||
function getTaskActionButtonType(code: Api.Project.ProjectTaskActionCode) {
|
||||
if (code === 'complete') return 'success' as const;
|
||||
if (code === 'cancel') return 'danger' as const;
|
||||
return 'primary' as const;
|
||||
}
|
||||
|
||||
function handleAddSubmitted() {
|
||||
function getTodoProgress(item: WorkbenchTodoItem) {
|
||||
if (typeof item.progressRate !== 'number' || !Number.isFinite(item.progressRate)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.min(100, Math.max(0, Math.round(item.progressRate)));
|
||||
}
|
||||
|
||||
function shouldShowTodoProgress(item: WorkbenchTodoItem) {
|
||||
return (item.category === 'task' || item.category === 'personal') && typeof item.progressRate === 'number';
|
||||
}
|
||||
|
||||
function handleOpenAdd() {
|
||||
personalOperateType.value = 'add';
|
||||
personalOperateRow.value = null;
|
||||
personalOperateVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleAddSubmitted() {
|
||||
activeTab.value = 'personal';
|
||||
activeDeadlineFilter.value = null;
|
||||
await loadPersonalTodoItems();
|
||||
}
|
||||
|
||||
function findPersonalItemRow(item: WorkbenchTodoItem) {
|
||||
return personalItemRows.value.find(row => `personal-${row.id}` === item.id) || null;
|
||||
}
|
||||
|
||||
function getPersonalCompleteAction(row: Api.PersonalItem.PersonalItem) {
|
||||
return row.availableActions?.find(action => action.actionCode === 'complete') || null;
|
||||
}
|
||||
|
||||
// 仅当个人事项当前可完成(availableActions 含 complete)时才渲染完成图标
|
||||
function canCompletePersonalItem(item: WorkbenchTodoItem) {
|
||||
const row = findPersonalItemRow(item);
|
||||
return Boolean(row && getPersonalCompleteAction(row));
|
||||
}
|
||||
|
||||
// 详情:复用个人事项操作弹层的 view 只读模式
|
||||
function openPersonalDetail(item: WorkbenchTodoItem) {
|
||||
const row = findPersonalItemRow(item);
|
||||
if (!row) return;
|
||||
|
||||
personalOperateType.value = 'view';
|
||||
personalOperateRow.value = row;
|
||||
personalOperateVisible.value = true;
|
||||
}
|
||||
|
||||
// 填报:拉最新详情后打开详情弹层的 worklog tab
|
||||
async function openPersonalWorklog(item: WorkbenchTodoItem) {
|
||||
const row = findPersonalItemRow(item);
|
||||
if (!row) return;
|
||||
|
||||
const { error, data } = await fetchGetPersonalItemDetail(row.id);
|
||||
personalDetailRow.value = error || !data ? row : data;
|
||||
personalDetailVisible.value = true;
|
||||
}
|
||||
|
||||
// 完成:走状态动作弹层(二次确认 + 按 needReason 收集原因)
|
||||
function openPersonalComplete(item: WorkbenchTodoItem) {
|
||||
const row = findPersonalItemRow(item);
|
||||
if (!row) return;
|
||||
|
||||
const completeAction = getPersonalCompleteAction(row);
|
||||
if (!completeAction) return;
|
||||
|
||||
personalStatusRow.value = row;
|
||||
personalStatusAction.value = completeAction;
|
||||
personalStatusVisible.value = true;
|
||||
}
|
||||
|
||||
async function handlePersonalStatusSubmit(reason: string | null) {
|
||||
if (!personalStatusRow.value || !personalStatusAction.value) return;
|
||||
|
||||
const { error } = await fetchChangePersonalItemStatus(personalStatusRow.value.id, {
|
||||
actionCode: personalStatusAction.value.actionCode,
|
||||
reason
|
||||
});
|
||||
|
||||
if (error) return;
|
||||
|
||||
personalStatusVisible.value = false;
|
||||
window.$message?.success(`${personalStatusAction.value.actionName}成功`);
|
||||
await loadPersonalTodoItems();
|
||||
}
|
||||
|
||||
async function handlePersonalWorklogChanged() {
|
||||
await loadPersonalTodoItems();
|
||||
}
|
||||
|
||||
function findMyTaskRow(item: WorkbenchTodoItem) {
|
||||
return myTaskRows.value.find(row => `task-${row.id}` === item.id) || null;
|
||||
}
|
||||
|
||||
// 状态变更接口挂在执行路径下,未挂执行的任务不渲染动作按钮
|
||||
function getTaskActions(item: WorkbenchTodoItem) {
|
||||
const row = findMyTaskRow(item);
|
||||
if (!row?.executionId) return [];
|
||||
return row.availableActions.filter(action => Boolean(TASK_ACTION_ICONS[action.actionCode]));
|
||||
}
|
||||
|
||||
// 填报工时同样依赖执行路径(工时接口挂在 project/execution/task 下)
|
||||
function canReportTaskWorklog(item: WorkbenchTodoItem) {
|
||||
return Boolean(findMyTaskRow(item)?.executionId);
|
||||
}
|
||||
|
||||
// 填报:工时弹层需要完整任务详情(负责人/状态/日期),按需拉一次详情再打开
|
||||
async function openTaskWorklog(item: WorkbenchTodoItem) {
|
||||
const row = findMyTaskRow(item);
|
||||
if (!row?.executionId) return;
|
||||
|
||||
const { error, data } = await fetchGetProjectTask(row.projectId, row.executionId, row.id);
|
||||
if (error || !data) return;
|
||||
|
||||
taskWorklogTask.value = data;
|
||||
taskWorklogVisible.value = true;
|
||||
}
|
||||
|
||||
// 填报会联动任务进度/状态(如 auto_start),变更后刷新任务列表
|
||||
async function handleTaskWorklogChanged(payload: WorklogChangedPayload) {
|
||||
await loadMyTaskItems();
|
||||
|
||||
// 与任务工作区联动一致:进度填到 100 且我是任务负责人时提示完成(仅单任务,不做级联)
|
||||
if (payload.mode === 'delete' || payload.progressRate !== 100) return;
|
||||
|
||||
const task = taskWorklogTask.value;
|
||||
if (!task || task.id !== payload.taskId || task.ownerId !== currentUserId.value) return;
|
||||
|
||||
// 以刷新后的行为准:后端按进度/负责人口径返回了 complete 才提示
|
||||
const row = myTaskRows.value.find(item => item.id === payload.taskId);
|
||||
const completeAction = row?.availableActions.find(action => action.actionCode === 'complete');
|
||||
if (!row || !completeAction) return;
|
||||
|
||||
taskStatusRow.value = row;
|
||||
taskStatusAction.value = completeAction;
|
||||
|
||||
try {
|
||||
await window.$messageBox?.confirm('任务进度已达 100%,是否完成当前任务?', '完成确认', {
|
||||
confirmButtonText: '完成任务',
|
||||
cancelButtonText: '仅保留工时',
|
||||
type: 'info'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
await submitTaskStatusChange(null);
|
||||
}
|
||||
|
||||
const taskStatusActionTitle = computed(() =>
|
||||
taskStatusAction.value ? `任务状态变更:${taskStatusAction.value.actionName}` : '任务状态变更'
|
||||
);
|
||||
|
||||
async function handleTaskAction(
|
||||
item: WorkbenchTodoItem,
|
||||
action: Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode>
|
||||
) {
|
||||
const row = findMyTaskRow(item);
|
||||
if (!row?.executionId) return;
|
||||
|
||||
taskStatusRow.value = row;
|
||||
taskStatusAction.value = action;
|
||||
|
||||
// 完成动作:二次确认后直接提交(与任务工作区同口径;有未完成子任务时后端会拒绝,按业务错误提示)
|
||||
if (action.actionCode === 'complete') {
|
||||
try {
|
||||
await window.$messageBox?.confirm(`确定要完成任务“${row.taskTitle}”吗?`, '完成确认', {
|
||||
confirmButtonText: '完成任务',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await submitTaskStatusChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他非必填原因的动作(暂停/恢复)直接提交,不弹原因弹层
|
||||
if (!action.needReason) {
|
||||
await submitTaskStatusChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
taskStatusVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitTaskStatusChange(reason: string | null) {
|
||||
const row = taskStatusRow.value;
|
||||
const action = taskStatusAction.value;
|
||||
if (!row?.executionId || !action) return;
|
||||
|
||||
const { error } = await fetchChangeProjectTaskStatus(row.projectId, row.executionId, {
|
||||
taskId: row.id,
|
||||
data: { actionCode: action.actionCode, reason }
|
||||
});
|
||||
|
||||
if (error) return;
|
||||
|
||||
taskStatusVisible.value = false;
|
||||
window.$message?.success(`${action.actionName}成功`);
|
||||
await loadMyTaskItems();
|
||||
}
|
||||
|
||||
const tabCounts = computed(() => {
|
||||
@@ -253,6 +523,15 @@ function handleClickItem(item: WorkbenchTodoItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 任务条目:带项目对象上下文跳进该项目的任务导航(执行池页)
|
||||
if (item.category === 'task' && item.projectId) {
|
||||
router.push({
|
||||
path: '/project/project/execution',
|
||||
query: { [OBJECT_CONTEXT_QUERY_KEY]: item.projectId }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.routeKey) return;
|
||||
routerPushByKey(item.routeKey as RouteKey);
|
||||
}
|
||||
@@ -336,6 +615,71 @@ async function handleWorkReportSubmitted() {
|
||||
await loadWorkReportApprovalItems();
|
||||
}
|
||||
|
||||
// 优先级角标用字典 label 原样回显(rdms_req_priority:P0~P3),不翻译成高/中/低
|
||||
const { getLabel: getPriorityLabel } = useDict(RDMS_REQ_PRIORITY_DICT_CODE);
|
||||
|
||||
// 任务优先级字典 value("0" P0 ~ "3" P3,数字越小越高)映射待办优先级(仅用于排序与高亮权重)
|
||||
function mapTaskTodoPriority(priority: string) {
|
||||
if (priority === '0') return 'high' as const;
|
||||
if (priority === '1') return 'mid' as const;
|
||||
return 'low' as const;
|
||||
}
|
||||
|
||||
// 我的任务:跨项目单接口聚合(负责/协办并集、只返回非终态,过滤排序、进度与可执行动作均由后端完成)
|
||||
async function loadMyTaskItems() {
|
||||
const { error, data } = await fetchGetMyTaskPage({ pageNo: 1, pageSize: -1 });
|
||||
|
||||
if (error || !data) {
|
||||
myTaskRows.value = [];
|
||||
myTaskItems.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
myTaskRows.value = data.list;
|
||||
myTaskItems.value = buildWorkbenchTodoItems(
|
||||
data.list.map(task => ({
|
||||
id: `task-${task.id}`,
|
||||
category: 'task' as const,
|
||||
title: task.taskTitle,
|
||||
createdTime: task.createTime,
|
||||
deadline: task.plannedEndDate,
|
||||
source: task.projectName,
|
||||
progressRate: task.progressRate,
|
||||
priority: mapTaskTodoPriority(task.priority),
|
||||
priorityLabel: getPriorityLabel(task.priority) || task.priority,
|
||||
projectId: task.projectId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// 待办口径:未到终态的个人事项(pending / active / paused),terminal 态(completed / cancelled)不进待办
|
||||
const PERSONAL_TODO_STATUSES: Api.PersonalItem.PersonalItemStatusCode[] = ['pending', 'active', 'paused'];
|
||||
|
||||
async function loadPersonalTodoItems() {
|
||||
const { error, data } = await fetchGetPersonalItemPage({ pageNo: 1, pageSize: 100 });
|
||||
|
||||
if (error || !data) {
|
||||
personalItemRows.value = [];
|
||||
personalTodoItems.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = data.list.filter(item => PERSONAL_TODO_STATUSES.includes(item.statusCode));
|
||||
personalItemRows.value = rows;
|
||||
personalTodoItems.value = buildWorkbenchTodoItems(
|
||||
rows.map(item => ({
|
||||
id: `personal-${item.id}`,
|
||||
category: 'personal',
|
||||
title: item.taskTitle,
|
||||
createdTime: item.createTime,
|
||||
deadline: item.plannedEndDate,
|
||||
source: '个人事项',
|
||||
progressRate: item.progressRate,
|
||||
routeKey: 'personal-center_my-item'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function loadOvertimeApprovalItems() {
|
||||
const { error, data } = await fetchGetOvertimeApplicationApprovalPage({
|
||||
pageNo: 1,
|
||||
@@ -443,7 +787,12 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadOvertimeApprovalItems(), loadWorkReportApprovalItems()]);
|
||||
await Promise.all([
|
||||
loadMyTaskItems(),
|
||||
loadPersonalTodoItems(),
|
||||
loadOvertimeApprovalItems(),
|
||||
loadWorkReportApprovalItems()
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -534,25 +883,51 @@ onMounted(async () => {
|
||||
|
||||
<div class="workbench-todo__content">
|
||||
<div v-if="pagedItems.length" class="workbench-todo__list">
|
||||
<article
|
||||
v-for="item in pagedItems"
|
||||
:key="item.id"
|
||||
class="workbench-todo__item"
|
||||
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey || item.approvalBizType) }"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
<article v-for="item in pagedItems" :key="item.id" class="workbench-todo__item">
|
||||
<div class="workbench-todo__leading">
|
||||
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
|
||||
{{ item.categoryLabel }}
|
||||
</span>
|
||||
<span v-if="item.priority === 'high'" class="workbench-todo__priority">高</span>
|
||||
<span
|
||||
v-if="item.priorityLabel"
|
||||
class="workbench-todo__priority"
|
||||
:class="{ 'workbench-todo__priority--high': item.priority === 'high' }"
|
||||
>
|
||||
{{ item.priorityLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__body">
|
||||
<h4 class="workbench-todo__item-title">{{ item.title }}</h4>
|
||||
<h4 class="workbench-todo__item-title">
|
||||
<span
|
||||
class="workbench-todo__item-title-text"
|
||||
:class="{
|
||||
'workbench-todo__item-title-text--clickable': Boolean(
|
||||
item.routeKey || item.approvalBizType || item.projectId
|
||||
)
|
||||
}"
|
||||
@click="handleClickItem(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="workbench-todo__meta">
|
||||
<span class="workbench-todo__source">{{ item.source }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowTodoProgress(item)"
|
||||
class="workbench-todo__progress"
|
||||
:aria-label="`进度 ${getTodoProgress(item)}%`"
|
||||
>
|
||||
<span class="workbench-todo__progress-label">进度:</span>
|
||||
<ElProgress
|
||||
class="workbench-todo__progress-bar"
|
||||
:percentage="getTodoProgress(item)"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
/>
|
||||
<span class="workbench-todo__progress-text">{{ getTodoProgress(item) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workbench-todo__trailing">
|
||||
@@ -574,6 +949,44 @@ onMounted(async () => {
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.category === 'task' && canReportTaskWorklog(item)"
|
||||
class="workbench-todo__actions"
|
||||
@click.stop
|
||||
>
|
||||
<ElTooltip content="填报工时">
|
||||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openTaskWorklog(item)">
|
||||
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip v-for="action in getTaskActions(item)" :key="action.actionCode" :content="action.actionName">
|
||||
<ElButton
|
||||
link
|
||||
:type="getTaskActionButtonType(action.actionCode)"
|
||||
class="workbench-todo__action-btn"
|
||||
@click="handleTaskAction(item, action)"
|
||||
>
|
||||
<component :is="TASK_ACTION_ICONS[action.actionCode]" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<div v-else-if="item.category === 'personal'" class="workbench-todo__actions" @click.stop>
|
||||
<ElTooltip content="详情">
|
||||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalDetail(item)">
|
||||
<component :is="PERSONAL_ACTION_ICONS.detail" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip content="填报工时">
|
||||
<ElButton link type="primary" class="workbench-todo__action-btn" @click="openPersonalWorklog(item)">
|
||||
<component :is="PERSONAL_ACTION_ICONS.worklog" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElTooltip v-if="canCompletePersonalItem(item)" content="完成">
|
||||
<ElButton link type="success" class="workbench-todo__action-btn" @click="openPersonalComplete(item)">
|
||||
<component :is="PERSONAL_ACTION_ICONS.complete" class="text-15px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
|
||||
{{ item.deadlineLabel }}
|
||||
</span>
|
||||
@@ -597,13 +1010,43 @@ onMounted(async () => {
|
||||
|
||||
<!-- append-to-body:脱离 grid item 的 transform 容器,弹窗才能正常全屏居中 -->
|
||||
<PersonalItemOperateDialog
|
||||
v-model:visible="addDialogVisible"
|
||||
operate-type="add"
|
||||
:row-data="null"
|
||||
v-model:visible="personalOperateVisible"
|
||||
:operate-type="personalOperateType"
|
||||
:row-data="personalOperateRow"
|
||||
append-to-body
|
||||
@submitted="handleAddSubmitted"
|
||||
/>
|
||||
|
||||
<PersonalItemDetailDialog
|
||||
v-model:visible="personalDetailVisible"
|
||||
:row-data="personalDetailRow"
|
||||
default-tab="worklog"
|
||||
append-to-body
|
||||
@changed="handlePersonalWorklogChanged"
|
||||
/>
|
||||
|
||||
<PersonalItemStatusActionDialog
|
||||
v-model:visible="personalStatusVisible"
|
||||
:action="personalStatusAction"
|
||||
append-to-body
|
||||
@submit="handlePersonalStatusSubmit"
|
||||
/>
|
||||
|
||||
<TaskStatusActionDialog
|
||||
v-model:visible="taskStatusVisible"
|
||||
:title="taskStatusActionTitle"
|
||||
:action="taskStatusAction"
|
||||
append-to-body
|
||||
@submit="submitTaskStatusChange"
|
||||
/>
|
||||
|
||||
<TaskWorklogDialog
|
||||
v-model:visible="taskWorklogVisible"
|
||||
:task="taskWorklogTask"
|
||||
append-to-body
|
||||
@changed="handleTaskWorklogChanged"
|
||||
/>
|
||||
|
||||
<OvertimeApplicationDetailDialog
|
||||
v-model:visible="overtimeDetailVisible"
|
||||
:row-data="currentOvertimeApplication"
|
||||
@@ -826,7 +1269,7 @@ onMounted(async () => {
|
||||
|
||||
.workbench-todo__item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-columns: 72px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
@@ -838,19 +1281,12 @@ onMounted(async () => {
|
||||
background-color 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-todo__item--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.workbench-todo__item--clickable:hover {
|
||||
border-color: rgb(14 116 144 / 60%);
|
||||
background-color: rgb(240 253 250 / 84%);
|
||||
}
|
||||
|
||||
.workbench-todo__leading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 72px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workbench-todo__category {
|
||||
@@ -891,20 +1327,26 @@ onMounted(async () => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 4px;
|
||||
border-radius: 6px;
|
||||
background-color: rgb(254 226 226 / 96%);
|
||||
color: rgb(220 38 38 / 96%);
|
||||
background-color: rgb(241 245 249 / 96%);
|
||||
color: rgb(71 85 105 / 96%);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.workbench-todo__priority--high {
|
||||
background-color: rgb(254 226 226 / 96%);
|
||||
color: rgb(220 38 38 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.workbench-todo__item-title {
|
||||
@@ -918,10 +1360,21 @@ onMounted(async () => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workbench-todo__item-title-text--clickable {
|
||||
cursor: pointer;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
|
||||
.workbench-todo__item-title-text--clickable:hover {
|
||||
color: rgb(14 116 144 / 96%);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.workbench-todo__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workbench-todo__source {
|
||||
@@ -933,6 +1386,41 @@ onMounted(async () => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workbench-todo__progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: min(100%, 260px);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.workbench-todo__progress-label {
|
||||
flex: 0 0 auto;
|
||||
color: rgb(100 116 139 / 92%);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workbench-todo__progress-bar {
|
||||
flex: 1 1 auto;
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.workbench-todo__progress-text {
|
||||
flex: 0 0 34px;
|
||||
color: rgb(71 85 105 / 94%);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.workbench-todo__progress :deep(.el-progress-bar__outer) {
|
||||
background-color: rgb(226 232 240 / 96%);
|
||||
}
|
||||
|
||||
.workbench-todo__trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -992,7 +1480,7 @@ onMounted(async () => {
|
||||
|
||||
@media (width <= 600px) {
|
||||
.workbench-todo__item {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workbench-todo__trailing {
|
||||
|
||||
Reference in New Issue
Block a user