li.shaoyi 3 gadi atpakaļ
vecāks
revīzija
c49ac887fb

+ 1 - 1
.env.mobile

@@ -1,4 +1,4 @@
 VUE_APP_ENV = 'mobile'
 VUE_APP_TITLE = MTP-Mobile
-VUE_APP_ROOT = src/packages/mobile
+VUE_APP_ROOT = src/packages/mobile/
 VUE_APP_HOST = localhost

+ 1 - 1
.env.pc

@@ -1,4 +1,4 @@
 VUE_APP_ENV = 'pc'
 VUE_APP_TITLE = MTP-PC
-VUE_APP_ROOT = src/packages/pc
+VUE_APP_ROOT = src/packages/pc/
 VUE_APP_HOST = localhost

+ 0 - 24
src/business/common/index.ts

@@ -1,5 +1,4 @@
 import { computed } from 'vue'
-import { useRoute } from 'vue-router'
 import { queryGoodsList } from '@/services/api/goods'
 import { queryTaAccounts } from '@/services/api/account'
 import { globalState, sessionCache, localCache } from '@/store'
@@ -74,29 +73,6 @@ export function getLoginInfo<Key extends keyof Proto.LoginRsp>(key: Key) {
 }
 
 /**
- * 获取用户操作权限
- * @param filtered 过滤的数据项
- * @param reverse 是否反向过滤
- * @returns 
- */
-export function getAccountAuth(filtered: string[] = [], reverse = false) {
-    const route = useRoute();
-    const result = route.meta.auth as Ermcp.AccountMenu.Auth[];
-
-    if (filtered.length) {
-        if (reverse) {
-            // 返回除指定的权限代码
-            return result.filter((e) => !filtered.includes(e.code));
-        } else {
-            // 返回指定的权限代码
-            return result.filter((e) => filtered.includes(e.code));
-        }
-    }
-
-    return result;
-}
-
-/**
  * 通过 goodscode 获取盘面实时行情
  * @param goodscode 
  * @returns 

+ 2 - 2
src/business/quote/index.ts

@@ -17,7 +17,7 @@ export const quote = new (class {
     constructor() {
         // 接收行情推送通知
         this.quotePushNotify = eventBus.$on('quotePushNotify', (res) => {
-            const tmpList = <Proto.GoodsQuote[]>res;
+            const tmpList = res as Proto.GoodsQuote[];
 
             tmpList.forEach((item) => {
                 const quoteList = globalState.getRef('quoteList');
@@ -33,7 +33,7 @@ export const quote = new (class {
                         if (Reflect.has(quote, key)) {
                             type QuoteKey = keyof Ermcp.QuoteDay;
                             type QuoteValue = Ermcp.QuoteDay[QuoteKey];
-                            (<QuoteValue>quote[<QuoteKey>key]) = <QuoteValue>(<unknown>item[key]);
+                            (quote[<QuoteKey>key] as QuoteValue) = item[key] as unknown as QuoteValue;
                         }
                     }
 

+ 6 - 2
src/components/base/modal/index.vue

@@ -2,11 +2,15 @@
 <template>
   <app-mask :class="['app-modal', direction]" :show="show" :delay="delay" @mask="emit('mask')">
     <div :class="['app-modal__container', transitionClass]" :style="transitionStyles">
-      <slot name="header"></slot>
+      <div class="app-modal__header">
+        <slot name="header"></slot>
+      </div>
       <div class="app-modal__body">
         <slot></slot>
       </div>
-      <slot name="footer"></slot>
+      <div class="app-modal__footer">
+        <slot name="footer"></slot>
+      </div>
     </div>
   </app-mask>
 </template>

+ 86 - 10
src/hooks/component/index.ts

@@ -1,15 +1,20 @@
-import { ref } from 'vue'
+import { defineAsyncComponent, ref, shallowRef } from 'vue'
+import { useMenu } from '@/hooks/menu'
+import { DynamicComponent } from './interface'
 
 /**
- * 处理动态组件
  * @param callback 组件关闭时的回调
+ * @returns 
  */
