浏览代码

引导指南,侧边栏搜索优化

xiaochan 1 年之前
父节点
当前提交
d583fd9ec8

+ 6 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "clipboard": "^2.0.11",
         "d3": "^5.16.0",
         "dayjs": "^1.11.7",
+        "driver.js": "^1.3.0",
         "echarts": "^5.2.0",
         "element-plus": "^2.2.6",
         "file-saver": "^2.0.5",
@@ -4174,6 +4175,11 @@
       "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
       "dev": true
     },
+    "node_modules/driver.js": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/driver.js/-/driver.js-1.3.0.tgz",
+      "integrity": "sha512-ilUkVc5iMIYfMd8FdWy8n5Wv//gsJuRP+lo8QfWpwP9c0UGOgD7P9nVQMZwcdW84aqAZHHUHrV7GgiopAN6HUQ=="
+    },
     "node_modules/duplexer": {
       "version": "0.1.2",
       "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz",

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "clipboard": "^2.0.11",
     "d3": "^5.16.0",
     "dayjs": "^1.11.7",
+    "driver.js": "^1.3.0",
     "echarts": "^5.2.0",
     "element-plus": "^2.2.6",
     "file-saver": "^2.0.5",

+ 1 - 1
src/App.vue

@@ -16,6 +16,7 @@ import sleep from "@/utils/sleep";
 import SoctetDialog from "@/components/xiao-chan/websocket/SoctetDialog.vue";
 import router from "@/router";
 import ProgressBar from "@/components/xiao-chan/progress-bar/ProgressBar.vue";
+import {initUserInfoConfig} from "@/utils/user-info-config";
 
 const store = useStore()
 
@@ -66,7 +67,6 @@ const createChannel = () => {
 }
 
 onMounted(() => {
-
   createChannel()
 
   setCallback('refreshToken', (data) => {

+ 1 - 0
src/components/zhu-yuan-yi-sheng/yi-zhu-lu-ru/EmrControlRuleDialog.vue

@@ -6,6 +6,7 @@
       <el-table-column prop="patName" label="患者名称"/>
       <el-table-column prop="emrName" label="病历"/>
       <el-table-column prop="name" label="质控意见"/>
+      <el-table-column prop="remark" label="备注"/>
       <el-table-column prop="approverName" label="审核人"/>
       <el-table-column prop="scoringCriteriaName" label="等级"/>
       <el-table-column prop="finalControl" label="是否终末">

+ 2 - 1
src/layout/HeaderV2/Logo.vue

@@ -9,7 +9,7 @@
       </h1>
     </div>
     <!-- 收缩按钮 -->
-    <div class="collapse-icon" @click="opendStateChange">
+    <div class="collapse-icon" @click="opendStateChange" id="tutorial_collapse">
       <el-icon>
         <Expand v-if="isCollapse"/>
         <Fold v-else/>
@@ -54,6 +54,7 @@ export default defineComponent({
   .collapse-icon {
     margin-right: 5px;
     font-size: 26px;
+
     &:hover {
       background: lightgray;
       cursor: pointer;

+ 2 - 2
src/layout/HeaderV2/ToolInfoBar.vue

@@ -13,7 +13,7 @@
             <div class="function-list-item">
                 <Message/>
             </div>
-            <div class="function-list-item">
+            <div class="function-list-item" id="tutorial_full_screen">
                 <Full-screen/>
             </div>
             <div class="function-list-item">
@@ -22,7 +22,7 @@
             <div class="function-list-item">
                 <theme/>
             </div>
-            <div class="function-list-item" style="width: auto; margin-left: 6px;padding: 0 6px">
+            <div class="function-list-item" style="width: auto; margin-left: 6px;padding: 0 6px" id="tutorial_user_info">
                 <user-info @password="showPasswordLayer" @loginOut="loginOut"/>
             </div>
         </div>

+ 31 - 8
src/layout/MenuV2/MenuItemV2.vue

@@ -1,25 +1,48 @@
 <template>
   <el-sub-menu v-if="data?.children && data?.children.length > 0"
                :index="data?.completeRoute"
+               :id="data?.completeRoute"
                :router="data?.completeRoute">
     <template #title>
-      <i :class="data?.meta?.icon" v-if="data?.meta?.icon"></i>
-      <span>{{ data?.metaTitle }}</span>
+      <i :class="data?.meta?.icon"
+         v-if="data?.meta?.icon"></i>
+      <span v-html="highlightTitle(data?.metaTitle)"></span>
     </template>
-    <menu-item-v2 :data="item" v-for="item in data?.children"/>
+    <menu-item-v2 :menu-text="menuText"
+                  :data="item"
+                  v-for="item in data?.children"/>
   </el-sub-menu>
   <el-menu-item v-else :index="data?.completeRoute"
+                :id="data?.completeRoute"
                 :router="data?.completeRoute">
     <i :class="data?.meta?.icon" v-if="data?.meta?.icon"></i>
-    <span>{{ data?.metaTitle }}</span>
+    <span v-html="highlightTitle(data?.metaTitle)"></span>
   </el-menu-item>
 </template>
 
-<script setup name='MenuItemV2' lang="ts">
+<script setup lang="ts">
+import {toRefs} from "vue";
 
-const props = defineProps({
-  data: Object
-})
+declare type MenuType = {
+  metaTitle: string
+  completeRoute: string
+  meta: {
+    icon: string
+  }
+  children: MenuType[]
+}
+
+const props = defineProps<{
+  data: MenuType,
+  menuText: string
+}>()
+
+const {data, menuText} = toRefs(props)
+
+const highlightTitle = (val) => {
+  val = val.replace(menuText.value, `<span style="color:red">${menuText.value}</span>`)
+  return val
+}
 
 </script>
 

+ 120 - 21
src/layout/MenuV2/MenuV2.vue

@@ -3,17 +3,25 @@
     <div ref="logoRef">
       <Logo/>
     </div>
-    <div style="height: 30px; padding: 0 8px 8px 8px" @click="expandMenu">
-      <el-input ref="searchRef" v-model="menuText" prefix-icon="Search" clearable
+    <div style="height: 30px; padding: 0 8px 8px 8px" @click="expandMenu" id="tutorial_search_menu">
+      <el-input ref="searchRef"
+                v-model="menuText"
+                prefix-icon="Search"
+                clearable
+                @input="searchInput"
                 placeholder="菜单太难找?在这里搜索。"></el-input>
     </div>
     <el-scrollbar :style="barHeight">
-      <el-menu router
-               :collapse-transition="false"
-               :collapse="isCollapse"
-               :default-active="activeMenu"
-               :unique-opened="expandOneMenu">
-        <menu-item-v2 v-for="item in cptMenu" :data="item"/>
+      <el-menu
+          ref="menuRef"
+          router
+          :collapse-transition="false"
+          :collapse="isCollapse"
+          :default-active="isSearch ? '' : activeMenu"
+          :unique-opened="isSearch ? false : expandOneMenu">
+        <menu-item-v2 v-for="item in isSearch ? searchData : data"
+                      :data="item"
+                      :menuText="menuText"/>
       </el-menu>
     </el-scrollbar>
   </div>
@@ -22,23 +30,95 @@
 <script setup name='MenuV2' lang="ts">
 import store from '@/store'
 import MenuItemV2 from "./MenuItemV2.vue";
-import {computed, nextTick, onMounted, reactive, Ref, ref} from "vue";
+import {computed, nextTick, onMounted, reactive, Ref, ref, watch} from "vue";
 import {useRoute} from "vue-router";
 import Logo from '../HeaderV2/Logo.vue'
+import XEUtils from "xe-utils";
+import router from '@/router'
 
-const menuText: Ref<string> = ref(null)
+const menuText: Ref<string> = ref('')
+const menuRef = ref()
 
 const data = store.state.user.routes
-const flatMenu = store.state.user.flatRoutes
 
-const cptMenu = computed(() => {
-  if (menuText.value) {
-    return flatMenu.filter(item => item['metaTitle'].indexOf(menuText.value) !== -1)
+const isSearch = ref<boolean>(false)
+const searchData = ref([])
+
+function filterMenu<D>(data: D[], iterator: (item: D) => boolean) {
+  data = JSON.parse(JSON.stringify(data))
+
+  function filterDataByVisible(tempData: D) {
+    return tempData.filter(item => {
+      if (item.children) {
+        item.children = filterDataByVisible(item.children);
+      }
+      if (item.$visible) {
+        return true;
+      }
+    });
+  }
+
+  const traverse = (tempData: D) => {
+    tempData.forEach(child => {
+      child.$visible = iterator(child)
+      if (child.children) traverse(child.children);
+      if (!child.$visible && child.children?.length) {
+        let $visible = !child.children.some(child => child.$visible);
+        child.$visible = $visible === false;
+      }
+    });
+  };
+
+  traverse(data);
+  return filterDataByVisible(data);
+}
+
+
+let searchActives = new Set()
+const searchInput = XEUtils.debounce(async (val) => {
+  menuText.value = val
+  searchActives.clear()
+
+  searchData.value = filterMenu<{
+    metaTitle: string
+  }>(data, (item) => {
+    const metaTitle: string = item.metaTitle
+    return metaTitle.includes(val);
+  })
+
+  isSearch.value = !!val
+
+  await nextTick()
+  expandNodes(searchData.value)
+
+}, 500)
+
+const expandNodes = (treeData) => {
+  const traverse = tempData => {
+    XEUtils.arrayEach(tempData, (item) => {
+      if (item.children !== null && item.children.length > 0) {
+        menuLaunch(item.completeRoute)
+        traverse(item.children);
+      }
+    })
   }
-  return data.filter(() => {
-    return true
-  });
-})
+
+  traverse(treeData)
+}
+
+const menuLaunch = async path => {
+  try {
+    await nextTick()
+    const li = document.getElementById(path)
+    const div = li.children[0]
+    const icon = div.getElementsByClassName('el-icon el-sub-menu__icon-arrow')
+    if (icon[0].style.transform === 'none') {
+      div.click()
+    }
+  } catch (e) {
+  }
+
+}
 
 const expandOneMenu = computed(() => store.state.app.expandOneMenu)
 const route = useRoute()
@@ -69,10 +149,29 @@ const activeMenu = computed(() => {
   return path
 })
 
-onMounted(() => {
-  nextTick(() => {
-    barHeight.height = (floatingRef.value.clientHeight - logoRef.value.clientHeight - 36) + 'px'
+const smoothScrolling = () => {
+  const routerPath = document.getElementById(router.currentRoute.value.fullPath)
+  if (!routerPath) return
+  routerPath.scrollIntoView({
+    block: 'start',
+    inline: 'nearest',
+    behavior: 'smooth'
   })
+}
+
+
+watch(() => router.currentRoute.value, async () => {
+  menuText.value = ''
+  isSearch.value = false
+  await menuRef.value.handleResize()
+  await nextTick()
+  smoothScrolling()
+})
+
+
+onMounted(async () => {
+  await nextTick()
+  barHeight.height = (floatingRef.value.clientHeight - logoRef.value.clientHeight - 36) + 'px'
 })
 
 </script>

+ 47 - 0
src/layout/index.vue

@@ -35,6 +35,9 @@ import HeaderV2 from "@/layout/HeaderV2/HeaderV2";
 import {setVisibleSize} from "@/utils/window-size";
 import Notice from "@/layout/HeaderV2/Notice.vue";
 import {uuid} from "@/utils/getUuid";
+import {driver} from "driver.js";
+import sleep from "@/utils/sleep";
+import {getUserConfigByKey, setUserConfigByCode, userInfoConfig} from "@/utils/user-info-config";
 
 
 const store = useStore()
@@ -66,10 +69,54 @@ watch(() => store.state.app.windowSize, async () => {
   })
 }, {deep: true, immediate: true})
 
+const basicTutorial = async () => {
+  store.commit('app/isCollapseChange', false)
+  await nextTick()
+  await sleep(500)
+  const driverObj = driver({
+    showProgress: true,
+    showButtons: true,
+    allowClose: false,
+    onDestroyStarted: () => {
+      userInfoConfig.value.systemBoot = false
+      setUserConfigByCode()
+      driverObj.destroy();
+    },
+    steps: [
+      {
+        element: '#tutorial_collapse',
+        popover: {title: '收缩', description: '点击此处收缩侧边栏。'}
+      },
+      {
+        element: '#tutorial_search_menu',
+        popover: {title: '查询菜单', description: '在这个位置可以查询菜单,输入菜单的名称就可以了。'}
+      },
+      {
+        element: '#tutorial_full_screen',
+        popover: {
+          title: '全屏',
+          description: '电脑屏幕太小了可以点击此处,或按下 F11 进入全屏,再次点击或按下F11取消全屏。'
+        }
+      },
+      {
+        element: '#tutorial_user_info',
+        popover: {title: '个人信息', description: '点击此处可以看到个人信息、浏览器版本、修改密码、退出登录等。'}
+      }
+    ]
+  });
+
+  driverObj.drive()
+
+}
+
 onMounted(() => {
   if (store.getters['user/token']) {
     initSocket()
   }
+  getUserConfigByKey('systemBoot', true)
+  if (userInfoConfig.value.systemBoot) {
+    basicTutorial()
+  }
 })
 
 </script>

+ 1 - 0
src/main.js

@@ -17,6 +17,7 @@ import 'vxe-table/lib/style.css'
 import print from 'vue3-print-nb'
 import VElBtn from "@/directives/v-el-btn";
 import VTitle from "@/directives/v-title";
+import "driver.js/dist/driver.css";
 
 addRoutes()
 

+ 2 - 1
src/store/modules/user.js

@@ -1,5 +1,6 @@
 import {fetchMenusApi, getWardsApi, loginApi} from '@/api/login'
 import router from '@/router'
+import {initUserInfoConfig} from "@/utils/user-info-config";
 
 const state = () => ({
     token: '', // 登录token
@@ -81,13 +82,13 @@ const actions = {
     login({commit, dispatch}, params) {
         return new Promise((resolve, reject) => {
             loginApi(params).then((res) => {
-                console.log(res)
                 commit('tokenChange', res.token)
                 commit('sidChange', res.sid)
                 commit('infoChange', res)
                 dispatch('getWards').then((infoRes) => {
                     resolve(res)
                 })
+                initUserInfoConfig(res.code)
             })
         })
     },

+ 46 - 0
src/utils/guided-operation.ts

@@ -0,0 +1,46 @@
+import XEUtils from "xe-utils";
+
+export declare type GuidedOperationMap = {
+    title: string,
+    index: number;
+    id: string
+}
+
+export class guidedOperation {
+    private readonly list: GuidedOperationMap[];
+    private readonly total: number = 0;
+    private current: number = 0;
+    private mask: HTMLElement;
+
+    constructor(map: GuidedOperationMap[]) {
+        this.list = XEUtils.orderBy(map, 'index')
+        this.total = this.list.length
+        this.renderMask()
+        this.next()
+    }
+
+    private renderMask() {
+        this.mask = document.createElement('div')
+        this.mask.className = 'el-overlay'
+    }
+
+    private next() {
+        if (this.current >= this.total) {
+            return
+        }
+        const currentDocument = window.document.getElementById(this.list[this.current].id)
+        document.body.append(this.mask)
+
+        this.current++
+    }
+
+    private render() {
+
+    }
+
+
+    getElement() {
+        return this.list
+    }
+
+}

+ 41 - 0
src/utils/user-info-config.ts

@@ -0,0 +1,41 @@
+import requestV2 from "./request-v2";
+import {useLocalStorage} from "@vueuse/core";
+import XEUtils from "xe-utils";
+
+const url: string = "/publicApi"
+
+export declare type UserInfoConfig = {
+    systemBoot: boolean;
+}
+
+export const userInfoConfig = useLocalStorage<UserInfoConfig>('userInfoConfig', {
+    systemBoot: true
+})
+
+export function initUserInfoConfig(code: string) {
+    requestV2<any>({
+        url: `${url}/getUserConfigByCode`,
+        method: 'get',
+        params: {code},
+        showLoading: false
+    }).then(res => {
+        userInfoConfig.value = res
+    })
+}
+
+export function setUserConfigByCode() {
+    requestV2({
+        url: `${url}/setUserConfigByCode`,
+        method: 'post',
+        data: userInfoConfig.value
+    }).then(_res => {
+    })
+}
+
+
+export function getUserConfigByKey(key: string, defaultValue: any) {
+    if (XEUtils.has(userInfoConfig.value, key)) {
+        return
+    }
+    userInfoConfig.value[key] = defaultValue
+}

+ 25 - 18
src/views/settings/Test.vue

@@ -1,27 +1,34 @@
 <template>
+
 </template>
 
 <script setup lang="ts">
-import {huoQuXiangMu} from "@/api/zhu-yuan-yi-sheng/yi-zhu-lu-ru";
-import {onMounted} from "vue";
-import sleep from "@/utils/sleep";
-
-
-onMounted(async () => {
-  huoQuXiangMu('12', '73').then((res) => {
-    console.log('12 then', res)
-  }).catch((res) => {
-    console.log('12 catch', res)
-  })
+function filterMenu<D>(data: D[], iterator: (item: D) => boolean) {
+  function filterDataByVisible(tempData: D) {
+    return tempData.filter(item => {
+      if (item.children) {
+        item.children = filterDataByVisible(item.children);
+      }
+      if (item.$visible) {
+        return true;
+      }
+    });
+  }
 
-  await sleep(500)
+  const traverse = (tempData: D) => {
+    tempData.forEach(child => {
+      child.$visible = iterator(child)
+      if (child.children) traverse(child.children);
+      if (!child.$visible && child.children?.length) {
+        let $visible = !child.children.some(child => child.$visible);
+        child.$visible = $visible === false;
+      }
+    });
+  };
 
-  huoQuXiangMu('23', '73').then((res) => {
-    console.log('23 then', res)
-  }).catch((res) => {
-    console.log('23 catch', res)
-  })
-})
+  traverse(data);
+  return filterDataByVisible(data);
+}
 
 
 </script>