src/views/dashboard/analytics/modules/quick-actions.vue
@@ -1,73 +1,49 @@
<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;
  gradient: string;
  shadow: 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:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' },
  { title: '生产报工', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' },
  { title: '质检管理', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' },
  { title: '设备点检', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' },
  { title: '生产排程', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' },
  { title: '安灯呼叫', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' },
  { title: '物料库存', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' },
  { title: '工艺路线', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' },
  { title: '数据报表', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' },
];
</script>
<template>
  <div class="glass-card rounded-2xl p-5">
    <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">快捷操作</h3>
    <div class="grid grid-cols-3 gap-3 sm:grid-cols-9">
      <div
        v-for="action in actions"
        :key="action.route"
        class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1"
        @click="emit('navigate', action.route)"
      >
        <div
          class="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br transition-shadow duration-300 group-hover:shadow-lg"
          :class="[action.gradient, action.shadow]"
        >
          <IconifyIcon :icon="action.icon" class="size-5 text-white" />
        </div>
        <span class="text-xs font-medium text-gray-600 dark:text-gray-400">{{ action.title }}</span>
      </div>
  <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>
  </section>
</template>
<style scoped>
.glass-card {
  background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255,255,255,0.6);
  box-shadow:
    0 4px 24px rgba(0,0,0,0.04),
    0 1px 4px rgba(0,0,0,0.02),
    inset 0 1px 0 rgba(255,255,255,0.8);
}
.dark .glass-card {
  background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow:
    0 4px 24px rgba(0,0,0,0.3),
    0 1px 4px rgba(0,0,0,0.2),
    inset 0 1px 0 rgba(255,255,255,0.04);
}
.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>