first commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { mergeProps } from 'vue'
|
||||
|
||||
const theme = useTheme()
|
||||
const drawer = useState('drawer')
|
||||
const route = useRoute()
|
||||
const breadcrumbs = computed(() => {
|
||||
return route!.matched
|
||||
.filter((item) => item.meta && item.meta.title)
|
||||
.map((r) => ({
|
||||
title: r.meta.title!,
|
||||
disabled: r.path === route.path || false,
|
||||
to: r.path,
|
||||
}))
|
||||
})
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return theme.global.name.value === 'dark' ? true : false
|
||||
},
|
||||
set(v) {
|
||||
theme.global.name.value = v ? 'dark' : 'light'
|
||||
},
|
||||
})
|
||||
// const { loggedIn, clear, user } = useUserSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar flat>
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer" />
|
||||
<v-breadcrumbs :items="breadcrumbs" />
|
||||
<v-spacer />
|
||||
<div id="app-bar" />
|
||||
<v-switch
|
||||
v-model="isDark"
|
||||
color=""
|
||||
hide-details
|
||||
density="compact"
|
||||
inset
|
||||
false-icon="mdi-white-balance-sunny"
|
||||
true-icon="mdi-weather-night"
|
||||
class="opacity-80"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
href="https://github.com/kingyue737/vitify-nuxt"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon size="30" icon="mdi-github" />
|
||||
</v-btn>
|
||||
<v-menu location="bottom">
|
||||
<template #activator="{ props: menu }">
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltip }">
|
||||
<v-btn icon v-bind="mergeProps(menu, tooltip)" class="ml-1">
|
||||
<!-- <v-icon v-if="!loggedIn" icon="mdi-account-circle" size="36" /> -->
|
||||
<!-- <v-avatar v-else color="primary" size="36"> -->
|
||||
<!-- <v-img :src="user?.avatar_url" /> -->
|
||||
<!-- </v-avatar> -->
|
||||
</v-btn>
|
||||
</template>
|
||||
<!-- <span>{{ loggedIn ? user!.login : 'User' }}</span> -->
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-list>
|
||||
<!-- <v-list-item
|
||||
v-if="!loggedIn"
|
||||
title="Login"
|
||||
prepend-icon="mdi-github"
|
||||
href="/api/auth/github"
|
||||
/> -->
|
||||
<!-- <v-list-item
|
||||
v-else
|
||||
title="Logout"
|
||||
prepend-icon="mdi-logout"
|
||||
@click="clear"
|
||||
/> -->
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const routes = router.getRoutes().filter((r) => r.path.lastIndexOf('/') === 0)
|
||||
const drawerState = useState('drawer', () => true)
|
||||
|
||||
const { mobile, lgAndUp, width } = useDisplay()
|
||||
const drawer = computed({
|
||||
get() {
|
||||
return drawerState.value || !mobile.value
|
||||
},
|
||||
set(val: boolean) {
|
||||
drawerState.value = val
|
||||
},
|
||||
})
|
||||
const rail = computed(() => !drawerState.value && !mobile.value)
|
||||
routes.sort((a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98))
|
||||
|
||||
drawerState.value = lgAndUp.value && width.value !== 1280
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:expand-on-hover="rail"
|
||||
:rail="rail"
|
||||
floating
|
||||
>
|
||||
<template #prepend>
|
||||
<v-list>
|
||||
<v-list-item class="pa-1">
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="custom:vitify-nuxt"
|
||||
size="x-large"
|
||||
class="drawer-header-icon"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title
|
||||
class="text-h5 font-weight-bold"
|
||||
style="line-height: 2rem"
|
||||
>
|
||||
Vitify <span class="text-primary">Admin</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
<v-list nav density="compact">
|
||||
<AppDrawerItem v-for="route in routes" :key="route.name" :item="route" />
|
||||
</v-list>
|
||||
<v-spacer />
|
||||
<template #append>
|
||||
<v-list-item class="drawer-footer px-0 d-flex flex-column justify-center">
|
||||
<div class="text-caption pt-6 pt-md-0 text-center text-no-wrap">
|
||||
© Copyright 2023
|
||||
<a
|
||||
href="https://github.com/kingyue737"
|
||||
class="font-weight-bold text-primary"
|
||||
target="_blank"
|
||||
>Yue JIN</a
|
||||
>
|
||||
<span> & </span>
|
||||
<a
|
||||
href="https://www.nustarnuclear.com/"
|
||||
class="font-weight-bold text-primary"
|
||||
target="_blank"
|
||||
>NuStar</a
|
||||
>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-navigation-drawer {
|
||||
transition-property:
|
||||
box-shadow, transform, visibility, width, height, left, right, top, bottom,
|
||||
border-radius !important;
|
||||
overflow: hidden;
|
||||
&.v-navigation-drawer--rail {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
&.v-navigation-drawer--is-hovering {
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
box-shadow:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 30%),
|
||||
0px 1px 3px 1px rgb(0 0 0 / 15%);
|
||||
}
|
||||
&:not(.v-navigation-drawer--is-hovering) {
|
||||
.drawer-footer {
|
||||
transform: translateX(-160px);
|
||||
}
|
||||
.drawer-header-icon {
|
||||
height: 1em !important;
|
||||
width: 1em !important;
|
||||
}
|
||||
.v-list-group {
|
||||
--list-indent-size: 0px;
|
||||
--prepend-width: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden;
|
||||
@supports (scrollbar-gutter: stable) {
|
||||
scrollbar-gutter: stable;
|
||||
> .v-list--nav {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
}
|
||||
.drawer-footer {
|
||||
transition: all 0.2s;
|
||||
min-height: 30px;
|
||||
}
|
||||
.drawer-header-icon {
|
||||
opacity: 1 !important;
|
||||
height: 1.2em !important;
|
||||
width: 1.2em !important;
|
||||
transition: all 0.2s;
|
||||
margin-right: -10px;
|
||||
}
|
||||
.v-list-group {
|
||||
--prepend-width: 10px;
|
||||
}
|
||||
.v-list-item {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: RouteRecordRaw
|
||||
}>()
|
||||
const visibleChildren = computed(() =>
|
||||
item.children
|
||||
?.filter((child) => child.meta?.icon)
|
||||
.sort((a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98)),
|
||||
)
|
||||
const visibleChildrenNum = computed(() => visibleChildren.value?.length || 0)
|
||||
const isItem = computed(() => !item.children || visibleChildrenNum.value <= 1)
|
||||
const title = toRef(() => item.meta?.title)
|
||||
const icon = toRef(() => item.meta?.icon)
|
||||
// @ts-expect-error unknown type miss match
|
||||
const to = computed<RouteRecordRaw>(() => ({
|
||||
name: item.name || visibleChildren.value?.[0].name,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="isItem && icon"
|
||||
:to="to"
|
||||
:prepend-icon="icon"
|
||||
active-class="text-primary"
|
||||
:title="title"
|
||||
/>
|
||||
<v-list-group v-else-if="icon" :prepend-icon="icon" color="primary">
|
||||
<template #activator="{ props: vProps }">
|
||||
<v-list-item :title="title" v-bind="vProps" />
|
||||
</template>
|
||||
<AppDrawerItem
|
||||
v-for="child in visibleChildren"
|
||||
:key="child.name"
|
||||
:item="child"
|
||||
/>
|
||||
</v-list-group>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-footer app>
|
||||
<v-spacer />
|
||||
<v-defaults-provider
|
||||
:defaults="{ VBtn: { variant: 'text', size: 'x-small' } }"
|
||||
>
|
||||
<AppNotification />
|
||||
<AppSettings />
|
||||
</v-defaults-provider>
|
||||
</v-footer>
|
||||
</template>
|
||||
<style>
|
||||
.v-footer {
|
||||
padding: 0px 10px !important;
|
||||
> .v-btn--icon {
|
||||
.v-icon {
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
const notificationStore = useNotificationStore()
|
||||
const { notifications } = storeToRefs(notificationStore)
|
||||
const notificationsShown = computed(() =>
|
||||
notifications.value.filter((notification) => notification.show).reverse(),
|
||||
)
|
||||
const showAll = ref(false)
|
||||
const timeout = computed(() => (showAll.value ? -1 : 5000))
|
||||
function deleteNotification(id: number) {
|
||||
notificationStore.delNotification(id)
|
||||
}
|
||||
function emptyNotifications() {
|
||||
notificationStore.$reset()
|
||||
}
|
||||
function toggleAll() {
|
||||
showAll.value = !showAll.value
|
||||
notifications.value.forEach((m) => {
|
||||
m.show = showAll.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Notification' }"
|
||||
:icon="notifications.length ? 'mdi-bell-badge-outline' : 'mdi-bell-outline'"
|
||||
:rounded="0"
|
||||
@click="toggleAll"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<teleport to="body">
|
||||
<v-card
|
||||
elevation="6"
|
||||
width="400"
|
||||
class="d-flex flex-column notification-card"
|
||||
:class="{ 'notification-card--open': showAll }"
|
||||
>
|
||||
<v-toolbar flat density="compact">
|
||||
<v-toolbar-title
|
||||
class="font-weight-light text-body-1"
|
||||
:text="
|
||||
notifications.length ? 'Notification' : 'No New Notifications'
|
||||
"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Clear All Notifications' }"
|
||||
size="small"
|
||||
icon="mdi-bell-remove"
|
||||
@click="emptyNotifications"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Hide Notifications' }"
|
||||
class="mr-0"
|
||||
size="small"
|
||||
icon="$expand"
|
||||
@click="toggleAll"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-slide-y-reverse-transition
|
||||
tag="div"
|
||||
class="d-flex flex-column notification-box"
|
||||
group
|
||||
hide-on-leave
|
||||
>
|
||||
<div
|
||||
v-for="notification in notificationsShown"
|
||||
:key="notification.id"
|
||||
class="notification-item-wrapper"
|
||||
>
|
||||
<AppNotificationItem
|
||||
v-model="notification.show"
|
||||
:notification="notification"
|
||||
:timeout="timeout"
|
||||
class="notification-item"
|
||||
@close="deleteNotification(notification.id)"
|
||||
/>
|
||||
</div>
|
||||
</v-slide-y-reverse-transition>
|
||||
</v-card>
|
||||
</teleport>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-item {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.notification-card {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
bottom: 48px;
|
||||
max-height: 100vh;
|
||||
overflow: visible;
|
||||
visibility: hidden;
|
||||
&.notification-card--open {
|
||||
visibility: visible;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 200px);
|
||||
.notification-box {
|
||||
overflow-y: overlay;
|
||||
pointer-events: auto;
|
||||
.notification-item-wrapper {
|
||||
transition: none !important;
|
||||
.notification-item {
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border-top: 1px solid #5656563d !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.notification-box {
|
||||
overflow-y: visible;
|
||||
visibility: visible;
|
||||
pointer-events: none;
|
||||
.notification-item {
|
||||
pointer-events: initial;
|
||||
user-select: initial;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { Notification } from '~/stores/notification'
|
||||
|
||||
const props = defineProps<{
|
||||
timeout: number
|
||||
notification: Notification
|
||||
}>()
|
||||
const emit = defineEmits(['close'])
|
||||
const isShow = defineModel<boolean>({ default: false })
|
||||
const timeout = toRef(props, 'timeout')
|
||||
const { start, stop } = useTimeoutFn(() => (isShow.value = false), timeout, {
|
||||
immediate: false,
|
||||
})
|
||||
watch(timeout, (v) => (v !== -1 ? start() : stop()), { immediate: true })
|
||||
const variant = computed(() => timeout.value === -1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-alert
|
||||
:border="variant ? 'start' : false"
|
||||
:variant="variant ? 'outlined' : undefined"
|
||||
:density="variant ? 'compact' : undefined"
|
||||
:theme="variant ? undefined : 'dark'"
|
||||
:elevation="variant ? 0 : 3"
|
||||
:type="notification.type"
|
||||
:text="notification.text"
|
||||
:title="notification.time.toLocaleString()"
|
||||
>
|
||||
<template #close>
|
||||
<v-btn icon="$close" @click="emit('close')" />
|
||||
</template>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.v-alert-title) {
|
||||
line-height: 1.25rem;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { mergeProps } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const theme = useTheme()
|
||||
const primary = useStorage('theme-primary', '#1697f6')
|
||||
const color = computed({
|
||||
get() {
|
||||
return theme.themes.value.light.colors.primary
|
||||
},
|
||||
set(val: string) {
|
||||
primary.value = val
|
||||
theme.themes.value.light.colors.primary = val
|
||||
theme.themes.value.dark.colors.primary = val
|
||||
},
|
||||
})
|
||||
const colors = [
|
||||
['#1697f6', '#ff9800'],
|
||||
['#4CAF50', '#FF5252'],
|
||||
['#9C27b0', '#E91E63'],
|
||||
['#304156', '#3f51b5'],
|
||||
['#002FA7', '#492d22'],
|
||||
]
|
||||
const menuShow = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-menu
|
||||
v-model="menuShow"
|
||||
:close-on-content-click="false"
|
||||
location="top right"
|
||||
offset="15"
|
||||
>
|
||||
<template #activator="{ props: menu }">
|
||||
<v-tooltip location="top" text="Theme Palette">
|
||||
<template #activator="{ props: tooltip }">
|
||||
<v-btn
|
||||
icon="mdi-palette-outline"
|
||||
v-bind="mergeProps(menu, tooltip)"
|
||||
:rounded="0"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-card width="320">
|
||||
<v-card-text class="text-center">
|
||||
<v-label class="mb-3"> Theme Palette </v-label>
|
||||
<v-color-picker
|
||||
v-model="color"
|
||||
show-swatches
|
||||
elevation="0"
|
||||
width="288"
|
||||
mode="rgb"
|
||||
:modes="['rgb', 'hex', 'hsl']"
|
||||
:swatches="colors"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
Reference in New Issue
Block a user