main
vue 276 lines 6.12 KB
Raw
1 <template>
2 <div class="pinned-pages flex items-center gap-5">
3 <TransitionGroup name="anim" tag="div" class="latest-list flex items-center gap-4 overflow-hidden">
4 <n-tag
5 v-for="page of latestSanitized"
6 :key="page.name"
7 round
8 :bordered="false"
9 :closable="false"
10 @close="removeLatestPage(page.name)"
11 >
12 <span class="page-name" :title="page.title" @click="gotoPage(page.name)">
13 {{ page.title }}
14 </span>
15 <template #icon>
16 <div class="icon-box" @click="pinPage(page)">
17 <Icon :size="14" :name="PinnedIcon" />
18 </div>
19 </template>
20 </n-tag>
21 </TransitionGroup>
22
23 <Transition name="anim" tag="div" class="shortcuts-container flex items-center">
24 <div v-if="pinned.length" class="flex items-center">
25 <n-popover :show-arrow="false" placement="bottom" trigger="hover" class="p-1!">
26 <template #trigger>
27 <button class="shortcuts-btn flex items-center gap-2 text-sm">
28 <span>Shortcuts</span>
29 <n-badge :value="pinned.length" />
30 </button>
31 </template>
32 <div class="flex flex-col">
33 <n-button
34 v-for="page of pinned"
35 :key="page.name"
36 size="small"
37 quaternary
38 class="justify-start!"
39 @click="gotoPage(page.name)"
40 >
41 <span class="flex items-center gap-1">
42 <Icon
43 :size="20"
44 :name="CloseIcon"
45 class="opacity-50 hover:text-red-500 hover:opacity-100"
46 @click.stop="removePinnedPage(page.name)"
47 />
48 <span class="px-2">
49 {{ page.title }}
50 </span>
51 </span>
52 </n-button>
53 </div>
54 </n-popover>
55 </div>
56 </Transition>
57 </div>
58 </template>
59
60 <script lang="ts" setup>
61 import type { RemovableRef } from "@vueuse/core"
62 import type { ComputedRef } from "vue"
63 import type { RouteLocationNormalized, RouteRecordName } from "vue-router"
64 import { useStorage } from "@vueuse/core"
65 import _split from "lodash/split"
66 import _takeRight from "lodash/takeRight"
67 import _uniqBy from "lodash/uniqBy"
68 import { NBadge, NButton, NPopover, NTag } from "naive-ui"
69 import { computed } from "vue"
70 import { useRouter } from "vue-router"
71 import Icon from "@/components/common/Icon.vue"
72
73 interface Page {
74 name: RouteRecordName | string
75 fullPath: string
76 title: string
77 }
78
79 const PinnedIcon = "tabler:pinned"
80 const CloseIcon = "carbon:close"
81 const router = useRouter()
82 const latest: RemovableRef<Page[]> = useStorage<Page[]>("latest-pages", [], sessionStorage)
83 const pinned: RemovableRef<Page[]> = useStorage<Page[]>("pinned-pages", [], localStorage)
84 const latestSanitized: ComputedRef<Page[]> = computed(() => {
85 return _takeRight(
86 latest.value.filter(page => pinned.value.findIndex(p => p.name === page.name) === -1).reverse(),
87 3
88 ) as Page[]
89 })
90
91 function removeLatestPage(pageName: RouteRecordName | string) {
92 latest.value = latest.value.filter(page => page.name !== pageName)
93 return true
94 }
95 function removePinnedPage(pageName: RouteRecordName | string) {
96 pinned.value = pinned.value.filter(page => page.name !== pageName)
97 return true
98 }
99 function gotoPage(pageName: RouteRecordName | string) {
100 router.push({ name: pageName })
101 return true
102 }
103
104 function pinPage(page: Page) {
105 const isPresent = pinned.value.findIndex(p => p.name === page.name) !== -1
106 if (!isPresent) {
107 pinned.value = [page, ...pinned.value]
108 }
109 return true
110 }
111
112 function checkRoute(route: RouteLocationNormalized) {
113 const splitName = _split(route.name?.toString(), "-")
114 const title = route.meta?.title || splitName.at(-1)
115
116 if (route.name && title && !route.meta?.skipPin) {
117 const page: Page = {
118 name: route.name,
119 fullPath: route.fullPath,
120 title
121 }
122 latest.value = _uniqBy([page, ...latest.value, page], "name")
123 }
124 }
125
126 router.afterEach(route => {
127 checkRoute(route)
128 })
129 </script>
130
131 <style lang="scss" scoped>
132 .pinned-pages {
133 position: relative;
134 overflow: hidden;
135 padding: 8px 0;
136 container-type: inline-size;
137 justify-content: flex-end;
138 width: 100%;
139
140 :deep() {
141 .n-tag {
142 background-color: transparent;
143 flex-shrink: 1;
144 overflow: hidden;
145 gap: 4px;
146
147 &.n-tag--round {
148 padding: 0 !important;
149 transition: all 0.3s;
150 }
151 .n-tag__icon {
152 margin: 0 !important;
153 }
154 .n-tag__content {
155 overflow: hidden;
156 text-overflow: ellipsis;
157 }
158 .n-tag__close {
159 overflow: hidden;
160 width: 0px;
161 margin-left: 0;
162 margin-right: 0;
163 transition: all 0.3s;
164 }
165
166 &.n-tag--closable {
167 &:hover {
168 background-color: var(--bg-sidebar-color);
169
170 &.n-tag--round {
171 padding: 0 calc(var(--n-height) / 3.6) 0 calc(var(--n-height) / 3.6);
172 }
173 .n-tag__close {
174 margin-left: 5px;
175 overflow: initial;
176 width: 14px;
177 }
178 }
179 }
180 }
181 }
182
183 .shortcuts-btn {
184 border-radius: 50px;
185 background-color: var(--bg-sidebar-color);
186 gap: 10px;
187 height: 32px;
188 cursor: pointer;
189 padding: 0px 6px;
190 padding-left: 12px;
191 outline: none;
192 transition: all 0.2s var(--bezier-ease);
193 border: 1px solid var(--border-color);
194
195 &:hover {
196 background-color: var(--hover-color);
197 }
198
199 :deep() {
200 .n-badge {
201 .n-badge-sup {
202 --size: 22px;
203 background-color: var(--bg-body-color);
204 color: var(--fg-secondary-color);
205 font-weight: 700;
206 height: var(--size);
207 line-height: var(--size);
208 min-width: var(--size);
209 border-radius: var(--size);
210 font-variant-numeric: tabular-nums;
211 justify-content: center;
212 padding: 0;
213 }
214 }
215 }
216 }
217
218 .page-name {
219 cursor: pointer;
220 overflow: hidden;
221 text-overflow: ellipsis;
222
223 &:hover {
224 text-decoration: underline;
225 text-decoration-thickness: 2px;
226 text-decoration-color: var(--primary-color);
227 }
228 }
229
230 .icon-box {
231 cursor: pointer;
232 transition: color 0.3s;
233
234 &:hover {
235 color: var(--primary-color);
236 }
237 }
238
239 .anim-move,
240 .anim-enter-active,
241 .anim-leave-active {
242 transition: all 0.5s var(--bezier-ease);
243 }
244
245 .anim-enter-from,
246 .anim-leave-to {
247 opacity: 0;
248 transform: scale(0);
249 }
250
251 .anim-leave-active {
252 position: absolute;
253 }
254
255 @container (max-width: 460px) {
256 .latest-list {
257 display: none;
258 }
259 }
260
261 @container (max-width: 140px) {
262 .shortcuts-container {
263 display: none;
264 }
265 }
266 }
267
268 .direction-rtl {
269 .pinned-pages {
270 .shortcuts-btn {
271 padding-right: 12px;
272 padding-left: 6px;
273 }
274 }
275 }
276 </style>