Browse Source

perf: 修改基本布局以及面包屑的显示

曾坤森 3 months ago
parent
commit
d2ebbda80e

+ 23 - 0
.prettierrc.cjs

@@ -0,0 +1,23 @@
+module.exports = {
+  printWidth: 80,
+  tabWidth: 2,
+  useTabs: false,
+  semi: true,
+  singleQuote: true,
+  quoteProps: 'as-needed',
+  jsxSingleQuote: false,
+  trailingComma: 'es5',
+  bracketSpacing: true,
+  bracketSameLine: false,
+  arrowParens: 'avoid',
+  rangeStart: 0,
+  rangeEnd: Infinity,
+  requirePragma: false,
+  insertPragma: false,
+  proseWrap: 'preserve',
+  htmlWhitespaceSensitivity: 'css',
+  vueIndentScriptAndStyle: false,
+  endOfLine: 'lf',
+  embeddedLanguageFormatting: 'auto',
+  singleAttributePerLine: false,
+};

+ 0 - 9
.prettierrc.js

@@ -1,9 +0,0 @@
-module.exports = {
-  tabWidth: 2,
-  semi: true,
-  printWidth: 80,
-  singleQuote: true,
-  quoteProps: 'consistent',
-  htmlWhitespaceSensitivity: 'strict',
-  vueIndentScriptAndStyle: true,
-};

+ 17 - 0
.stylelintrc.cjs

@@ -0,0 +1,17 @@
+module.exports = {
+  extends: [
+    'stylelint-config-standard',
+    'stylelint-config-rational-order',
+    'stylelint-config-recommended-vue',
+  ],
+  plugins: ['stylelint-order'],
+  rules: {
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: ['extends', 'tailwind', 'apply', 'variants', 'responsive', 'screen'],
+      },
+    ],
+    'no-descending-specificity': null,
+  },
+};

+ 0 - 29
.stylelintrc.js

@@ -1,29 +0,0 @@
-module.exports = {
-  extends: [
-    'stylelint-config-standard',
-    'stylelint-config-rational-order',
-    'stylelint-config-recommended-vue',
-  ],
-  defaultSeverity: 'warning',
-  plugins: ['stylelint-order'],
-  rules: {
-    'at-rule-no-unknown': [
-      true,
-      {
-        ignoreAtRules: ['plugin'],
-      },
-    ],
-    'rule-empty-line-before': [
-      'always',
-      {
-        except: ['after-single-line-comment', 'first-nested'],
-      },
-    ],
-    'selector-pseudo-class-no-unknown': [
-      true,
-      {
-        ignorePseudoClasses: ['deep'],
-      },
-    ],
-  },
-};

+ 22 - 0
commitlint.config.cjs

@@ -0,0 +1,22 @@
+module.exports = {
+  extends: ['@commitlint/config-conventional'],
+  rules: {
+    'type-enum': [
+      2,
+      'always',
+      [
+        'build',
+        'chore',
+        'ci',
+        'docs',
+        'feat',
+        'fix',
+        'perf',
+        'refactor',
+        'revert',
+        'style',
+        'test',
+      ],
+    ],
+  },
+};

+ 0 - 3
commitlint.config.js

@@ -1,3 +0,0 @@
-module.exports = {
-  extends: ['@commitlint/config-conventional'],
-};

+ 71 - 0
eslint.config.cjs

@@ -0,0 +1,71 @@
+// eslint.config.cjs
+module.exports = {
+  root: true,
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    sourceType: 'module',
+    ecmaVersion: 2020,
+    ecmaFeatures: {
+      jsx: true,
+    },
+  },
+  env: {
+    'browser': true,
+    'node': true,
+    'vue/setup-compiler-macros': true,
+    'es2020': true
+  },
+  plugins: ['@typescript-eslint'],
+  extends: [
+    'airbnb-base',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:import/recommended',
+    'plugin:import/typescript',
+    'plugin:vue/vue3-recommended',
+    'plugin:prettier/recommended',
+  ],
+  settings: {
+    'import/resolver': {
+      typescript: {
+        project: './tsconfig.json',
+      },
+    },
+  },
+  rules: {
+    'prettier/prettier': 1,
+    // Vue: Recommended rules to be closed or modify
+    'vue/require-default-prop': 0,
+    'vue/singleline-html-element-content-newline': 0,
+    'vue/max-attributes-per-line': 0,
+    // Vue: Add extra rules
+    'vue/custom-event-name-casing': [2, 'camelCase'],
+    'vue/no-v-text': 1,
+    'vue/padding-line-between-blocks': 1,
+    'vue/require-direct-export': 1,
+    'vue/multi-word-component-names': ['error', {
+      ignores: ['index', 'Index']
+    }],
+    // Allow @ts-ignore comment
+    '@typescript-eslint/ban-ts-comment': 0,
+    '@typescript-eslint/no-unused-vars': 1,
+    '@typescript-eslint/no-empty-function': 1,
+    '@typescript-eslint/no-explicit-any': 0,
+    '@typescript-eslint/no-require-imports': 0,
+    'no-undef': 0,
+    'import/extensions': [
+      2,
+      'ignorePackages',
+      {
+        js: 'never',
+        jsx: 'never',
+        ts: 'never',
+        tsx: 'never',
+      },
+    ],
+    'no-debugger': 'off',
+    'no-param-reassign': 0,
+    'prefer-regex-literals': 0,
+    'import/no-extraneous-dependencies': 0,
+  },
+};

