feat: 重构各级导航
chore: 配置 prettier
This commit is contained in:
parent
7727166bf5
commit
28f84bca92
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"singleAttributePerLine": true
|
||||||
|
}
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"eslint.useFlatConfig": true,
|
"eslint.useFlatConfig": true,
|
||||||
"prettier.bracketSameLine": true,
|
|
||||||
"prettier.semi": false,
|
"prettier.semi": false,
|
||||||
"prettier.singleAttributePerLine": true,
|
"prettier.singleAttributePerLine": true,
|
||||||
"prettier.singleQuote": true
|
"prettier.singleQuote": true
|
||||||
|
104
components/SubNav.vue
Normal file
104
components/SubNav.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export interface SubNavItem {
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
navs: SubNavItem[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const isCurrentPath = (path: string) => {
|
||||||
|
return route.path === path
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-end ml-4 z-0">
|
||||||
|
<div
|
||||||
|
v-for="(nav, i) in navs"
|
||||||
|
:key="i"
|
||||||
|
class="subnav-item"
|
||||||
|
:class="`${isCurrentPath(nav.to) ? 'active' : ''}`"
|
||||||
|
:style="{
|
||||||
|
'z-index': isCurrentPath(nav.to) ? 10 : -i,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="absolute inset-0 aspect-auto"
|
||||||
|
viewBox="0 0 206.5 72"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
:id="`paint_linear_${i}`"
|
||||||
|
gradient-units="objectBoundingBox"
|
||||||
|
x1="0.5"
|
||||||
|
y1="0"
|
||||||
|
x2="0.5"
|
||||||
|
y2="1"
|
||||||
|
>
|
||||||
|
<stop stop-color="var(--svg-stop1)" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="var(--svg-stop2)"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
id="subnav_item_active"
|
||||||
|
d="M51.9 0L154.6 0C172.19 0 187.72 11.48 192.86 28.31L206.5 72L0 72L13.35 28.31C18.49 11.48 34.02 0 51.9 0Z"
|
||||||
|
:fill="`url(#paint_linear_${i})`"
|
||||||
|
fill-opacity="1.000000"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<NuxtLink
|
||||||
|
class="text-lg font-medium z-10 select-none"
|
||||||
|
:class="{
|
||||||
|
'text-secondary': isCurrentPath(nav.to),
|
||||||
|
'text-neutral-400 dark:text-neutral-500': !isCurrentPath(nav.to),
|
||||||
|
}"
|
||||||
|
:to="nav.to"
|
||||||
|
>
|
||||||
|
{{ nav.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subnav-item {
|
||||||
|
@apply relative flex justify-center items-center px-4 pt-1 drop-shadow-md;
|
||||||
|
|
||||||
|
--svg-stop1: #ffffff;
|
||||||
|
--svg-stop2: #f3f3f3;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
@apply -ml-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
--svg-stop1: #6059f4;
|
||||||
|
--svg-stop2: #8e59f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .subnav-item {
|
||||||
|
--svg-stop1: #000000;
|
||||||
|
--svg-stop2: #0c0c0c;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
--svg-stop1: #6d7ca6;
|
||||||
|
--svg-stop2: #6f6da6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
29
components/app/Container.vue
Normal file
29
components/app/Container.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { SubNavItem } from '../SubNav.vue'
|
||||||
|
|
||||||
|
defineProps<{ subnavs?: SubNavItem[] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-1 flex-col p-8 page-bg-gradient">
|
||||||
|
<!-- <h1 class="pl-2 text-xl font-medium">外部标题</h1> -->
|
||||||
|
<slot name="subnav">
|
||||||
|
<SubNav
|
||||||
|
v-if="subnavs && subnavs.length"
|
||||||
|
:navs="subnavs"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<div
|
||||||
|
class="bg-white h-full rounded-lg shadow-sm p-8 dark:bg-neutral-900 z-20"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-bg-gradient {
|
||||||
|
@apply bg-gradient-to-br from-[#D5DEF9]/50 to-[#D6C9F9]/50;
|
||||||
|
@apply dark:from-[#36477A]/50 dark:to-[#7C6DA6]/50;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,20 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRight, type LucideIcon } from 'lucide-vue-next'
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { SidebarNavItem } from './Sidebar.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
nav: {
|
nav: {
|
||||||
label?: string
|
label?: string
|
||||||
items: {
|
items: SidebarNavItem[]
|
||||||
title: string
|
|
||||||
url?: RouteLocationRaw | string
|
|
||||||
icon: LucideIcon | string
|
|
||||||
isActive?: boolean
|
|
||||||
items?: {
|
|
||||||
title: string
|
|
||||||
url: string
|
|
||||||
}[]
|
|
||||||
}[]
|
|
||||||
}[]
|
}[]
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
@ -41,16 +32,16 @@ defineProps<{
|
|||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="item.url"
|
v-if="item.url"
|
||||||
v-slot="{ isActive, href, navigate }"
|
v-slot="{ isActive, href, navigate }"
|
||||||
class="py-6"
|
class="py-5"
|
||||||
:to="item.url"
|
:to="item.url"
|
||||||
custom
|
custom
|
||||||
>
|
>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
as="a"
|
as="a"
|
||||||
class="flex justify-start text-base pl-8"
|
|
||||||
:tooltip="item.title"
|
:tooltip="item.title"
|
||||||
:is-active="isActive"
|
:is-active="isActive"
|
||||||
:href
|
:href
|
||||||
|
:target="item.isExternal ? '_blank' : undefined"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
>
|
>
|
||||||
<!-- 图标名 -->
|
<!-- 图标名 -->
|
||||||
@ -71,6 +62,13 @@ defineProps<{
|
|||||||
v-if="item.items"
|
v-if="item.items"
|
||||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||||
/>
|
/>
|
||||||
|
<!-- 外部链接 -->
|
||||||
|
<Icon
|
||||||
|
v-if="item.isExternal"
|
||||||
|
name="tabler:external-link"
|
||||||
|
class="ml-auto !size-4 opacity-50"
|
||||||
|
size="16px"
|
||||||
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<!-- 无跳转链接 -->
|
<!-- 无跳转链接 -->
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { SidebarNavGroup } from './Sidebar.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
sidebarNav: SidebarNavGroup[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
props,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<SidebarProvider style="--sidebar-width: 200px">
|
|
||||||
<slot
|
|
||||||
name="sidebar"
|
|
||||||
:sidebar-nav="sidebarNav"
|
|
||||||
>
|
|
||||||
<AppSidebar :nav="sidebarNav" />
|
|
||||||
</slot>
|
|
||||||
<SidebarInset>
|
|
||||||
<slot name="topbar">
|
|
||||||
<AppTopbar />
|
|
||||||
</slot>
|
|
||||||
<div class="flex flex-1 flex-col gap-4 p-4">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</SidebarInset>
|
|
||||||
</SidebarProvider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
@ -8,6 +8,7 @@ export interface SidebarNavItem {
|
|||||||
url?: string | RouteLocationRaw
|
url?: string | RouteLocationRaw
|
||||||
icon: LucideIcon | string
|
icon: LucideIcon | string
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
isExternal?: boolean
|
||||||
items?: {
|
items?: {
|
||||||
title: string
|
title: string
|
||||||
url: string
|
url: string
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'
|
import type { IBreadcrumbItem } from '~/types'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
breadcrumbs: {
|
||||||
|
type: Array as () => IBreadcrumbItem[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
hideTrigger: {
|
hideTrigger: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -48,16 +52,43 @@ export const topbarNavDefaults = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear border-b sticky top-0 z-30 bg-background"
|
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear border-b sticky top-0 z-30 bg-background">
|
||||||
>
|
|
||||||
<!-- group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 -->
|
<!-- group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 -->
|
||||||
<div class="flex items-center gap-2 px-4 w-full">
|
<div class="flex justify-between items-center gap-2 px-4 w-full">
|
||||||
<SidebarTrigger
|
<div
|
||||||
v-if="!hideTrigger"
|
class="flex items-center gap-2"
|
||||||
class="-ml-1"
|
:class="`${hideTrigger ? 'px-7' : ''}`">
|
||||||
/>
|
<SidebarTrigger v-if="!hideTrigger" class="-ml-1" />
|
||||||
|
<img
|
||||||
|
v-if="hideTrigger"
|
||||||
|
src="/images/xsh_logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
class="w-10 aspect-square" />
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
class="mr-2 h-4" />
|
||||||
|
<Breadcrumb v-if="breadcrumbs.length > 0">
|
||||||
|
<BreadcrumbList>
|
||||||
|
<template
|
||||||
|
v-for="(crumb, i) in breadcrumbs"
|
||||||
|
:key="i">
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink
|
||||||
|
v-if="crumb.path"
|
||||||
|
:href="crumb.path">
|
||||||
|
{{ crumb.label }}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
<BreadcrumbPage v-else>
|
||||||
|
{{ crumb.label }}
|
||||||
|
</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator v-if="i < breadcrumbs.length - 1" />
|
||||||
|
</template>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
<slot name="title-area" />
|
<slot name="title-area" />
|
||||||
<div class="flex-1 flex justify-center">
|
</div>
|
||||||
|
<!-- <div class="flex-1 flex justify-center">
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
<NavigationMenuItem
|
<NavigationMenuItem
|
||||||
@ -87,21 +118,18 @@ export const topbarNavDefaults = [
|
|||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</div>
|
</div> -->
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon">
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="tabler:moon"
|
name="tabler:moon"
|
||||||
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
/>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="tabler:sun"
|
name="tabler:sun"
|
||||||
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
/>
|
|
||||||
<span class="sr-only">Toggle theme</span>
|
<span class="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
import prettier from 'prettier'
|
||||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
export default withNuxt(
|
export default withNuxt(
|
||||||
@ -6,6 +7,10 @@ export default withNuxt(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'vue/html-self-closing': 'off',
|
'vue/html-self-closing': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
prettier,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,63 @@
|
|||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import type { SidebarNavGroup } from '~/components/app/Sidebar.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { breadcrumbs } = toRefs(useBreadcrumbs())
|
||||||
|
|
||||||
|
const sidebarNav: SidebarNavGroup[] = [
|
||||||
|
{
|
||||||
|
label: '备课制课',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '课程管理',
|
||||||
|
url: `/course`,
|
||||||
|
icon: 'tabler:books',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '课程资源',
|
||||||
|
url: `/course/resources`,
|
||||||
|
icon: 'tabler:users-group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'AI 资源',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: 'AI 备课',
|
||||||
|
url: `/course/prep`,
|
||||||
|
icon: 'tabler:books',
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AI 教科研',
|
||||||
|
url: `/course/research`,
|
||||||
|
icon: 'tabler:users-group',
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full min-h-screen flex font-sans">
|
<div class="w-full min-h-screen flex font-sans">
|
||||||
|
<SidebarProvider style="--sidebar-width: 200px">
|
||||||
|
<AppSidebar
|
||||||
|
v-if="!route.meta.hideSidebar"
|
||||||
|
:nav="sidebarNav"
|
||||||
|
/>
|
||||||
|
<SidebarInset>
|
||||||
|
<AppTopbar
|
||||||
|
:hide-trigger="route.meta.hideSidebar"
|
||||||
|
:breadcrumbs
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -2,17 +2,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full min-h-screen flex flex-col font-sans">
|
<div class="w-full min-h-screen flex flex-col font-sans">
|
||||||
<AppTopbar hide-trigger>
|
<AppTopbar hide-trigger />
|
||||||
<template #title-area>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 w-[calc(var(--sidebar-width)+24px)] overflow-hidden"
|
|
||||||
>
|
|
||||||
<h1 class="text-lg font-medium">
|
|
||||||
智课教学平台
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</AppTopbar>
|
|
||||||
<div class="min-h-[100vh] flex-1 md:min-h-min p-4">
|
<div class="min-h-[100vh] flex-1 md:min-h-min p-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,7 @@ export default defineNuxtConfig({
|
|||||||
indent: 2,
|
indent: 2,
|
||||||
quotes: 'single',
|
quotes: 'single',
|
||||||
semi: false,
|
semi: false,
|
||||||
|
commaDangle: 'only-multiline',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,9 @@
|
|||||||
"@nuxt/test-utils": "3.17.2",
|
"@nuxt/test-utils": "3.17.2",
|
||||||
"@tanstack/vue-table": "^8.21.2",
|
"@tanstack/vue-table": "^8.21.2",
|
||||||
"@vee-validate/zod": "^4.15.0",
|
"@vee-validate/zod": "^4.15.0",
|
||||||
|
"@vue-office/docx": "^1.6.3",
|
||||||
|
"@vue-office/excel": "^1.7.14",
|
||||||
|
"@vue-office/pdf": "^2.0.10",
|
||||||
"@vue-office/pptx": "^1.0.1",
|
"@vue-office/pptx": "^1.0.1",
|
||||||
"@vueuse/core": "^13.0.0",
|
"@vueuse/core": "^13.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@ -49,7 +52,9 @@
|
|||||||
"@vueuse/nuxt": "^13.0.0",
|
"@vueuse/nuxt": "^13.0.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dayjs-nuxt": "^2.1.11",
|
"dayjs-nuxt": "^2.1.11",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"shadcn-nuxt": "2.0.1",
|
"shadcn-nuxt": "2.0.1",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { IResponse } from '~/api'
|
import type { IResponse } from '~/api'
|
||||||
import { getCourseDetail } from '~/api/course'
|
import { getCourseDetail } from '~/api/course'
|
||||||
import type { SidebarNavGroup } from '~/components/app/Sidebar.vue'
|
|
||||||
import { topbarNavDefaults } from '~/components/app/Topbar.vue'
|
|
||||||
import type { ICourse } from '~/types'
|
import type { ICourse } from '~/types'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -10,61 +8,44 @@ const {
|
|||||||
params: { id },
|
params: { id },
|
||||||
} = useRoute()
|
} = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { setBreadcrumbs } = useBreadcrumbs()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: course,
|
data: course,
|
||||||
status: courseStatus,
|
status: courseStatus,
|
||||||
error: courseError,
|
error: courseError,
|
||||||
} = await useAsyncData<
|
} = useAsyncData<
|
||||||
IResponse<{
|
IResponse<{
|
||||||
data: ICourse
|
data: ICourse
|
||||||
}>,
|
}>,
|
||||||
IResponse
|
IResponse
|
||||||
>(() => getCourseDetail(id as string))
|
>(() => getCourseDetail(id as string))
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: `${course.value?.data.courseName || '课程不存在'} - 课程管理`,
|
|
||||||
})
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
hideSidebar: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sideNav: SidebarNavGroup[] = [
|
watch(
|
||||||
{
|
() => course.value,
|
||||||
items: [
|
(data) => {
|
||||||
{
|
useHead({
|
||||||
title: '课程章节',
|
title: `${course.value?.data.courseName || '课程不存在'} - 课程管理`,
|
||||||
url: `/course/${id}/chapters`,
|
})
|
||||||
icon: 'tabler:books',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '教师团队',
|
|
||||||
url: `/course/${id}/team`,
|
|
||||||
icon: 'tabler:users-group',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '学生班级',
|
|
||||||
url: `/course/${id}/classes`,
|
|
||||||
icon: 'tabler:school',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '学生评价',
|
|
||||||
url: `/course/${id}/evaluation`,
|
|
||||||
icon: 'tabler:mood-smile',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const topNav = [
|
if (data?.data.courseName) {
|
||||||
|
setBreadcrumbs([
|
||||||
{
|
{
|
||||||
title: '课程管理',
|
label: '课程管理',
|
||||||
to: `/course/${id}`,
|
path: '/course',
|
||||||
icon: 'tabler:layout-dashboard',
|
|
||||||
},
|
},
|
||||||
...topbarNavDefaults.slice(1),
|
{
|
||||||
]
|
label: data.data.courseName || '课程不存在',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fullPath === `/course/${id}`) {
|
if (fullPath === `/course/${id}`) {
|
||||||
@ -72,42 +53,37 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => useRoute().fullPath, (newPath) => {
|
watch(
|
||||||
|
() => useRoute().fullPath,
|
||||||
|
(newPath) => {
|
||||||
if (newPath === `/course/${id}`) {
|
if (newPath === `/course/${id}`) {
|
||||||
router.replace(`/course/${id}/chapters`)
|
router.replace(`/course/${id}/chapters`)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppPageWithSidebar :sidebar-nav="sideNav">
|
<AppContainer
|
||||||
<template #topbar>
|
:subnavs="[
|
||||||
<AppTopbar :nav="topNav" />
|
{
|
||||||
</template>
|
label: '课程章节',
|
||||||
|
to: `/course/${id}/chapters`,
|
||||||
<template #sidebar="{ sidebarNav }">
|
},
|
||||||
<AppSidebar :nav="sidebarNav">
|
{
|
||||||
<template #extra-header>
|
label: '教师团队',
|
||||||
<div class="px-4">
|
to: `/course/${id}/team`,
|
||||||
<div class="flex flex-col items-center gap-1 overflow-hidden">
|
},
|
||||||
<NuxtImg
|
{
|
||||||
:src="course?.data.previewUrl || '/images/bg_home.jpg'"
|
label: '学生班级',
|
||||||
alt="课程封面"
|
to: `/course/${id}/classes`,
|
||||||
class="w-full aspect-video rounded-md shadow-md"
|
},
|
||||||
/>
|
{
|
||||||
<h1
|
label: '学生评价',
|
||||||
class="text-base font-medium drop-shadow-md text-ellipsis line-clamp-1"
|
to: `/course/${id}/evaluation`,
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
{{ course?.data.courseName || "未知课程" }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</AppSidebar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<Suspense>
|
|
||||||
<div v-if="courseStatus === 'error'">
|
<div v-if="courseStatus === 'error'">
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
title="课程加载失败"
|
title="课程加载失败"
|
||||||
@ -124,9 +100,7 @@ watch(() => useRoute().fullPath, (newPath) => {
|
|||||||
v-else
|
v-else
|
||||||
:page-key="fullPath"
|
:page-key="fullPath"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</AppContainer>
|
||||||
</ClientOnly>
|
|
||||||
</AppPageWithSidebar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -6,7 +6,6 @@ import type { FetchError } from 'ofetch'
|
|||||||
import { createCourse, deleteCourse, listUserCourses } from '~/api/course'
|
import { createCourse, deleteCourse, listUserCourses } from '~/api/course'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'no-sidebar',
|
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -100,7 +99,8 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto flex flex-col gap-8">
|
<AppContainer>
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Form
|
<Form
|
||||||
@ -302,13 +302,13 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
@click="deleteMode = !deleteMode"
|
@click="deleteMode = !deleteMode"
|
||||||
>
|
>
|
||||||
{{ deleteMode ? "退出删除" : "删除课程" }}
|
{{ deleteMode ? '退出删除' : '删除课程' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="coursesList?.rows && coursesList.rows.length > 0"
|
v-if="coursesList?.rows && coursesList.rows.length > 0"
|
||||||
class="grid grid-cols-5 gap-8"
|
class="grid grid-cols-6 gap-8"
|
||||||
>
|
>
|
||||||
<CourseCard
|
<CourseCard
|
||||||
v-for="course in coursesList?.rows"
|
v-for="course in coursesList?.rows"
|
||||||
@ -323,9 +323,7 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
title="暂无课程"
|
title="暂无课程"
|
||||||
icon="fluent-color:people-list-24"
|
icon="fluent-color:people-list-24"
|
||||||
>
|
>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">还没有创建或加入课程</p>
|
||||||
还没有创建或加入课程
|
|
||||||
</p>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -339,6 +337,7 @@ const onDeleteCourse = (courseId: number) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</EmptyScreen>
|
</EmptyScreen>
|
||||||
</div>
|
</div>
|
||||||
|
</AppContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'no-sidebar',
|
hideSidebar: true,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'no-sidebar',
|
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import VueOfficePptx from '@vue-office/pptx'
|
import VueOfficePptx from '@vue-office/pptx'
|
||||||
|
import VueOfficeDocx from '@vue-office/docx'
|
||||||
|
import VueOfficeExcel from '@vue-office/excel'
|
||||||
|
import VueOfficePdf from '@vue-office/pdf'
|
||||||
|
import '@vue-office/docx/lib/index.css'
|
||||||
|
import '@vue-office/excel/lib/index.css'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
hideSidebar: true,
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
params: { resource_url },
|
params: { resource_url },
|
||||||
} = useRoute()
|
} = useRoute()
|
||||||
|
const { setBreadcrumbs } = useBreadcrumbs()
|
||||||
|
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
return atob(resource_url as string)
|
return atob(resource_url as string)
|
||||||
@ -28,36 +38,76 @@ const fileType = computed(() => {
|
|||||||
if (ext === 'mp3' || ext === 'wav') return 'audio'
|
if (ext === 'mp3' || ext === 'wav') return 'audio'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
onMounted(() => {
|
||||||
<div class="w-full h-screen">
|
useHead({
|
||||||
<div v-if="!url">
|
title: `${fileType.value.toUpperCase()} 资源预览`,
|
||||||
<div class="flex items-center justify-center h-full">
|
})
|
||||||
<p class="text-muted-foreground">
|
|
||||||
资源链接无效
|
setBreadcrumbs([
|
||||||
</p>
|
{
|
||||||
</div>
|
label: `${fileType.value.toUpperCase()} 资源预览`,
|
||||||
</div>
|
},
|
||||||
<div
|
])
|
||||||
v-else
|
})
|
||||||
class="w-full h-full"
|
|
||||||
>
|
const vueOfficeOptions = {
|
||||||
<VueOfficePptx
|
|
||||||
v-if="fileType === 'ppt'"
|
|
||||||
:src="url"
|
|
||||||
class="w-full h-full"
|
|
||||||
:options="{
|
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
autoWidth: true,
|
autoWidth: true,
|
||||||
autoScale: true,
|
autoScale: true,
|
||||||
autoRotate: true,
|
autoRotate: true,
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
}"
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppContainer>
|
||||||
|
<div v-if="!url">
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<p class="text-muted-foreground">资源链接无效</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full h-full border rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<VueOfficeDocx
|
||||||
|
v-if="fileType === 'word'"
|
||||||
|
class="w-full h-full"
|
||||||
|
:src="url"
|
||||||
|
:options="vueOfficeOptions"
|
||||||
/>
|
/>
|
||||||
|
<VueOfficePptx
|
||||||
|
v-else-if="fileType === 'ppt'"
|
||||||
|
class="w-full h-full"
|
||||||
|
:src="url"
|
||||||
|
:options="vueOfficeOptions"
|
||||||
|
/>
|
||||||
|
<VueOfficeExcel
|
||||||
|
v-else-if="fileType === 'excel'"
|
||||||
|
class="w-full h-full"
|
||||||
|
:src="url"
|
||||||
|
:options="vueOfficeOptions"
|
||||||
|
/>
|
||||||
|
<VueOfficePdf
|
||||||
|
v-else-if="fileType === 'pdf'"
|
||||||
|
class="w-full h-full"
|
||||||
|
:src="url"
|
||||||
|
:options="vueOfficeOptions"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="fluent-color:notebook-question-mark-24"
|
||||||
|
class="text-6xl"
|
||||||
|
/>
|
||||||
|
<p>暂不支持该资源类型</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</AppContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -6,6 +6,10 @@ import * as z from 'zod'
|
|||||||
import type { FetchError } from 'ofetch'
|
import type { FetchError } from 'ofetch'
|
||||||
import { userLogin, type LoginResponse } from '~/api'
|
import { userLogin, type LoginResponse } from '~/api'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'blank',
|
||||||
|
})
|
||||||
|
|
||||||
const loginState = useLoginState()
|
const loginState = useLoginState()
|
||||||
const {
|
const {
|
||||||
query: { redirect },
|
query: { redirect },
|
||||||
|
116
pnpm-lock.yaml
generated
116
pnpm-lock.yaml
generated
@ -26,6 +26,15 @@ importers:
|
|||||||
'@vee-validate/zod':
|
'@vee-validate/zod':
|
||||||
specifier: ^4.15.0
|
specifier: ^4.15.0
|
||||||
version: 4.15.0(vue@3.5.13(typescript@5.8.2))(zod@3.24.2)
|
version: 4.15.0(vue@3.5.13(typescript@5.8.2))(zod@3.24.2)
|
||||||
|
'@vue-office/docx':
|
||||||
|
specifier: ^1.6.3
|
||||||
|
version: 1.6.3(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||||
|
'@vue-office/excel':
|
||||||
|
specifier: ^1.7.14
|
||||||
|
version: 1.7.14(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||||
|
'@vue-office/pdf':
|
||||||
|
specifier: ^2.0.10
|
||||||
|
version: 2.0.10(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||||
'@vue-office/pptx':
|
'@vue-office/pptx':
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
version: 1.0.1(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))
|
||||||
@ -111,9 +120,15 @@ importers:
|
|||||||
dayjs-nuxt:
|
dayjs-nuxt:
|
||||||
specifier: ^2.1.11
|
specifier: ^2.1.11
|
||||||
version: 2.1.11(magicast@0.3.5)
|
version: 2.1.11(magicast@0.3.5)
|
||||||
|
eslint-plugin-prettier:
|
||||||
|
specifier: ^5.2.6
|
||||||
|
version: 5.2.6(eslint@9.23.0(jiti@2.4.2))(prettier@3.5.3)
|
||||||
pinia-plugin-persistedstate:
|
pinia-plugin-persistedstate:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))))(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
|
version: 4.2.0(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))))(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
|
||||||
|
prettier:
|
||||||
|
specifier: ^3.5.3
|
||||||
|
version: 3.5.3
|
||||||
shadcn-nuxt:
|
shadcn-nuxt:
|
||||||
specifier: 2.0.1
|
specifier: 2.0.1
|
||||||
version: 2.0.1(magicast@0.3.5)
|
version: 2.0.1(magicast@0.3.5)
|
||||||
@ -959,6 +974,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-25L86MyPvnlQoX2MTIV2OiUcb6vJ6aRbFa9pbwByn95INKD5mFH2smgjDhq+fwJoqAgvgbdJLj6Tz7V9X5CFAQ==}
|
resolution: {integrity: sha512-25L86MyPvnlQoX2MTIV2OiUcb6vJ6aRbFa9pbwByn95INKD5mFH2smgjDhq+fwJoqAgvgbdJLj6Tz7V9X5CFAQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@pkgr/core@0.2.4':
|
||||||
|
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.28':
|
'@polka/url@1.0.0-next.28':
|
||||||
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
|
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
|
||||||
|
|
||||||
@ -1404,6 +1423,36 @@ packages:
|
|||||||
vue:
|
vue:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@vue-office/docx@1.6.3':
|
||||||
|
resolution: {integrity: sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.7.1
|
||||||
|
vue: ^2.0.0 || >=3.0.0
|
||||||
|
vue-demi: ^0.14.6
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue-office/excel@1.7.14':
|
||||||
|
resolution: {integrity: sha512-pVUgt+emDQUnW7q22CfnQ+jl43mM/7IFwYzOg7lwOwPEbiVB4K4qEQf+y/bc4xGXz75w1/e3Kz3G6wAafmFBFg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.7.1
|
||||||
|
vue: ^2.0.0 || >=3.0.0
|
||||||
|
vue-demi: ^0.14.6
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vue-office/pdf@2.0.10':
|
||||||
|
resolution: {integrity: sha512-yHVLrMAKpMPBkhBwofFyGEtEeJF0Zd7oGmf56Pe5aj/xObdRq3E1CIZqTqhWJNgHV8oLQqaX0vs4p5T1zq+GIA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.7.1
|
||||||
|
vue: ^2.0.0 || >=3.0.0
|
||||||
|
vue-demi: ^0.14.6
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vue-office/pptx@1.0.1':
|
'@vue-office/pptx@1.0.1':
|
||||||
resolution: {integrity: sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg==}
|
resolution: {integrity: sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2268,6 +2317,20 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
|
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
|
||||||
|
|
||||||
|
eslint-plugin-prettier@5.2.6:
|
||||||
|
resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/eslint': '>=8.0.0'
|
||||||
|
eslint: '>=8.0.0'
|
||||||
|
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
|
||||||
|
prettier: '>=3.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/eslint':
|
||||||
|
optional: true
|
||||||
|
eslint-config-prettier:
|
||||||
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-regexp@2.7.0:
|
eslint-plugin-regexp@2.7.0:
|
||||||
resolution: {integrity: sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==}
|
resolution: {integrity: sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
@ -2379,6 +2442,9 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-diff@1.3.0:
|
||||||
|
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||||
|
|
||||||
fast-fifo@1.3.2:
|
fast-fifo@1.3.2:
|
||||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||||
|
|
||||||
@ -3632,6 +3698,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
prettier-linter-helpers@1.0.0:
|
||||||
|
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
prettier@3.5.3:
|
||||||
|
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
@ -4038,6 +4113,10 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
synckit@0.11.4:
|
||||||
|
resolution: {integrity: sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
synckit@0.9.2:
|
synckit@0.9.2:
|
||||||
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
|
resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
@ -5759,6 +5838,8 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.2': {}
|
'@pkgr/core@0.2.2': {}
|
||||||
|
|
||||||
|
'@pkgr/core@0.2.4': {}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.28': {}
|
'@polka/url@1.0.0-next.28': {}
|
||||||
|
|
||||||
'@poppinss/colors@4.1.4':
|
'@poppinss/colors@4.1.4':
|
||||||
@ -6178,6 +6259,21 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vue: 3.5.13(typescript@5.8.2)
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
|
||||||
|
'@vue-office/docx@1.6.3(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
vue-demi: 0.14.6(vue@3.5.13(typescript@5.8.2))
|
||||||
|
|
||||||
|
'@vue-office/excel@1.7.14(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
vue-demi: 0.14.6(vue@3.5.13(typescript@5.8.2))
|
||||||
|
|
||||||
|
'@vue-office/pdf@2.0.10(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
|
vue-demi: 0.14.6(vue@3.5.13(typescript@5.8.2))
|
||||||
|
|
||||||
'@vue-office/pptx@1.0.1(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
'@vue-office/pptx@1.0.1(vue-demi@0.14.6(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.8.2)
|
vue: 3.5.13(typescript@5.8.2)
|
||||||
@ -7091,6 +7187,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
eslint-plugin-prettier@5.2.6(eslint@9.23.0(jiti@2.4.2))(prettier@3.5.3):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.23.0(jiti@2.4.2)
|
||||||
|
prettier: 3.5.3
|
||||||
|
prettier-linter-helpers: 1.0.0
|
||||||
|
synckit: 0.11.4
|
||||||
|
|
||||||
eslint-plugin-regexp@2.7.0(eslint@9.23.0(jiti@2.4.2)):
|
eslint-plugin-regexp@2.7.0(eslint@9.23.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0(jiti@2.4.2))
|
'@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0(jiti@2.4.2))
|
||||||
@ -7254,6 +7357,8 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-diff@1.3.0: {}
|
||||||
|
|
||||||
fast-fifo@1.3.2: {}
|
fast-fifo@1.3.2: {}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
@ -8741,6 +8846,12 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
prettier-linter-helpers@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
fast-diff: 1.3.0
|
||||||
|
|
||||||
|
prettier@3.5.3: {}
|
||||||
|
|
||||||
pretty-bytes@6.1.1: {}
|
pretty-bytes@6.1.1: {}
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
@ -9216,6 +9327,11 @@ snapshots:
|
|||||||
csso: 5.0.5
|
csso: 5.0.5
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
synckit@0.11.4:
|
||||||
|
dependencies:
|
||||||
|
'@pkgr/core': 0.2.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
synckit@0.9.2:
|
synckit@0.9.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pkgr/core': 0.1.2
|
'@pkgr/core': 0.1.2
|
||||||
|
24
stores/breadcrumbs.ts
Normal file
24
stores/breadcrumbs.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { IBreadcrumbItem } from '~/types'
|
||||||
|
|
||||||
|
export const useBreadcrumbs = defineStore('breadcrumbs', () => {
|
||||||
|
const breadcrumbs = ref<IBreadcrumbItem[]>([])
|
||||||
|
|
||||||
|
const setBreadcrumbs = (items: IBreadcrumbItem[]) => {
|
||||||
|
breadcrumbs.value = items
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBreadcrumbs = () => {
|
||||||
|
breadcrumbs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBreadcrumbs = () => {
|
||||||
|
return breadcrumbs.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbs,
|
||||||
|
getBreadcrumbs,
|
||||||
|
setBreadcrumbs,
|
||||||
|
clearBreadcrumbs,
|
||||||
|
}
|
||||||
|
})
|
@ -2,3 +2,8 @@ export * from './user'
|
|||||||
export * from './course'
|
export * from './course'
|
||||||
|
|
||||||
export type { FetchError } from 'ofetch'
|
export type { FetchError } from 'ofetch'
|
||||||
|
|
||||||
|
export interface IBreadcrumbItem {
|
||||||
|
label: string
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
8
types/nuxt.d.ts
vendored
Normal file
8
types/nuxt.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
declare module '#app' {
|
||||||
|
interface PageMeta {
|
||||||
|
/** 隐藏侧边栏 */
|
||||||
|
hideSidebar?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
Loading…
Reference in New Issue
Block a user