1.1.57 首页样式初始化。

dev
yinq 5 days ago
parent 37ae086d2a
commit acfb43ad74

@ -0,0 +1,67 @@
import router from '@/router';
/**
* component router.push path
*/
const ROUTE_FALLBACK: Record<string, string> = {
'workflow/task/taskWaiting': '/task/taskWaiting',
'workflow/task/taskFinish': '/task/taskFinish',
'workflow/task/taskCopyList': '/task/taskCopyList',
'workflow/task/myDocument': '/task/myDocument',
'oa/erp/timesheetInfo/index': '/timesheet/timesheetInfo',
'oa/erp/projectReport/index': '/project/projectReport',
'oa/erp/projectInfo/index': '/project/projectInfo',
'oa/erp/contractInfo/index': '/contract/contractInfo',
'oa/erp/orderLedger/contractOrderTodoIndex': '/contract/contractOrderTodo',
'system/notice/index': '/system/notice',
'user/profile': '/user/profile'
};
/**
* component path
*/
export function resolveMenuPath(component: string): string | null {
if (!component) {
return null;
}
const normalized = component.replace(/^\//, '').replace(/\.vue$/, '');
const withoutIndex = normalized.replace(/\/index$/, '');
const fallback = ROUTE_FALLBACK[normalized] ?? ROUTE_FALLBACK[withoutIndex];
if (fallback && router.getRoutes().some((r) => r.path === fallback)) {
return fallback;
}
const segments = withoutIndex.split('/');
const leaf = segments[segments.length - 1] || '';
const leafShort = leaf.replace(/Index$/, '');
let bestPath: string | null = null;
let bestScore = 0;
for (const route of router.getRoutes()) {
const path = route.path;
if (!path || path === '/' || path.includes('redirect') || path.includes(':')) {
continue;
}
let score = 0;
if (path === `/${withoutIndex}` || path === `/${normalized}`) {
score = 200;
} else if (path.endsWith(`/${leaf}`) || path.endsWith(`/${leafShort}`)) {
score = 150;
} else if (path.includes(leafShort) && leafShort.length > 4) {
score = 100;
} else if (segments.length > 1 && segments.every((s) => path.includes(s))) {
score = 80;
}
if (score > bestScore || (score === bestScore && bestPath && path.length < bestPath.length)) {
bestScore = score;
bestPath = path;
}
}
if (bestPath) {
return bestPath;
}
return fallback ?? null;
}

@ -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>

Loading…
Cancel
Save