czh-20260609-增加门户卡片数据获取前端接口;增加顶部栏相应logo
This commit is contained in:
@@ -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 |
@@ -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">中国船舶集团有限公司第七〇四研究所 数智化中心©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 {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
<!-- 底部版权 -->
|
<!-- 底部版权 -->
|
||||||
<div class="footer-text">
|
<div class="footer-text">
|
||||||
中国船舶集团有限公司第七〇四研究所 数智化中心@2026
|
中国船舶集团有限公司第七〇四研究所 数智化中心©2026
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user