云
23 小时以前 91bfcf61f44f353dfebc54a57b5b774721de3e9a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben/types';
 
import { computed, ref } from 'vue';
 
import { IconifyIcon } from '@vben/icons';
import { preferences } from '@vben/preferences';
 
import PortalFooter from '#/components/portal/portal-footer.vue';
import { portalCategory, usePortalMenus } from '#/components/portal/use-portal-menus';
 
import KpiCards from './modules/kpi-cards.vue';
import NoticePanel from './modules/notice-panel.vue';
import QuickActions from './modules/quick-actions.vue';
import ResearchHighlights from './modules/research-highlights.vue';
import TodoPanel from './modules/todo-panel.vue';
 
defineOptions({ name: 'DashboardAnalytics' });
 
const { menus, leaves, features, goMenu } = usePortalMenus();
const appName = computed(() => preferences.copyright.companyName);
const slideIndex = ref(0);
const slides = [
  { title: '服务供销合作 助力乡村振兴', motto: '开放协作 · 学术研究 · 服务发展', description: '推动供销合作经济研究,服务行业高质量发展' },
  { title: '凝聚学术力量 深化课题研究', motto: '课题指南 · 在线申报 · 进度跟踪', description: '围绕研究课题,连接申报、审批与协作服务' },
  { title: '协同高效办公 共享发展成果', motto: '流程协同 · 通知互通 · 便捷办理', description: '汇聚日常业务服务,让各项工作有序推进' },
];
const currentSlide = computed(() => slides[slideIndex.value] ?? slides[0]!);
const showWork = computed(() => Boolean(features.value.todo || features.value.application || features.value.reimbursement || features.value.guideline));
const topicMenus = computed(() => menus.value.slice(0, 4));
const serviceLinks = computed(() => leaves.value.slice(0, 6));
 
function description(menu: MenuRecordRaw) {
  switch (portalCategory(menu)) {
    case 'research': return '聚焦学术研究,汇集课题服务';
    case 'office': return '协同办理流程,提升办公效率';
    case 'finance': return '规范费用办理,便捷财务服务';
    default: return '汇聚业务信息,提供协同服务';
  }
}
</script>
 