+ 0 - 21
eslint.config.mts

@@ -1,21 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import tseslint from "typescript-eslint";
-import pluginVue from "eslint-plugin-vue";
-import { defineConfig } from "eslint/config";
-
-export default defineConfig([
-  { 
-    files: ["**/*.{js,mjs,cjs,ts,mts,cts,vue}"],
-    plugins: { js }, 
-    extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
-    tseslint.configs.recommended,
-    pluginVue.configs["flat/essential"],
-  { 
-    files: ["**/*.vue"], 
-    languageOptions: 
-    { parserOptions: 
-      { parser: tseslint.parser }
-    } 
-  },
-]);

+ 6 - 5
package.json

@@ -3,6 +3,7 @@
   "description": "Arco Design Pro for Vue",
   "version": "1.0.0",
   "private": true,
+  "type": "module",
   "author": "kun",
   "license": "MIT",
   "scripts": {
@@ -30,8 +31,8 @@
     ]
   },
   "dependencies": {
-    "@arco-design/web-vue": ">=2.0.0-beta.7",
-    "@vueuse/core": "^13.7.0",
+    "@arco-design/web-vue": ">=2.57.0",
+    "@vueuse/core": "^13.8.0",
     "axios": "^1.11.0",
     "dayjs": "^1.11.5",
     "lodash": "^4.17.21",
@@ -45,12 +46,12 @@
     "vue-router": "^4.5.1"
   },
   "devDependencies": {
-    "@arco-design/web-vue": "^2.0.0-beta.7",
+    "@arco-design/web-vue": "^2.57.0",
     "@arco-plugins/vite-vue": "^1.4.5",
     "@commitlint/cli": "^17.1.2",
     "@commitlint/config-conventional": "^17.1.0",
     "@eslint/js": "^9.34.0",
-    "@types/lodash": "^4.14.186",
+    "@types/lodash": "^4.17.20",
     "@types/minimatch": "^6.0.0",
     "@types/mockjs": "^1.0.7",
     "@types/nprogress": "^0.2.3",
@@ -104,6 +105,6 @@
     "gifsicle": "5.2.0"
   },
   "peerDependencies": {
-    "@arco-design/web-vue": ">=2.0.0-beta.7"
+    "@arco-design/web-vue": ">=2.57.0"
   }
 }

+ 15 - 15
pnpm-lock.yaml

@@ -14,11 +14,11 @@ importers:
   .:
     dependencies:
       '@arco-design/web-vue':
-        specifier: '>=2.0.0-beta.7'
+        specifier: '>=2.57.0'
         version: 2.57.0(vue@3.5.20(typescript@5.9.2))
       '@vueuse/core':
-        specifier: ^13.7.0
-        version: 13.7.0(vue@3.5.20(typescript@5.9.2))
+        specifier: ^13.8.0
+        version: 13.9.0(vue@3.5.20(typescript@5.9.2))
       axios:
         specifier: ^1.11.0
         version: 1.11.0
@@ -66,7 +66,7 @@ importers:
         specifier: ^9.34.0
         version: 9.34.0
       '@types/lodash':
-        specifier: ^4.14.186
+        specifier: ^4.17.20
         version: 4.17.20
       '@types/minimatch':
         specifier: ^6.0.0
@@ -1193,16 +1193,16 @@ packages:
   '@vue/shared@3.5.20':
     resolution: {integrity: sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==}
 
-  '@vueuse/core@13.7.0':
-    resolution: {integrity: sha512-myagn09+c6BmS6yHc1gTwwsdZilAovHslMjyykmZH3JNyzI5HoWhv114IIdytXiPipdHJ2gDUx0PB93jRduJYg==}
+  '@vueuse/core@13.9.0':
+    resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
     peerDependencies:
       vue: ^3.5.0
 
-  '@vueuse/metadata@13.7.0':
-    resolution: {integrity: sha512-8okFhS/1ite8EwUdZZfvTYowNTfXmVCOrBFlA31O0HD8HKXhY+WtTRyF0LwbpJfoFPc+s9anNJIXMVrvP7UTZg==}
+  '@vueuse/metadata@13.9.0':
+    resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
 
-  '@vueuse/shared@13.7.0':
-    resolution: {integrity: sha512-Wi2LpJi4UA9kM0OZ0FCZslACp92HlVNw1KPaDY6RAzvQ+J1s7seOtcOpmkfbD5aBSmMn9NvOakc8ZxMxmDXTIg==}
+  '@vueuse/shared@13.9.0':
+    resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
     peerDependencies:
       vue: ^3.5.0
 
@@ -6442,16 +6442,16 @@ snapshots:
 
   '@vue/shared@3.5.20': {}
 
