diff --git a/components/app/Container.vue b/components/app/Container.vue
index 7c901b3..b1784e7 100644
--- a/components/app/Container.vue
+++ b/components/app/Container.vue
@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import type { SubNavItem } from '../SubNav.vue'
+import type { SubNavItem } from '../nav/Secondary.vue'
 
 defineProps<{ subnavs?: SubNavItem[] }>()
 </script>
@@ -8,7 +8,7 @@ defineProps<{ subnavs?: SubNavItem[] }>()
   <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
+      <NavSecondary
         v-if="subnavs && subnavs.length"
         :navs="subnavs"
       />
diff --git a/components/app/Sidebar.vue b/components/app/sidebar/index.vue
similarity index 86%
rename from components/app/Sidebar.vue
rename to components/app/sidebar/index.vue
index 0b86c04..33a3cf5 100644
--- a/components/app/Sidebar.vue
+++ b/components/app/sidebar/index.vue
@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { LucideIcon } from 'lucide-vue-next'
 import type { RouteLocationRaw } from 'vue-router'
-import type { SidebarProps } from '../ui/sidebar'
+import type { SidebarProps } from '~/components/ui/sidebar'
 
 export interface SidebarNavItem {
   title: string
@@ -46,17 +46,15 @@ const loginState = useLoginState()
           alt="Logo"
           class="w-9 max-w-9 aspect-square group-has-[[data-collapsible=icon]]/sidebar-wrapper:w-full transition-all duration-200 ease-in-out"
         />
-        <h1 class="text-lg font-medium">
-          智课教学平台
-        </h1>
+        <h1 class="text-lg font-medium">智课教学平台</h1>
       </div>
     </SidebarHeader>
     <SidebarContent>
       <slot name="extra-header" />
-      <AppNavMain :nav="nav" />
+      <AppSidebarNavMain :nav="nav" />
     </SidebarContent>
     <SidebarFooter>
-      <AppNavUser :user="loginState.user" />
+      <AppSidebarNavUser :user="loginState.user" />
     </SidebarFooter>
     <SidebarRail />
   </Sidebar>
diff --git a/components/app/NavMain.vue b/components/app/sidebar/nav/Main.vue
similarity index 100%
rename from components/app/NavMain.vue
rename to components/app/sidebar/nav/Main.vue
diff --git a/components/app/NavUser.vue b/components/app/sidebar/nav/User.vue
similarity index 98%
rename from components/app/NavUser.vue
rename to components/app/sidebar/nav/User.vue
index 25d1d7b..c235c42 100644
--- a/components/app/NavUser.vue
+++ b/components/app/sidebar/nav/User.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { ChevronsUpDown } from 'lucide-vue-next'
-import { useSidebar } from '../ui/sidebar'
+import { useSidebar } from '../../../ui/sidebar'
 import type { IUser } from '~/types'
 
 const props = defineProps<{
diff --git a/components/CourseCard.vue b/components/course/Card.vue
similarity index 100%
rename from components/CourseCard.vue
rename to components/course/Card.vue
diff --git a/components/SubNav.vue b/components/nav/Secondary.vue
similarity index 90%
rename from components/SubNav.vue
rename to components/nav/Secondary.vue
index 046f873..72bf6b5 100644
--- a/components/SubNav.vue
+++ b/components/nav/Secondary.vue
@@ -29,7 +29,7 @@ const isCurrentPath = (path: string) => {
       }"
     >
       <svg
-        class="absolute inset-0 aspect-auto"
+        class="absolute inset-0 aspect-auto top-1"
         viewBox="0 0 206.5 72"
         fill="none"
         xmlns="http://www.w3.org/2000/svg"
@@ -62,7 +62,7 @@ const isCurrentPath = (path: string) => {
         </g>
       </svg>
       <NuxtLink
-        class="text-lg font-medium z-10 select-none"
+        class="text-base font-medium z-10 select-none pb-0.5"
         :class="{
           'text-secondary': isCurrentPath(nav.to),
           'text-neutral-400 dark:text-neutral-500': !isCurrentPath(nav.to),
@@ -77,13 +77,13 @@ const isCurrentPath = (path: string) => {
 
 <style scoped>
 .subnav-item {
-  @apply relative flex justify-center items-center px-4 pt-1 drop-shadow-md;
+  @apply relative flex justify-center items-center px-5 pt-2 drop-shadow-lg;
 
   --svg-stop1: #ffffff;
   --svg-stop2: #f3f3f3;
 
   &:not(:first-of-type) {
-    @apply -ml-3;
+    @apply -ml-4;
   }
 
   &.active {
diff --git a/package.json b/package.json
index 2e8dc63..94598b9 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "dotenv": "^16.5.0",
+    "dplayer": "^1.27.1",
     "eslint": "^9.0.0",
     "lucide-vue-next": "^0.484.0",
     "nuxt": "^3.16.1",
@@ -49,6 +50,7 @@
     "@nuxtjs/color-mode": "^3.5.2",
     "@nuxtjs/tailwindcss": "^6.13.2",
     "@pinia/nuxt": "^0.10.1",
+    "@types/dplayer": "^1.25.5",
     "@vueuse/nuxt": "^13.0.0",
     "dayjs": "^1.11.13",
     "dayjs-nuxt": "^2.1.11",
diff --git a/pages/preview/[resource_url].vue b/pages/preview/[resource_url].vue
index 2dfdae0..5d760d9 100644
--- a/pages/preview/[resource_url].vue
+++ b/pages/preview/[resource_url].vue
@@ -5,6 +5,7 @@ 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'
+import DPlayer from 'dplayer'
 
 definePageMeta({
   hideSidebar: true,
@@ -39,6 +40,12 @@ const fileType = computed(() => {
   return ''
 })
 
+const containerClass = computed(() => {
+  return fileType.value === 'video'
+    ? 'max-w-6xl mx-auto'
+    : 'w-full h-full border rounded-lg overflow-hidden'
+})
+
 onMounted(() => {
   useHead({
     title: `${fileType.value.toUpperCase()} 资源预览`,
@@ -49,6 +56,18 @@ onMounted(() => {
       label: `${fileType.value.toUpperCase()} 资源预览`,
     },
   ])
+
+  // initialize video player
+  if (fileType.value === 'video') {
+    const dp = new DPlayer({
+      container: document.getElementById('dplayer'),
+      screenshot: true,
+      video: {
+        url: url.value,
+      },
+    })
+    dp.play()
+  }
 })
 
 const vueOfficeOptions = {
@@ -70,10 +89,16 @@ const vueOfficeOptions = {
     </div>
     <div
       v-else
-      class="w-full h-full border rounded-lg overflow-hidden"
+      :class="containerClass"
     >
+      <!-- Video -->
+      <div
+        v-if="fileType === 'video'"
+        id="dplayer"
+      />
+      <!-- Vue Office -->
       <VueOfficeDocx
-        v-if="fileType === 'word'"
+        v-else-if="fileType === 'word'"
         class="w-full h-full"
         :src="url"
         :options="vueOfficeOptions"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d98b06b..ea0aa29 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
       dotenv:
         specifier: ^16.5.0
         version: 16.5.0
+      dplayer:
+        specifier: ^1.27.1
+        version: 1.27.1
       eslint:
         specifier: ^9.0.0
         version: 9.23.0(jiti@2.4.2)
@@ -111,6 +114,9 @@ importers:
       '@pinia/nuxt':
         specifier: ^0.10.1
         version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
+      '@types/dplayer':
+        specifier: ^1.25.5
+        version: 1.25.5
       '@vueuse/nuxt':
         specifier: ^13.0.0
         version: 13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
@@ -1212,6 +1218,9 @@ packages:
   '@types/doctrine@0.0.9':
     resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
 
+  '@types/dplayer@1.25.5':
+    resolution: {integrity: sha512-p/7O94dHDo0Irn2KWIqFE+fBCA4DS7QL3jfCOjCUPBAOgppyyTjmNZjKEfiJa1M3n1oVQqG7xnPwhiIuCqOzkQ==}
+
   '@types/estree@1.0.6':
     resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
 
@@ -1650,6 +1659,9 @@ packages:
   async@3.2.6:
     resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
 
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
   at-least-node@1.0.0:
     resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
     engines: {node: '>= 4.0.0'}
@@ -1661,12 +1673,18 @@ packages:
     peerDependencies:
       postcss: ^8.1.0
 
+  axios@1.2.3:
+    resolution: {integrity: sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==}
+
   b4a@1.6.7:
     resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
 
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  balloon-css@1.2.0:
+    resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==}
+
   bare-events@2.5.4:
     resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==}
 
@@ -1879,6 +1897,10 @@ packages:
   colord@2.9.3:
     resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
 
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
   commander@2.20.3:
     resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
 
@@ -2131,6 +2153,10 @@ packages:
   defu@6.1.4:
     resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
 
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
   delegates@1.0.0:
     resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
 
@@ -2203,6 +2229,9 @@ packages:
     resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
     engines: {node: '>=12'}
 
+  dplayer@1.27.1:
+    resolution: {integrity: sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==}
+
   dunder-proto@1.0.1:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
@@ -2265,6 +2294,10 @@ packages:
     resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
     engines: {node: '>= 0.4'}
 
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
   esbuild@0.25.1:
     resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==}
     engines: {node: '>=18'}
@@ -2502,6 +2535,15 @@ packages:
   flatted@3.3.3:
     resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
 
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   fontaine@0.5.0:
     resolution: {integrity: sha512-vPDSWKhVAfTx4hRKT777+N6Szh2pAosAuzLpbppZ6O3UdD/1m6OlHjNcC3vIbgkRTIcLjzySLHXzPeLO2rE8cA==}
 
@@ -2512,6 +2554,10 @@ packages:
     resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
     engines: {node: '>=14'}
 
+  form-data@4.0.2:
+    resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
+    engines: {node: '>= 6'}
+
   fraction.js@4.3.7:
     resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
 
@@ -3718,6 +3764,9 @@ packages:
     resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
     engines: {node: '>= 0.6.0'}
 
+  promise-polyfill@8.3.0:
+    resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==}
+
   prompts@2.4.2:
     resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
     engines: {node: '>= 6'}
@@ -3725,6 +3774,9 @@ packages:
   protocols@2.0.2:
     resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
 
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
   pump@3.0.2:
     resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
 
@@ -6022,6 +6074,8 @@ snapshots:
 
   '@types/doctrine@0.0.9': {}
 
+  '@types/dplayer@1.25.5': {}
+
   '@types/estree@1.0.6': {}
 
   '@types/estree@1.0.7': {}
@@ -6528,6 +6582,8 @@ snapshots:
 
   async@3.2.6: {}
 
+  asynckit@0.4.0: {}
+
   at-least-node@1.0.0: {}
 
   autoprefixer@10.4.21(postcss@8.5.3):
@@ -6540,10 +6596,20 @@ snapshots:
       postcss: 8.5.3
       postcss-value-parser: 4.2.0
 
+  axios@1.2.3:
+    dependencies:
+      follow-redirects: 1.15.9
+      form-data: 4.0.2
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
   b4a@1.6.7: {}
 
   balanced-match@1.0.2: {}
 
+  balloon-css@1.2.0: {}
+
   bare-events@2.5.4:
     optional: true
 
@@ -6769,6 +6835,10 @@ snapshots:
 
   colord@2.9.3: {}
 
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
   commander@2.20.3: {}
 
   commander@4.1.1: {}
@@ -6989,6 +7059,8 @@ snapshots:
 
   defu@6.1.4: {}
 
+  delayed-stream@1.0.0: {}
+
   delegates@1.0.0: {}
 
   denque@2.1.0: {}
@@ -7043,6 +7115,14 @@ snapshots:
 
   dotenv@16.5.0: {}
 
+  dplayer@1.27.1:
+    dependencies:
+      axios: 1.2.3
+      balloon-css: 1.2.0
+      promise-polyfill: 8.3.0
+    transitivePeerDependencies:
+      - debug
+
   dunder-proto@1.0.1:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -7091,6 +7171,13 @@ snapshots:
     dependencies:
       es-errors: 1.3.0
 
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
   esbuild@0.25.1:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.25.1
@@ -7413,6 +7500,8 @@ snapshots:
 
   flatted@3.3.3: {}
 
+  follow-redirects@1.15.9: {}
+
   fontaine@0.5.0:
     dependencies:
       '@capsizecss/metrics': 2.2.0
@@ -7442,6 +7531,13 @@ snapshots:
       cross-spawn: 7.0.6
       signal-exit: 4.1.0
 
+  form-data@4.0.2:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      mime-types: 2.1.35
+
   fraction.js@4.3.7: {}
 
   fresh@0.5.2: {}
@@ -8858,6 +8954,8 @@ snapshots:
 
   process@0.11.10: {}
 
+  promise-polyfill@8.3.0: {}
+
   prompts@2.4.2:
     dependencies:
       kleur: 3.0.3
@@ -8865,6 +8963,8 @@ snapshots:
 
   protocols@2.0.2: {}
 
+  proxy-from-env@1.1.0: {}
+
   pump@3.0.2:
     dependencies:
       end-of-stream: 1.4.4