|
|
|
|
@ -1,13 +1,892 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="app-container home">
|
|
|
|
|
<div v-loading="pageLoading" class="app-container dashboard">
|
|
|
|
|
<!-- A. 欢迎区 -->
|
|
|
|
|
<section class="welcome-banner">
|
|
|
|
|
<div class="welcome-banner__main">
|
|
|
|
|
<div class="welcome-banner__greet">
|
|
|
|
|
<span class="welcome-banner__time">{{ greetingText }},</span>
|
|
|
|
|
<span class="welcome-banner__name">{{ displayName }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="welcome-banner__meta">
|
|
|
|
|
<span v-if="displayDept"><svg-icon icon-class="tree" class="meta-icon" />{{ displayDept }}</span>
|
|
|
|
|
<span class="welcome-banner__date"><svg-icon icon-class="date" class="meta-icon" />{{ todayText }}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p class="welcome-banner__tip">{{ welcomeTip }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="welcome-banner__actions">
|
|
|
|
|
<el-button type="primary" plain icon="Refresh" @click="handleRefresh">刷新</el-button>
|
|
|
|
|
<el-badge :value="unreadMessageCount" :hidden="unreadMessageCount === 0" :max="99">
|
|
|
|
|
<el-button plain icon="Bell" @click="focusMessages">消息</el-button>
|
|
|
|
|
</el-badge>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- B. 个人概览卡片 -->
|
|
|
|
|
<section class="summary-row">
|
|
|
|
|
<div
|
|
|
|
|
v-for="card in summaryCards"
|
|
|
|
|
:key="card.key"
|
|
|
|
|
class="summary-card"
|
|
|
|
|
:class="`summary-card--${card.theme}`"
|
|
|
|
|
@click="onSummaryClick(card)"
|
|
|
|
|
>
|
|
|
|
|
<div class="summary-card__icon">
|
|
|
|
|
<el-icon :size="22"><component :is="card.icon" /></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-card__body">
|
|
|
|
|
<div class="summary-card__value">{{ card.displayValue }}</div>
|
|
|
|
|
<div class="summary-card__label">{{ card.label }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="16" class="dashboard-main">
|
|
|
|
|
<!-- 左侧主栏 -->
|
|
|
|
|
<el-col :xs="24" :lg="16">
|
|
|
|
|
<!-- C. 我的待办 -->
|
|
|
|
|
<el-card shadow="hover" class="block-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="block-card__header">
|
|
|
|
|
<span class="block-card__title">
|
|
|
|
|
<el-icon><Tickets /></el-icon>
|
|
|
|
|
我的待办
|
|
|
|
|
</span>
|
|
|
|
|
<el-link type="primary" :underline="false" @click="openMenu('workflow/task/taskWaiting')">查看全部</el-link>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div v-if="taskWaitList.length" class="task-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in taskWaitList"
|
|
|
|
|
:key="item.id"
|
|
|
|
|
class="task-list__item"
|
|
|
|
|
@click="onTaskClick(item)"
|
|
|
|
|
>
|
|
|
|
|
<div class="task-list__title">{{ item.businessTitle }}</div>
|
|
|
|
|
<div class="task-list__meta">
|
|
|
|
|
<el-tag size="small" effect="plain">{{ item.flowName }}</el-tag>
|
|
|
|
|
<span class="task-list__node">{{ item.nodeName }}</span>
|
|
|
|
|
<span v-if="taskCreateByName(item)" class="task-list__user">发起人:{{ taskCreateByName(item) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="task-list__time">{{ item.createTime }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-empty v-else description="暂无待办,辛苦了" :image-size="72" />
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- E. 我发起的流程 -->
|
|
|
|
|
<el-card shadow="hover" class="block-card block-card--last">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="block-card__header">
|
|
|
|
|
<span class="block-card__title">
|
|
|
|
|
<el-icon><Document /></el-icon>
|
|
|
|
|
我发起的(进行中)
|
|
|
|
|
</span>
|
|
|
|
|
<el-link type="primary" :underline="false" @click="openMenu('workflow/task/myDocument')">查看全部</el-link>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table v-loading="processLoading" :data="myProcessList" stripe size="small" class="compact-table" @row-click="onProcessRowClick">
|
|
|
|
|
<el-table-column prop="businessTitle" label="业务标题" min-width="160" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="flowName" label="流程" width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="nodeName" label="当前节点" width="110" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="flowStatus" label="状态" width="90" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.statusType" size="small">{{ row.flowStatusLabel }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="createTime" label="发起时间" width="150" align="center" />
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧辅栏 -->
|
|
|
|
|
<el-col :xs="24" :lg="8">
|
|
|
|
|
<!-- D. 公告与消息 -->
|
|
|
|
|
<el-card shadow="hover" class="block-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="block-card__header">
|
|
|
|
|
<span class="block-card__title">
|
|
|
|
|
<el-icon><Message /></el-icon>
|
|
|
|
|
公告与消息
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-tabs v-model="noticeTab" class="notice-tabs">
|
|
|
|
|
<el-tab-pane label="系统公告" name="notice">
|
|
|
|
|
<ul v-if="noticeList.length" class="notice-feed">
|
|
|
|
|
<li
|
|
|
|
|
v-for="n in noticeList"
|
|
|
|
|
:key="n.noticeId"
|
|
|
|
|
class="notice-feed__item"
|
|
|
|
|
@click="onNoticeClick(n)"
|
|
|
|
|
>
|
|
|
|
|
<el-tag size="small" :type="n.tagType">{{ n.noticeTypeLabel }}</el-tag>
|
|
|
|
|
<span class="notice-feed__title">{{ n.noticeTitle }}</span>
|
|
|
|
|
<span class="notice-feed__time">{{ n.createTime }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<el-empty v-else description="暂无公告" :image-size="64" />
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
<el-tab-pane name="message">
|
|
|
|
|
<template #label>
|
|
|
|
|
<span>站内消息</span>
|
|
|
|
|
<el-badge v-if="unreadMessageCount" :value="unreadMessageCount" class="tab-badge" />
|
|
|
|
|
</template>
|
|
|
|
|
<ul v-if="messageList.length" class="notice-feed">
|
|
|
|
|
<li
|
|
|
|
|
v-for="(m, k) in messageList"
|
|
|
|
|
:key="k"
|
|
|
|
|
class="notice-feed__item"
|
|
|
|
|
:class="{ 'is-unread': !m.read }"
|
|
|
|
|
>
|
|
|
|
|
<span v-if="!m.read" class="notice-feed__dot" />
|
|
|
|
|
<span class="notice-feed__title">{{ m.message }}</span>
|
|
|
|
|
<span class="notice-feed__time">{{ m.time }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<el-empty v-else description="暂无消息" :image-size="64" />
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- F. 快捷入口 -->
|
|
|
|
|
<el-card shadow="hover" class="block-card block-card--last">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="block-card__header">
|
|
|
|
|
<span class="block-card__title">
|
|
|
|
|
<el-icon><Grid /></el-icon>
|
|
|
|
|
快捷入口
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="shortcut-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="s in visibleShortcuts"
|
|
|
|
|
:key="s.key"
|
|
|
|
|
class="shortcut-grid__item"
|
|
|
|
|
@click="onShortcutClick(s)"
|
|
|
|
|
>
|
|
|
|
|
<div class="shortcut-grid__icon" :style="{ background: s.bg }">
|
|
|
|
|
<el-icon :size="20"><component :is="s.icon" /></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="shortcut-grid__label">{{ s.label }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
|
|
import {
|
|
|
|
|
Briefcase,
|
|
|
|
|
Calendar,
|
|
|
|
|
Document,
|
|
|
|
|
EditPen,
|
|
|
|
|
Grid,
|
|
|
|
|
Message,
|
|
|
|
|
Notebook,
|
|
|
|
|
Reading,
|
|
|
|
|
Tickets,
|
|
|
|
|
Timer,
|
|
|
|
|
User
|
|
|
|
|
} from '@element-plus/icons-vue';
|
|
|
|
|
import { pageByTaskWait, pageByTaskCopy } from '@/api/workflow/task';
|
|
|
|
|
import { FlowTaskVO } from '@/api/workflow/task/types';
|
|
|
|
|
import { pageByCurrent } from '@/api/workflow/instance';
|
|
|
|
|
import { FlowInstanceVO } from '@/api/workflow/instance/types';
|
|
|
|
|
import workflowCommon from '@/api/workflow/workflowCommon';
|
|
|
|
|
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
|
|
|
|
import { listNotice } from '@/api/system/notice';
|
|
|
|
|
import { NoticeVO } from '@/api/system/notice/types';
|
|
|
|
|
import { getUserProfile } from '@/api/system/user';
|
|
|
|
|
import { listContractInfo } from '@/api/oa/erp/contractInfo';
|
|
|
|
|
import { listTimesheetInfo } from '@/api/oa/erp/timesheetInfo';
|
|
|
|
|
import { TimesheetInfoVO } from '@/api/oa/erp/timesheetInfo/types';
|
|
|
|
|
import { useUserStore } from '@/store/modules/user';
|
|
|
|
|
import { useNoticeStore } from '@/store/modules/notice';
|
|
|
|
|
import { resolveMenuPath } from '@/utils/resolveMenuPath';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
const noticeStore = useNoticeStore();
|
|
|
|
|
|
|
|
|
|
const { wf_business_status, sys_notice_type, timesheet_status } = toRefs<any>(
|
|
|
|
|
proxy?.useDict('wf_business_status', 'sys_notice_type', 'timesheet_status')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const RUNNING_FLOW_STATUS = ['waiting', 'back', 'draft'];
|
|
|
|
|
|
|
|
|
|
const pageLoading = ref(false);
|
|
|
|
|
const processLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
const profileDept = ref('');
|
|
|
|
|
|
|
|
|
|
const summary = reactive({
|
|
|
|
|
taskWaitCount: 0,
|
|
|
|
|
taskCopyCount: 0,
|
|
|
|
|
myProcessRunningCount: 0,
|
|
|
|
|
timesheetWeekStatus: 'NOT_FILLED',
|
|
|
|
|
timesheetWeekLabel: '未填报',
|
|
|
|
|
contractCount: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const taskWaitList = ref<FlowTaskVO[]>([]);
|
|
|
|
|
const myProcessList = ref<
|
|
|
|
|
{
|
|
|
|
|
id: string | number;
|
|
|
|
|
businessId: string;
|
|
|
|
|
businessTitle: string;
|
|
|
|
|
flowName: string;
|
|
|
|
|
nodeName: string;
|
|
|
|
|
flowStatus: string;
|
|
|
|
|
flowStatusLabel: string;
|
|
|
|
|
statusType: 'success' | 'warning' | 'info' | 'danger' | 'primary';
|
|
|
|
|
createTime: string;
|
|
|
|
|
formCustom: string;
|
|
|
|
|
formPath: string;
|
|
|
|
|
}[]
|
|
|
|
|
>([]);
|
|
|
|
|
|
|
|
|
|
const noticeList = ref<
|
|
|
|
|
{
|
|
|
|
|
noticeId: number;
|
|
|
|
|
noticeTitle: string;
|
|
|
|
|
noticeTypeLabel: string;
|
|
|
|
|
tagType: 'success' | 'warning' | 'info' | 'danger' | 'primary';
|
|
|
|
|
createTime: string;
|
|
|
|
|
}[]
|
|
|
|
|
>([]);
|
|
|
|
|
|
|
|
|
|
const messageList = computed(() => noticeStore.state.notices);
|
|
|
|
|
|
|
|
|
|
const shortcutDefs = [
|
|
|
|
|
{ key: 'taskWait', label: '我的待办', icon: Tickets, bg: 'linear-gradient(135deg, #409eff 0%, #66b1ff 100%)', menuComponent: 'workflow/task/taskWaiting' },
|
|
|
|
|
{ key: 'myDoc', label: '我发起的', icon: Document, bg: 'linear-gradient(135deg, #67c23a 0%, #85ce61 100%)', menuComponent: 'workflow/task/myDocument' },
|
|
|
|
|
{ key: 'profile', label: '个人中心', icon: User, bg: 'linear-gradient(135deg, #909399 0%, #b1b3b8 100%)', menuComponent: 'user/profile' },
|
|
|
|
|
{ key: 'timesheet', label: '工时填报', icon: Timer, bg: 'linear-gradient(135deg, #e6a23c 0%, #ebb563 100%)', menuComponent: 'oa/erp/timesheetInfo/index' },
|
|
|
|
|
{ key: 'report', label: '项目周报', icon: EditPen, bg: 'linear-gradient(135deg, #f56c6c 0%, #f78989 100%)', menuComponent: 'oa/erp/projectReport/index' },
|
|
|
|
|
{ key: 'project', label: '项目信息', icon: Briefcase, bg: 'linear-gradient(135deg, #409eff 0%, #53a8ff 100%)', menuComponent: 'oa/erp/projectInfo/index' },
|
|
|
|
|
{ key: 'notice', label: '系统公告', icon: Reading, bg: 'linear-gradient(135deg, #00bcd4 0%, #26c6da 100%)', menuComponent: 'system/notice/index' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const visibleShortcuts = computed(() =>
|
|
|
|
|
shortcutDefs.filter((s) => resolveMenuPath(s.menuComponent) != null)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const noticeTab = ref('notice');
|
|
|
|
|
|
|
|
|
|
const displayName = computed(() => userStore.nickname || '用户');
|
|
|
|
|
const displayDept = computed(() => {
|
|
|
|
|
const user = profileDept.value;
|
|
|
|
|
return user || '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
|
|
|
|
const todayText = computed(() => {
|
|
|
|
|
const d = dayjs();
|
|
|
|
|
return `${d.format('YYYY年MM月DD日')} ${weekDays[d.day()]}`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const greetingText = computed(() => {
|
|
|
|
|
const h = dayjs().hour();
|
|
|
|
|
if (h < 12) return '上午好';
|
|
|
|
|
if (h < 18) return '下午好';
|
|
|
|
|
return '晚上好';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const welcomeTip = computed(() => {
|
|
|
|
|
const n = summary.taskWaitCount;
|
|
|
|
|
if (n > 0) return `您有 ${n} 条待办待处理,建议优先处理今日到达的审批任务。`;
|
|
|
|
|
return '今日暂无待办。';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const unreadMessageCount = computed(() => messageList.value.filter((m) => !m.read).length);
|
|
|
|
|
|
|
|
|
|
const summaryCards = computed(() => [
|
|
|
|
|
{
|
|
|
|
|
key: 'taskWait',
|
|
|
|
|
label: '待我审批',
|
|
|
|
|
displayValue: summary.taskWaitCount,
|
|
|
|
|
theme: 'primary',
|
|
|
|
|
icon: Tickets,
|
|
|
|
|
menuComponent: 'workflow/task/taskWaiting'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'taskCopy',
|
|
|
|
|
label: '抄送我的',
|
|
|
|
|
displayValue: summary.taskCopyCount,
|
|
|
|
|
theme: 'purple',
|
|
|
|
|
icon: Message,
|
|
|
|
|
menuComponent: 'workflow/task/taskCopyList'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'myProcess',
|
|
|
|
|
label: '发起进行中',
|
|
|
|
|
displayValue: summary.myProcessRunningCount,
|
|
|
|
|
theme: 'success',
|
|
|
|
|
icon: Document,
|
|
|
|
|
menuComponent: 'workflow/task/myDocument'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'timesheet',
|
|
|
|
|
label: '本周工时',
|
|
|
|
|
displayValue: summary.timesheetWeekLabel,
|
|
|
|
|
theme: summary.timesheetWeekStatus === 'NOT_FILLED' ? 'warning' : 'info',
|
|
|
|
|
icon: Calendar,
|
|
|
|
|
menuComponent: 'oa/erp/timesheetInfo/index'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'contract',
|
|
|
|
|
label: '合同信息',
|
|
|
|
|
displayValue: summary.contractCount,
|
|
|
|
|
theme: 'cyan',
|
|
|
|
|
icon: Notebook,
|
|
|
|
|
menuComponent: 'oa/erp/contractInfo/index'
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
/** 自然周周一至周日 */
|
|
|
|
|
const getCurrentWeekRange = () => {
|
|
|
|
|
const d = dayjs();
|
|
|
|
|
const day = d.day();
|
|
|
|
|
const mondayOffset = day === 0 ? -6 : 1 - day;
|
|
|
|
|
const monday = d.add(mondayOffset, 'day');
|
|
|
|
|
return {
|
|
|
|
|
start: monday.format('YYYY-MM-DD'),
|
|
|
|
|
end: monday.add(6, 'day').format('YYYY-MM-DD')
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const flowStatusLabel = (status: string) => {
|
|
|
|
|
return proxy?.selectDictLabel(wf_business_status.value, status) || status || '—';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const flowStatusTagType = (status: string): 'success' | 'warning' | 'info' | 'danger' | 'primary' => {
|
|
|
|
|
if (status === 'finish') return 'success';
|
|
|
|
|
if (status === 'waiting') return 'warning';
|
|
|
|
|
if (status === 'back' || status === 'cancel') return 'danger';
|
|
|
|
|
if (status === 'draft') return 'info';
|
|
|
|
|
return 'primary';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openMenu = (menuComponent: string, query?: Record<string, unknown>) => {
|
|
|
|
|
const path = resolveMenuPath(menuComponent);
|
|
|
|
|
if (!path) {
|
|
|
|
|
proxy?.$modal.msgWarning('未找到对应菜单路由,请确认已分配菜单权限');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
proxy?.$tab.openPage(path, undefined, query);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadProfile = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getUserProfile();
|
|
|
|
|
const user = res.data?.user;
|
|
|
|
|
if (user?.deptName) {
|
|
|
|
|
profileDept.value = user.deptName;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
/* 忽略 */
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadTaskWait = async () => {
|
|
|
|
|
const res = await pageByTaskWait({ pageNum: 1, pageSize: 5 });
|
|
|
|
|
taskWaitList.value = res.rows || [];
|
|
|
|
|
summary.taskWaitCount = res.total ?? taskWaitList.value.length;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadTaskCopy = async () => {
|
|
|
|
|
const res = await pageByTaskCopy({ pageNum: 1, pageSize: 1 });
|
|
|
|
|
summary.taskCopyCount = res.total ?? 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadMyProcess = async () => {
|
|
|
|
|
processLoading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res = await pageByCurrent({ pageNum: 1, pageSize: 20 });
|
|
|
|
|
const rows = (res.rows || []) as FlowInstanceVO[];
|
|
|
|
|
const running = rows.filter((r) => RUNNING_FLOW_STATUS.includes(r.flowStatus));
|
|
|
|
|
summary.myProcessRunningCount = running.length;
|
|
|
|
|
myProcessList.value = running.slice(0, 5).map((row) => {
|
|
|
|
|
const nodeName = row.flowTaskList?.[0]?.nodeName || row.flowStatusName || '—';
|
|
|
|
|
return {
|
|
|
|
|
id: row.id,
|
|
|
|
|
businessId: row.businessId,
|
|
|
|
|
businessTitle: row.businessTitle || '—',
|
|
|
|
|
flowName: row.flowName || '—',
|
|
|
|
|
nodeName,
|
|
|
|
|
flowStatus: row.flowStatus,
|
|
|
|
|
flowStatusLabel: flowStatusLabel(row.flowStatus),
|
|
|
|
|
statusType: flowStatusTagType(row.flowStatus),
|
|
|
|
|
createTime: row.createTime ? proxy.parseTime(row.createTime) : '—',
|
|
|
|
|
formCustom: row.flowTaskList?.[0]?.formCustom ?? '',
|
|
|
|
|
formPath: row.flowTaskList?.[0]?.formPath ?? ''
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
processLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resolveTimesheetWeek = async () => {
|
|
|
|
|
const week = getCurrentWeekRange();
|
|
|
|
|
const res = await listTimesheetInfo({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 1,
|
|
|
|
|
userId: userStore.userId,
|
|
|
|
|
startTime: week.start,
|
|
|
|
|
endTime: week.end
|
|
|
|
|
});
|
|
|
|
|
const row = (res.rows?.[0] || null) as TimesheetInfoVO | null;
|
|
|
|
|
if (!row) {
|
|
|
|
|
summary.timesheetWeekStatus = 'NOT_FILLED';
|
|
|
|
|
summary.timesheetWeekLabel = '未填报';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const status = row.timesheetStatus || '';
|
|
|
|
|
summary.timesheetWeekStatus = status === '1' ? 'DRAFT' : status === '3' ? 'APPROVED' : status === '2' ? 'SUBMITTED' : status;
|
|
|
|
|
summary.timesheetWeekLabel =
|
|
|
|
|
proxy?.selectDictLabel(timesheet_status.value, status) ||
|
|
|
|
|
(status === '1' ? '暂存' : status === '2' ? '审批中' : status === '3' ? '已审批' : '已填报');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadContractCount = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await listContractInfo({ pageNum: 1, pageSize: 1 });
|
|
|
|
|
summary.contractCount = res.total ?? 0;
|
|
|
|
|
} catch {
|
|
|
|
|
summary.contractCount = 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadNotices = async () => {
|
|
|
|
|
const res = await listNotice({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 5,
|
|
|
|
|
noticeTitle: '',
|
|
|
|
|
createByName: '',
|
|
|
|
|
status: '0',
|
|
|
|
|
noticeType: ''
|
|
|
|
|
});
|
|
|
|
|
const rows = (res.rows || []) as NoticeVO[];
|
|
|
|
|
noticeList.value = rows.map((n) => ({
|
|
|
|
|
noticeId: n.noticeId,
|
|
|
|
|
noticeTitle: n.noticeTitle,
|
|
|
|
|
noticeTypeLabel: proxy?.selectDictLabel(sys_notice_type.value, n.noticeType) || '公告',
|
|
|
|
|
tagType: (n.noticeType === '1' ? 'success' : 'info') as 'success' | 'info',
|
|
|
|
|
createTime: n.createTime ? proxy.parseTime(n.createTime, '{y}-{m}-{d}') : ''
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadDashboard = async () => {
|
|
|
|
|
pageLoading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await Promise.allSettled([
|
|
|
|
|
loadProfile(),
|
|
|
|
|
loadTaskWait(),
|
|
|
|
|
loadTaskCopy(),
|
|
|
|
|
loadMyProcess(),
|
|
|
|
|
resolveTimesheetWeek(),
|
|
|
|
|
loadContractCount(),
|
|
|
|
|
loadNotices()
|
|
|
|
|
]);
|
|
|
|
|
} finally {
|
|
|
|
|
pageLoading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRefresh = () => {
|
|
|
|
|
loadDashboard();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const focusMessages = () => {
|
|
|
|
|
noticeTab.value = 'message';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onSummaryClick = (card: { menuComponent?: string }) => {
|
|
|
|
|
if (card.menuComponent) {
|
|
|
|
|
openMenu(card.menuComponent);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const taskCreateByName = (row: FlowTaskVO) => (row as FlowTaskVO & { createByName?: string }).createByName;
|
|
|
|
|
|
|
|
|
|
const onTaskClick = (row: FlowTaskVO) => {
|
|
|
|
|
const routerJumpVo = reactive<RouterJumpVo>({
|
|
|
|
|
businessId: row.businessId,
|
|
|
|
|
taskId: row.id,
|
|
|
|
|
type: 'approval',
|
|
|
|
|
formCustom: row.formCustom,
|
|
|
|
|
formPath: row.formPath
|
|
|
|
|
});
|
|
|
|
|
workflowCommon.routerJump(routerJumpVo, proxy);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onProcessRowClick = (row: (typeof myProcessList.value)[0]) => {
|
|
|
|
|
const routerJumpVo = reactive<RouterJumpVo>({
|
|
|
|
|
businessId: row.businessId,
|
|
|
|
|
taskId: row.id,
|
|
|
|
|
type: 'view',
|
|
|
|
|
formCustom: row.formCustom,
|
|
|
|
|
formPath: row.formPath
|
|
|
|
|
});
|
|
|
|
|
workflowCommon.routerJump(routerJumpVo, proxy);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onNoticeClick = (_n?: { noticeTitle?: string }) => {
|
|
|
|
|
openMenu('system/notice/index');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onShortcutClick = (s: { menuComponent: string }) => {
|
|
|
|
|
openMenu(s.menuComponent);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadDashboard();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.dashboard {
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
background: var(--el-bg-color-page, #f0f2f5);
|
|
|
|
|
min-height: calc(100vh - 84px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A. 欢迎区 */
|
|
|
|
|
.welcome-banner {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: linear-gradient(120deg, var(--el-color-primary) 0%, #53a8ff 45%, #79bbff 100%);
|
|
|
|
|
color: #fff;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
|
|
|
|
|
|
|
|
|
|
&__greet {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__name {
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin: 8px 0 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
opacity: 0.92;
|
|
|
|
|
|
|
|
|
|
.meta-icon {
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__date {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__tip {
|
|
|
|
|
margin: 10px 0 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
opacity: 0.88;
|
|
|
|
|
max-width: 560px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
|
|
|
|
:deep(.el-button) {
|
|
|
|
|
--el-button-bg-color: rgba(255, 255, 255, 0.15);
|
|
|
|
|
--el-button-border-color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
--el-button-hover-bg-color: rgba(255, 255, 255, 0.28);
|
|
|
|
|
--el-button-text-color: #fff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* B. 概览卡片 */
|
|
|
|
|
.summary-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
background: var(--el-bg-color);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__icon {
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__value {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--primary .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #409eff, #66b1ff);
|
|
|
|
|
}
|
|
|
|
|
&--purple .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #9b59b6, #b07cc6);
|
|
|
|
|
}
|
|
|
|
|
&--success .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #67c23a, #85ce61);
|
|
|
|
|
}
|
|
|
|
|
&--warning .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #e6a23c, #ebb563);
|
|
|
|
|
}
|
|
|
|
|
&--info .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #909399, #b1b3b8);
|
|
|
|
|
}
|
|
|
|
|
&--cyan .summary-card__icon {
|
|
|
|
|
background: linear-gradient(135deg, #00bcd4, #26c6da);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dashboard-main {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.block-card {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
&--last {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__title {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* C. 待办列表 */
|
|
|
|
|
.task-list {
|
|
|
|
|
&__item {
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
border-bottom: 1px dashed var(--el-border-color-lighter);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:first-child {
|
|
|
|
|
padding-top: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
margin: 0 -12px;
|
|
|
|
|
padding-left: 12px;
|
|
|
|
|
padding-right: 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__title {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__node::before {
|
|
|
|
|
content: '·';
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__time {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.compact-table {
|
|
|
|
|
:deep(.el-table__row) {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* D. 公告 */
|
|
|
|
|
.notice-tabs {
|
|
|
|
|
:deep(.el-tabs__header) {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-badge {
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notice-feed {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
&__item {
|
|
|
|
|
position: relative;
|
|
|
|
|
padding: 10px 0 10px 0;
|
|
|
|
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover .notice-feed__title {
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.is-unread {
|
|
|
|
|
padding-left: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__dot {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 14px;
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--el-color-danger);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__title {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
transition: color 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__time {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-placeholder);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* F. 快捷入口 */
|
|
|
|
|
.shortcut-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
|
gap: 16px 12px;
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: transform 0.15s;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
|
|
|
|
.shortcut-grid__label {
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--el-text-color-regular);
|
|
|
|
|
text-align: center;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|