浏览代码

feat: add responsive design

nian 2 月之前
父节点
当前提交
48ad81b504

+ 1 - 1
public/assets/preloader.css

@@ -1 +1 @@
-#appLoader{--color:#fafafa;--clock-color:#28364c;.dark &{--clock-color:#bcbcbc;--color:#171717;}position:fixed;inset:0;height:100dvh;background-color:var(--color);display:grid;place-items:center;}#clock{width:30px;height:30px;border:1px solid;border-radius:50%;color:var(--clock-color);transform:scale(2.5);&::before,&::after{position:absolute;width:0;border-left:1px solid var(--clock-color);content:'';border-radius:1px;transform-origin:bottom center;animation:clockwise linear infinite;left:50%;margin-left:-0.5px;}&::before{height:12px;top:calc(50% - 12px);animation-duration:1s;}&::after{height:10px;top:calc(50% - 10px);animation-duration:60s;}}@keyframes clockwise{to{transform:rotate(360deg);}}
+#appLoader{--color:#fafafa;--clock-color:#28364c;.dark &{--clock-color:#bcbcbc;--color:#171717;}position:fixed;inset:0;height:100svh;background-color:var(--color);display:grid;place-items:center;}#clock{width:30px;height:30px;border:1px solid;border-radius:50%;color:var(--clock-color);transform:scale(2.5);&::before,&::after{position:absolute;width:0;border-left:1px solid var(--clock-color);content:'';border-radius:1px;transform-origin:bottom center;animation:clockwise linear infinite;left:50%;margin-left:-0.5px;}&::before{height:12px;top:calc(50% - 12px);animation-duration:1s;}&::after{height:10px;top:calc(50% - 10px);animation-duration:60s;}}@keyframes clockwise{to{transform:rotate(360deg);}}

+ 11 - 2
src/components/ButtonAnimation.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
+import { mergeWith } from 'lodash-es'
 import { NButton } from 'naive-ui'
-import { ref } from 'vue'
+import { ref, inject, computed, useAttrs } from 'vue'
 
 import type { ButtonProps } from 'naive-ui'
 
@@ -11,10 +12,18 @@ interface ButtonAnimationProps extends /* @vue-ignore */ ButtonProps {
   animation?: Animation | boolean
 }
 
+const injectButtonAnimation = inject('ButtonAnimationInject', null) as ButtonProps | null
+
 const { duration = 600, animation = 'beat' } = defineProps<ButtonAnimationProps>()
 
+const buttonAnimationAttrs = useAttrs() as ButtonProps
+
 const isAnimating = ref(false)
 