<template>
  <div class="portal-home">
    <section class="portal-hero" aria-labelledby="portal-hero-title">
      <div class="portal-hero-copy">
        <h1 id="portal-hero-title"><span v-for="part in currentSlide.title.split(' ')" :key="part">{{ part }}</span></h1>
        <div class="portal-hero-organization"><span></span><strong>{{ appName }}</strong><span></span></div>
        <p class="portal-hero-motto">{{ currentSlide.motto }}</p>
        <p class="portal-hero-description">{{ currentSlide.description }}</p>
      </div>
      <div class="portal-hero-pages" aria-label="首页主题切换">
        <button v-for="(slide, index) in slides" :key="slide.title" type="button" :aria-label="`查看主题:${slide.title}`" :aria-pressed="slideIndex === index" :class="{ active: slideIndex === index }" @click="slideIndex = index"></button>
      </div>
    </section>
 
    <div class="portal-content">
      <QuickActions />
 
      <section v-if="features.application || features.todo || features.notice" class="portal-news-grid" aria-label="业务动态与通知">
        <ResearchHighlights v-if="features.application" :menu="features.application" />
        <TodoPanel v-else-if="features.todo" />
        <NoticePanel v-if="features.notice" :menu-path="features.notice.path" />
      </section>
 
      <section v-if="menus.length" class="portal-columns" aria-label="业务栏目">
        <article v-for="menu in menus" :key="menu.path" class="portal-column" :class="`category-${portalCategory(menu)}`">
          <div class="portal-column-heading"><span><IconifyIcon :icon="typeof menu.icon === 'string' ? menu.icon : 'lucide:layers'" /></span><div><h2>{{ menu.name }}</h2><p>{{ description(menu) }}</p></div></div>
          <div class="portal-column-body">
            <div class="portal-column-links">
              <button v-for="link in (menu.children ?? []).slice(0, 4)" :key="link.path" type="button" @click="goMenu(link)"><IconifyIcon icon="lucide:chevron-right" /><span>{{ link.name }}</span></button>
              <p v-if="!menu.children?.length">{{ menu.name }}相关业务办理</p>
            </div>
            <div class="portal-column-photo portal-photo" :class="`photo-${portalCategory(menu)}`" aria-hidden="true"></div>
          </div>
          <button class="portal-column-enter" type="button" @click="goMenu(menu)">进入栏目 <IconifyIcon icon="lucide:chevron-right" /></button>
        </article>
      </section>
 
      <section v-if="topicMenus.length" class="portal-topic-strip" aria-label="业务直达">
        <div class="portal-topic-label"><h2>业务直达</h2><p>汇聚服务资源<br />深化业务协同</p></div>
        <button v-for="menu in topicMenus" :key="menu.path" type="button" class="portal-topic portal-photo" :class="`photo-${portalCategory(menu)}`" @click="goMenu(menu)"><span>{{ menu.name }}</span><IconifyIcon icon="lucide:arrow-up-right" /></button>
      </section>
 
      <section v-if="showWork" class="portal-work-section" aria-labelledby="work-title">
        <div class="portal-panel-heading"><h2 id="work-title"><IconifyIcon icon="lucide:briefcase-business" />我的工作</h2><span>业务数据概览</span></div>
        <KpiCards />
      </section>
 
      <section class="portal-service-grid">
        <article class="portal-service-intro">
          <div class="portal-panel-heading"><h2><IconifyIcon icon="lucide:landmark" />平台服务</h2></div>
          <div class="portal-service-intro-body"><div class="portal-service-photo" aria-hidden="true"></div><div><h3>{{ appName }}</h3><p>围绕课题研究、流程协同与日常业务,汇聚服务入口,连接信息与资源,为各项工作的有序开展提供便捷支持。</p><button v-if="features.todo" type="button" @click="goMenu(features.todo)">查看我的待办 <IconifyIcon icon="lucide:chevron-right" /></button></div></div>
        </article>
        <article v-if="serviceLinks.length" class="portal-service-links">
          <div class="portal-panel-heading"><h2><IconifyIcon icon="lucide:link" />常用功能</h2><span>一站式业务服务</span></div>
          <div class="portal-service-link-grid"><button v-for="menu in serviceLinks" :key="menu.path" type="button" @click="goMenu(menu)"><IconifyIcon :icon="typeof menu.icon === 'string' ? menu.icon : 'lucide:arrow-up-right'" /><span>{{ menu.name }}</span></button></div>
        </article>
      </section>
    </div>
    <PortalFooter />
  </div>
</template>
 