-  '@vueuse/core@13.7.0(vue@3.5.20(typescript@5.9.2))':
+  '@vueuse/core@13.9.0(vue@3.5.20(typescript@5.9.2))':
     dependencies:
       '@types/web-bluetooth': 0.0.21
-      '@vueuse/metadata': 13.7.0
-      '@vueuse/shared': 13.7.0(vue@3.5.20(typescript@5.9.2))
+      '@vueuse/metadata': 13.9.0
+      '@vueuse/shared': 13.9.0(vue@3.5.20(typescript@5.9.2))
       vue: 3.5.20(typescript@5.9.2)
 
-  '@vueuse/metadata@13.7.0': {}
+  '@vueuse/metadata@13.9.0': {}
 
-  '@vueuse/shared@13.7.0(vue@3.5.20(typescript@5.9.2))':
+  '@vueuse/shared@13.9.0(vue@3.5.20(typescript@5.9.2))':
     dependencies:
       vue: 3.5.20(typescript@5.9.2)
 

+ 16 - 16
src/components/breadcrumb/index.vue

@@ -9,29 +9,29 @@
   </a-breadcrumb>
 </template>
 
-<script lang="ts" setup>
-  import { PropType } from 'vue';
+<script lang="ts" setup name="Breadcrumb">
+import { PropType } from 'vue';
 
