!25 czh-20260609-增加门户卡片数据获取前端接口;增加顶部栏相应logo

Merge pull request !25 from 陈志浩/feature/dashboard
This commit is contained in:
陈志浩
2026-06-09 01:49:00 +00:00
committed by Gitee
12 changed files with 248 additions and 164 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 715 KiB

+37 -27
View File
@@ -1,19 +1,19 @@
<template> <template>
<Footer :class="prefixCls" v-if="true" ref="footerRef"> <Footer :class="prefixCls" v-if="getShowLayoutFooter" ref="footerRef">
<div class="footer-text">中国船舶集团有限公司第七〇四研究所 数智化中心@2026</div> <div class="footer-text">中国船舶集团有限公司第七〇四研究所 数智化中心&copy;2026</div>
</Footer> </Footer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, unref, ref } from 'vue'; import { computed, defineComponent, unref, ref, watch, nextTick } from 'vue';
import { Layout } from 'ant-design-vue'; import { Layout } from 'ant-design-vue';
import { GithubFilled } from '@ant-design/icons-vue'; // import { GithubFilled } from '@ant-design/icons-vue';
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting'; // import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
import { openWindow } from '/@/utils'; // import { openWindow } from '/@/utils';
import { useI18n } from '/@/hooks/web/useI18n'; // import { useI18n } from '/@/hooks/web/useI18n';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
@@ -22,40 +22,50 @@
export default defineComponent({ export default defineComponent({
name: 'LayoutFooter', name: 'LayoutFooter',
components: { Footer: Layout.Footer, GithubFilled }, components: { Footer: Layout.Footer /* , GithubFilled */ },
setup() { setup() {
const { t } = useI18n(); // const { t } = useI18n();
const { getShowFooter } = useRootSetting(); const { getShowFooter } = useRootSetting();
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-footer'); const { prefixCls } = useDesign('layout-footer');
const footerRef = ref<ComponentRef>(null); const footerRef = ref<ComponentRef>(null);
const { setFooterHeight } = useLayoutHeight(); const { setFooterHeight } = useLayoutHeight();
//当前主题 // 当前主题
const { getDarkMode } = useRootSetting(); const { getDarkMode } = useRootSetting();
const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK); const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK);
// const getShowLayoutFooter = computed(() => { // 是否显示 footer:受全局设置 + 路由 meta.hiddenFooter 控制
// if (unref(getShowFooter)) { const getShowLayoutFooter = computed(() => {
// const footerEl = unref(footerRef)?.$el; return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
// setFooterHeight(footerEl?.offsetHeight || 0); });
// } else {
// setFooterHeight(0); // 监听 footer 显隐,同步高度到内容布局
// } watch(
// return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter; getShowLayoutFooter,
// }); async (show) => {
//鼠标移入的颜色设置 if (show) {
await nextTick();
const footerEl = unref(footerRef)?.$el;
setFooterHeight(footerEl?.offsetHeight || 0);
} else {
setFooterHeight(0);
}
},
{ immediate: true }
);
// 鼠标移入的颜色设置
const hoverColor = computed(() => { const hoverColor = computed(() => {
return unref(isDark) ? 'rgba(255, 255, 255, 1)' : 'rgba(0, 0, 0, 0.85)'; return unref(isDark) ? 'rgba(255, 255, 255, 1)' : 'rgba(0, 0, 0, 0.85)';
}); });
return { return {
// getShowLayoutFooter, getShowLayoutFooter,
prefixCls, prefixCls,
t, // t,
DOC_URL, // DOC_URL,
GITHUB_URL, // GITHUB_URL,
SITE_URL, // SITE_URL,
openWindow, // openWindow,
footerRef, footerRef,
hoverColor, hoverColor,
}; };
@@ -65,7 +75,7 @@
<style lang="less" scoped> <style lang="less" scoped>
@prefix-cls: ~'@{namespace}-layout-footer'; @prefix-cls: ~'@{namespace}-layout-footer';
@normal-color: rgba(0, 0, 0, 0.45); // @normal-color: rgba(0, 0, 0, 0.45);
// update-begin-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失 // update-begin-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失
@hover-color: v-bind(hoverColor); @hover-color: v-bind(hoverColor);
// update-end-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失 // update-end-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失
@@ -1,12 +1,7 @@
<template> <template>
<Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`"> <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex"> <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
<!-- <img :class="`${prefixCls}__header`" :src="getAvatarUrl" /> --> <img :class="`${prefixCls}__logo`" :src="deptLogo" />
<span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name `" class="truncate">
{{ getUserInfo.realname }}
</span>
</span>
</span> </span>
<template #overlay> <template #overlay>
@@ -47,6 +42,7 @@
import { useMessage } from '/src/hooks/web/useMessage'; import { useMessage } from '/src/hooks/web/useMessage';
import { useGo } from '/@/hooks/web/usePage'; import { useGo } from '/@/hooks/web/usePage';
import headerImg from '/@/assets/images/header.jpg'; import headerImg from '/@/assets/images/header.jpg';
import deptLogo from '/@/assets/images/department-logo.png';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { openWindow } from '/@/utils'; import { openWindow } from '/@/utils';
@@ -178,6 +174,7 @@
return { return {
prefixCls, prefixCls,
t, t,
deptLogo,
getUserInfo, getUserInfo,
// getAvatarUrl, // getAvatarUrl,
handleMenuClick, handleMenuClick,
@@ -207,15 +204,20 @@
img { img {
width: 24px; width: 24px;
height: 24px; height: 24px;
margin-right: 12px;
} }
&__header { &__header {
border-radius: 50%; border-radius: 50%;
} }
&__name { img&__logo {
font-size: 14px; width: 32px;
height: 32px;
margin-right: 0;
vertical-align: middle;
background: #fff;
border-radius: 4px;
padding: 2px 4px;
} }
&--dark { &--dark {
+12 -1
View File
@@ -11,7 +11,10 @@
/> />
<!-- <LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" /> --> <!-- <LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" /> -->
<!-- 欢迎语 --> <!-- 欢迎语 -->
<span v-if="getShowContent && getShowBreadTitle && !getIsMobile" :class="[prefixCls, `${prefixCls}--${getHeaderTheme}`,'headerIntroductionClass']"> {{t('layout.header.welcomeIn')}} {{ title }} </span> <span v-if="getShowContent && getShowBreadTitle && !getIsMobile" :class="[prefixCls, `${prefixCls}--${getHeaderTheme}`,'headerIntroductionClass']">
<img src="../../../assets/images/header-brand.png" class="header-logo" />
{{t('layout.header.welcomeIn')}} {{ title }}
</span>
</div> </div>
<!-- left end --> <!-- left end -->
@@ -239,6 +242,14 @@
margin-bottom: 2px; margin-bottom: 2px;
border-bottom: 0px; border-bottom: 0px;
border-left: 0px; border-left: 0px;
display: flex;
align-items: center;
gap: 10px;
}
.header-logo {
height: 32px;
width: auto;
} }
&--light { &--light {
@@ -0,0 +1,14 @@
import { defHttp } from '/@/utils/http/axios';
const URL = {
stats: '/dashboard/processPortal/stats',
};
export interface ProcessPortalStats {
todoCount: number;
doneThisMonth: number;
avgDuration: string;
overdueCount: number;
}
export const getProcessPortalStats = () => defHttp.get<ProcessPortalStats>({ url: URL.stats });
@@ -112,9 +112,9 @@
function goMore() { function goMore() {
if (activeTab.value === 'todo') { if (activeTab.value === 'todo') {
router.push('/task/myHandleTaskInfo'); router.push('/task/myTodoTaskInfo');
} else { } else {
router.push('/task/myCcTaskList'); router.push('/task/myCcTaskListInfo');
} }
} }
@@ -1,115 +1,110 @@
<template> <template>
<a-row :gutter="16" class="stat-cards"> <a-row :gutter="16" class="stat-cards">
<a-col :xs="24" :sm="12" :lg="6" v-for="item in statList" :key="item.title"> <!-- 骨架屏加载态 -->
<div class="stat-card" hoverable> <template v-if="loading">
<div class="stat-card__header"> <a-col :xs="24" :sm="12" :lg="6" v-for="i in 4" :key="'skeleton-' + i">
<span class="stat-card__label">{{ item.title }}</span> <div class="stat-card stat-card--skeleton">
<div class="stat-card__icon-wrap" :style="{ background: item.bgColor }"> <div class="stat-card__icon skeleton-box" />
<component :is="item.icon" :style="{ color: item.iconColor, fontSize: '18px' }" /> <div class="stat-card__info">
</div> <div class="skeleton-line skeleton-line--label" />
</div> <div class="skeleton-line skeleton-line--value" />
<div class="stat-card__footer">
<div class="stat-card__value-group">
<span class="stat-card__value">{{ item.value }}</span>
<span class="stat-card__change" :class="'stat-card__change--' + item.trend">
<CaretUpOutlined v-if="item.trend === 'up'" />
<CaretDownOutlined v-if="item.trend === 'down'" />
{{ item.change }}
</span>
</div>
<svg class="stat-card__sparkline" width="80" height="32" :viewBox="'0 0 80 32'">
<defs>
<linearGradient :id="'grad-' + item.key" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" :stop-color="item.sparkColor" stop-opacity="0.3" />
<stop offset="100%" :stop-color="item.sparkColor" stop-opacity="0.05" />
</linearGradient>
</defs>
<polygon :points="item.fillPoints" :fill="'url(#grad-' + item.key + ')'" />
<path :d="item.pathD" fill="none" :stroke="item.sparkColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div> </div>
</div> </div>
</a-col> </a-col>
</template>
<!-- 真实数据 -->
<template v-else>
<a-col :xs="24" :sm="12" :lg="6" v-for="item in statList" :key="item.title">
<div class="stat-card" :style="{ '--accent': item.iconColor, '--accent-bg': item.bgColor }">
<div class="stat-card__glow" />
<div class="stat-card__icon" :style="{ background: item.bgColor }">
<component :is="item.icon" :style="{ color: item.iconColor, fontSize: '22px' }" />
</div>
<div class="stat-card__info">
<span class="stat-card__label">{{ item.title }}</span>
<span class="stat-card__value">{{ item.value }}</span>
</div>
</div>
</a-col>
</template>
</a-row> </a-row>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { import {
FileTextOutlined, FileTextOutlined,
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
WarningOutlined, WarningOutlined,
CaretUpOutlined,
CaretDownOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { getProcessPortalStats } from '../api/processPortal.api';
function buildSparkline(data: number[]) { const ICON_MAP = {
const max = Math.max(...data); todo: FileTextOutlined,
const min = Math.min(...data); done: CheckCircleOutlined,
const range = max - min || 1; avg: ClockCircleOutlined,
const width = 80; warn: WarningOutlined,
const height = 32; } as const;
const pad = 2;
const points = data.map((v, i) => {
const x = pad + (i / (data.length - 1)) * (width - pad * 2);
const y = height - pad - ((v - min) / range) * (height - pad * 2);
return `${x},${y}`;
});
const pathD = `M ${points.join(' L ')}`;
const fillPoints = [`${pad},${height - pad}`, ...points, `${width - pad},${height - pad}`].join(' ');
return { pathD, fillPoints };
}
const statList = [ const COLOR_MAP = {
todo: { bg: 'rgba(24, 144, 255, 0.1)', icon: '#1890ff' },
done: { bg: 'rgba(82, 196, 26, 0.1)', icon: '#52c41a' },
avg: { bg: 'rgba(168, 85, 247, 0.1)', icon: '#a855f7' },
warn: { bg: 'rgba(255, 77, 79, 0.1)', icon: '#ff4d4f' },
} as const;
const loading = ref(true);
const stats = ref({ todoCount: 0, doneThisMonth: 0, avgDuration: '0h', overdueCount: 0 });
const statList = computed(() => [
{ {
key: 'todo', key: 'todo',
title: '待办事项', title: '待办事项',
value: '28', value: stats.value.todoCount,
change: '+4', icon: ICON_MAP.todo,
trend: 'up', bgColor: COLOR_MAP.todo.bg,
icon: FileTextOutlined, iconColor: COLOR_MAP.todo.icon,
bgColor: 'rgba(24, 144, 255, 0.1)',
iconColor: '#1890ff',
sparkColor: '#3b82f6',
...buildSparkline([20, 22, 18, 24, 21, 26, 28]),
}, },
{ {
key: 'done', key: 'done',
title: '本月已办', title: '本月已办',
value: '342', value: stats.value.doneThisMonth,
change: '+18.2%', icon: ICON_MAP.done,
trend: 'up', bgColor: COLOR_MAP.done.bg,
icon: CheckCircleOutlined, iconColor: COLOR_MAP.done.icon,
bgColor: 'rgba(82, 196, 26, 0.1)',
iconColor: '#52c41a',
sparkColor: '#22c55e',
...buildSparkline([280, 295, 310, 305, 320, 335, 342]),
}, },
{ {
key: 'avg', key: 'avg',
title: '平均时长', title: '平均时长',
value: '2.4h', value: stats.value.avgDuration,
change: '-0.6h', icon: ICON_MAP.avg,
trend: 'down', bgColor: COLOR_MAP.avg.bg,
icon: ClockCircleOutlined, iconColor: COLOR_MAP.avg.icon,
bgColor: 'rgba(168, 85, 247, 0.1)',
iconColor: '#a855f7',
sparkColor: '#a855f7',
...buildSparkline([3.2, 3.0, 2.8, 2.9, 2.6, 2.5, 2.4]),
}, },
{ {
key: 'warn', key: 'warn',
title: '超时预警', title: '超时预警',
value: '6', value: stats.value.overdueCount,
change: '+2', icon: ICON_MAP.warn,
trend: 'up', bgColor: COLOR_MAP.warn.bg,
icon: WarningOutlined, iconColor: COLOR_MAP.warn.icon,
bgColor: 'rgba(255, 77, 79, 0.1)',
iconColor: '#ff4d4f',
sparkColor: '#ef4444',
...buildSparkline([3, 4, 2, 5, 4, 5, 6]),
}, },
]; ]);
onMounted(async () => {
try {
const res = await getProcessPortalStats();
if (res) {
stats.value = res;
}
} catch (e) {
console.error('获取流程门户统计数据失败', e);
} finally {
loading.value = false;
}
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -118,49 +113,67 @@
} }
.stat-card { .stat-card {
position: relative;
background: #fff; background: #fff;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
border-radius: 12px; border-radius: 14px;
padding: 20px; padding: 22px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s;
cursor: default;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 16px;
margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
cursor: default;
overflow: hidden;
&:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
.stat-card__glow {
opacity: 0.12;
transform: scale(1.15);
}
}
&__glow {
position: absolute;
right: -20px;
top: -20px;
width: 100px;
height: 100px;
border-radius: 50%;
background: var(--accent);
opacity: 0.06;
transition: all 0.4s ease;
pointer-events: none;
}
&__icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
z-index: 1;
}
&__info {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
position: relative;
z-index: 1;
} }
&__label { &__label {
font-size: 13px; font-size: 13px;
color: rgba(0, 0, 0, 0.45); color: rgba(0, 0, 0, 0.45);
} line-height: 1.3;
&__icon-wrap {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
&__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
&__value-group {
display: flex;
flex-direction: column;
gap: 2px;
} }
&__value { &__value {
@@ -170,26 +183,48 @@
line-height: 1.2; line-height: 1.2;
letter-spacing: -0.5px; letter-spacing: -0.5px;
} }
&__change {
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 2px;
&--up {
color: #52c41a;
} }
&--down { // ---- 骨架屏 ----
color: #ff4d4f; .stat-card--skeleton {
cursor: default;
&:hover {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
} }
} }
&__sparkline { .skeleton-box {
flex-shrink: 0; background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
overflow: visible; background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
.skeleton-line {
height: 14px;
border-radius: 6px;
background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
&--label {
width: 56px;
margin-bottom: 4px;
}
&--value {
width: 64px;
height: 28px;
border-radius: 8px;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
} }
} }
</style> </style>
@@ -5,10 +5,6 @@
<div class="welcome-banner__content"> <div class="welcome-banner__content">
<div class="welcome-banner__left"> <div class="welcome-banner__left">
<h2 class="welcome-banner__title">{{ greeting }}{{ userinfo.realname }}<span class="welcome-banner__wave">👋</span></h2> <h2 class="welcome-banner__title">{{ greeting }}{{ userinfo.realname }}<span class="welcome-banner__wave">👋</span></h2>
<p class="welcome-banner__desc">
今天有 <span class="welcome-banner__highlight">8 项待办</span>
<span class="welcome-banner__warn">2 项即将超时</span>
</p>
</div> </div>
<div class="welcome-banner__right"> <div class="welcome-banner__right">
<div class="welcome-banner__date-btn"> <div class="welcome-banner__date-btn">
+1 -1
View File
@@ -53,7 +53,7 @@
<!-- 底部版权 --> <!-- 底部版权 -->
<div class="footer-text"> <div class="footer-text">
中国船舶集团有限公司第七〇四研究所 数智化中心@2026 中国船舶集团有限公司第七〇四研究所 数智化中心&copy;2026
</div> </div>
</div> </div>
</template> </template>