<style scoped>
.portal-home { --ink: #153a62; --muted: #5c7187; color: var(--ink); background: #f2f7fc; }
.portal-hero { position: relative; display: grid; min-height: 350px; place-items: center; overflow: hidden; background: url('/static/imgs/portal-hero-v2.png') center 59% / cover; }
.portal-hero::before { position: absolute; inset: 0; background: radial-gradient(ellipse at 52% 37%, rgb(255 255 255 / 86%) 0%, rgb(255 255 255 / 64%) 25%, rgb(255 255 255 / 5%) 67%); content: ''; }
.portal-hero-copy { position: relative; z-index: 1; width: min(1180px, 90%); padding: 24px 0 42px; text-align: center; }
.portal-hero h1 span { display: inline-block; white-space: nowrap; }.portal-hero h1 span + span { margin-left: .7em; }
.portal-hero h1 { margin: 0 0 12px; color: #ac131b; font-family: 'KaiTi', 'STKaiti', 'Noto Serif SC', serif; font-size: clamp(32px, 4vw, 54px); font-weight: 800; letter-spacing: .055em; line-height: 1.45; text-shadow: 0 2px 0 rgb(255 255 255 / 75%); }
.portal-hero-organization { display: flex; align-items: center; justify-content: center; gap: 16px; color: #103965; }
.portal-hero-organization strong { font-size: 23px; font-weight: 650; letter-spacing: .14em; }
.portal-hero-organization span { width: 58px; height: 1px; background: #527ba4; }
.portal-hero-motto { margin: 18px 0 5px; color: #153b67; font-family: 'KaiTi', 'STKaiti', serif; font-size: 23px; font-weight: 600; letter-spacing: .18em; }
.portal-hero-description { margin: 0; color: #234f78; font-size: 14px; letter-spacing: .12em; }
.portal-hero-pages { position: absolute; bottom: 13px; z-index: 2; display: flex; gap: 0; }
.portal-hero-pages button { display: grid; width: 28px; height: 28px; place-items: center; }
.portal-hero-pages button::before { width: 10px; height: 10px; border: 1px solid rgb(21 58 98 / 16%); border-radius: 50%; background: #fff; content: ''; }
.portal-hero-pages button.active::before { width: 19px; border-color: #bb2029; border-radius: 7px; background: #bb2029; }
.portal-content { width: min(1280px, calc(100% - 80px)); margin: 0 auto; padding-top: 22px; }
.portal-news-grid { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); gap: 16px; margin-top: 22px; }
.portal-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); gap: 14px; margin-top: 18px; }
.portal-column { display: flex; min-width: 0; min-height: 278px; flex-direction: column; padding: 19px 18px 16px; border: 1px solid #e0eaf4; border-radius: 6px; background: #fff; box-shadow: 0 3px 10px rgb(24 66 106 / 4%); }
.portal-column-heading { display: flex; align-items: center; gap: 11px; }
.portal-column-heading > span { display: grid; width: 46px; height: 46px; flex: none; place-items: center; border-radius: 50%; color: #216dc1; background: #eaf3ff; font-size: 25px; }
.category-office .portal-column-heading > span { color: #c42a22; background: #fff0ed; }
.category-finance .portal-column-heading > span { color: #1d5277; background: #edf3f7; }
.portal-column-heading h2 { margin: 0; color: #153a62; font-size: 17px; font-weight: 700; }
.portal-column-heading p { margin: 5px 0 0; color: #5c7187; font-size: 11px; }
.portal-column-body { display: flex; min-height: 132px; justify-content: space-between; gap: 12px; margin: 18px 0 14px; }
.portal-column-links { display: flex; min-width: 0; flex: 1; flex-direction: column; }
.portal-column-links button { display: flex; min-height: 31px; align-items: center; gap: 5px; color: #315273; font-size: 12px; text-align: left; }
.portal-column-links button > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.portal-column-links :deep(svg) { width: 11px; flex: none; color: #8ca7c0; }
.portal-column-links p { color: #5c7187; font-size: 12px; line-height: 1.7; }
.portal-photo { background-image: url('/static/imgs/portal-modules-v2.png'); background-size: 200% auto; }
.photo-research { background-position: left top; }.photo-office { background-position: right top; }.photo-finance { background-position: left bottom; }.photo-archive { background-position: right bottom; }
.portal-column-photo { width: 43%; min-width: 100px; flex: none; border-radius: 4px; }
.portal-column-enter { display: inline-flex; min-height: 28px; align-self: flex-start; align-items: center; gap: 5px; margin-top: auto; padding: 3px 11px; border: 1px solid #d9e7f5; border-radius: 4px; color: #315f90; background: #f8fbff; font-size: 11px; }
.portal-topic-strip { display: grid; grid-template-columns: 190px repeat(auto-fit, minmax(170px, 1fr)); align-items: stretch; gap: 12px; margin-top: 18px; padding: 12px; border: 1px solid #e1ebf4; border-radius: 6px; background: #fff; }
.portal-topic-label { margin: -12px 0 -12px -12px; padding: 25px 24px; border-radius: 6px 0 0 6px; color: #fff; background: linear-gradient(110deg, #204d7b, #6499c0); }
.portal-topic-label h2 { margin: 0 0 7px; font-size: 23px; font-weight: 650; }
.portal-topic-label p { margin: 0; color: #edf6ff; font-size: 12px; line-height: 1.7; }
.portal-topic { position: relative; display: flex; min-height: 108px; align-items: flex-end; justify-content: center; overflow: hidden; border-radius: 4px; color: #fff; background-size: 200% 200%; }
.portal-topic::before { position: absolute; inset: 0; background: linear-gradient(transparent 25%, rgb(9 36 60 / 82%)); content: ''; }
.portal-topic span { position: relative; padding: 10px; font-size: 15px; font-weight: 650; }
.portal-topic :deep(svg) { position: absolute; top: 10px; right: 10px; width: 16px; }
.portal-work-section { margin-top: 18px; padding: 18px; border: 1px solid #e1ebf4; border-radius: 6px; background: #fff; }
.portal-panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 15px; }
.portal-panel-heading h2 { display: flex; align-items: center; gap: 9px; margin: 0; font-size: 16px; font-weight: 700; }
.portal-panel-heading h2 :deep(svg) { width: 18px; color: #bb212b; }
.portal-panel-heading > span { color: #5c7187; font-size: 11px; }
.portal-service-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 18px; padding: 20px; border: 1px solid #e1ebf4; border-radius: 6px; background: #fff; }
.portal-service-intro-body { display: flex; gap: 18px; }
.portal-service-photo { width: 155px; min-height: 130px; flex: none; border-radius: 4px; background: url('/static/imgs/portal-hero-v2.png') 90% 72% / 310%; }
.portal-service-intro h3 { margin: 0 0 7px; font-size: 13px; font-weight: 650; }
.portal-service-intro p { margin: 0; color: #5c7187; font-size: 12px; line-height: 1.9; }
.portal-service-intro button { display: inline-flex; min-height: 27px; align-items: center; gap: 4px; margin-top: 10px; padding: 2px 9px; border-radius: 4px; color: #fff; background: #b7242d; font-size: 11px; }
.portal-service-link-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.portal-service-link-grid button { display: flex; min-height: 45px; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 4px; color: #315273; background: #f3f7fc; font-size: 11px; text-align: left; }
.portal-service-link-grid :deep(svg) { width: 21px; height: 21px; flex: none; color: #16803f; }
.portal-home button { cursor: pointer; }
.portal-home button:focus-visible { outline: 2px solid #2c70b2; outline-offset: 3px; }
.portal-column-links button:hover, .portal-column-enter:hover { color: #b7242d; }
@media (max-width: 1050px) { .portal-content { width: calc(100% - 48px); } .portal-topic-strip { grid-template-columns: 150px repeat(auto-fit, minmax(135px, 1fr)); } .portal-service-photo { width: 105px; } .portal-service-link-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .portal-hero-organization strong { font-size: 20px; } }
@media (max-width: 767px) { .portal-hero { min-height: 280px; background-position: 62% 58%; } .portal-hero h1 { font-size: 32px; letter-spacing: .015em; } .portal-hero-organization { gap: 8px; } .portal-hero-organization strong { font-size: 17px; letter-spacing: .03em; } .portal-hero-organization span { width: 22px; } .portal-hero-motto { font-size: 18px; letter-spacing: .03em; } .portal-hero-description { font-size: 12px; letter-spacing: 0; } .portal-content { width: calc(100% - 28px); padding-top: 16px; } .portal-news-grid, .portal-service-grid { grid-template-columns: 1fr; } .portal-topic-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } .portal-topic-label { grid-column: 1 / -1; margin: -12px -12px 0; padding: 14px 18px; border-radius: 6px 6px 0 0; } .portal-topic-label h2 { font-size: 19px; } .portal-topic-label p { display: none; } .portal-service-grid { padding: 16px; } .portal-service-photo { width: 105px; } }
@media (max-width: 767px) { .portal-hero h1 span { display: block; }.portal-hero h1 span + span { margin-left: 0; }.portal-hero::before { background: radial-gradient(ellipse at 52% 40%, rgb(255 255 255 / 92%) 0%, rgb(255 255 255 / 75%) 42%, rgb(255 255 255 / 8%) 90%); } }
@media (max-width: 420px) { .portal-hero h1 { font-size: 29px; } .portal-hero-organization strong { font-size: 15px; } .portal-hero-organization span { width: 16px; } .portal-column { min-height: 260px; } }
</style>