| | |
| | | <script lang="ts" setup> |
| | | import type { MenuRecordRaw } from '@vben/types'; |
| | | import { computed } from 'vue'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { usePortalMenus } from '#/components/portal/use-portal-menus'; |
| | | |
| | | defineOptions({ name: 'DashboardQuickActions' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | interface QuickAction { |
| | | title: string; |
| | | icon: string; |
| | | route: string; |
| | | const { features, leaves, goMenu } = usePortalMenus(); |
| | | const actions = computed(() => { |
| | | const f = features.value; |
| | | const preferred = [f.todo, f.create, f.application, f.guideline, f.reimbursement, f.notice].filter((item): item is MenuRecordRaw => Boolean(item)); |
| | | return [...new Map([...preferred, ...leaves.value].map((menu) => [menu.path, menu])).values()].slice(0, 6); |
| | | }); |
| | | function caption(menu: MenuRecordRaw) { |
| | | if (/todo|待办/.test(menu.path + menu.name)) return '任务处理 · 进度跟进'; |
| | | if (/application|申报/.test(menu.path + menu.name)) return '在线申报 · 进度查询'; |
| | | if (/guideline|指南/.test(menu.path + menu.name)) return '指南查询 · 申报参考'; |
| | | if (/notice|公告/.test(menu.path + menu.name)) return '通知发布 · 信息查阅'; |
| | | if (/reimbursement|报销/.test(menu.path + menu.name)) return '费用报销 · 单据查询'; |
| | | return '业务办理 · 信息查询'; |
| | | } |
| | | |
| | | /** 快捷入口:对应系统实际菜单(审批中心 / 课题管理 / 财务报销 / 通知公告) */ |
| | | const actions: QuickAction[] = [ |
| | | { title: '发起流程', icon: 'lucide:circle-plus', route: 'BpmProcessInstanceCreate' }, |
| | | { title: '待办任务', icon: 'lucide:list-todo', route: 'BpmTaskTodo' }, |
| | | { title: '已办任务', icon: 'lucide:circle-check-big', route: 'BpmTaskDone' }, |
| | | { title: '我的流程', icon: 'lucide:git-branch', route: 'BpmProcessInstance' }, |
| | | { title: '课题申报', icon: 'lucide:file-text', route: 'ResearchApplication' }, |
| | | { title: '课题指南', icon: 'lucide:book-open', route: 'ResearchGuideline' }, |
| | | { title: '财务报销', icon: 'lucide:receipt', route: 'FinanceReimbursement' }, |
| | | { title: '通知公告', icon: 'lucide:bell', route: 'BpmOaNotice' }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="elite-card p-5"> |
| | | <div class="elite-head"> |
| | | <h3 class="elite-title"> |
| | | <span class="elite-bar"></span> |
| | | 快捷入口 |
| | | </h3> |
| | | <section v-if="actions.length" class="portal-quick-strip" aria-labelledby="quick-title"> |
| | | <div class="portal-quick-label"><h2 id="quick-title">快捷入口</h2><p>一站式服务<br />高效便捷</p><IconifyIcon icon="lucide:circle-arrow-right" /></div> |
| | | <div class="portal-quick-actions" :style="{ '--columns': actions.length }"> |
| | | <button v-for="menu in actions" :key="menu.path" type="button" @click="goMenu(menu)"><span class="portal-quick-icon"><IconifyIcon :icon="typeof menu.icon === 'string' ? menu.icon : 'lucide:arrow-up-right'" /></span><strong>{{ menu.name }}</strong><span class="portal-quick-caption">{{ caption(menu) }}</span></button> |
| | | </div> |
| | | <div class="grid grid-cols-4 gap-3"> |
| | | <div |
| | | v-for="action in actions" |
| | | :key="action.route" |
| | | class="elite-action group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-colors duration-300" |
| | | @click="emit('navigate', action.route)" |
| | | > |
| | | <div |
| | | class="elite-icon flex size-10 items-center justify-center rounded-xl transition-shadow duration-300" |
| | | > |
| | | <IconifyIcon :icon="action.icon" class="size-5 text-[#e3c567]" /> |
| | | </div> |
| | | <span class="elite-action-label">{{ action.title }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .elite-card { |
| | | background: #fdfcfa; |
| | | border: 1px solid rgba(212, 175, 55, 0.22); |
| | | border-radius: 14px; |
| | | box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05); |
| | | transition: |
| | | border-color 0.3s ease, |
| | | box-shadow 0.3s ease; |
| | | } |
| | | |
| | | .elite-card:hover { |
| | | border-color: rgba(212, 175, 55, 0.5); |
| | | box-shadow: 0 6px 20px rgba(15, 36, 64, 0.09); |
| | | } |
| | | |
| | | .dark .elite-card { |
| | | background: #101a2b; |
| | | border-color: rgba(212, 175, 55, 0.2); |
| | | box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3); |
| | | } |
| | | |
| | | .elite-head { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .elite-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | letter-spacing: 1px; |
| | | color: #0f2440; |
| | | } |
| | | |
| | | .dark .elite-title { |
| | | color: #f0ead8; |
| | | } |
| | | |
| | | .elite-bar { |
| | | display: inline-block; |
| | | width: 3px; |
| | | height: 15px; |
| | | border-radius: 2px; |
| | | background: linear-gradient(180deg, #e3c567, #b8901f); |
| | | } |
| | | |
| | | /* 图标底:藏青渐变 + 金色图标 */ |
| | | .elite-icon { |
| | | color: #e3c567; |
| | | background: linear-gradient(135deg, #1e4571, #0f2440); |
| | | border: 1px solid rgba(212, 175, 55, 0.3); |
| | | box-shadow: 0 4px 12px rgba(15, 36, 64, 0.18); |
| | | } |
| | | |
| | | /* 悬停只提亮金色描边,不做位移 */ |
| | | .elite-action:hover .elite-icon { |
| | | box-shadow: |
| | | 0 0 0 1px rgba(212, 175, 55, 0.7), |
| | | 0 4px 14px rgba(15, 36, 64, 0.22); |
| | | } |
| | | |
| | | .elite-action-label { |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | letter-spacing: 0.5px; |
| | | color: #8a94a6; |
| | | } |
| | | |
| | | .elite-action:hover .elite-action-label { |
| | | color: #b8901f; |
| | | } |
| | | |
| | | .dark .elite-action:hover .elite-action-label { |
| | | color: #e3c567; |
| | | } |
| | | .portal-quick-strip { display: flex; overflow: hidden; border-radius: 8px; background: #fff; box-shadow: 0 5px 24px rgb(25 70 110 / 6%); } |
| | | .portal-quick-label { width: 170px; flex: none; padding: 24px 30px; color: #fff; background: #b8222c; clip-path: polygon(0 0, 88% 0, 100% 50%, 88% 100%, 0 100%); } |
| | | .portal-quick-label h2 { margin: 0 0 9px; font-size: 23px; font-weight: 650; white-space: nowrap; } |
| | | .portal-quick-label p { margin: 0 0 10px; color: #fff4f4; font-size: 13px; line-height: 1.6; letter-spacing: .07em; } |
| | | .portal-quick-label :deep(svg) { width: 18px; height: 18px; } |
| | | .portal-quick-actions { display: grid; min-width: 0; flex: 1; grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); align-items: center; padding: 18px 12px; } |
| | | .portal-quick-actions button { display: flex; min-width: 0; min-height: 116px; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 4px 6px; border-right: 1px solid #e4edf7; text-align: center; cursor: pointer; } |
| | | .portal-quick-actions button:last-child { border-right: 0; } |
| | | .portal-quick-icon { display: grid; width: 55px; height: 55px; place-items: center; border-radius: 50%; color: #2877d1; background: #eaf3ff; font-size: 29px; } |
| | | .portal-quick-actions button:nth-child(4) .portal-quick-icon { color: #139d8a; background: #e8f7f4; } |
| | | .portal-quick-actions strong { max-width: 100%; overflow: hidden; color: #173958; font-size: 14px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } |
| | | .portal-quick-caption { color: #5c7187; font-size: 10px; white-space: nowrap; } |
| | | .portal-quick-actions button:hover { background: #f4f8fd; } |
| | | button:focus-visible { outline: 2px solid #2c70b2; outline-offset: -2px; } |
| | | @media (max-width: 1050px) { .portal-quick-label { width: 145px; padding: 24px 22px; } .portal-quick-caption { font-size: 9px; } } |
| | | @media (max-width: 767px) { .portal-quick-strip { flex-direction: column; } .portal-quick-label { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 15px; padding: 14px 18px; clip-path: none; } .portal-quick-label h2 { margin: 0; font-size: 20px; } .portal-quick-label p { margin: 0; font-size: 11px; } .portal-quick-label p br { display: none; } .portal-quick-actions { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 0; padding: 16px 6px; } .portal-quick-actions button:nth-child(3n) { border-right: 0; } .portal-quick-caption { font-size: 9px; } .portal-quick-icon { width: 46px; height: 46px; font-size: 24px; } .portal-quick-actions strong { font-size: 13px; } } |
| | | </style> |