-export function useComponent(callback?: () => void) {
+export function useComponent<T>(callback?: () => void) {
     // 组件名
-    const componentId = ref('');
+    const componentId = ref<keyof T>();
 
-    // 打开组件
-    const openComponent = (componentName: string) => {
+    /**
+     * 打开组件
+     * @param componentName 
+     */
+    const openComponent = (componentName: keyof T) => {
         if (componentName) {
             componentId.value = componentName;
             console.log('打开组件:' + componentName);
@@ -18,11 +23,14 @@ export function useComponent(callback?: () => void) {
         }
     }
 
-    // 关闭组件
-    const closeComponent = (isRefresh?: boolean) => {
-        componentId.value = '';
+    /**
+     * 关闭组件
+     * @param isCallback 
+     */
+    const closeComponent = (isCallback?: boolean) => {
+        componentId.value = undefined;
         // 是否刷新数据
-        if (isRefresh && callback) {
+        if (isCallback && callback) {
             callback();
         }
     }
@@ -32,4 +40,72 @@ export function useComponent(callback?: () => void) {
         openComponent,
         closeComponent,
     }
+}
+
+/**
+ * @param routeName 
+ * @returns 
+ */
+export function useDynamicComponent(routeName?: string) {
+    const { getChildrenMenu } = useMenu();
+    const dynamicComponents = shallowRef<DynamicComponent[]>([]);
+    const currentTabComponent = shallowRef<DynamicComponent>(); // 当前选中的组件
+
+    /**
+     * 转换动态组件
+     * @param items 
+     * @returns 
+     */
+    const parseComponent = (items: Ermcp.AccountMenu[]) => {
+        let result: DynamicComponent[] = [];
+
+        items.forEach((menu) => {
+            const { title, code, component, children, auth } = menu;
+            if (component) {
+                const componentString = component.replace(/^\/+/, ''); // 过滤字符串前面所有 '/' 字符
+                const componentPath = componentString.replace(/\.\w+$/, ''); // 过滤后缀名,为了让 import 加入 .vue ,不然会有警告提示...
+                const asyncComponent = defineAsyncComponent(() => import('/' + process.env.VUE_APP_ROOT + componentPath + '.vue'));
+
+                const dynamicComponent: DynamicComponent = {
+                    title: title,
+                    code: code,
+                    auth: auth?.map((e) => ({
+                        label: e.label,
+                        code: e.code,
+                    })) ?? [],
+                    component: asyncComponent
+                }
+
+                if (children?.length) {
+                    dynamicComponent.children = parseComponent(children);
+                }
+
+                result = [...result, dynamicComponent];
+            }
+        })
+
+        return result;
+    }
+
+    /**
+     * 切换组件
+     * @param index 
+     */
+    const componentChange = (index: number) => {
+        currentTabComponent.value = dynamicComponents.value[index];
+    }
+
+    const childrenMenus = getChildrenMenu(routeName);
+    const components = parseComponent(childrenMenus);
+
+    if (components.length) {
+        dynamicComponents.value = components;
+        componentChange(0);
+    }
+
+    return {
+        dynamicComponents,
+        currentTabComponent,
+        componentChange,
+    }
 }

+ 12 - 0
src/hooks/component/interface.ts

@@ -0,0 +1,12 @@
+import { Component } from 'vue'
+
+/**
+ * 动态组件
+ */
+export interface DynamicComponent {
+    title: string,
+    code: string,
+    component: Component,
+    auth: Ermcp.AccountMenu.Auth[],
+    children?: DynamicComponent[]
+}

+ 1 - 1
src/hooks/echarts/candlestick/index.ts

@@ -143,7 +143,7 @@ export function useCandlestickChart(goodscode: string) {
                         ma5: '-',
                         ma10: '-',
                         ma15: '-',
-                    });
+                    })
 
                     // 新增MACD数据
                     macd.source.push({

+ 79 - 0
src/hooks/menu/index.ts

@@ -0,0 +1,79 @@
+import { useRoute } from 'vue-router'
+import { sessionCache } from '@/store'
+import { AuthMenu } from './interface'
+
+export function useMenu() {
+    const route = useRoute();
+    const menus = sessionCache.getValue('menus');
+
+    /**
+     * 通过code获取对应的子菜单
+     * @param routeName 
+     * @returns 
+     */
+    const getChildrenMenu = (routeName?: string) => {
+        const filter = (items: Ermcp.AccountMenu[], name?: string) => {
+            let result: Ermcp.AccountMenu[] = [];
+            if (name) {
+                for (let i = 0; i < items.length; i++) {
+                    const { code, children } = items[i];
+                    if (code === name) return children ?? result;
+                    if (children) {
+                        result = filter(children, name)
+                        if (result.length) return result;
+                    }
+                }
+            }
+            return result;
+        }
+        return filter(menus, routeName ?? route.name?.toString());
+    }
+
+    /**
+     * 获取权限菜单
+     * @returns 
+     */
+    const getAuthMenu = () => {
+        const filter = (menu: Ermcp.AccountMenu) => ({
+            path: menu.path,
+            name: menu.code,
+            label: menu.title,
+            icon: menu.icon,
+        })
+
+        return menus.map((menu) => {
+            const result: AuthMenu = filter(menu);
+            if (menu.children) {
+                result.children = menu.children.map((child) => filter(child));
+            }
+            return result;
+        })
+    }
+
+    /**
+     * 获取当前页面操作权限
+     * @param filtered 过滤的数据项
+     * @param reverse 是否反向过滤
+     * @returns 
+     */
+    const getAuth = (filtered: string[] = [], reverse = false) => {
+        const result = route.meta.auth as Ermcp.AccountMenu.Auth[];
+        if (filtered.length) {
+            if (reverse) {
+                // 返回除指定的权限代码
+                return result.filter((e) => !filtered.includes(e.code));
+            } else {
+                // 返回指定的权限代码
+                return result.filter((e) => filtered.includes(e.code));
+            }
+        }
+        return result;
+    }
+
+    return {
+        menus,
+        getChildrenMenu,
+        getAuth,
+        getAuthMenu,
+    }
+}

+ 10 - 0
src/hooks/menu/interface.ts

@@ -0,0 +1,10 @@
+/** 
+ * 权限菜单
+ */
+export interface AuthMenu {
+    path: string;
+    name: string;
+    label: string;
+    icon: string;
+    children?: AuthMenu[];
+}

+ 24 - 1
src/mock/router.ts

@@ -46,7 +46,30 @@ const appmenu = {
                                 remark: '',
                             }
                         ],
-                        children: [],
+                        children: [
+                            {
+                                sort: 1,
+                                title: '现货明细',
+                                rulekey: 'market_quote_spot',
+                                code: 'market_quote_spot',
+                                component: 'views/market/quote/components/spot/index.vue',
+                                icon: "el-icon-s-platform",
+                                isshow: true,
+                                remark: '',
+                                auth: []
+                            },
+                            {
+                                sort: 2,
+                                title: '期货明细',
+                                rulekey: 'market_quote_futures',
+                                code: 'market_quote_futures',
+                                component: 'views/market/quote/components/futures/index.vue',
+                                icon: "el-icon-s-platform",
+                                isshow: true,
+                                remark: '',
+                                auth: []
+                            }
+                        ],
                     },
                     {
                         sort: 2,

+ 5 - 2
src/packages/mobile/components/modules/echarts-timeline/index.vue

@@ -1,3 +1,6 @@
 <template>
-    <div></div>
-</template>
+    <div>待开发</div>
+</template>
+
+<script lang="ts" setup>
+</script>

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

@@ -3,8 +3,8 @@
     <app-navbar title="详情" />
     <div class="g-flex__body">
       <app-tab theme="menu" :data-source="tabs" :data-index="1" @change="tabChange" />
-      <component :is="components['echartsTimeline']" v-if="selectedCycleType === EChartsCycleType.time" />
-      <component :is="components['echartsKline']" :cycle-type="selectedCycleType" v-else />
+      <component :is="components.echartsTimeline" v-if="selectedCycleType === EChartsCycleType.time" />
+      <component :is="components.echartsKline" :cycle-type="selectedCycleType" v-else />
     </div>
     <div class="order-detail__footer">
       <Button @click="openComponent('trade')" type="primary" round block>挂牌求购</Button>

+ 17 - 46
src/packages/pc/components/layouts/sidebar/index.vue

@@ -1,64 +1,35 @@
 <template>
-  <el-scrollbar :class="[ 'app-sidebar', modelValue && 'is-hide' ]" view-class="app-sidebar__view">
+  <el-scrollbar :class="['app-sidebar', modelValue && 'is-hide']" view-class="app-sidebar__view">
     <div class="app-sidebar__menu">
       <el-menu :default-active="$route.name" :collapse="modelValue" @select="routerTo" unique-opened>
         <app-submenu :data-source="menus"></app-submenu>
       </el-menu>
     </div>
-    <div :class="[ 'app-sidebar__mask', modelValue && 'is-hide' ]" @click="hideSidebar(true)" v-if="isMobile"></div>
+    <div :class="['app-sidebar__mask', modelValue && 'is-hide']" @click="hideSidebar(true)" v-if="isMobile"></div>
   </el-scrollbar>
 </template>
 
 <script lang="ts" setup>
-import router from "@pc/router"
-import { computed, ref } from "vue"
-import { sessionCache, globalState } from "@/store"
-import { SideMenu } from './interface'
-import AppSubmenu from "../submenu/index.vue"
+import { computed, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useMenu } from '@/hooks/menu'
+import { globalState } from '@/store'
+import AppSubmenu from '../submenu/index.vue'
 
 const emit = defineEmits(["update:modelValue"]);
+
 defineProps({
   // 折叠收起菜单栏
   modelValue: Boolean,
-});
-
-const menus = ref<SideMenu[]>([]);
-const hideSidebar = (value: boolean) => {
-  emit("update:modelValue", value);
-};
+})
 
-const getAuthMenu = (): SideMenu[] => {
-  const filterRouter = (routes: Ermcp.AccountMenu[], parentPath = ""): SideMenu[] => {
-    let result: SideMenu[] = [];
-    routes.forEach((item) => {
-      let menuPath = item.path;
-      if (parentPath) {
-        menuPath = [
-          parentPath.length > 1 ? parentPath : "",
-          "/",
-          item.path,
-        ].join("");
-      }
-      const menu: SideMenu = {
-        path: menuPath,
-        name: item.code,
-        label: item.title,
-        icon: item.icon,
-      };
-      if (item.children && item.children.length) {
-        const children = filterRouter(item.children, menuPath);
-        if (children.length) {
-          menu.children = children;
-        }
-      }
-      result = [...result, menu];
-    });
-    return result;
-  };
-  return filterRouter(sessionCache.getValue('menus'));
-};
+const { getAuthMenu } = useMenu();
+const router = useRouter();
+const menus = ref(getAuthMenu());
 
-menus.value = getAuthMenu();
+const hideSidebar = (value: boolean) => {
+  emit('update:modelValue', value);
+}
 
 // 监听设备变化
 const isMobile = computed(() => {
@@ -69,13 +40,13 @@ const isMobile = computed(() => {
     hideSidebar(false);
   }
   return flag;
-});
+})
 
 // 菜单跳转
 const routerTo = (routerName: string) => {
   isMobile.value && hideSidebar(true);
   router.replace({ name: routerName });
-};
+}
 </script>
 
 <style lang="less">

