first commit

This commit is contained in:
2025-04-22 10:56:56 +07:00
commit af123c091b
147 changed files with 778063 additions and 0 deletions
+82
View File
@@ -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>
+135
View File
@@ -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">
&copy; 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>
+40
View File
@@ -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>
+22
View File
@@ -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>
+125
View File
@@ -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>
+41
View File
@@ -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>
+60
View File
@@ -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>