li.shaoyi 3 лет назад
Родитель
Сommit
b60be46ea0
32 измененных файлов с 541 добавлено и 388 удалено
  1. 19 16
      src/business/common/index.ts
  2. 3 3
      src/business/customer/index.ts
  3. 0 26
      src/components/base/tab/index.less
  4. 0 53
      src/components/base/tab/index.vue
  5. 37 0
      src/components/base/tabs/index.less
  6. 59 0
      src/components/base/tabs/index.vue
  7. 67 31
      src/hooks/auth/index.ts
  8. 26 18
      src/mock/router.ts
  9. 26 0
      src/packages/mobile/assets/themes/default/variable.less
  10. 6 5
      src/packages/mobile/views/order/detail/index.vue
  11. 1 0
      src/packages/pc/App.vue
  12. 127 45
      src/packages/pc/assets/themes/default/default.less
  13. 52 0
      src/packages/pc/components/layouts/main/index.less
  14. 34 8
      src/packages/pc/components/layouts/main/index.vue
  15. 1 0
      src/packages/pc/components/layouts/page/index.vue
  16. 14 8
      src/packages/pc/components/layouts/sidebar/index.vue
  17. 16 4
      src/packages/pc/components/layouts/sidemenu/index.vue
  18. 0 93
      src/packages/pc/components/modules/auth-component/index.less
  19. 20 38
      src/packages/pc/components/modules/auth-component/index.vue
  20. 3 4
      src/packages/pc/components/modules/echarts-kline/index.vue
  21. 2 2
      src/packages/pc/router/asyncRouter.ts
  22. 7 13
      src/packages/pc/views/boot/index.vue
  23. 7 6
      src/packages/pc/views/market/chart/index.vue
  24. 0 0
      src/packages/pc/views/setting/customer/main/components/detail/index.less
  25. 0 0
      src/packages/pc/views/setting/customer/main/components/detail/index.vue
  26. 1 1
      src/packages/pc/views/setting/customer/main/components/edit/index.vue
  27. 5 7
      src/packages/pc/views/setting/customer/main/index.vue
  28. 1 1
      src/packages/pc/views/system/menu/components/edit/index.vue
  29. 2 2
      src/packages/pc/views/system/menu/index.vue
  30. 2 2
      src/packages/pc/views/system/role/components/auth/index.vue
  31. 1 0
      src/shims-vue.d.ts
  32. 2 2
      src/stores/modules/storage.ts

+ 19 - 16
src/business/common/index.ts

@@ -3,6 +3,7 @@ import { timerTask } from '@/utils/timer'
 import { queryTableDefine } from '@/services/api/common'
 import { tokenCheck } from '@/services/api/account'
 import { storageData, commonStore, futuresStore, resetStore, accountStore } from '@/stores'
+import service from '@/services'
 import eventBus from '@/services/bus'
 import socket from '@/services/socket'
 