+ 1 - 1
src/packages/pc/router/dynamicRouter.ts

@@ -53,7 +53,7 @@ const addRoutes = (routes: Ermcp.AccountMenu[], parentName = ''): void => {
             }
         }
     })
-};
+}
 
 // 防止动态路由无限加载
 let routerComplete = false;

+ 12 - 0
src/packages/pc/views/market/quote/components/futures/index.vue

@@ -0,0 +1,12 @@
+<template>
+    <div>这是期货动态组件</div>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+    parentComponent: {
+        type: Object,
+        required: true
+    }
+})
+</script>

+ 12 - 0
src/packages/pc/views/market/quote/components/spot/index.vue

@@ -0,0 +1,12 @@
+<template>
+    <div>这是现货动态组件</div>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+    parentComponent: {
+        type: Object,
+        required: true
+    }
+})
+</script>

+ 11 - 1
src/packages/pc/views/market/quote/index.less

@@ -1 +1,11 @@
-.market-quote {}
+.market-quote {
+    .tab {
+        display      : flex;
+        margin-bottom: 10px;
+
+        &-item {
+            margin-right: 10px;
+            cursor      : pointer;
+        }
+    }
+}

+ 18 - 5
src/packages/pc/views/market/quote/index.vue

@@ -10,6 +10,18 @@
     </app-table>
     <el-button @click="socket.connectTrade()">连接交易服务</el-button>
     <el-button @click="socket.closeTradeServer()">断开交易服务</el-button>
