Explorar o código

wip(Layout): Layout developing

陈凯龙 %!s(int64=3) %!d(string=hai) anos
pai
achega
2fe9543b84

+ 26 - 12
pnpm-lock.yaml

@@ -41,6 +41,7 @@ specifiers:
   pretty-quick: ^3.1.3
   qs: ^6.10.3
   rimraf: ^3.0.2
+  screenfull: ^6.0.0
   stylelint: ^14.2.0
   stylelint-config-html: ^1.0.0
   stylelint-config-prettier: ^9.0.3
@@ -75,6 +76,7 @@ dependencies:
   nprogress: registry.npmmirror.com/nprogress/0.2.0
   pinia: registry.npmmirror.com/pinia/2.0.9_typescript@4.5.4+vue@3.2.26
   qs: registry.npmmirror.com/qs/6.10.3
+  screenfull: registry.npmmirror.com/screenfull/6.0.0
   vue: registry.npmmirror.com/vue/3.2.26
   vue-i18n: registry.npmmirror.com/vue-i18n/9.1.9_vue@3.2.26
   vue-router: registry.npmmirror.com/vue-router/4.0.12_vue@3.2.26
@@ -7064,8 +7066,8 @@ packages:
       vue-i18n:
         optional: true
     dependencies:
-      '@intlify/message-compiler': registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.26
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.26
+      '@intlify/message-compiler': registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.27
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
       jsonc-eslint-parser: registry.npmmirror.com/jsonc-eslint-parser/1.4.1
       source-map: registry.nlark.com/source-map/0.6.1
       vue-i18n: registry.npmmirror.com/vue-i18n/9.1.9_vue@3.2.26
@@ -7121,18 +7123,18 @@ packages:
       source-map: registry.nlark.com/source-map/0.6.1
     dev: false
 
-  registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.26:
+  registry.npmmirror.com/@intlify/message-compiler/9.2.0-beta.27:
     resolution:
       {
-        integrity: sha512-qtDgHCMqrXNTekKXGzm0Dm6r3+/X7/jFXP+E07hx+PJbPMv7DzK1iU8h5LlAMQ1/jr2UIRBgXvR5wh35OKoGrA==,
+        integrity: sha512-T3mBTm0559VX6l+lh8p5gDJ9/IS1XbVXeeMNJ2zTzxrf4lXg8OuotNjaxG3ZsuauQ5OqqlArkMYryXGyZnHolA==,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/@intlify/message-compiler/download/@intlify/message-compiler-9.2.0-beta.26.tgz
+        tarball: https://registry.npmmirror.com/@intlify/message-compiler/download/@intlify/message-compiler-9.2.0-beta.27.tgz
       }
     name: '@intlify/message-compiler'
-    version: 9.2.0-beta.26
+    version: 9.2.0-beta.27
     engines: { node: '>= 12' }
     dependencies:
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.26
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
       source-map: registry.nlark.com/source-map/0.6.1
     dev: true
 
@@ -7176,15 +7178,15 @@ packages:
     engines: { node: '>= 10' }
     dev: false
 
-  registry.npmmirror.com/@intlify/shared/9.2.0-beta.26:
+  registry.npmmirror.com/@intlify/shared/9.2.0-beta.27:
     resolution:
       {
-        integrity: sha512-MjUlkjNThqkqy8yXUcFKBiW/hIfqAn5cP3Vd0b4wdOHS8rPCEbvSbAnF08uiZDkVv8gTcsLyymX21GaU6oYyyQ==,
+        integrity: sha512-+Av77mIHy0qFkAq96mMAQGYcColMGN7e5+rUsyn3XxBw6oC3AGqYn/cQ6U/T3qOrzcHgcA+etAaLN3IxFqkJDw==,
         registry: https://registry.npm.taobao.org/,
-        tarball: https://registry.npmmirror.com/@intlify/shared/download/@intlify/shared-9.2.0-beta.26.tgz
+        tarball: https://registry.npmmirror.com/@intlify/shared/download/@intlify/shared-9.2.0-beta.27.tgz
       }
     name: '@intlify/shared'