@@ -30,25 +31,27 @@ export const business = new (class {
  * @param callback 
  */
 export async function initBaseData(callback?: () => void) {
-    if (storageData.getLoginInfo('Token')) {
-        // 连接交易服务
-        socket.connectTrade()
-        await checkToken()
+    await service.onReady(async () => {
+        if (storageData.getLoginInfo('Token')) {
+            // 连接交易服务
+            socket.connectTrade()
+            await checkToken()
 
-        const asyncTask = [
-            commonStore.getLoginData(),
-            futuresStore.getGoodsList(),
-        ]
+            const asyncTask = [
+                commonStore.getLoginData(),
+                futuresStore.getGoodsList(),
+            ]
 
-        await Promise.all(asyncTask).then(() => {
-            accountStore.getAccountList()
+            await Promise.all(asyncTask).then(() => {
+                accountStore.getAccountList()
+                callback && callback()
+            }).catch(() => {
+                return Promise.reject('初始化失败')
+            })
+        } else {
             callback && callback()
-        }).catch(() => {
-            return Promise.reject('初始化失败')
-        })
-    } else {
-        callback && callback()
-    }
+        }
+    })
 }
 
 /**

+ 3 - 3
src/business/customer/index.ts

@@ -1,4 +1,4 @@
-import { ref, shallowRef } from 'vue'
+import { ref } from 'vue'
 import { useDataTable } from '@/hooks/datatable'
 import { queryCustomerInfo, customerInfoOperate, userInfoOperate } from '@/services/api/customer'
 import { UserInfoType, CustomerQueryType } from '@/constants/enum/customer'
@@ -8,7 +8,7 @@ import { getTableColumns } from '../common'
 export function useCustomer(queryType: CustomerQueryType) {
     const { dataList, selectList, inputList, buttonList } = useDataTable<Ermcp.CustomerInfoRsp>()
     const { columns } = getTableColumns('table_pcweb_userinfo')
-    const loading = shallowRef(false)
+    const loading = ref(false)
 
     selectList.value = [
         {
@@ -54,7 +54,7 @@ export function useCustomer(queryType: CustomerQueryType) {
 
 export function useCustomerForm(selectedRow?: Ermcp.CustomerInfoRsp) {
     const loginUserId = storageData.getLoginInfo('UserID')
-    const loading = shallowRef(false)
+    const loading = ref(false)
     const formItem = ref<Proto.CustomerInfoOperateReq>({
         operatetype: 1,
         userinfotype: UserInfoType.Enterprise,

+ 0 - 26
src/components/base/tab/index.less

@@ -1,26 +0,0 @@
-.app-tab {
-    &__list {
-        display  : flex;
-        flex-wrap: wrap;
-
-        &-item {
-            display         : flex;
-            justify-content : center;
-            align-items     : center;
-            color           : #626675;
-            cursor          : pointer;
-            border-radius   : 4px;
-            background-color: #f0f0f1;
-            padding         : 8px 16px;
-
-            &:not(:first-child) {
-                margin-left: 10px;
-            }
-
-            &.active {
-                color      : #222;
-                font-weight: bold;
-            }
-        }
-    }
-}

+ 0 - 53
src/components/base/tab/index.vue

@@ -1,53 +0,0 @@
-<!-- 标签栏组件 -->
-<template>
-  <div class="app-tab">
-    <ul class="app-tab__list" v-if="dataList.length">
-      <li :class="['app-tab__list-item', selectedIndex === index && 'active']" v-for="(item, index) in dataList"
-        :key="index" @click="onChange(index)">
-        {{ propLabel ? item[propLabel] : '标签' + index }}
-      </li>
-    </ul>
-    <slot></slot>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ref, watch } from 'vue'
-
-const emit = defineEmits(['update:dataIndex', 'change']);
-
-const props = defineProps({
-  dataList: {
-    type: Array,
-    default: () => ([]),
-  },
-  // 当前标签索引
-  dataIndex: {
-    type: Number,
-    default: 0,
-  },
-  // 标签显示的属性
-  propLabel: {
-    type: String,
-    default: 'label',
-  },
-})
-
-const selectedIndex = ref(props.dataIndex);
-
-const onChange = (index: number) => {
-  if (selectedIndex.value !== index) {
-    selectedIndex.value = index;
-    emit('update:dataIndex', index);
-    emit('change', index, props.dataList[index]);
-  }
-}
-
-watch(() => props.dataIndex, (val) => {
-  selectedIndex.value = val;
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

+ 37 - 0
src/components/base/tabs/index.less

@@ -0,0 +1,37 @@
+.app-tabs {
+    display       : flex;
+    flex-direction: column;
+    overflow      : hidden;
+
+    &--top,
+    &--bottom {
+        .tabs {
+            display: flex;
+        }
+    }
+
+    &--bottom {
+        flex-flow: column-reverse;
+    }
+
+    &--left {
+        flex-direction: row;
+    }
+
+    &--right {
+        flex-direction : row;
+        flex-flow      : row-reverse;
+        justify-content: left;
+    }
+
+    &__navbar {
+        display        : flex;
+        align-items    : center;
+        justify-content: space-between;
+    }
+
+    &__container {
+        flex      : 1;
+        overflow-y: auto;
+    }
+}

+ 59 - 0
src/components/base/tabs/index.vue

@@ -0,0 +1,59 @@
+<template>
+    <div :class="['app-tabs', 'app-tabs--' + direction]">
+        <div class="app-tabs__navbar">
+            <ul class="tabs" v-if="dataList.length">
+                <li :class="['tabs-item', index === selectedIndex && 'is-active']" v-for="(item, index) in dataList"
+                    :key="index" @click="onTabChange(index)">
+                    {{ item[propLabel] ?? '标签' + index }}
+                </li>
+            </ul>
+            <slot name="navbar"></slot>
+        </div>
+        <div class="app-tabs__container">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch, PropType } from 'vue'
+
+const props = defineProps({
+    dataList: {
+        type: Array,
+        default: () => ([])
+    },
+    // 选中的标签
+    dataIndex: {
+        type: Number,
+        default: 0,
+    },
+    // 标签属性
+    propLabel: {
+        type: String,
+        default: 'label',
+    },
+    // 组件方向
+    direction: {
+        type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
+        default: 'top',
+    },
+})
+
+const emit = defineEmits(['update:dataIndex', 'change'])
+const selectedIndex = ref(props.dataIndex)
+
+const onTabChange = (index: number) => {
+    if (selectedIndex.value !== index) {
+        selectedIndex.value = index;
+        emit('update:dataIndex', index);
+        emit('change', index, props.dataList[index]);
+    }
+}
+
+watch(() => props.dataIndex, (index) => selectedIndex.value = index)
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 67 - 31
src/hooks/auth/index.ts

@@ -1,57 +1,90 @@
 import { defineAsyncComponent, Component } from 'vue'
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import { storageData } from '@/stores'
 import { AuthType } from '@/constants/enum/menu'
 import { AuthMenu } from './interface'
 
 export function useAuth(code?: string) {
-    const route = useRoute();
-    const menus = storageData.getMenus();
-    const componentMap = new Map<string, Component>();
+    const route = useRoute()
+    const router = useRouter()
+    const accountMenus = storageData.getAccountMenus()
+    const componentMap = new Map<string, Component>()
 
     /**
-     * 获取权限菜单(只需到二级菜单)
+     * 获取路由菜单(无限级)
+     * @param level 
      * @returns 
      */
-    const getAuthMenu = () => {
-        const filter = (item: Ermcp.AccountMenu, parentPath = ''): AuthMenu => ({
-            path: (parentPath ? parentPath + '/' : '') + item.url,
-            name: item.code,
-            label: item.title,
-            icon: item.icon,
-        })
+    const getMenus = (level = 0) => {
+        // 过滤层级
+        const filterLevel = (list: Ermcp.AccountMenu[], n: number): Ermcp.AccountMenu[] => {
+            if (level) {
+                return list.map((e) => ({ ...e, children: n <= 1 ? [] : filterLevel(e.children ?? [], n - 1) }))
+            }
+            return list
+        }
+        // 过滤菜单
+        const filterMenu = (list: Ermcp.AccountMenu[], parentPath = '') => {
+            const result: AuthMenu[] = []
+            list.forEach((e) => {
+                if (e.authType === AuthType.Menu) {
+                    const routePath = (parentPath ? parentPath + '/' : '') + e.url
+                    result.push({
+                        path: routePath,
+                        name: e.code,
+                        label: e.title,
+                        icon: e.icon,
+                        children: e.children ? filterMenu(e.children, routePath) : [],
+                    })
+                }
+            })
+            return result
+        }
+        return filterMenu(filterLevel(accountMenus, level))
+    }
 
-        return menus.reduce((res, cur) => {
-            if (cur.authType === AuthType.Menu) {
-                const item = filter(cur);
-                if (cur.children) {
-                    item.children = cur.children.map((e) => filter(e, cur.url));
+    /**
+     * 获取路由子菜单
+     * @param routeName 
+     * @returns 
+     */
+    const getChildrenMenus = (routeName?: string) => {
+        const filter = (list: AuthMenu[]): AuthMenu[] => {
+            if (routeName) {
+                for (const item of list) {
+                    const { name, children } = item
+                    if (name === routeName) return children ?? []
+                    if (children) {
+                        const res = filter(children)
+                        if (res.length) return res
+                    }
                 }
-                res.push(item);
             }
-            return res;
-        }, [] as AuthMenu[])
+            return []
+        }
+        return filter(getMenus())
     }
 
     /**
      * 获取权限子菜单
      * @returns 
      */
-    const getChildrenMenu = () => {
-        const filter = (items: Ermcp.AccountMenu[], name?: string): Ermcp.AccountMenu | undefined => {
-            if (name) {
-                for (let i = 0; i < items.length; i++) {
-                    const { code, children } = items[i];
-                    if (code === name) return items[i];
+    const getChildrenAuth = () => {
+        const routeName = code ?? route.name?.toString()
+        const filter = (list: Ermcp.AccountMenu[]): Ermcp.AccountMenu | undefined => {
+            if (routeName) {
+                for (const item of list) {
+                    const { code, children } = item;
+                    if (code === routeName) return item;
                     if (children) {
-                        const res = filter(children, name);
+                        const res = filter(children);
                         if (res) return res;
                     }
                 }
             }
             return undefined;
         }
-        return filter(menus, code ?? route.name?.toString());
+        return filter(accountMenus);
     }
 
     /**
@@ -59,7 +92,7 @@ export function useAuth(code?: string) {
      * @returns 
      */
     const getAuth = (authType: AuthType) => {
-        const auth = getChildrenMenu();
+        const auth = getChildrenAuth();
         if (auth && auth.children) {
             return auth.children.reduce((res, cur) => {
                 if (cur.authType === authType) {
@@ -114,9 +147,12 @@ export function useAuth(code?: string) {
     const getAuthComponent = (filtered: string[] = [], reverse = false) => authFilter(AuthType.Component, filtered, reverse);
 
     return {
-        menus,
+        route,
+        router,
+        accountMenus,
         componentMap,
-        getAuthMenu,
+        getMenus,
+        getChildrenMenus,
         getAuthButton,
         getAuthComponent,
     }

+ 26 - 18
src/mock/router.ts

@@ -88,24 +88,26 @@ const appmenu = {
                         component: 'Main',
                         children: [
                             {
-                                authType: 2,
+                                authType: 1,
                                 sort: 1,
                                 title: '未提交',
                                 code: 'setting_customer_unsubmit',
-                                component: 'views/setting/customer/common/index.vue',
+                                url: 'unsubmit',
+                                urlType: 1,
+                                component: 'views/setting/customer/main/index.vue',
                                 children: [
                                     {
                                         authType: 3,
                                         title: '新增',
                                         code: 'setting_customer_unsubmit_add',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'add',
                                     },
                                     {
                                         authType: 3,
                                         title: '修改',
                                         code: 'setting_customer_unsubmit_modify',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'modify',
                                     },
                                     {
@@ -118,23 +120,25 @@ const appmenu = {
                                         authType: 3,
                                         title: '详情',
                                         code: 'setting_customer_unsubmit_detail',
-                                        component: 'views/setting/customer/common/components/detail/index.vue',
+                                        component: 'views/setting/customer/main/components/detail/index.vue',
                                         buttonName: 'detail',
                                     }
                                 ],
                             },
                             {
-                                authType: 2,
+                                authType: 1,
                                 sort: 2,
                                 title: '待审核',
                                 code: 'setting_customer_pending',
-                                component: 'views/setting/customer/common/index.vue',
+                                url: 'pending',
+                                urlType: 1,
+                                component: 'views/setting/customer/main/index.vue',
                                 children: [
                                     {
                                         authType: 3,
                                         title: '新增',
                                         code: 'setting_customer_pending_add',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'add',
                                     },
                                     {
@@ -165,30 +169,32 @@ const appmenu = {
                                         authType: 3,
                                         title: '详情',
                                         code: 'setting_customer_pending_detail',
-                                        component: 'views/setting/customer/common/components/detail/index.vue',
+                                        component: 'views/setting/customer/main/components/detail/index.vue',
                                         buttonName: 'detail',
                                     }
                                 ],
                             },
                             {
-                                authType: 2,
+                                authType: 1,
                                 sort: 3,
                                 title: '正常',
                                 code: 'setting_customer_normal',
-                                component: 'views/setting/customer/common/index.vue',
+                                url: 'normal',
+                                urlType: 1,
+                                component: 'views/setting/customer/main/index.vue',
                                 children: [
                                     {
                                         authType: 3,
                                         title: '新增',
                                         code: 'setting_customer_normal_add',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'add',
                                     },
                                     {
                                         authType: 3,
                                         title: '修改',
                                         code: 'setting_customer_normal_modify',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'modify',
                                     },
                                     {
@@ -201,23 +207,25 @@ const appmenu = {
                                         authType: 3,
                                         title: '详情',
                                         code: 'setting_customer_normal_detail',
-                                        component: 'views/setting/customer/common/components/detail/index.vue',
+                                        component: 'views/setting/customer/main/components/detail/index.vue',
                                         buttonName: 'detail',
                                     }
                                 ],
                             },
                             {
-                                authType: 2,
+                                authType: 1,
                                 sort: 4,
                                 title: '停用',
                                 code: 'setting_customer_disabled',
-                                component: 'views/setting/customer/common/index.vue',
+                                url: 'disabled',
+                                urlType: 1,
+                                component: 'views/setting/customer/main/index.vue',
                                 children: [
                                     {
                                         authType: 3,
                                         title: '新增',
                                         code: 'setting_customer_disabled_add',
-                                        component: 'views/setting/customer/common/components/edit/index.vue',
+                                        component: 'views/setting/customer/main/components/edit/index.vue',
                                         buttonName: 'add',
                                     },
                                     {
@@ -230,7 +238,7 @@ const appmenu = {
                                         authType: 3,
                                         title: '详情',
                                         code: 'setting_customer_disabled_detail',
-                                        component: 'views/setting/customer/common/components/detail/index.vue',
+                                        component: 'views/setting/customer/main/components/detail/index.vue',
                                         buttonName: 'detail',
                                     }
                                 ],

+ 26 - 0
src/packages/mobile/assets/themes/default/variable.less

@@ -28,4 +28,30 @@
 
     /* 内容边距 */
     --content-inset: .24rem;
+
+    .app-tabs {
+        .tabs {
+            flex-wrap: wrap;
+
+            &-item {
+                display         : flex;
+                justify-content : center;
+                align-items     : center;
+                color           : #626675;
+                cursor          : pointer;
+                border-radius   : 4px;
+                background-color: #f0f0f1;
+                padding         : 8px 16px;
+
+                &:not(:first-child) {
+                    margin-left: 10px;
+                }
+
+                &.is-active {
+                    color      : #222;
+                    font-weight: bold;
+                }
+            }
+        }
+    }
 }

+ 6 - 5
src/packages/mobile/views/order/detail/index.vue

@@ -2,10 +2,11 @@
   <div class="order-detail g-flex">
     <app-navbar title="详情" />
     <div class="g-flex__body">
-      <app-tab theme="menu" :data-list="chartCycleTypeList" :data-index="1" @change="tabChange" />
-      <component :is="componentMap.get('echartsTimeline')" goodscode="cu2208"
-        v-if="selectedCycleType === ChartCycleType.Time" />
-      <component :is="componentMap.get('echartsKline')" goodscode="cu2208" :cycle-type="selectedCycleType" v-else />
+      <app-tabs :data-list="chartCycleTypeList" :data-index="1" @change="tabChange">
+        <component :is="componentMap.get('echartsTimeline')" goodscode="cu2208"
+          v-if="selectedCycleType === ChartCycleType.Time" />
+        <component :is="componentMap.get('echartsKline')" goodscode="cu2208" :cycle-type="selectedCycleType" v-else />
+      </app-tabs>
     </div>
     <div class="order-detail__footer">
       <Button @click="openComponent('trade')" type="primary" round block>挂牌求购</Button>
@@ -20,7 +21,7 @@ import { onBeforeRouteLeave } from 'vue-router'
 import { Button } from 'vant'
 import { useComponent } from '@/hooks/component'
 import { ChartCycleType, getChartCycleTypeList } from '@/constants/enum/chart'
-import AppTab from '@/components/base/tab/index.vue'
+import AppTabs from '@/components/base/tabs/index.vue'
 
 const componentMap = new Map();
 componentMap.set('trade', defineAsyncComponent(() => import('@mobile/components/modules/trade/index.vue')));

+ 1 - 0
src/packages/pc/App.vue

@@ -1,5 +1,6 @@
 <template>
   <el-config-provider :locale="zhCn">
+    <!-- 一级路由 -->
     <router-view />
   </el-config-provider>
 </template>

+ 127 - 45
src/packages/pc/assets/themes/default/default.less

@@ -31,11 +31,6 @@
     --sidebar-menu-item-hover : #3a87f7;
     --sidebar-menu-item-active: #3a87f7;
 
-    .el-select {
-        --el-select-border-color-hover      : #45535e;
-        --el-select-input-focus-border-color: var(--el-select-border-color-hover);
-    }
-
     .el-button {
         --el-button-bg-color             : #29538c;
         --el-button-border-color         : var(--el-button-bg-color);
@@ -83,70 +78,157 @@
             min-width : auto;
             background: transparent;
         }
-    }
 
-    .el-button-group {
-        .el-button {
-            min-width: auto;
+        &-group {
+            .el-button {
+                min-width: auto;
+            }
         }
     }
 
-    .el-form.vertical {
-        .el-input {
-            --el-input-bg-color          : #252d34;
-            --el-input-border-color      : var(--el-input-bg-color);
-            --el-input-hover-border-color: #45535e;
-            --el-input-focus-border-color: var(--el-input-hover-border-color);
-            --el-input-text-color        : var(--color-default);
-            --el-text-color-placeholder  : #4f5f6c;
-        }
+    .el-select {
+        --el-select-border-color-hover      : #45535e;
+        --el-select-input-focus-border-color: var(--el-select-border-color-hover);
     }
 
-    .el-form.horizontal {
-        --el-text-color-regular: var(--color-secondary);
+    .el-form {
+        &--vertical {
+            .el-input {
+                --el-input-bg-color          : #252d34;
+                --el-input-border-color      : var(--el-input-bg-color);
+                --el-input-hover-border-color: #45535e;
+                --el-input-focus-border-color: var(--el-input-hover-border-color);
+                --el-input-text-color        : var(--color-default);
+                --el-text-color-placeholder  : #4f5f6c;
+            }
+        }
 
-        display  : flex;
-        flex-wrap: wrap;
+        &--horizontal {
+            --el-text-color-regular: var(--color-secondary);
 
-        .el-form-item {
-            width: 50%;
+            display  : flex;
+            flex-wrap: wrap;
 
-            &.row {
-                width: 100%;
+            .el-form-item {
+                width: 50%;
+
+                &.row {
+                    width: 100%;
 
-                .el-form-item__content {
-                    display: flex;
+                    .el-form-item__content {
+                        display: flex;
 
-                    >* {
-                        flex: 1;
+                        >* {
+                            flex: 1;
 
-                        &:not(:first-child) {
-                            margin-left: 10px;
+                            &:not(:first-child) {
+                                margin-left: 10px;
+                            }
                         }
                     }
                 }
+
+                &__label {
+                    padding: 0 12px;
+                }
+
+                &__content {
+                    align-items  : flex-start;
+                    padding-right: 80px;
+                }
             }
 
-            &__label {
-                padding: 0 12px;
+            .el-input {
+                --el-input-bg-color          : #15202b;
+                --el-input-border-color      : #2b3f52;
+                --el-input-hover-border-color: var(--el-color-primary);
+                --el-input-text-color        : var(--color-default);
+                --el-text-color-placeholder  : #4f5f6c;
             }
 
-            &__content {
-                align-items  : flex-start;
-                padding-right: 80px;
+            .el-select {
+                width: 100%;
             }
         }
+    }
+
+    .app-tabs {
+        &--primary {
+            height: 100%;
+
+            &.app-tabs--top>.app-tabs__navbar {
+                background-color: #181e22;
+                border-bottom   : 1px solid #3a87f7;
+                padding         : 2px;
+                padding-bottom  : 0;
+
+                .tabs-item {
+                    height       : 32px;
+                    line-height  : 32px;
+                    min-width    : 120px;
+                    color        : #88a0ae;
+                    text-align   : center;
+                    cursor       : pointer;
+                    border-radius: 5px 5px 0px 0px;
+                    background   : linear-gradient(0deg, #262e35 0%, #283139 100%);
+                    padding      : 0 12px;
+
+                    &:not(:first-child) {
+                        margin-left: 3px;
+                    }
 
-        .el-input {
-            --el-input-bg-color          : #15202b;
-            --el-input-border-color      : #2b3f52;
-            --el-input-hover-border-color: var(--el-color-primary);
-            --el-input-text-color        : var(--color-default);
-            --el-text-color-placeholder  : #4f5f6c;
+                    &.is-active {
+                        color     : #fff;
+                        background: linear-gradient(0deg, #26487c 0%, #29538c 100%);
+                    }
+                }
+            }
+
+            &.app-tabs--bottom>.app-tabs__navbar {
+                padding: 2px;
+
+                .tabs-item {
+                    height     : 26px;
+                    line-height: 26px;
+                    min-width  : 120px;
+                    color      : #88a0ae;
+                    text-align : center;
+                    cursor     : pointer;
+                    background : linear-gradient(0deg, #262e35 0%, #283139 100%);
+                    padding    : 0 12px;
+
+                    &:not(:first-child) {
+                        margin-left: 3px;
+                    }
+
+                    &.is-active {
+                        color     : #fff;
+                        background: linear-gradient(0deg, #26487c 0%, #29538c 100%);
+                    }
+                }
+            }
         }
 
-        .el-select {
-            width: 100%;
+        &--info {
+            .tabs-item {
+                display         : flex;
+                justify-content : center;
+                align-items     : center;
+                color           : #626675;
+                cursor          : pointer;
+                border-radius   : 4px;
+                background-color: #f0f0f1;
+                padding         : 8px 16px;
+
+                &:not(:first-child) {
+                    margin-left: 10px;
+                }
+
+                &.is-active {
+                    color      : #222;
+                    font-weight: bold;
+                }
+            }
         }
     }
 }

+ 52 - 0
src/packages/pc/components/layouts/main/index.less

@@ -0,0 +1,52 @@
+.app-main {
+    display       : flex;
+    flex-direction: column;
+    height        : 100%;
+
+    &__navbar {
+        z-index         : 1;
+        display         : flex;
+        align-items     : center;
+        background-color: #fff;
+        border-top      : 1px solid #f2f2f2;
+        box-shadow      : 0 2px 16px 0 rgba(0, 0, 0, .1);
+        padding         : 8px 20px 0 20px;
+
+        .el-menu {
+            flex: 1;
+
+            &--horizontal {
+                border-bottom: 0;
+            }
+
+            .el-menu-item {
+                height                 : 40px;
+                //line-height: 40px; bug 会导致鼠标经过严重卡顿
+                min-width              : auto;
+                color                  : #777;
+                border-top-left-radius : 4px;
+                border-top-right-radius: 4px;
+
+                &:not(:first-child) {
+                    margin-left: 4px;
+                }
+
+                &:hover {
+                    color           : inherit;
+                    background-color: var(--tabbar-hover);
+                }
+
+                &.is-active {
+                    color           : #fff !important;
+                    background-color: var(--tabbar-active);
+                    border-bottom   : 0;
+                }
+            }
+        }
+    }
+
+    &__container {
+        flex      : 1;
+        overflow-y: auto;
+    }
+}

+ 34 - 8
src/packages/pc/components/layouts/main/index.vue

@@ -1,15 +1,41 @@
 <template>
-    <app-auth-component :selected-tab="query.tab?.toString()" @change="onTabChange" />
+    <app-tabs class="app-tabs--primary" :data-list="threeMenus" v-model:data-index="dataIndex" @change="onTabChange">
+        <template #navbar>
+            <slot></slot>
+        </template>
+        <!-- 三级路由 -->
+        <router-view v-slot="{ Component, route }">
+            <component :is="Component" :key="route.fullPath" />
+        </router-view>
+    </app-tabs>
 </template>
 
 <script lang="ts" setup>
-import { useRoute, useRouter } from 'vue-router'
-import AppAuthComponent from '@pc/components/modules/auth-component/index.vue'
+import { shallowRef } from 'vue'
+import { useAuth } from '@/hooks/auth'
+import { AuthMenu } from '@/hooks/auth/interface'
+import AppTabs from '@/components/base/tabs/index.vue'
 
-const { fullPath, query } = useRoute()
-const router = useRouter()
+const { route, router, getChildrenMenus } = useAuth()
+const parentRoute = route.matched[route.matched.length - 2] // 父级路由信息
+const threeMenus = shallowRef<AuthMenu[]>([]) // 三级菜单
+const dataIndex = shallowRef(0) // 选中的标签
 
-const onTabChange = (code: string) => {
-    router.replace({ path: fullPath, query: { tab: code } })
+if (parentRoute) {
+    const menus = getChildrenMenus(parentRoute.name?.toString())
+    const index = menus.findIndex((e) => e.name === route.name)
+    threeMenus.value = menus
+    if (index > -1) {
+        dataIndex.value = index
+    }
 }
-</script>
+
+// 切换标签
+const onTabChange = (index: number, { name }: AuthMenu) => {
+    router.push({ name })
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 1 - 0
src/packages/pc/components/layouts/page/index.vue

@@ -14,6 +14,7 @@
         <app-sidebar v-model:collapse="isCollapse" />
       </div>
       <div class="app-page__main">
+        <!-- 二级路由 -->
         <router-view v-slot="{ Component, route }">
           <component :is="Component" :key="route.fullPath" />
         </router-view>

+ 14 - 8
src/packages/pc/components/layouts/sidebar/index.vue

@@ -13,7 +13,7 @@
 
 <script lang="ts" setup>
 import { watch } from 'vue'
-import { useRouter } from 'vue-router'
+import { useAuth } from '@/hooks/auth'
 import client from '@/utils/client'
 import AppSidemenu from '../sidemenu/index.vue'
 
@@ -24,23 +24,29 @@ defineProps({
   collapse: Boolean,
 })
 
-const { state } = client;
-const router = useRouter();
+const { router, getChildrenMenus } = useAuth()
+const { state } = client
 
 const hideSidebar = () => {
-  emit('update:collapse', state.isMobile);
+  emit('update:collapse', state.isMobile)
 }
 
 // 菜单跳转
-const routerTo = (name: string) => {
+const routerTo = (routeName: string) => {
   if (state.isMobile) {
-    hideSidebar();
+    hideSidebar()
+  }
+  const submenus = getChildrenMenus(routeName)
+  // 判断是否存在子菜单
+  if (submenus.length) {
+    router.push({ name: submenus[0].name })
+  } else {
+    router.push({ name: routeName })
   }
-  router.push({ name });
 }
 
 // 监听设备变化
-watch(() => state.isMobile, () => hideSidebar());
+watch(() => state.isMobile, () => hideSidebar())
 </script>
 
 <style lang="less">

+ 16 - 4
src/packages/pc/components/layouts/sidemenu/index.vue

@@ -1,12 +1,24 @@
 <template>
-    <el-menu class="app-sidemenu" :default-active="$route.name" unique-opened>
-        <AppSubmenu :data-list="getAuthMenu()" />
+    <el-menu class="app-sidemenu" :default-active="activeMenu" unique-opened>
+        <AppSubmenu :data-list="menus" />
     </el-menu>
 </template>
 
 <script lang="ts" setup>
+import { computed } from 'vue'
 import { useAuth } from '@/hooks/auth'
-import AppSubmenu from './submenu.vue';
+import AppSubmenu from './submenu.vue'
 
-const { getAuthMenu } = useAuth();
+const { route, getMenus } = useAuth()
+const level = 2 // 菜单层级
+const menus = getMenus(level) // 如果是无限级菜单,activeMenu 应该直接返回 route.name
+
+// 高亮菜单
+const activeMenu = computed(() => {
+    const flag = menus.some((e) => route.matched.some((m) => m.name === e.name))
+    if (flag) {
+        return route.matched[level - 1].name
+    }
+    return ''
+})
 </script>

+ 0 - 93
src/packages/pc/components/modules/auth-component/index.less

@@ -1,93 +0,0 @@
-.app-auth-component {
-    display       : flex;
-    flex-direction: column;
-    height        : 100%;
-    overflow      : hidden;
-
-    &.top {
-        >.auth-navbar {
-            background-color: #181e22;
-            border-bottom   : 1px solid #3a87f7;
-            padding         : 2px;
-            padding-bottom  : 0;
-
-            .tabs {
-                display: flex;
-
-                &-item {
-                    height       : 32px;
-                    line-height  : 32px;
-                    min-width    : 120px;
-                    color        : #88a0ae;
-                    text-align   : center;
-                    cursor       : pointer;
-                    border-radius: 5px 5px 0px 0px;
-                    background   : linear-gradient(0deg, #262e35 0%, #283139 100%);
-                    padding      : 0 12px;
-
-                    &:not(:first-child) {
-                        margin-left: 3px;
-                    }
-
-                    &.is-active {
-                        color     : #fff;
-                        background: linear-gradient(0deg, #26487c 0%, #29538c 100%);
-                    }
-                }
-            }
-        }
-    }
-
-    &.bottom {
-        flex-flow: column-reverse;
-
-        >.auth-navbar {
-            padding: 2px;
-
-            .tabs {
-                display: flex;
-
-                &-item {
-                    height     : 26px;
-                    line-height: 26px;
-                    min-width  : 120px;
-                    color      : #88a0ae;
-                    text-align : center;
-                    cursor     : pointer;
-                    background : linear-gradient(0deg, #262e35 0%, #283139 100%);
-                    padding    : 0 12px;
-
-                    &:not(:first-child) {
-                        margin-left: 3px;
-                    }
-
-                    &.is-active {
-                        color     : #fff;
-                        background: linear-gradient(0deg, #26487c 0%, #29538c 100%);
-                    }
-                }
-            }
-        }
-    }
-
-    &.left {
-        flex-direction: row;
-    }
-
-    &.right {
-        flex-direction : row;
-        flex-flow      : row-reverse;
-        justify-content: left;
-    }
-
-    .auth-navbar {
-        display        : flex;
-        align-items    : center;
-        justify-content: space-between;
-    }
-
-    .auth-container {
-        flex      : 1;
-        overflow-y: auto;
-    }
-}

+ 20 - 38
src/packages/pc/components/modules/auth-component/index.vue

@@ -1,33 +1,22 @@
 <!-- 动态组件 -->
 <template>
-    <div :class="['app-auth-component', direction]">
-        <div class="auth-navbar">
-            <ul class="tabs" v-if="dataList.length">
-                <li :class="['tabs-item', item.code === componentId && 'is-active']" v-for="(item, index) in dataList"
-                    :key="index" @click="onTabChange(item.code)">
-                    {{ item.title }}
-                </li>
-            </ul>
+    <app-tabs class="app-tabs--primary" :data-list="dataList" v-model:data-index="dataIndex" prop-label="title"
+        @change="onTabChange">
+        <template #navbar>
             <slot></slot>
-        </div>
-        <div class="auth-container">
-            <component :is="componentMap.get(componentId)" :code="componentId" v-bind="options" v-if="componentId" />
-        </div>
-    </div>
+        </template>
+        <component :is="componentMap.get(componentId)" :code="componentId" v-bind="options" v-if="componentId" />
+    </app-tabs>
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, PropType, onMounted, useAttrs } from 'vue'
+import { shallowRef, PropType, onMounted, useAttrs, computed } from 'vue'
 import { useAuth } from '@/hooks/auth'
+import AppTabs from '@/components/base/tabs/index.vue'
 
 const props = defineProps({
     code: String,
     options: Object,
-    // 组件方向
-    direction: {
-        type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
-        default: 'top',
-    },
     // 过滤标签
     tabs: {
         type: Array as PropType<string[]>,
@@ -42,16 +31,16 @@ const props = defineProps({
 
 const { componentMap, getAuthComponent } = useAuth(props.code)
 const { onChange } = useAttrs()
-const componentId = shallowRef<string>()
 const dataList = shallowRef<Ermcp.AccountMenu[]>([]) // 数据列表
+const dataIndex = shallowRef(0) // 选中的标签
+
+const componentId = computed(() => dataList.value[dataIndex.value]?.code)
 
-const onTabChange = (code: string) => {
-    if (componentId.value !== code) {
-        if (onChange instanceof Function) {
-            onChange(code)
-        } else {
-            componentId.value = code
-        }
+const onTabChange = (index: number, { code }: Ermcp.AccountMenu) => {
+    if (onChange instanceof Function) {
+        onChange(code)
+    } else {
+        dataIndex.value = index
     }
 }
 
@@ -64,16 +53,9 @@ onMounted(() => {
         dataList.value = auth
     }
 
-    const tabItem = dataList.value.find((e) => e.code === props.selectedTab)
-
-    if (tabItem) {
-        componentId.value = tabItem.code
-    } else {
-        componentId.value = dataList.value[0]?.code
+    const tabIndex = dataList.value.findIndex((e) => e.code === props.selectedTab)
+    if (tabIndex > -1) {
+        dataIndex.value = tabIndex
     }
 })
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>
+</script>

+ 3 - 4
src/packages/pc/components/modules/echarts-kline/index.vue

@@ -19,8 +19,7 @@
         </ul>
         <app-echarts :option="options.candlestick" v-model:dataIndex="dataIndex" @ready="mainReady" />
       </div>
-      <template v-if="showIndicator">
-        <app-tab theme="menu" :data-list="chartSeriesTypeList" @change="tabChange" />
+      <app-tabs class="app-tabs--info" :data-list="chartSeriesTypeList" @change="tabChange" v-if="showIndicator">
         <div class="app-echats-kline__container indicator">
           <!-- MACD -->
           <section class="section" v-if="activeSeriesType === ChartSeriesType.MACD">
@@ -55,7 +54,7 @@
             <app-echarts :option="options.cci" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
           </section>
         </div>
-      </template>
+      </app-tabs>
     </template>
   </div>
 </template>
@@ -66,7 +65,7 @@ import { echarts } from '@/components/base/echarts/core'
 import { ChartCycleType, ChartSeriesType, getChartSeriesTypeList } from '@/constants/enum/chart'
 import { useCandlestickChart } from '@/hooks/echarts/candlestick'
 import AppEcharts from '@/components/base/echarts/index.vue'
-import AppTab from '@/components/base/tab/index.vue'
+import AppTabs from '@/components/base/tabs/index.vue'
 
 const props = defineProps({
   goodscode: {

+ 2 - 2
src/packages/pc/router/asyncRouter.ts

@@ -69,7 +69,7 @@ export default new (class {
      * @returns 
      */
     registerRoutes() {
-        const menus = storageData.getMenus();
+        const menus = storageData.getAccountMenus();
         return new Promise((resolve, reject) => {
             this.addNotFound();
             if (menus.length) {
@@ -78,7 +78,7 @@ export default new (class {
             } else {
                 queryAccountMenu({
                     success: (res) => {
-                        storageData.setMenus(res.data);
+                        storageData.setAccountMenus(res.data);
                         this.addRoutes(res.data);
                         resolve(true);
                     },

+ 7 - 13
src/packages/pc/views/boot/index.vue

@@ -10,7 +10,6 @@ import { ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { useRoute, useRouter } from 'vue-router'
 import { initBaseData } from '@/business/common'
-import service from '@/services'
 
 const route = useRoute(),
   router = useRouter(),
@@ -18,18 +17,13 @@ const route = useRoute(),
 
 const initService = () => {
   loading.value = true;
-  service.onReady(() => {
-    initBaseData(() => {
-      const redirect = route.query.redirect;
-      if (redirect) {
-        router.replace(redirect.toString());
-      } else {
-        router.replace('/');
-      }
-    }).catch((err) => {
-      ElMessage.error(err);
-      loading.value = false;
-    })
+  initBaseData(() => {
+    const redirect = route.query.redirect;
+    if (redirect) {
+      router.replace(redirect.toString());
+    } else {
+      router.replace('/');
+    }
   }).catch((err) => {
     ElMessage.error(err);
     loading.value = false;

+ 7 - 6
src/packages/pc/views/market/chart/index.vue

@@ -1,10 +1,11 @@
 <template>
   <app-view class="futures-goods" @ready="onReady">
-    <app-tab theme="menu" :data-list="chartCycleTypeList" :data-index="1" @change="tabChange" />
-    <component :is="components.echartsTimeline" goodscode="cu2208" style="height:500px;"
-      v-if="selectedCycleType === ChartCycleType.Time" />
-    <component :is="components.echartsKline" goodscode="cu2208" :cycle-type="selectedCycleType" style="height:500px;"
-      v-else />
+    <app-tabs class="app-tabs--info" :data-list="chartCycleTypeList" :data-index="1" @change="tabChange">
+      <component :is="components.echartsTimeline" goodscode="cu2208" style="height:500px;"
+        v-if="selectedCycleType === ChartCycleType.Time" />
+      <component :is="components.echartsKline" goodscode="cu2208" :cycle-type="selectedCycleType" style="height:500px;"
+        v-else />
+    </app-tabs>
   </app-view>
 </template>
 
@@ -13,7 +14,7 @@ import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue'
 import { timerInterceptor } from '@/utils/timer'
 import { ChartCycleType, getChartCycleTypeList } from '@/constants/enum/chart'
 import subscribe from '@/services/subscribe'
-import AppTab from '@/components/base/tab/index.vue'
+import AppTabs from '@/components/base/tabs/index.vue'
 
 const components = {
   echartsKline: defineAsyncComponent(() => import('@pc/components/modules/echarts-kline/index.vue')),

+ 0 - 0
src/packages/pc/views/setting/customer/common/components/detail/index.less → src/packages/pc/views/setting/customer/main/components/detail/index.less


+ 0 - 0
src/packages/pc/views/setting/customer/common/components/detail/index.vue → src/packages/pc/views/setting/customer/main/components/detail/index.vue


+ 1 - 1
src/packages/pc/views/setting/customer/common/components/edit/index.vue → src/packages/pc/views/setting/customer/main/components/edit/index.vue

@@ -1,7 +1,7 @@
 <!-- 新增客户资料 -->
 <template>
     <app-drawer :title="selectedRow.userid ? '修改客户资料' : '新增客户资料'" :width="880" v-model:show="show" :refresh="refresh">
-        <el-form ref="formRef" class="horizontal" label-width="130px" label-position="left" :model="formItem"
+        <el-form ref="formRef" class="el-form--horizontal" label-width="130px" label-position="left" :model="formItem"
             :rules="formRules">
             <el-form-item label="客户类型" prop="userinfotype">
                 <el-select v-model="formItem.userinfotype" :disabled="selectedRow.userid > 0">

+ 5 - 7
src/packages/pc/views/setting/customer/common/index.vue → src/packages/pc/views/setting/customer/main/index.vue

@@ -4,13 +4,12 @@
     :expand-row-keys="expandKeys" @row-click="rowClick">
     <template #header>
       <app-filter v-bind="{ selectList, inputList, buttonList }">
-        <app-auth-operation :code="code" :menus="headerButtons" />
+        <app-auth-operation :menus="headerButtons" />
       </app-filter>
     </template>
     <!-- 展开行 -->
     <template #expand="{ row }">
-      <app-auth-operation :code="code" :menus="handleExpandButtons(row)" :options="{ selectedRow: row }"
-        @closed="getCustomerList" />
+      <app-auth-operation :menus="handleExpandButtons(row)" :options="{ selectedRow: row }" @closed="getCustomerList" />
     </template>
     <!-- 客户类型 -->
     <template #userinfotype="{ value }">
@@ -20,6 +19,7 @@
 </template>
 
 <script lang="ts" setup>
+import { useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { useTable } from '@pc/components/base/table'
 import { useCustomer } from '@/business/customer'
@@ -28,12 +28,10 @@ import AppTable from '@pc/components/base/table/index.vue'
 import AppFilter from '@pc/components/base/table-filter/index.vue'
 import AppAuthOperation from '@pc/components/modules/auth-operation/index.vue'
 
-const props = defineProps({
-  code: String
-})
+const route = useRoute()
 
 const queryType = (() => {
-  switch (props.code) {
+  switch (route.name) {
     case 'setting_customer_unsubmit': {
       return CustomerQueryType.Unsubmitted
     }

+ 1 - 1
src/packages/pc/views/system/menu/components/edit/index.vue

@@ -95,7 +95,7 @@ const props = defineProps({
     }
 })
 
-const { menus } = useAuth()
+const { accountMenus } = useAuth()
 const show = ref(true)
 const form = reactive({ ...props.selectedRow })
 

+ 2 - 2
src/packages/pc/views/system/menu/index.vue

@@ -21,12 +21,12 @@ import { useAuth } from '@/hooks/auth'
 import AppAuthOperation from '@pc/components/modules/auth-operation/index.vue'
 import AppTable from '@pc/components/base/table/index.vue'
 
-const { menus } = useAuth()
+const { accountMenus } = useAuth()
 const { dataList } = useDataTable<Ermcp.AccountMenu>()
 const tableRef = ref()
 const isRowExpansion = ref(false)
 
-dataList.value = menus;
+dataList.value = accountMenus;
 
 const columns = ref([
     {

+ 2 - 2
src/packages/pc/views/system/role/components/auth/index.vue

@@ -6,7 +6,7 @@
             <el-breadcrumb-item>{{ selectedRow.roleName }}</el-breadcrumb-item>
         </el-breadcrumb>
         <el-scrollbar height="300px">
-            <el-tree :data="menus" :props="{ label: 'title' }" :expand-on-click-node="false" show-checkbox
+            <el-tree :data="accountMenus" :props="{ label: 'title' }" :expand-on-click-node="false" show-checkbox
                 check-on-click-node default-expand-all />
         </el-scrollbar>
         <template #footer>
@@ -28,7 +28,7 @@ defineProps({
     }
 })
 
-const { menus } = useAuth()
+const { accountMenus } = useAuth()
 const show = ref(true)
 </script>
 

+ 1 - 0
src/shims-vue.d.ts

@@ -5,6 +5,7 @@ declare module '*.vue' {
   export default component
 }
 
+// 参考https://webpack.html.cn/loaders/worker-loader.html
 declare module 'worker-loader!*' {
   // You need to change `Worker`, if you specified a different value for the `workerType` option
   class WebpackWorker extends Worker {

+ 2 - 2
src/stores/modules/storage.ts

@@ -77,11 +77,11 @@ export default new (class {
         this.sessionData.setValue('loginInfo', value)
     }
 
-    getMenus = () => {
+    getAccountMenus = () => {
         return this.sessionData.getValue('menus')
     }
 
-    setMenus = (value: Ermcp.AccountMenu[]) => {
+    setAccountMenus = (value: Ermcp.AccountMenu[]) => {
         return this.sessionData.setValue('menus', value)
     }