+    <!-- 底部动态组件 -->
+    <template #footer>
+      <ul class="tab">
+        <li class="tab-item" v-for="(item, index) in dynamicComponents" :key="index" @click="componentChange(index)">
+          {{ item.title }}
+        </li>
+      </ul>
+      <div style="height:100px">
+        <component :is="currentTabComponent.component" :parent-component="currentTabComponent"
+          v-if="currentTabComponent"></component>
+      </div>
+    </template>
     <!-- 右键菜单 -->
     <app-contextmenu :data-source="handleButton(selectedRow)" @click="openComponent" />
     <!-- 弹窗组件 -->
@@ -20,8 +32,8 @@
 <script lang="ts" setup>
 import { defineAsyncComponent, onBeforeUnmount } from 'vue'
 import { queryGoodsList } from '@/services/api/goods'
-import { getAccountAuth } from '@/business/common'
-import { useComponent } from '@/hooks/component'
+import { useComponent, useDynamicComponent } from '@/hooks/component'
+import { useMenu } from '@/hooks/menu'
 import { useTable } from '@pc/components/base/table'
 import AppTable from '@pc/components/base/table/index.vue'
 import AppContextmenu from '@pc/components/base/contextmenu/index.vue'
@@ -36,7 +48,10 @@ const components = {
 }
 
 const { tableList, columns, selectedRow, rowKey, expands, rowClickExpand, rowContextmenu, search } = useTable<Ermcp.Goods>('id');