-  defineProps({
-    items: {
-      type: Array as PropType<string[]>,
-      default() {
-        return [];
-      },
+defineProps({
+  items: {
+    type: Array as PropType<string[]>,
+    default() {
+      return [];
     },
-  });
+  },
+});
 </script>
 
 <style scoped lang="less">
-  .container-breadcrumb {
-    margin: 6px 0;
+.container-breadcrumb {
+  margin: 6px 0;
 
-    :deep(.arco-breadcrumb-item) {
-      color: rgb(var(--gray-6));
+  :deep(.arco-breadcrumb-item) {
+    color: rgb(var(--gray-6));
 
-      &:last-child {
-        color: rgb(var(--gray-8));
-      }
+    &:last-child {
+      color: rgb(var(--gray-8));
     }
   }
+}
 </style>

+ 105 - 0
src/components/global-breadcrumb/index.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="global-breadcrumb">
+    <a-breadcrumb>
+      <a-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="index">
+        <span
+          v-if="index === breadcrumbItems.length - 1"
+          class="breadcrumb-text"
+        >
+          {{ item.title }}
+        </span>
+        <a-link
+          v-else
+          @click="goToRoute(item.defRouter)"
+          :hoverable="false"
+          class="breadcrumb-link"
+        >
+          {{ item.title }}
+        </a-link>
+      </a-breadcrumb-item>
+    </a-breadcrumb>
+  </div>
+</template>
+
+<script name="GlobalBreadcrumb" lang="ts" setup>
+import { ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+const route = useRoute();
+const router = useRouter();
+const { t } = useI18n();
+
+const breadcrumbItems = ref<
+  { title: string; path: string; defRouter: string }[]
+>([]);
+
+// 获取面包屑项
+const getBreadcrumbItems = () => {
+  const matched = route.matched.filter(item => {
+    return item.meta && item.meta.locale;
+  });
+  const items = matched.map(item => {
+    return {
+      title: t(item.meta.locale as string),
+      path: item.path,
+      defRouter: item.children[0]?.path || item?.path,
+    };
+  });
+  return items;
+};
+
+// 监听路由变化更新面包屑
+watch(
+  () => route.matched,
+  () => {
+    breadcrumbItems.value = getBreadcrumbItems();
+  },
+  { immediate: true }
+);
+
+// 跳转到指定路由
+const goToRoute = (path: string) => {
+  console.log('path', path);
+  if (path) {
+    router.push(path);
+  }
+};
+</script>
+
+<style scoped lang="less">
+.global-breadcrumb {
+  margin-left: 16px;
+
+  :deep(.arco-breadcrumb) {
+    display: flex;
+    align-items: center;
+    height: 50px;
+  }
+
+  :deep(.arco-breadcrumb-item) {
+    display: flex;
+    align-items: center;
+
+    &:last-child .arco-breadcrumb-item-inner {
+      color: var(--color-text-1);
+      font-weight: 500;
+    }
+  }
+}
+
+.breadcrumb-link {
+  color: rgb(var(--color-text-1));
+  text-decoration: none;
+  cursor: pointer;
+
+  &:hover {
+    color: rgb(var(--color-text-4));
+    // text-decoration: underline;
+  }
+}
+
+.breadcrumb-text {
+  color: var(--color-text-1);
+}
+</style>

+ 140 - 141
src/components/menu/index.vue

@@ -1,161 +1,160 @@
 <script lang="tsx">
-  import { defineComponent, ref, h, compile, computed } from 'vue';
-  import { useI18n } from 'vue-i18n';
-  import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
-  import type { RouteMeta } from 'vue-router';
-  import { useAppStore } from '@/store';
-  import { listenerRouteChange } from '@/utils/route-listener';
-  import { openWindow, regexUrl } from '@/utils';
-  import useMenuTree from './use-menu-tree';
+import { defineComponent, ref, h, compile, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
+import type { RouteMeta } from 'vue-router';
+import { useAppStore } from '@/store';
+import { listenerRouteChange } from '@/utils/route-listener';
+import { openWindow, regexUrl } from '@/utils';
+import useMenuTree from './use-menu-tree';
 
-  export default defineComponent({
-    emit: ['collapse'],
-    setup() {
-      const { t } = useI18n();
-      const appStore = useAppStore();
-      const router = useRouter();
-      const route = useRoute();
-      const { menuTree } = useMenuTree();
-      const collapsed = computed({
-        get() {
-          if (appStore.device === 'desktop') return appStore.menuCollapse;
-          return false;
-        },
-        set(value: boolean) {
-          appStore.updateSettings({ menuCollapse: value });
-        },
-      });
+export default defineComponent({
+  emit: ['collapse'],
+  setup() {
+    const { t } = useI18n();
+    const appStore = useAppStore();
+    const router = useRouter();
+    const route = useRoute();
+    const { menuTree } = useMenuTree();
+    const collapsed = computed({
+      get() {
+        if (appStore.device === 'desktop') return appStore.menuCollapse;
+        return false;
+      },
+      set(value: boolean) {
+        appStore.updateSettings({ menuCollapse: value });
+      },
+    });
 
-      const topMenu = computed(() => appStore.topMenu);
-      const openKeys = ref<string[]>([]);
-      const selectedKey = ref<string[]>([]);
+    const topMenu = computed(() => appStore.topMenu);
+    const openKeys = ref<string[]>([]);
+    const selectedKey = ref<string[]>([]);
 
-      const goto = (item: RouteRecordRaw) => {
-        // Open external link
-        if (regexUrl.test(item.path)) {
-          openWindow(item.path);
-          selectedKey.value = [item.name as string];
+    const goto = (item: RouteRecordRaw) => {
+      // Open external link
+      if (regexUrl.test(item.path)) {
+        openWindow(item.path);
+        selectedKey.value = [item.name as string];
+        return;
+      }
+      // Eliminate external link side effects
+      const { hideInMenu, activeMenu } = item.meta as RouteMeta;
+      if (route.name === item.name && !hideInMenu && !activeMenu) {
+        selectedKey.value = [item.name as string];
+        return;
+      }
+      // Trigger router change
+      router.push({
+        name: item.name,
+      });
+    };
+    const findMenuOpenKeys = (target: string) => {
+      const result: string[] = [];
+      let isFind = false;
+      const backtrack = (item: RouteRecordRaw, keys: string[]) => {
+        if (item.name === target) {
+          isFind = true;
+          result.push(...keys);
           return;
         }
-        // Eliminate external link side effects
-        const { hideInMenu, activeMenu } = item.meta as RouteMeta;
-        if (route.name === item.name && !hideInMenu && !activeMenu) {
-          selectedKey.value = [item.name as string];
-          return;
+        if (item.children?.length) {
+          item.children.forEach(el => {
+            backtrack(el, [...keys, el.name as string]);
+          });
         }
-        // Trigger router change
-        router.push({
-          name: item.name,
-        });
-      };
-      const findMenuOpenKeys = (target: string) => {
-        const result: string[] = [];
-        let isFind = false;
-        const backtrack = (item: RouteRecordRaw, keys: string[]) => {
-          if (item.name === target) {
-            isFind = true;
-            result.push(...keys);
-            return;
-          }
-          if (item.children?.length) {
-            item.children.forEach((el) => {
-              backtrack(el, [...keys, el.name as string]);
-            });
-          }
-        };
-        menuTree.value.forEach((el: RouteRecordRaw) => {
-          if (isFind) return; // Performance optimization
-          backtrack(el, [el.name as string]);
-        });
-        return result;
       };
-      listenerRouteChange((newRoute) => {
-        const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
-        if (requiresAuth && (!hideInMenu || activeMenu)) {
-          const menuOpenKeys = findMenuOpenKeys(
-            (activeMenu || newRoute.name) as string
-          );
+      menuTree.value.forEach((el: RouteRecordRaw) => {
+        if (isFind) return; // Performance optimization
+        backtrack(el, [el.name as string]);
+      });
+      return result;
+    };
+    listenerRouteChange(newRoute => {
+      const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
+      if (requiresAuth && (!hideInMenu || activeMenu)) {
+        const menuOpenKeys = findMenuOpenKeys(
+          (activeMenu || newRoute.name) as string
+        );
 
-          const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
-          openKeys.value = [...keySet];
+        const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
+        openKeys.value = [...keySet];
 
-          selectedKey.value = [
-            activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
-          ];
-        }
-      }, true);
-      const setCollapse = (val: boolean) => {
-        if (appStore.device === 'desktop')
-          appStore.updateSettings({ menuCollapse: val });
-      };
+        selectedKey.value = [
+          activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
+        ];
+      }
+    }, true);
+    const setCollapse = (val: boolean) => {
+      if (appStore.device === 'desktop')
+        appStore.updateSettings({ menuCollapse: val });
+    };
 
-      const renderSubMenu = () => {
-        function travel(_route: RouteRecordRaw[], nodes = []) {
-          if (_route) {
-            _route.forEach((element) => {
-              // This is demo, modify nodes as needed
-              const icon = element?.meta?.icon
-                ? () => h(compile(`<${element?.meta?.icon}/>`))
-                : null;
-              const node =
-                element?.children && element?.children.length !== 0 ? (
-                  <a-sub-menu
-                    key={element?.name}
-                    v-slots={{
-                      icon,
-                      title: () => h(compile(t(element?.meta?.locale || ''))),
-                    }}
-                  >
-                    {travel(element?.children)}
-                  </a-sub-menu>
-                ) : (
-                  <a-menu-item
-                    key={element?.name}
-                    v-slots={{ icon }}
-                    onClick={() => goto(element)}
-                  >
-                    {t(element?.meta?.locale || '')}
-                  </a-menu-item>
-                );
-              nodes.push(node as never);
-            });
-          }
-          return nodes;
+    const renderSubMenu = () => {
+      function travel(_route: RouteRecordRaw[], nodes = []) {
+        if (_route) {
+          _route.forEach(element => {
+            // This is demo, modify nodes as needed
+            const icon = element?.meta?.icon
+              ? () => h(compile(`<${element?.meta?.icon}/>`))
+              : null;
+            const node =
+              element?.children && element?.children.length !== 0 ? (
+                <a-sub-menu
+                  key={element?.name}
+                  v-slots={{
+                    icon,
+                    title: () => h(compile(t(element?.meta?.locale || ''))),
+                  }}
+                >
+                  {travel(element?.children)}
+                </a-sub-menu>
+              ) : (
+                <a-menu-item
+                  key={element?.name}
+                  v-slots={{ icon }}
+                  onClick={() => goto(element)}
+                >
+                  {t(element?.meta?.locale || '')}
+                </a-menu-item>
+              );
+            nodes.push(node as never);
+          });
         }
-        return travel(menuTree.value);
-      };
+        return nodes;
+      }
+      return travel(menuTree.value);
+    };
 
-      return () => (
-        <a-menu
-          mode={topMenu.value ? 'horizontal' : 'vertical'}
-          v-model:collapsed={collapsed.value}
-          v-model:open-keys={openKeys.value}
-          show-collapse-button={appStore.device !== 'mobile'}
-          auto-open={false}
-          selected-keys={selectedKey.value}
-          auto-open-selected={true}
-          level-indent={34}
-          style="height: 100%;width:100%;"
-          onCollapse={setCollapse}
-        >
-          {renderSubMenu()}
-        </a-menu>
-      );
-    },
-  });
+    return () => (
+      <a-menu
+        mode={topMenu.value ? 'horizontal' : 'vertical'}
+        v-model:collapsed={collapsed.value}
+        v-model:open-keys={openKeys.value}
+        show-collapse-button={false}
+        auto-open={false}
+        selected-keys={selectedKey.value}
+        auto-open-selected={true}
+        level-indent={34}
+        style="height: 100%;width:100%;"
+      >
+        {renderSubMenu()}
+      </a-menu>
+    );
+  },
+});
 </script>
 
 <style lang="less" scoped>
-  :deep(.arco-menu-inner) {
-    .arco-menu-inline-header {
-      display: flex;
-      align-items: center;
-    }
+:deep(.arco-menu-inner) {
+  .arco-menu-inline-header {
+    display: flex;
+    align-items: center;
+  }
 
-    .arco-icon {
-      &:not(.arco-icon-down) {
-        font-size: 18px;
-      }
+  .arco-icon {
+    &:not(.arco-icon-down) {
+      font-size: 18px;
     }
   }
+}
 </style>

+ 148 - 129
src/components/navbar/index.vue

@@ -1,24 +1,22 @@
 <template>
-  <div class="navbar">
+  <div class="navbar" :style="{ width: currentWidth }">
     <div class="left-side">
-      <a-space>
-        <img
-          alt="logo"
-          src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
-        />
-        <a-typography-title
-          :style="{ margin: 0, fontSize: '18px' }"
-          :heading="5"
-        >
-          site web
-        </a-typography-title>
-        <icon-menu-fold
-          v-if="!topMenu && appStore.device === 'mobile'"
-          style="font-size: 22px; cursor: pointer"
-          @click="toggleDrawerMenu"
-        />
-      </a-space>
+      <a-button
+        type="text"
+        :style="{
+          padding: '0 7px',
+          height: '25px',
+          lineHeight: '25px',
+          color: 'var(--color-text-3)',
+        }"
+        @click="toggleCollapse"
+      >
+        <icon-menu-unfold v-if="collapsed" />
+        <icon-menu-fold v-else />
+      </a-button>
     </div>
+    <GlobalBreadcrumb v-show="!topMenu" />
+
     <div class="center-side">
       <Menu v-if="topMenu" />
     </div>
@@ -192,136 +190,157 @@
   </div>
 </template>
 
-<script lang="ts" setup>
-  import { computed, ref, inject } from 'vue';
-  import { Message } from '@arco-design/web-vue';
-  import { useDark, useFullscreen } from '@vueuse/core';
-  import { useAppStore, useUserStore } from '@/store';
-  import { LOCALE_OPTIONS } from '@/locale';
-  import useLocale from '@/hooks/locale';
-  import useUser from '@/hooks/user';
-  import Menu from '@/components/menu/index.vue';
-  import MessageBox from '../message-box/index.vue';
+<script name="NavBar" lang="ts" setup>
+import { computed, ref } from 'vue';
+import { Message } from '@arco-design/web-vue';
+import { useDark, useToggle, useFullscreen } from '@vueuse/core';
+import { useAppStore, useUserStore } from '@/store';
+import { LOCALE_OPTIONS } from '@/locale';
+import useLocale from '@/hooks/locale';
+import useUser from '@/hooks/user';
+import Menu from '@/components/menu/index.vue';
+import MessageBox from '../message-box/index.vue';
+import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
 
-  const appStore = useAppStore();
-  const userStore = useUserStore();
-  const { logout } = useUser();
-  const { changeLocale, currentLocale } = useLocale();
-  const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
-  const locales = [...LOCALE_OPTIONS];
-  const avatar = computed(() => {
-    return userStore.avatar;
-  });
-  const theme = computed(() => {
-    return appStore.theme;
+const appStore = useAppStore();
+const userStore = useUserStore();
+const { logout } = useUser();
+const { changeLocale, currentLocale } = useLocale();
+const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
+const locales = [...LOCALE_OPTIONS];
+const avatar = computed(() => {
+  return userStore.avatar;
+});
+const theme = computed(() => {
+  return appStore.theme;
+});
+const topMenu = computed(() => appStore.topMenu && appStore.menu);
+const collapsed = computed(() => {
+  return appStore.menuCollapse;
+});
+const menuWidth = computed(() => {
+  return appStore.menuWidth;
+});
+const currentWidth = computed(() => {
+  let width = menuWidth.value - 12;
+  return collapsed.value ? 'calc(100% - 38px)' : `calc(100% - ${width}px)`;
+});
+
+const isDark = useDark({
+  selector: 'body',
+  attribute: 'arco-theme',
+  valueDark: 'dark',
+  valueLight: 'light',
+  storageKey: 'arco-theme',
+  onChanged(dark: boolean) {
+    // overridden default behavior
+    appStore.toggleTheme(dark);
+  },
+});
+const toggleTheme = useToggle(isDark);
+const handleToggleTheme = () => {
+  toggleTheme();
+};
+const setVisible = () => {
+  appStore.updateSettings({ globalSettings: true });
+};
+const refBtn = ref();
+const triggerBtn = ref();
+const setPopoverVisible = () => {
+  const event = new MouseEvent('click', {
+    view: window,
+    bubbles: true,
+    cancelable: true,
   });
-  const topMenu = computed(() => appStore.topMenu && appStore.menu);
-  const isDark = useDark({
-    selector: 'body',
-    attribute: 'arco-theme',
-    valueDark: 'dark',
-    valueLight: 'light',
-    storageKey: 'arco-theme',
-    onChanged(dark: boolean) {
-      // overridden default behavior
-      appStore.toggleTheme(dark);
-    },
+  refBtn.value.dispatchEvent(event);
+};
+const handleLogout = () => {
+  logout();
+};
+const setDropDownVisible = () => {
+  const event = new MouseEvent('click', {
+    view: window,
+    bubbles: true,
+    cancelable: true,
   });
-  const handleToggleTheme = () => {
-    isDark.value = !isDark.value;
-  };
-  const setVisible = () => {
-    appStore.updateSettings({ globalSettings: true });
-  };
-  const refBtn = ref();
-  const triggerBtn = ref();
-  const setPopoverVisible = () => {
-    const event = new MouseEvent('click', {
-      view: window,
-      bubbles: true,
-      cancelable: true,
-    });
-    refBtn.value.dispatchEvent(event);
-  };
-  const handleLogout = () => {
-    logout();
-  };
-  const setDropDownVisible = () => {
-    const event = new MouseEvent('click', {
-      view: window,
-      bubbles: true,
-      cancelable: true,
-    });
-    triggerBtn.value.dispatchEvent(event);
-  };
-  const switchRoles = async () => {
-    const res = await userStore.switchRoles();
-    Message.success(res as string);
-  };
-  const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
+  triggerBtn.value.dispatchEvent(event);
+};
+const switchRoles = async () => {
+  const res = await userStore.switchRoles();
+  Message.success(res as string);
+};
+const setCollapse = (val: boolean) => {
+  if (appStore.device === 'desktop')
+    appStore.updateSettings({ menuCollapse: val });
+};
+const toggleCollapse = () => {
+  setCollapse(!collapsed.value);
+  // toggleWidth(collapsed.value);
+};
 </script>
 
 <style scoped lang="less">
-  .navbar {
-    display: flex;
-    justify-content: space-between;
-    height: 100%;
-    background-color: var(--color-bg-2);
-    border-bottom: 1px solid var(--color-border);
+.navbar {
+  display: flex;
+  justify-content: space-between;
+  justify-self: end;
+  height: 100%;
+  background-color: var(--color-bg-2);
+  border-bottom: 1px solid var(--color-border);
+}
+
+.left-side {
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+}
+
+.center-side {
+  flex: 1;
+}
+
+.right-side {
+  display: flex;
+  padding-right: 20px;
+  list-style: none;
+
+  :deep(.locale-select) {
+    border-radius: 20px;
   }
 
-  .left-side {
+  li {
     display: flex;
     align-items: center;
-    padding-left: 20px;
+    padding: 0 10px;
   }
 
-  .center-side {
-    flex: 1;
+  a {
+    color: var(--color-text-1);
+    text-decoration: none;
   }
 
-  .right-side {
-    display: flex;
-    padding-right: 20px;
-    list-style: none;
-
-    :deep(.locale-select) {
-      border-radius: 20px;
-    }
-
-    li {
-      display: flex;
-      align-items: center;
-      padding: 0 10px;
-    }
-
-    a {
-      color: var(--color-text-1);
-      text-decoration: none;
-    }
-
-    .nav-btn {
-      color: rgb(var(--gray-8));
-      font-size: 16px;
-      border-color: rgb(var(--gray-2));
-    }
+  .nav-btn {
+    color: rgb(var(--gray-8));
+    font-size: 16px;
+    border-color: rgb(var(--gray-2));
+  }
 
-    .trigger-btn,
-    .ref-btn {
-      position: absolute;
-      bottom: 14px;
-    }
+  .trigger-btn,
+  .ref-btn {
+    position: absolute;
+    bottom: 14px;
+  }
 
-    .trigger-btn {
-      margin-left: 14px;
-    }
+  .trigger-btn {
+    margin-left: 14px;
   }
+}
 </style>
 
 <style lang="less">
-  .message-popover {
-    .arco-popover-content {
-      margin-top: 0;
-    }
+.message-popover {
+  .arco-popover-content {
+    margin-top: 0;
   }
+}
 </style>

+ 1 - 1
src/config/settings.json

@@ -8,7 +8,7 @@
   "menuCollapse": false,
   "footer": true,
   "themeColor": "#165DFF",
-  "menuWidth": 220,
+  "menuWidth": 200,
   "globalSettings": false,
   "device": "desktop",
   "tabBar": false,

+ 140 - 115
src/layout/default-layout.vue

@@ -13,10 +13,25 @@
           :collapsed="collapsed"
           :collapsible="true"
           :width="menuWidth"
-          :style="{ paddingTop: navbar ? '50px' : '' }"
+          :style="{ paddingTop: navbar ? '10px' : '' }"
           :hide-trigger="true"
           @collapse="setCollapsed"
         >
+          <div class="left-side">
+            <a-space>
+              <img
+                alt="logo"
+                src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
+              />
+              <a-typography-title
+                :style="{ margin: 0, fontSize: '18px' }"
+                :heading="5"
+                v-if="!collapsed"
+              >
+                Arco Pro
+              </a-typography-title>
+            </a-space>
+          </div>
           <div class="menu-wrapper">
             <Menu />
           </div>
@@ -45,136 +60,146 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref, computed, watch, provide, onMounted } from 'vue';
-  import { useRouter, useRoute } from 'vue-router';
-  import { useAppStore, useUserStore } from '@/store';
-  import NavBar from '@/components/navbar/index.vue';
-  import Menu from '@/components/menu/index.vue';
-  import Footer from '@/components/footer/index.vue';
-  import TabBar from '@/components/tab-bar/index.vue';
-  import usePermission from '@/hooks/permission';
-  import useResponsive from '@/hooks/responsive';
-  import PageLayout from './page-layout.vue';
+import { ref, computed, watch, provide, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { useAppStore, useUserStore } from '@/store';
+import NavBar from '@/components/navbar/index.vue';
+import Menu from '@/components/menu/index.vue';
+import Footer from '@/components/footer/index.vue';
+import TabBar from '@/components/tab-bar/index.vue';
+import usePermission from '@/hooks/permission';
+import useResponsive from '@/hooks/responsive';
+import PageLayout from './page-layout.vue';
 
-  const isInit = ref(false);
-  const appStore = useAppStore();
-  const userStore = useUserStore();
-  const router = useRouter();
-  const route = useRoute();
-  const permission = usePermission();
-  useResponsive(true);
-  const navbarHeight = `50px`;
-  const navbar = computed(() => appStore.navbar);
-  const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
-  const hideMenu = computed(() => appStore.hideMenu);
-  const footer = computed(() => appStore.footer);
-  const menuWidth = computed(() => {
-    return appStore.menuCollapse ? 48 : appStore.menuWidth;
-  });
-  const collapsed = computed(() => {
-    return appStore.menuCollapse;
-  });
-  const paddingStyle = computed(() => {
-    const paddingLeft =
-      renderMenu.value && !hideMenu.value
-        ? { paddingLeft: `${menuWidth.value}px` }
-        : {};
-    const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
-    return { ...paddingLeft, ...paddingTop };
-  });
-  const setCollapsed = (val: boolean) => {
-    if (!isInit.value) return; // for page initialization menu state problem
-    appStore.updateSettings({ menuCollapse: val });
-  };
-  watch(
-    () => userStore.role,
-    (roleValue) => {
-      if (roleValue && !permission.accessRouter(route))
-        router.push({ name: 'notFound' });
-    }
-  );
-  const drawerVisible = ref(false);
-  const drawerCancel = () => {
-    drawerVisible.value = false;
-  };
-  provide('toggleDrawerMenu', () => {
-    drawerVisible.value = !drawerVisible.value;
-  });
-  onMounted(() => {
-    isInit.value = true;
-  });
+const isInit = ref(false);
+const appStore = useAppStore();
+const userStore = useUserStore();
+const router = useRouter();
+const route = useRoute();
+const permission = usePermission();
+useResponsive(true);
+const navbarHeight = `50px`;
+const navbar = computed(() => appStore.navbar);
+const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
+const hideMenu = computed(() => appStore.hideMenu);
+const footer = computed(() => appStore.footer);
+const menuWidth = computed(() => {
+  return appStore.menuCollapse ? 48 : appStore.menuWidth;
+});
+const collapsed = computed(() => {
+  return appStore.menuCollapse;
+});
+const paddingStyle = computed(() => {
+  const paddingLeft =
+    renderMenu.value && !hideMenu.value
+      ? { paddingLeft: `${menuWidth.value}px` }
+      : {};
+  const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
+  return { ...paddingLeft, ...paddingTop };
+});
+const setCollapsed = (val: boolean) => {
+  if (!isInit.value) return; // for page initialization menu state problem
+  appStore.updateSettings({ menuCollapse: val });
+};
+watch(
+  () => userStore.role,
+  roleValue => {
+    if (roleValue && !permission.accessRouter(route))
+      router.push({ name: 'notFound' });
+  }
+);
+const drawerVisible = ref(false);
+const drawerCancel = () => {
+  drawerVisible.value = false;
+};
+provide('toggleDrawerMenu', () => {
+  drawerVisible.value = !drawerVisible.value;
+});
+onMounted(() => {
+  isInit.value = true;
+});
 </script>
 
 <style scoped lang="less">
-  @nav-size-height: 50px;
-  @layout-max-width: 1100px;
+@nav-size-height: 50px;
+@layout-max-width: 1100px;
 
-  .layout {
-    width: 100%;
-    height: 100%;
-  }
+.layout {
+  width: 100%;
+  height: 100%;
+}
 
-  .layout-navbar {
-    position: fixed;
-    top: 0;
-    left: 0;
-    z-index: 100;
-    width: 100%;
-    height: @nav-size-height;
-  }
+.layout-navbar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  width: 100%;
+  height: @nav-size-height;
+  background-color: white;
+}
+
+.layout-sider {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  height: 100%;
+  transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
 
-  .layout-sider {
-    position: fixed;
+  &::after {
+    position: absolute;
     top: 0;
-    left: 0;
-    z-index: 99;
+    right: -1px;
+    display: block;
+    width: 1px;
     height: 100%;
-    transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
-
-    &::after {
-      position: absolute;
-      top: 0;
-      right: -1px;
-      display: block;
-      width: 1px;
-      height: 100%;
-      background-color: var(--color-border);
-      content: '';
-    }
+    background-color: var(--color-border);
+    content: '';
+  }
 
-    > :deep(.arco-layout-sider-children) {
-      overflow-y: hidden;
-    }
+  > :deep(.arco-layout-sider-children) {
+    overflow-y: hidden;
   }
+}
 
-  .menu-wrapper {
-    height: 100%;
-    overflow: auto;
-    overflow-x: hidden;
+.left-side {
+  display: flex;
+  align-items: center;
+  width: 180px;
+  height: 30px;
+  padding-left: 10px;
+  overflow: hidden;
+}
 
-    :deep(.arco-menu) {
-      ::-webkit-scrollbar {
-        width: 12px;
-        height: 4px;
-      }
+.menu-wrapper {
+  height: 100%;
+  overflow: auto;
+  overflow-x: hidden;
 
-      ::-webkit-scrollbar-thumb {
-        background-color: var(--color-text-4);
-        background-clip: padding-box;
-        border: 4px solid transparent;
-        border-radius: 7px;
-      }
+  :deep(.arco-menu) {
+    ::-webkit-scrollbar {
+      width: 12px;
+      height: 4px;
+    }
 
-      ::-webkit-scrollbar-thumb:hover {
-        background-color: var(--color-text-3);
-      }
+    ::-webkit-scrollbar-thumb {
+      background-color: var(--color-text-4);
+      background-clip: padding-box;
+      border: 4px solid transparent;
+      border-radius: 7px;
     }
-  }
 
-  .layout-content {
-    min-height: 100vh;
-    overflow-y: hidden;
-    background-color: var(--color-fill-2);
-    transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+    ::-webkit-scrollbar-thumb:hover {
+      background-color: var(--color-text-3);
+    }
   }
+}
+
+.layout-content {
+  min-height: 100vh;
+  overflow-y: hidden;
+  background-color: var(--color-fill-2);
+  transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
+}
 </style>