+const buttonAnimationProps = computed<ButtonProps>(() => {
+  return mergeWith({}, injectButtonAnimation, buttonAnimationAttrs)
+})
+
 const onButtonClicked = () => {
   if (isAnimating.value) return
 
@@ -30,7 +39,7 @@ const onButtonClicked = () => {
   <NButton
     quaternary
     circle
-    v-bind="$attrs"
+    v-bind="buttonAnimationProps"
     @click.stop="onButtonClicked"
   >
     <template #icon>

+ 6 - 0
src/components/Logo.vue

@@ -0,0 +1,6 @@
+<script setup lang="ts"></script>
+<template>
+  <div class="size-full shrink-0">
+    <div class="size-full rounded bg-primary/10"></div>
+  </div>
+</template>

+ 6 - 0
src/injection/index.ts

@@ -0,0 +1,6 @@
+import type { MediaQueryInjection } from './interface'
+import type { InjectionKey } from 'vue'
+
+export const mediaQueryInjectionKey = Symbol() as InjectionKey<MediaQueryInjection>
+
+export * from './interface'

+ 9 - 0
src/injection/interface.ts

@@ -0,0 +1,9 @@
+import type { Ref } from 'vue'
+
+export interface MediaQueryInjection {
+  sm: Ref<boolean>
+  md: Ref<boolean>
+  lg: Ref<boolean>
+  xl: Ref<boolean>
+  '2xl': Ref<boolean>
+}

+ 1 - 1
src/layouts/aside/component/UserCard.vue

@@ -41,7 +41,7 @@ const onUserDropdownSelected = (key: string) => {
 </script>
 <template>
   <div
-    class="mb-4 flex cursor-pointer items-center transition-[background-color,border-radius,margin,padding] hover:bg-neutral-200/90 dark:hover:bg-neutral-750/65"
+    class="flex cursor-pointer items-center transition-[background-color,border-radius,margin,padding] hover:bg-neutral-200/90 dark:hover:bg-neutral-750/65"
     :class="
       preferencesStore.preferences.menu.collapsed
         ? 'mx-2 rounded'

+ 1 - 1
src/layouts/aside/index.vue

@@ -28,7 +28,7 @@ function handleCollapseClick() {
 </script>
 <template>
   <div
-    class="relative flex h-full flex-col justify-between gap-y-4 border-r border-naive-border bg-naive-card transition-[background-color,border-color,width]"
+    class="relative flex h-full flex-col justify-between gap-y-4 border-r border-naive-border bg-naive-card pb-4 transition-[background-color,border-color,width] max-sm:hidden"
     :style="{
       width: `${menuCollapseWidth}px`,
     }"

+ 21 - 0
src/layouts/aside/mobile/MobileLeftAside.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import { usePreferencesStore } from '@/stores/preferences'
+
+import LogoArea from '../../header/LogoArea.vue'
+import Menu from '../component/Menu.vue'
+import UserCard from '../component/UserCard.vue'
+
+const preferencesStore = usePreferencesStore()
+</script>
+<template>
+  <div
+    class="absolute top-0 left-0 flex h-svh flex-col gap-y-4 py-4 transition-[translate] sm:hidden"
+    :class="{
+      '-translate-x-full': preferencesStore.preferences.mobile.mainLayoutSlider !== 'right',
+    }"
+  >
+    <LogoArea />
+    <Menu />
+    <UserCard />
+  </div>
+</template>

+ 30 - 0
src/layouts/aside/mobile/MobileRightAside.vue

@@ -0,0 +1,30 @@
+<script setup lang="ts">
+import { provide, reactive } from 'vue'
+
+import { usePreferencesStore } from '@/stores/preferences'
+
+import Actions from '../../header/actions/index.vue'
+
+import type { ButtonProps } from 'naive-ui'
+
+const preferencesStore = usePreferencesStore()
+
+const provideButtonAnimation = reactive<ButtonProps>({
+  size: 'large',
+  circle: false,
+})
+
+provide('ButtonAnimationInject', provideButtonAnimation)
+</script>
+<template>
+  <div
+    class="absolute top-0 right-0 h-svh p-4 transition-[translate] sm:hidden"
+    :class="{
+      'translate-x-full': preferencesStore.preferences.mobile.mainLayoutSlider !== 'left',
+    }"
+  >
+    <div class="flex h-full items-center justify-between">
+      <Actions class="flex-col justify-center gap-y-4" />
+    </div>
+  </div>
+</template>

+ 4 - 3
src/layouts/header/Logo.vue → src/layouts/header/LogoArea.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 
+import Logo from '@/components/Logo.vue'
 import { usePreferencesStore } from '@/stores/preferences'
 
 const APP_NAME = import.meta.env.VITE_APP_NAME
@@ -15,7 +16,7 @@ const collapseWidth = computed(() => {
 </script>
 <template>
   <div
-    class="shrink-0 border-r border-naive-border transition-[border-color,width]"
+    class="shrink-0 border-r border-naive-border transition-[border-color,width] max-sm:border-0"
     :style="{
       width: `${collapseWidth}px`,
     }"
@@ -29,8 +30,8 @@ const collapseWidth = computed(() => {
         },
       ]"
     >
-      <div class="size-10 shrink-0">
-        <div class="size-full rounded bg-primary/10"></div>
+      <div class="size-10">
+        <Logo />
       </div>
       <div
         class="flex-1 overflow-hidden transition-[margin-left,max-width]"

+ 1 - 4
src/layouts/header/actions/component/FullScreen.vue

@@ -11,10 +11,7 @@ const { isFullscreen, enter, exit } = useFullscreen()
       @click="isFullscreen ? exit() : enter()"
       :title="isFullscreen ? '退出全屏' : '全屏'"
     >
-      <span
-        class="size-5"
-        :class="isFullscreen ? 'iconify ph--arrows-in' : 'iconify ph--arrows-out'"
-      />
+      <span :class="isFullscreen ? 'iconify ph--arrows-in' : 'iconify ph--arrows-out'" />
     </ButtonAnimation>
   </div>
 </template>

+ 27 - 1
src/layouts/header/actions/component/PreferencesDrawer.vue

@@ -9,11 +9,12 @@ import {
   useModal,
   NScrollbar,
 } from 'naive-ui'
-import { h, ref } from 'vue'
+import { h, ref, inject, watch } from 'vue'
 
 import { ButtonAnimation } from '@/components'
 import { useComponentThemeOverrides } from '@/composable/useComponentThemeOverrides'
 import { usePersonalization } from '@/composable/usePersonalization'
+import { mediaQueryInjectionKey } from '@/injection'
 import { usePreferencesStore } from '@/stores/preferences'
 import { useSystemStore } from '@/stores/system'
 import twColors from '@/utils/tailwindColor'
@@ -21,6 +22,8 @@ import twColors from '@/utils/tailwindColor'
 import NoiseModal from './NoiseModal.vue'
 import WatermarkModal from './WatermarkModal.vue'
 
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
 const preferencesStore = usePreferencesStore()
 const systemStore = useSystemStore()
 
@@ -93,6 +96,24 @@ const showNoiseModal = () => {
     zIndex: 99999,
   })
 }
+
+watch(
+  () => mediaQuery?.sm.value,
+  (newVal) => {
+    if (newVal) {
+      modify({
+        menu: {
+          collapsed: false,
+        },
+        showTabs: false,
+        showFooter: false,
+      })
+    }
+  },
+  {
+    immediate: true,
+  },
+)
 </script>
 <template>
   <div>