-const { componentId, openComponent, closeComponent } = useComponent();
+const { componentId, openComponent, closeComponent } = useComponent<typeof components>();
+const { dynamicComponents, currentTabComponent, componentChange } = useDynamicComponent();
+const { getAuth } = useMenu();
+const buttons = getAuth();
 
 columns.value = [
   {
@@ -57,8 +72,6 @@ columns.value = [
   }
 ]
 
-const buttons = getAccountAuth();
-
 // 处理权限按钮显示
 const handleButton = (item?: Ermcp.Goods) => {
   switch (item?.goodsCode) {

+ 5 - 5
src/types/ermcp/account.d.ts

@@ -4,14 +4,14 @@ declare namespace Ermcp {
     interface AccountMenu {
         sort: number; // 排序
         title: string; // 标题
-        code: string;  // 路由名称
-        path: string; // 路由路径
+        code: string;  // 路由名称(唯一)
+        path: string; // 路由地址,为空时作为组件使用
         url: string; // 管理端地址
-        component: string; // 组件名或文件地址
+        component: string; // 组件名或组件位置
         icon: string; // 菜单图标
-        isshow: boolean;
+        isshow: boolean; // 是否显示
         remark: string; // 备注
-        auth: Auth[]
+        auth: Auth[] // 控制权限
         children: AccountMenu[],
     }
 

+ 2 - 2
vue.config.js

@@ -10,8 +10,8 @@ module.exports = defineConfig({
   publicPath: './',
   pages: {
     indexPath: {
-      entry: process.env.VUE_APP_ROOT + '/main.ts',
-      template: process.env.VUE_APP_ROOT + '/index.html',
+      entry: process.env.VUE_APP_ROOT + 'main.ts',
+      template: process.env.VUE_APP_ROOT + 'index.html',
       filename: 'index.html',
       title: process.env.VUE_APP_TITLE,
       meta: {