-    version: 9.2.0-beta.26
+    version: 9.2.0-beta.27
     engines: { node: '>= 12' }
     dev: true
 
@@ -7210,7 +7212,7 @@ packages:
         optional: true
     dependencies:
       '@intlify/bundle-utils': registry.npmmirror.com/@intlify/bundle-utils/2.2.0_vue-i18n@9.1.9
-      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.26
+      '@intlify/shared': registry.npmmirror.com/@intlify/shared/9.2.0-beta.27
       '@rollup/pluginutils': registry.npmmirror.com/@rollup/pluginutils/4.1.2
       debug: registry.npmmirror.com/debug/4.3.3
       fast-glob: registry.nlark.com/fast-glob/3.2.7
@@ -11607,6 +11609,18 @@ packages:
       tslib: registry.npmmirror.com/tslib/2.3.1
     dev: true
 
+  registry.npmmirror.com/screenfull/6.0.0:
+    resolution:
+      {
+        integrity: sha512-LGY0nhNQkC4FX4DT4pZdJ5cZH5EOz9Gfh9KcVMl779pS677k4IV1Wv7sY/CwC9VKFT21fYgCh7zkTVVefi5XKA==,
+        registry: https://registry.npm.taobao.org/,
+        tarball: https://registry.npmmirror.com/screenfull/download/screenfull-6.0.0.tgz
+      }
+    name: screenfull
+    version: 6.0.0
+    engines: { node: ^14.13.1 || >=16.0.0 }
+    dev: false
+
   registry.npmmirror.com/shebang-regex/3.0.0:
     resolution:
       {

+ 1 - 0
src/App.vue

@@ -32,6 +32,7 @@ html,
 body {
   padding: 0;
   margin: 0;
+  overflow: hidden;
   .size;
 
   #app {

BIN=BIN
src/assets/imgs/avatar.png


+ 3 - 0
src/components/Breadcrumb/index.ts

@@ -0,0 +1,3 @@
+import Breadcrumb from './src/Breadcrumb.vue'
+
+export { Breadcrumb }

+ 0 - 0
src/components/Breadcrumb/src/Breadcrumb.vue


+ 3 - 0
src/components/Collapse/index.ts

@@ -0,0 +1,3 @@
+import Collapse from './src/Collapse.vue'
+
+export { Collapse }

+ 25 - 0
src/components/Collapse/src/Collapse.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import { computed, unref } from 'vue'
+import { Icon } from '@/components/Icon'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+
+const collapse = computed(() => appStore.getCollapse)
+
+function toggleCollapse() {
+  const collapsed = unref(collapse)
+  appStore.setCollapse(!collapsed)
+}
+</script>
+
+<template>
+  <div>
+    <Icon
+      :size="18"
+      :icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
+      class="cursor-pointer"
+      @click="toggleCollapse"
+    />
+  </div>
+</template>

+ 1 - 0
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -25,6 +25,7 @@ function setLang(lang: LocaleType) {
 <template>
   <ElDropdown trigger="click" @command="setLang">
     <Icon
+      :size="18"
       icon="ion:language-sharp"
       color="var(--el-text-color-primary)"
       class="cursor-pointer"

+ 53 - 3
src/components/Menu/src/Menu.vue

@@ -22,8 +22,8 @@ export default defineComponent({
 
     const preFixCls = getPrefixCls('menu')
 
-    const menuMode = computed(() => {
-      // 水平模式
+    const menuMode = computed((): 'vertical' | 'horizontal' => {
+      // 
       const vertical: LayoutType[] = ['classic']
 
       if (vertical.includes(appStore.getLayout)) {
@@ -77,7 +77,7 @@ export default defineComponent({
           >
             {{
               default: () => {
-                const { renderMenuItem } = useRenderMenuItem(routers.value)
+                const { renderMenuItem } = useRenderMenuItem(routers.value, menuMode.value)
                 return renderMenuItem()
               }
             }}
@@ -93,7 +93,10 @@ export default defineComponent({
 @prefix-cls: ~'@{namespace}-menu';
 
 .@{prefix-cls} {
+  transition: width var(--transition-time-02);
+
   :deep(.el-menu) {
+    width: 100% !important;
     border-right: none;
 
     // 设置选中时子标题的颜色
@@ -132,8 +135,55 @@ export default defineComponent({
     }
   }
 
+  // 折叠时的最小宽度
   :deep(.el-menu--collapse) {
     width: var(--left-menu-min-width);
+
+    & > .is-active,
+    & > .is-active > .el-sub-menu__title {
+      background-color: var(--left-menu-collapse-bg-active-color) !important;
+    }
+  }
+
+  // 折叠动画的时候,就需要把文字给隐藏掉
+  :deep(.horizontal-collapse-transition) {
+    transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
+    .@{prefix-cls}__title {
+      display: none;
+    }
+  }
+}
+</style>
+
+<style lang="less">
+@prefix-cls: ~'@{namespace}-menu-popper';
+
+.@{prefix-cls} {
+  &--vertical {
+    // 设置选中时子标题的颜色
+    .is-active {
+      & > .el-sub-menu__title {
+        color: var(--left-menu-text-active-color) !important;
+      }
+    }
+
+    // 设置子菜单悬停的高亮和背景色
+    .el-sub-menu__title,
+    .el-menu-item {
+      &:hover {
+        color: var(--left-menu-text-active-color) !important;
+        background-color: var(--left-menu-bg-color) !important;
+      }
+    }
+
+    // 设置选中时的高亮背景
+    .el-menu-item.is-active {
+      background-color: var(--left-menu-bg-active-color) !important;
+
+      &:hover {
+        background-color: var(--left-menu-bg-active-color) !important;
+      }
+    }
   }
 }
 </style>

+ 17 - 4
src/components/Menu/src/components/useRenderMenuItem.tsx

@@ -3,8 +3,13 @@ import type { RouteMeta } from 'vue-router'
 import { getAllParentPath, hasOneShowingChild } from '../helper'
 import { isUrl } from '@/utils/is'
 import { useRenderMenuTitle } from './useRenderMenuTitle'
+import { useDesign } from '@/hooks/web/useDesign'
+import { pathResolve } from '@/utils/routerHelper'
 
-export function useRenderMenuItem(allRouters: AppRouteRecordRaw[] = []) {
+export function useRenderMenuItem(
+  allRouters: AppRouteRecordRaw[] = [],
+  menuMode: 'vertical' | 'horizontal'
+) {
   function renderMenuItem(routers?: AppRouteRecordRaw[]) {
     return (routers || allRouters).map((v) => {
       const meta = (v.meta ?? {}) as RouteMeta
@@ -23,15 +28,23 @@ export function useRenderMenuItem(allRouters: AppRouteRecordRaw[] = []) {
           !meta?.alwaysShow
         ) {
           return (
-            <ElMenuItem index={fullPath}>
+            <ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
               {{
-                default: () => renderMenuTitle(meta)
+                default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
               }}
             </ElMenuItem>
           )
         } else {
+          const { getPrefixCls } = useDesign()
+
+          const preFixCls = getPrefixCls('menu-popper')
           return (
-            <ElSubMenu index={fullPath}>
+            <ElSubMenu
+              index={fullPath}
+              popperClass={
+                menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
+              }
+            >
               {{
                 title: () => renderMenuTitle(meta),
                 default: () => renderMenuItem(v.children)

+ 2 - 2
src/components/Menu/src/components/useRenderMenuTitle.tsx

@@ -10,10 +10,10 @@ export function useRenderMenuTitle() {
     return icon ? (
       <>
         <Icon icon={meta.icon}></Icon>
-        {t(title as string)}
+        <span>{t(title as string)}</span>
       </>
     ) : (
-      t(title as string)
+      <span>{t(title as string)}</span>
     )
   }
 

+ 3 - 0
src/components/Screenfull/index.ts

@@ -0,0 +1,3 @@
+import Screenfull from './src/Screenfull.vue'
+
+export { Screenfull }

+ 16 - 0
src/components/Screenfull/src/Screenfull.vue

@@ -0,0 +1,16 @@
+<script setup lang="ts">
+import { Icon } from '@/components/Icon'
+import { useFullscreen } from '@vueuse/core'
+
+const { toggle, isFullscreen } = useFullscreen()
+
+function toggleFullscreen() {
+  toggle()
+}
+</script>
+
+<template>
+  <div @click="toggleFullscreen">
+    <Icon :size="18" :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" />
+  </div>
+</template>

+ 6 - 1
src/components/SizeDropdown/src/SizeDropdown.vue

@@ -16,7 +16,12 @@ function setSize(size: ElememtPlusSzie) {
 
 <template>
   <ElDropdown trigger="click" @command="setSize">
-    <Icon icon="mdi:format-size" color="var(--el-text-color-primary)" class="cursor-pointer" />
+    <Icon
+      :size="18"
+      icon="mdi:format-size"
+      color="var(--el-text-color-primary)"
+      class="cursor-pointer"
+    />
     <template #dropdown>
       <ElDropdownMenu>
         <ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">

+ 3 - 0
src/components/UserInfo/index.ts

@@ -0,0 +1,3 @@
+import UserInfo from './src/UserInfo.vue'
+
+export { UserInfo }

+ 46 - 0
src/components/UserInfo/src/UserInfo.vue

@@ -0,0 +1,46 @@
+<script setup lang="ts">
+import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useCache } from '@/hooks/web/useCache'
+import { resetRouter } from '@/router'
+import { useRouter } from 'vue-router'
+
+const { t } = useI18n()
+
+const { wsCache } = useCache()
+
+const { replace } = useRouter()
+
+function loginOut() {
+  ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    type: 'warning'
+  })
+    .then(() => {
+      wsCache.clear()
+      resetRouter() // 重置静态路由表
+      replace('/login')
+    })
+    .catch(() => {})
+}
+</script>
+
+<template>
+  <ElDropdown trigger="click">
+    <div class="flex items-center">
+      <img src="@/assets/imgs/avatar.png" alt="" class="w-[calc(var(--tags-view-height)-10px)]" />
+      <span class="<lg:hidden text-14px pl-[5px] text-dark-50">Archer</span>
+    </div>
+    <template #dropdown>
+      <ElDropdownMenu>
+        <ElDropdownItem>
+          <div>{{ t('common.document') }}</div>
+        </ElDropdownItem>
+        <ElDropdownItem divided>
+          <div @click="loginOut">{{ t('common.loginOut') }}</div>
+        </ElDropdownItem>
+      </ElDropdownMenu>
+    </template>
+  </ElDropdown>
+</template>

+ 30 - 2
src/layout/Layout.vue

@@ -4,6 +4,11 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useAppStore } from '@/store/modules/app'
 import { Menu } from '@/components/Menu'
 import { useDesign } from '@/hooks/web/useDesign'
+import { Collapse } from '@/components/Collapse'
+import { LocaleDropdown } from '@/components/LocaleDropdown'
+import { SizeDropdown } from '@/components/SizeDropdown'
+import { UserInfo } from '@/components/UserInfo'
+import { Screenfull } from '@/components/Screenfull'
 // import { TagsView } from '@/components/TagsView'
 
 const tagsViewStore = useTagsViewStore()
@@ -43,10 +48,18 @@ export default defineComponent({
           <div
             class={[
               `${perFixCls}-right__tool`,
-              'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center'
+              'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
             ]}
           >
-            ssss
+            <div class="h-full flex items-center">
+              <Collapse class="header__tigger"></Collapse>
+            </div>
+            <div class="h-full flex items-center">
+              <Screenfull class="header__tigger"></Screenfull>
+              <SizeDropdown class="header__tigger"></SizeDropdown>
+              <LocaleDropdown class="header__tigger"></LocaleDropdown>
+              <UserInfo class="header__tigger"></UserInfo>
+            </div>
           </div>
           <router-view>
             {{
@@ -67,8 +80,23 @@ export default defineComponent({
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-app';
 
+.header__tigger {
+  display: flex;
+  height: 100%;
+  padding: 1px 10px 0;
+  cursor: pointer;
+  align-items: center;
+  transition: background var(--transition-time-02);
+
+  &:hover {
+    background-color: #f6f6f6;
+  }
+}
+
 .@{prefix-cls} {
   &-right {
+    transition: left var(--transition-time-02);
+
     &__tool {
       &::after {
         position: absolute;

+ 7 - 1
src/locales/en.ts

@@ -5,7 +5,13 @@ export default {
     startTimeText: 'Start time',
     endTimeText: 'End time',
     login: 'Login',
-    required: 'This is required'
+    required: 'This is required',
+    loginOut: 'Login out',
+    document: 'Document',
+    reminder: 'Reminder',
+    loginOutMessage: 'Exit the system?',
+    ok: 'OK',
+    cancel: 'Cancel'
   },
   size: {
     default: 'Default',

+ 7 - 1
src/locales/zh-CN.ts

@@ -5,7 +5,13 @@ export default {
     startTimeText: '开始时间',
     endTimeText: '结束时间',
     login: '登录',
-    required: '该项为必填项'
+    required: '该项为必填项',
+    loginOut: '退出系统',
+    document: '项目文档',
+    reminder: '温馨提示',
+    loginOutMessage: '是否退出本系统?',
+    ok: '确定',
+    cancel: '取消'
   },
   size: {
     default: '默认',

+ 17 - 0
src/router/index.ts

@@ -94,6 +94,23 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         }
       }
     ]
+  },
+  {
+    path: '/icon',
+    component: Layout,
+    name: 'IconsDemo',
+    meta: {},
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/Level/Menu2.vue'),
+        name: 'Icons',
+        meta: {
+          title: '图标',
+          icon: 'carbon:skill-level-advanced'
+        }
+      }
+    ]
   }
 ]
 

+ 7 - 1
src/styles/var.css

@@ -1,6 +1,7 @@
 :root {
   --dark-bg-color: #293146;
 
+  /* left menu start */
   --left-menu-max-width: 200px;
 
   --left-menu-min-width: 64px;
@@ -15,11 +16,16 @@
 
   --left-menu-text-active-color: #fff;
 
+  --left-menu-collapse-bg-active-color: var(--el-color-primary);
+  /* left menu end */
+
   --top-tool-height: 40px;
 
-  --top-tool-p-x: 20px;
+  --top-tool-p-x: 0;
 
   --top-tool-border-color: #eee;
 
   --tags-view-height: 40px;
+
+  --transition-time-02: 0.2s;
 }

+ 0 - 4
src/utils/color.ts

@@ -149,7 +149,3 @@ function subtractLight(color: string, amount: number) {
   const c = cc < 0 ? 0 : cc
   return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
 }
-
-export function setCssVar(prop: string, val: any, dom = document.documentElement) {
-  dom.style.setProperty(prop, val)
-}

+ 4 - 0
src/utils/index.ts

@@ -34,3 +34,7 @@ export function underlineToHump(str: string): string {
     return letter.toUpperCase()
   })
 }
+
+export function setCssVar(prop: string, val: any, dom = document.documentElement) {
+  dom.style.setProperty(prop, val)
+}

+ 2 - 1
src/utils/routerHelper.ts

@@ -122,7 +122,8 @@ export function generateRoutesFn2(routes: AppRouteRecordRaw[]): AppRouteRecordRa
 }
 
 export function pathResolve(parentPath: string, path: string) {
-  return `${parentPath}/${path}`
+  const childPath = path.startsWith('/') || !path ? path : `/${path}`
+  return `${parentPath}${childPath}`
 }
 
 // 路由降级

+ 1 - 1
src/views/Level/Menu111.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div>Menu111</div>
+  <div>Menu111 <input type="text" /></div>
 </template>

+ 1 - 1
src/views/Level/Menu12.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div>Menu12</div>
+  <div>Menu12 <input type="text" /></div>
 </template>

+ 1 - 1
src/views/Level/Menu2.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div>Menu2</div>
+  <div>Menu2 <input type="text" /></div>
 </template>