@@ -140,6 +161,7 @@ const showNoiseModal = () => {
                 :value="preferencesStore.preferences.menu.collapsed"
                 :checked-value="false"
                 :unchecked-value="true"
+                :disabled="mediaQuery?.sm.value"
                 @update-value="
                   (value) =>
                     modify({
@@ -178,6 +200,7 @@ const showNoiseModal = () => {
               <span>显示导航按钮</span>
               <NSwitch
                 :value="preferencesStore.preferences.showNavigation"
+                :disabled="mediaQuery?.sm.value"
                 @update-value="
                   (value) =>
                     modify({
@@ -190,6 +213,7 @@ const showNoiseModal = () => {
               <span>显示面包屑</span>
               <NSwitch
                 :value="preferencesStore.preferences.showBreadcrumb"
+                :disabled="mediaQuery?.sm.value"
                 @update-value="
                   (value) =>
                     modify({
@@ -202,6 +226,7 @@ const showNoiseModal = () => {
               <span>显示标签页</span>
               <NSwitch
                 :value="preferencesStore.preferences.showTabs"
+                :disabled="mediaQuery?.sm.value"
                 @update-value="
                   (value) =>
                     modify({
@@ -226,6 +251,7 @@ const showNoiseModal = () => {
               <span>显示底部</span>
               <NSwitch
                 :value="preferencesStore.preferences.showFooter"
+                :disabled="mediaQuery?.sm.value"
                 @update-value="
                   (value) =>
                     modify({

+ 1 - 1
src/layouts/header/actions/component/SignOut.vue

@@ -27,6 +27,6 @@ function cleanupUserInfo() {
 </script>
 <template>
   <ButtonAnimation @click="handleSignOutClick">
-    <span class="iconify size-5 ph--sign-out" />
+    <span class="iconify ph--sign-out" />
   </ButtonAnimation>
 </template>

+ 1 - 4
src/layouts/header/actions/component/ThemeDropdown.vue

@@ -71,10 +71,7 @@ function renderSelectLabel(option: (typeof themeDropdownOptions)[number]) {
     @update-value="onThemePopselectUpdated"
   >
     <ButtonAnimation title="主题">
-      <span
-        :class="themeIconName"
-        class="size-5"
-      />
+      <span :class="themeIconName" />
     </ButtonAnimation>
   </NPopselect>
 </template>

+ 2 - 2
src/layouts/header/actions/index.vue

@@ -11,14 +11,14 @@ defineOptions({
 })
 </script>
 <template>
-  <div class="flex items-center gap-x-3">
+  <div class="flex items-center">
     <ButtonAnimation
       :animation="false"
       tag="a"
       href="https://github.com/tenianon/lithe-admin"
       target="_blank"
     >
-      <span class="iconify-[mdi--github] size-5" />
+      <span class="iconify-[mdi--github]" />
     </ButtonAnimation>
     <FullScreen />
     <ThemeDropdown />

+ 43 - 38
src/layouts/header/index.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { usePreferencesStore } from '@/stores/preferences'
 
-import Actions from './Actions/index.vue'
+import Actions from './actions/index.vue'
 import Breadcrumb from './Breadcrumb.vue'
-import Logo from './Logo.vue'
+import LogoArea from './LogoArea.vue'
+import MobileHeader from './mobile/MobileHeader.vue'
 import Navigation from './Navigation.vue'
 
 defineOptions({
@@ -13,45 +14,49 @@ defineOptions({
 const preferencesStore = usePreferencesStore()
 </script>
 <template>
-  <header class="flex">
-    <Logo />
-    <div class="flex flex-1 items-center p-4">
-      <div class="flex flex-1 items-center">
-        <Transition
-          type="transition"
-          enter-active-class="transition-[grid-template-columns]"
-          leave-active-class="transition-[grid-template-columns]"
-          enter-from-class="grid-cols-[0fr]"
-          leave-to-class="grid-cols-[0fr]"
-          enter-to-class="grid-cols-[1fr]"
-          leave-from-class="grid-cols-[1fr]"
-        >
-          <div
-            v-if="preferencesStore.preferences.showNavigation"
-            class="grid overflow-hidden"
+  <header
+    class="border-b border-naive-border bg-naive-card transition-[background-color,border-color]"
+  >
+    <div class="flex max-sm:hidden">
+      <LogoArea />
+      <div class="flex flex-1 items-center p-4">
+        <div class="flex flex-1 items-center">
+          <Transition
+            type="transition"
+            enter-active-class="transition-[grid-template-columns]"
+            leave-active-class="transition-[grid-template-columns]"
+            enter-from-class="grid-cols-[0fr]"
+            leave-to-class="grid-cols-[0fr]"
+            enter-to-class="grid-cols-[1fr]"
+            leave-from-class="grid-cols-[1fr]"
           >
-            <Navigation />
-          </div>
-        </Transition>
-
-        <Transition
-          type="transition"
-          enter-active-class="transition-[grid-template-columns]"
-          leave-active-class="transition-[grid-template-columns]"
-          enter-from-class="grid-cols-[0fr]"
-          leave-to-class="grid-cols-[0fr]"
-          enter-to-class="grid-cols-[1fr]"
-          leave-from-class="grid-cols-[1fr]"
-        >
-          <div
-            v-if="preferencesStore.preferences.showBreadcrumb"
-            class="grid overflow-hidden"
+            <div
+              v-if="preferencesStore.preferences.showNavigation"
+              class="grid overflow-hidden"
+            >
+              <Navigation />
+            </div>
+          </Transition>
+          <Transition
+            type="transition"
+            enter-active-class="transition-[grid-template-columns]"
+            leave-active-class="transition-[grid-template-columns]"
+            enter-from-class="grid-cols-[0fr]"
+            leave-to-class="grid-cols-[0fr]"
+            enter-to-class="grid-cols-[1fr]"
+            leave-from-class="grid-cols-[1fr]"
           >
-            <Breadcrumb />
-          </div>
-        </Transition>
+            <div
+              v-if="preferencesStore.preferences.showBreadcrumb"
+              class="grid overflow-hidden"
+            >
+              <Breadcrumb />
+            </div>
+          </Transition>
+        </div>
+        <Actions class="gap-x-3" />
       </div>
-      <Actions />
     </div>
+    <MobileHeader />
   </header>
 </template>

+ 42 - 0
src/layouts/header/mobile/MobileHeader.vue

@@ -0,0 +1,42 @@
+<script setup lang="ts">
+import { ButtonAnimation } from '@/components'
+import Logo from '@/components/Logo.vue'
+import router from '@/router'
+import { usePreferencesStore } from '@/stores/preferences'
+
+import type { PreferencesOptions } from '@/stores/preferences'
+
+const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
+
+const mainLayoutSliderToRight = (direction: PreferencesOptions['mobile']['mainLayoutSlider']) => {
+  modify({
+    mobile: {
+      mainLayoutSlider:
+        preferencesStore.preferences.mobile.mainLayoutSlider === direction ? null : direction,
+    },
+  })
+}
+</script>
+<template>
+  <div class="flex items-center justify-between px-4 py-3 sm:hidden">
+    <div
+      class="size-9"
+      @click="mainLayoutSliderToRight('right')"
+    >
+      <Logo />
+    </div>
+    <div class="flex items-center gap-x-2">
+      <span
+        class="size-6"
+        :class="router.currentRoute.value.meta.icon"
+      />
+      <span class="text-base">{{ router.currentRoute.value.meta.title }}</span>
+    </div>
+    <div class="flex items-center gap-x-2">
+      <ButtonAnimation @click="mainLayoutSliderToRight('left')">
+        <span class="iconify ph--list" />
+      </ButtonAnimation>
+    </div>
+  </div>
+</template>

+ 89 - 55
src/layouts/index.vue

@@ -1,19 +1,26 @@
 <script setup lang="ts">
+import { useMediaQuery } from '@vueuse/core'
 import { isEmpty } from 'lodash-es'
 import { NScrollbar } from 'naive-ui'
+import { computed, provide } from 'vue'
 
 import texturePng from '@/assets/texture.png'
 import { EmptyPlaceholder } from '@/components'
 import { useComponentThemeOverrides } from '@/composable/useComponentThemeOverrides'
+import { mediaQueryInjectionKey } from '@/injection'
 import { usePreferencesStore } from '@/stores/preferences'
 import { useTabsStore } from '@/stores/tabs'
 
 import AsideLayout from './aside/index.vue'
+import MobileLeftAside from './aside/mobile/MobileLeftAside.vue'
+import MobileRightAside from './aside/mobile/MobileRightAside.vue'
 import Tabs from './component/Tabs.vue'
 import FooterLayout from './footer/index.vue'
 import HeaderLayout from './header/index.vue'
 import MainLayout from './main/index.vue'
 
+import type { CSSProperties } from 'vue'
+
 defineOptions({
   name: 'Layout',
 })
@@ -21,70 +28,97 @@ defineOptions({
 const tabsStore = useTabsStore()
 const preferencesStore = usePreferencesStore()
 const { scrollbarInMainLayout } = useComponentThemeOverrides()
+
+const mainLayoutStyle = computed<CSSProperties | null>(() => {
+  return preferencesStore.preferences.mobile.mainLayoutSlider === 'right'
+    ? {
+        transform: `translate(${preferencesStore.preferences.menu.maxWidth || 0}px) scale(0.88)`,
+      }
+    : preferencesStore.preferences.mobile.mainLayoutSlider === 'left'
+      ? {
+          transform: `translate(-${52}px) scale(0.88)`,
+        }
+      : null
+})
+
+provide(mediaQueryInjectionKey, {
+  sm: useMediaQuery('(max-width: 640px)'),
+  md: useMediaQuery('(max-width: 768px)'),
+  lg: useMediaQuery('(max-width: 1024px)'),
+  xl: useMediaQuery('(max-width: 1280px)'),
+  '2xl': useMediaQuery('(max-width: 1536px)'),
+})
 </script>
 <template>
-  <div class="flex h-dvh flex-col overflow-hidden">
+  <div
+    class="relative flex h-svh overflow-hidden"
+    :style="{ backgroundImage: `url(${texturePng})` }"
+  >
+    <MobileLeftAside />
     <div
-      class="border-b border-naive-border bg-naive-card transition-[background-color,border-color]"
+      class="relative flex h-full w-full flex-col transition-[border-color,rounded,transform] max-sm:rounded-xl sm:transform-none!"
+      :class="{
+        'overflow-hidden rounded-xl border border-naive-border': mainLayoutStyle,
+        'border-transparent': !mainLayoutStyle,
+      }"
+      :style="mainLayoutStyle"
     >
       <HeaderLayout />
-    </div>
-    <div class="flex flex-1 overflow-hidden">
-      <AsideLayout />
-      <div class="relative flex flex-1 flex-col overflow-x-hidden">
-        <Transition
-          type="transition"
-          enter-active-class="transition-[grid-template-rows]"
-          leave-active-class="transition-[grid-template-rows]"
-          enter-from-class="grid-rows-[0fr]"
-          leave-to-class="grid-rows-[0fr]"
-          enter-to-class="grid-rows-[1fr]"
-          leave-from-class="grid-rows-[1fr]"
-        >
-          <div
-            v-if="!isEmpty(tabsStore.tabs) && preferencesStore.preferences.showTabs"
-            class="grid shrink-0 items-baseline overflow-hidden"
+      <div class="flex flex-1 overflow-hidden">
+        <AsideLayout />
+        <div class="relative flex flex-1 flex-col overflow-x-hidden">
+          <Transition
+            type="transition"
+            enter-active-class="transition-[grid-template-rows]"
+            leave-active-class="transition-[grid-template-rows]"
+            enter-from-class="grid-rows-[0fr]"
+            leave-to-class="grid-rows-[0fr]"
+            enter-to-class="grid-rows-[1fr]"
+            leave-from-class="grid-rows-[1fr]"
           >
-            <Tabs />
-          </div>
-        </Transition>
-        <NScrollbar
-          container-class="main-container"
-          :container-style="{
-            backgroundImage: `url(${texturePng})`,
-          }"
-          :theme-overrides="scrollbarInMainLayout"
-        >
-          <MainLayout />
-        </NScrollbar>
-        <EmptyPlaceholder
-          :show="isEmpty(tabsStore.tabs)"
-          description="空标签页"
-          size="huge"
-        >
-          <template #icon>
-            <div class="flex items-center justify-center">
-              <span class="iconify ph--rectangle" />
+            <div
+              v-if="!isEmpty(tabsStore.tabs) && preferencesStore.preferences.showTabs"
+              class="grid shrink-0 items-baseline overflow-hidden max-sm:hidden"
+            >
+              <Tabs />
             </div>
-          </template>
-        </EmptyPlaceholder>
-        <Transition
-          type="transition"
-          enter-active-class="transition-[grid-template-rows]"
-          leave-active-class="transition-[grid-template-rows]"
-          enter-from-class="grid-rows-[0fr]"
-          leave-to-class="grid-rows-[0fr]"
-          enter-to-class="grid-rows-[1fr]"
-          leave-from-class="grid-rows-[1fr]"
-        >
-          <div
-            v-if="preferencesStore.preferences.showFooter"
-            class="grid shrink-0 items-baseline overflow-hidden"
+          </Transition>
+          <NScrollbar
+            container-class="main-container"
+            :theme-overrides="scrollbarInMainLayout"
+          >
+            <MainLayout />
+          </NScrollbar>
+          <EmptyPlaceholder
+            :show="isEmpty(tabsStore.tabs)"
+            description="空标签页"
+            size="huge"
           >
-            <FooterLayout />
-          </div>
-        </Transition>
+            <template #icon>
+              <div class="flex items-center justify-center">
+                <span class="iconify ph--rectangle" />
+              </div>
+            </template>
+          </EmptyPlaceholder>
+          <Transition
+            type="transition"
+            enter-active-class="transition-[grid-template-rows]"
+            leave-active-class="transition-[grid-template-rows]"
+            enter-from-class="grid-rows-[0fr]"
+            leave-to-class="grid-rows-[0fr]"
+            enter-to-class="grid-rows-[1fr]"
+            leave-from-class="grid-rows-[1fr]"
+          >
+            <div
+              v-if="preferencesStore.preferences.showFooter"
+              class="grid shrink-0 items-baseline overflow-hidden"
+            >
+              <FooterLayout />
+            </div>
+          </Transition>
+        </div>
       </div>
     </div>
+    <MobileRightAside />
   </div>
 </template>

+ 6 - 0
src/stores/preferences.ts

@@ -11,6 +11,9 @@ export interface PreferencesOptions {
     width: number
     maxWidth: number
   }>
+  mobile: {
+    mainLayoutSlider: 'left' | 'right' | null
+  }
   shouldRefreshTab: boolean
   showFooter: boolean
   showLogo: boolean
@@ -33,6 +36,9 @@ const DEFAULT_PREFERENCES_OPTIONS: PreferencesOptions = {
     width: 64,
     maxWidth: 272,
   },
+  mobile: {
+    mainLayoutSlider: null,
+  },
   shouldRefreshTab: false,
   showFooter: true,
   showTabs: true,

+ 17 - 5
src/views/about/index.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
 import { NCard, NSplit, NButton, NScrollbar, NTag } from 'naive-ui'
-import { onMounted, ref } from 'vue'
+import { onMounted, ref, inject } from 'vue'
 
 import packageJson from '@/../package.json'
+import { mediaQueryInjectionKey } from '@/injection'
 
 defineOptions({
   name: 'About',
@@ -10,6 +11,8 @@ defineOptions({
 
 let codeToHtml: any
 
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
 const APP_NAME = import.meta.env.VITE_APP_NAME
 
 const { dependencies, devDependencies } = packageJson
@@ -183,7 +186,10 @@ onMounted(async () => {
 </script>
 <template>
   <div class="flex flex-col gap-y-4 p-4">
-    <NCard :title="`关于 ${APP_NAME}`">
+    <NCard
+      :title="`关于 ${APP_NAME}`"
+      :size="mediaQuery?.md.value ? 'small' : undefined"
+    >
       <p class="text-base">
         {{ APP_NAME }} 是一个轻盈而优雅的后台管理模板,主要技术栈由
         <a
@@ -252,13 +258,19 @@ onMounted(async () => {
         构建。
       </p>
     </NCard>
-    <div class="flex gap-x-2">
-      <NCard title="目录结构">
+    <div class="flex gap-x-2 max-lg:flex-col">
+      <NCard
+        title="目录结构"
+        :size="mediaQuery?.md.value ? 'small' : undefined"
+      >
         <NScrollbar container-style="max-height: 1100px;">
           <div v-html="directoryStructureHighlight"></div>
         </NScrollbar>
       </NCard>
-      <NCard title="依赖信息">
+      <NCard
+        title="依赖信息"
+        :size="mediaQuery?.md.value ? 'small' : undefined"
+      >
         <NSplit
           direction="vertical"
           pane1-class="pb-4"

+ 13 - 9
src/views/data-show/data-form/index.vue

@@ -20,9 +20,10 @@ import {
   NRadio,
   useMessage,
 } from 'naive-ui'
-import { computed, ref, useTemplateRef, watch } from 'vue'
+import { computed, ref, useTemplateRef, watch, inject } from 'vue'
 
 import { useResettableReactive } from '@/composable/useResettable'
+import { mediaQueryInjectionKey } from '@/injection'
 
 import type { FormRules } from 'naive-ui'
 
@@ -44,6 +45,8 @@ defineOptions({
 
 let codeToHtml: any
 
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
 const message = useMessage()
 
 const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
@@ -226,11 +229,12 @@ watch(
     >
       一个数据表单的例子,做了一些验证的限制,右边是规则和表单的数据,你可以拖动它们之间的分割线
     </NAlert>
-    <NCard>
+    <NCard :size="mediaQuery?.md.value ? 'small' : undefined">
       <NSplit
-        pane1-class="pr-8"
-        pane2-class="pl-8"
+        :pane1-class="mediaQuery?.lg.value ? 'pb-4' : 'pr-8'"
+        :pane2-class="mediaQuery?.lg.value ? 'pt-4' : 'pl-8'"
         :default-size="0.6"
+        :direction="mediaQuery?.xl.value ? 'vertical' : 'horizontal'"
       >
         <template #1>
           <NScrollbar>
@@ -239,10 +243,10 @@ watch(
               :model="form"
               :rules="rules"
               label-placement="left"
-              :label-width="100"
+              :label-width="78"
               :disabled="formDisabled"
             >
-              <div class="flex gap-x-8">
+              <div class="flex gap-x-8 max-lg:flex-col">
                 <div class="flex-1">
                   <NFormItem
                     label="姓名"
@@ -467,7 +471,7 @@ watch(
                 </div>
               </div>
             </NForm>
-            <div class="flex items-center">
+            <div class="flex items-center max-lg:flex-col">
               <NFormItem
                 label="禁用表单"
                 label-placement="left"
@@ -476,7 +480,7 @@ watch(
                 <NSwitch v-model:value="formDisabled" />
               </NFormItem>
 
-              <div class="ml-8 flex gap-x-4">
+              <div class="flex gap-4 max-md:flex-wrap max-md:gap-2 md:ml-8">
                 <NButton
                   type="success"
                   :disabled="formDisabled"
@@ -535,7 +539,7 @@ watch(
         </template>
         <template #resize-trigger>
           <div
-            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] dark:bg-neutral-700"
+            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] max-lg:h-px max-lg:w-full dark:bg-neutral-700"
           ></div>
         </template>
       </NSplit>

+ 21 - 9
src/views/data-show/data-table/index.vue

@@ -17,7 +17,12 @@ import {
   NNumberAnimation,
   NAlert,
 } from 'naive-ui'
-import { defineComponent, reactive, ref, useTemplateRef, nextTick } from 'vue'
+
+import { mediaQueryInjectionKey } from '@/injection'
+
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
+import { defineComponent, reactive, ref, useTemplateRef, nextTick, inject } from 'vue'
 
 import { useComponentModifier } from '@/composable/useComponentModifier'
 import { useDataTable } from '@/composable/useDataTable'
@@ -447,14 +452,16 @@ getDataList()
     >
       一个数据表格的例子,不算复杂,有一个高度的计算,也许对你有帮助
     </NAlert>
-    <NCard>
-      <div class="mb-2 flex gap-x-4">
+    <NCard :size="mediaQuery?.md.value ? 'small' : undefined">
+      <div class="mb-2 flex justify-end gap-x-4 max-xl:mb-4 max-xl:flex-wrap">
         <NForm
           ref="formRef"
           :model="form"
           :rules="rules"
-          inline
+          :inline="!mediaQuery?.lg.value"
           label-placement="left"
+          class="max-lg:w-full max-lg:flex-col"
+          :label-width="mediaQuery?.lg.value ? 70 : undefined"
         >
           <NFormItem
             label="姓名"
@@ -462,7 +469,6 @@ getDataList()
           >
             <NInput
               v-model:value="form.fullName"
-              style="width: 120px"
               clearable
             />
           </NFormItem>
@@ -473,7 +479,7 @@ getDataList()
             <NSelect
               v-model:value="form.sex"
               :options="sexOptions"
-              style="width: 88px"
+              style="min-width: 88px"
               clearable
             />
           </NFormItem>
@@ -534,7 +540,7 @@ getDataList()
           ref="dataTableRef"
           v-model:checked-row-keys="checkedRowKeys"
           :remote="true"
-          :max-height="maxHeight"
+          :max-height="mediaQuery?.md.value ? undefined : maxHeight"
           :scroll-x="enableScrollX ? 1800 : 0"
           :min-height="166.6"
           :columns="columns"
@@ -545,8 +551,8 @@ getDataList()
           :row-props="rowProps"
           :single-line="enableSingleLine"
         />
-        <div class="mt-3 flex items-end justify-between">
-          <div class="flex items-center gap-x-3">
+        <div class="mt-3 flex items-end justify-between max-xl:flex-col max-xl:gap-y-2">
+          <div class="flex items-center justify-between gap-x-3">
             <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
             <NButtonGroup
               size="small"
@@ -575,6 +581,7 @@ getDataList()
               </NButton>
 
               <NButton
+                v-show="!mediaQuery?.md.value"
                 @click="enableContextmenu = !enableContextmenu"
                 :type="enableContextmenu ? 'primary' : 'default'"
                 secondary
@@ -582,6 +589,7 @@ getDataList()
                 右键菜单
               </NButton>
               <NButton
+                v-show="!mediaQuery?.md.value"
                 @click="handleDownloadCsvClick"
                 secondary
                 type="info"
@@ -593,6 +601,10 @@ getDataList()
           <NPagination
             v-bind="pagination"
             :prefix="paginationPrefix"
+            :page-slot="mediaQuery?.md.value ? 5 : undefined"
+            :show-quick-jump-dropdown="!mediaQuery?.md.value"
+            :show-quick-jumper="!mediaQuery?.md.value"
+            :show-size-picker="!mediaQuery?.md.value"
           />
         </div>
       </div>

+ 44 - 28
src/views/drag-drop/index.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { NAlert, NCard, NSplit, NScrollbar, NButton } from 'naive-ui'
-import { ref, watch } from 'vue'
+import { ref, watch, inject } from 'vue'
 import { VueDraggable } from 'vue-draggable-plus'
 
 import { EmptyPlaceholder } from '@/components'
+import { mediaQueryInjectionKey } from '@/injection'
 
 import type { UseDraggableReturn } from 'vue-draggable-plus'
 
@@ -13,6 +14,8 @@ defineOptions({
 
 let codeToHtml: any
 
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
 const APP_NAME = import.meta.env.VITE_APP_NAME
 
 const baseListCodeHighlight = ref()
@@ -119,12 +122,17 @@ watch(
       {{ APP_NAME }} 的 Tabs 栏的拖拽模块使用了
       vue-draggable-plus,在一些拖拽的场景下,它也许可以帮助到你
     </NAlert>
-    <NCard title="基础使用">
+    <NCard
+      title="基础使用"
+      :size="mediaQuery?.md.value ? 'small' : undefined"
+    >
       <NSplit
-        direction="horizontal"
-        pane1-class="pr-8"
-        pane2-class="pl-8"
-        style="height: 280px"
+        :direction="mediaQuery?.md.value ? 'vertical' : 'horizontal'"
+        :pane1-class="mediaQuery?.md.value ? 'pb-4' : 'pr-8'"
+        :pane2-class="mediaQuery?.md.value ? 'pt-4' : 'pl-8'"
+        :style="{
+          height: mediaQuery?.md.value ? '580px' : '280px',
+        }"
       >
         <template #1>
           <NScrollbar>
@@ -154,24 +162,27 @@ watch(
         </template>
         <template #resize-trigger>
           <div
-            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] dark:bg-neutral-700"
+            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] max-lg:h-px max-lg:w-full dark:bg-neutral-700"
           ></div>
         </template>
       </NSplit>
     </NCard>
-    <NCard title="网格布局">
-      <template #header-extra>
-        <div>
-          你可以把<span class="text-primary">网格布局</span>的元素拖进<span class="text-primary"
-            >基础使用</span
-          >中,它们可以相互拖放
-        </div>
-      </template>
+    <NCard
+      title="网格布局"
+      :size="mediaQuery?.md.value ? 'small' : undefined"
+    >
+      <div class="mb-4">
+        你可以把<span class="text-primary">网格布局</span>的元素拖进<span class="text-primary"
+          >基础使用</span
+        >中,它们可以相互拖放
+      </div>
       <NSplit
-        direction="horizontal"
-        pane1-class="pr-8"
-        pane2-class="pl-8"
-        style="height: 280px"
+        :direction="mediaQuery?.md.value ? 'vertical' : 'horizontal'"
+        :pane1-class="mediaQuery?.md.value ? 'pb-4' : 'pr-8'"
+        :pane2-class="mediaQuery?.md.value ? 'pt-4' : 'pl-8'"
+        :style="{
+          height: mediaQuery?.md.value ? '680px' : '280px',
+        }"
         :default-size="0.7"
       >
         <template #1>
@@ -182,7 +193,7 @@ watch(
               :animation="150"
               ghostClass="ghost"
               group="drag1"
-              class="m-auto grid grid-cols-8 gap-2 rounded bg-neutral-500/5 p-4 select-none"
+              class="m-auto grid grid-cols-8 gap-2 rounded bg-neutral-500/5 p-4 select-none max-lg:grid-cols-4"
             >
               <div
                 v-for="item in gridList"
@@ -201,21 +212,26 @@ watch(
         </template>
         <template #resize-trigger>
           <div
-            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] dark:bg-neutral-700"
+            class="h-full w-px cursor-col-resize bg-neutral-200 transition-[background-color] max-lg:h-px max-lg:w-full dark:bg-neutral-700"
           ></div>
         </template>
       </NSplit>
     </NCard>
-    <NCard title="克隆使用">
+    <NCard
+      title="克隆使用"
+      :size="mediaQuery?.md.value ? 'small' : undefined"
+    >
       <NSplit
-        direction="horizontal"
-        pane1-class="pr-8"
-        pane2-class="pl-8"
-        style="height: 300px"
+        :direction="mediaQuery?.md.value ? 'vertical' : 'horizontal'"
+        :pane1-class="mediaQuery?.md.value ? 'pb-4' : 'pr-8'"
+        :pane2-class="mediaQuery?.md.value ? 'pt-4' : 'pl-8'"
+        :style="{
+          height: mediaQuery?.md.value ? '780px' : '280px',
+        }"
         :default-size="0.7"
       >
         <template #1>
-          <div class="flex h-full gap-x-4">
+          <div class="flex h-full gap-4 max-lg:flex-col">
             <NScrollbar>
               <VueDraggable
                 ref="taskDragRef"
@@ -239,7 +255,7 @@ watch(
             <NScrollbar>
               <EmptyPlaceholder
                 :show="cloneTaskList.length <= 0"
-                description="把左边的任务拖拽到这里"
+                :description="`${mediaQuery?.md.value ? '上' : ''}边的任务拖拽到这里`"
               />
               <VueDraggable
                 ref="cloneTaskListDragRef"

+ 7 - 2
src/views/dynamic-route/index.vue

@@ -1,7 +1,12 @@
 <script setup lang="ts">
 import { NCard, NAlert, NButton } from 'naive-ui'
+import { inject } from 'vue'
 import { RouterLink, useRouter } from 'vue-router'
 
+import { mediaQueryInjectionKey } from '@/injection'
+
+const mediaQuery = inject(mediaQueryInjectionKey, null)
+
 defineOptions({
   name: 'DynamicRoute',
 })
@@ -16,8 +21,8 @@ const router = useRouter()
     >
       在路由配置的 meta 中添加 enableMultiTab 属性,访问不同的动态路径时都会创建新的 tab
     </NAlert>
-    <NCard>
-      <div class="grid grid-cols-5 gap-4">
+    <NCard :size="mediaQuery?.md.value ? 'small' : undefined">
+      <div class="grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-2 max-sm:grid-cols-1">
         <RouterLink
           v-for="value in 50"
           :key="value"

+ 4 - 3
src/views/error-page/404.vue

@@ -46,13 +46,14 @@ const changeStateCode200 = () => {
       </div>
     </NAlert>
     <div
-      class="absolute flex h-screen w-full items-center justify-center"
+      class="absolute left-0 flex h-screen w-full items-center justify-center"
       style="padding-bottom: 240px"
     >
       <ErrorPage
         v-bind="errorState"
-        container-class="w-full"
-        container-style="width: 500px"
+        :container-style="{
+          maxWidth: '500px',
+        }"
       >
         <NButton
           size="large"

+ 6 - 3
src/views/error-page/index.vue

@@ -1,9 +1,11 @@
 <script setup lang="ts">
 import { NNumberAnimation, NButton } from 'naive-ui'
-import { ref, type StyleValue } from 'vue'
+import { ref } from 'vue'
 
 import router from '@/router'
 
+import type { StyleValue } from 'vue'
+
 interface ErrorPageProps {
   code?: number
   content?: string
@@ -28,9 +30,9 @@ const prevCode = ref(0)
 <template>
   <div class="grid h-dvh w-full flex-col place-items-center">
     <div
-      class="flex flex-col"
+      class="flex w-full flex-col"
       :class="containerClass"
-      style="width: 600px"
+      style="max-width: 600px"
       :style="containerStyle"
     >
       <svg
@@ -39,6 +41,7 @@ const prevCode = ref(0)
         xml:space="preserve"
         class="text-primary"
         viewBox="0 0 500 500"
+        width="100%"
       >
         <path
           fill="currentColor"

+ 1 - 1
src/views/sign-in/index.vue

@@ -4,7 +4,7 @@ import { computed, onMounted, onUnmounted, reactive, ref, useTemplateRef } from
 
 import topographySvg from '@/assets/topography.svg'
 import { usePersonalization } from '@/composable/usePersonalization'
-import ThemeDropdown from '@/layouts/header/Actions/component/ThemeDropdown.vue'
+import ThemeDropdown from '@/layouts/header/actions/component/ThemeDropdown.vue'
 import router from '@/router'
 import { useUserStore } from '@/stores/user'
 import twc from '@/utils/tailwindColor'