Ver Fonte

添加操作手册

lex-xin há 5 meses atrás
pai
commit
ee3fd8f252

+ 1 - 1
dev-dist/sw.js

@@ -82,7 +82,7 @@ define(['./workbox-88bf3160'], (function (workbox) { 'use strict';
     "revision": "3ca0b8505b4bec776b69afdba2768812"
   }, {
     "url": "index.html",
-    "revision": "0.3v6rh5n8l9g"
+    "revision": "0.do2tqkj2vn"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 6 - 0
package-lock.json

@@ -20,6 +20,7 @@
         "cropperjs": "^1.5.13",
         "crunker": "^2.4.0",
         "dayjs": "^1.11.7",
+        "driver.js": "^1.3.1",
         "echarts": "^5.4.2",
         "eventemitter3": "^5.0.1",
         "file-saver": "^2.0.5",
@@ -4106,6 +4107,11 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/driver.js": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/driver.js/-/driver.js-1.3.1.tgz",
+      "integrity": "sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ=="
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "dev": true,

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "cropperjs": "^1.5.13",
     "crunker": "^2.4.0",
     "dayjs": "^1.11.7",
+    "driver.js": "^1.3.1",
     "echarts": "^5.4.2",
     "eventemitter3": "^5.0.1",
     "file-saver": "^2.0.5",

+ 5 - 1
postcss.config.js

@@ -12,7 +12,10 @@ module.exports = {
       minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
       mediaQuery: true, // 媒体查询里的单位是否需要转换单位
       replace: true, //  是否直接更换属性值,而不添加备用属性
-      exclude: undefined, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
+      exclude: [
+        /src\/components\/layout\/guide-section/,
+        /node_modules\/driver.js/
+      ], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
       include: undefined, // 如果设置了include,那将只有匹配到的文件才会被转换
       landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
       landscapeUnit: 'vw', // 横屏时使用的单位
@@ -20,3 +23,4 @@ module.exports = {
     }
   }
 };
+

+ 1 - 1
public/version.json

@@ -1 +1 @@
-{"version":1736407581806}
+{"version":1736687065873}

+ 12 - 0
src/api/user.ts

@@ -95,3 +95,15 @@ export const api_musicTagTree = (params?: any) => {
     params
   });
 };
+
+/** 获取所有操作手册 */
+export const api_sysTeacherManualPage = (params: any) => {
+  return request.post('/edu-app/sysTeacherManual/page', {
+    data: params
+  });
+};
+
+/** 老师操作手册详情 */
+export const api_sysTeacherManual_detail = (id: string | number) => {
+  return request.get('/edu-app/sysTeacherManual/detail/' + id);
+};

+ 110 - 112
src/components/card-preview/index.tsx

@@ -84,109 +84,108 @@ export default defineComponent({
     }
 
     return () => (
-      <>
-        <NModal
-          maskClosable={modalClickMask}
-          style={
-            props.from === 'class' ? cardPreviewBoxDragData.styleDrag.value : {}
+      <NModal
+        maskClosable={modalClickMask}
+        style={
+          props.from === 'class' ? cardPreviewBoxDragData.styleDrag.value : {}
+        }
+        v-model:show={show.value}
+        onUpdate:show={() => {
+          emit('update:show', show.value);
+          if (!show.value) {
+            pptLoading.value = true;
           }
-          v-model:show={show.value}
-          onUpdate:show={() => {
-            emit('update:show', show.value);
-            if (!show.value) {
-              pptLoading.value = true;
+        }}
+        preset="card"
+        showIcon={false}
+        class={[
+          'modalTitle background',
+          cardPreviewBoxClass,
+          props.from === 'class' && styles.classCard,
+          styles.cardPreview,
+          item.value.type === 'PPT' && styles.maxCard,
+          props.size === 'large' && styles.cardLarge
+        ]}
+        title={item.value.type === 'MUSIC' ? '曲目预览' : item.value.title}
+        blockScroll={false}>
+        {item.value.type === 'VIDEO' && (
+          <VideoModal
+            title={
+              item.value.title +
+              (props.item.studentName ? '-' + props.item.studentName : '')
             }
-          }}
-          preset="card"
-          showIcon={false}
-          class={[
-            'modalTitle background',
-            cardPreviewBoxClass,
-            props.from === 'class' && styles.classCard,
-            styles.cardPreview,
-            item.value.type === 'PPT' && styles.maxCard,
-            props.size === 'large' && styles.cardLarge
-          ]}
-          title={item.value.type === 'MUSIC' ? '曲目预览' : item.value.title}
-          blockScroll={false}>
-          {item.value.type === 'VIDEO' && (
-            <VideoModal
-              title={
-                item.value.title +
-                (props.item.studentName ? '-' + props.item.studentName : '')
-              }
-              poster={item.value.url}
-              src={item.value.content}
-              isDownload={props.isDownload}
-              fullscreen={props.fullscreen}
-            />
-          )}
-          {item.value.type === 'MUSIC' && (
-            <MusicModal
-              class={styles.musicPreview}
-              item={item.value}
-              from={props.from}
-            />
-          )}
-          {item.value.type === 'SONG' && (
-            <SongModal
-              item={item.value}
-              isDownload={props.isDownload}
-              fullscreen={props.fullscreen}
-            />
-          )}
-          {item.value.type === 'PPT' && (
-            <NSpin show={pptLoading.value}>
-              <iframe
-                class={styles.pptBox}
-                src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
-                  item.value.content
-                )}`}
-                onLoad={() => {
-                  console.log('loading end');
-                  pptLoading.value = false;
-                }}
-                width="100%"
-                height="100%"
-                frameborder="1"></iframe>
-            </NSpin>
-          )}
-          {item.value.type === 'RHYTHM' && (
-            <RhythmModal class={styles.musicPreview} item={item.value} />
-          )}
+            poster={item.value.url}
+            src={item.value.content}
+            isDownload={props.isDownload}
+            fullscreen={props.fullscreen}
+          />
+        )}
+        {item.value.type === 'MUSIC' && (
+          <MusicModal
+            class={styles.musicPreview}
+            item={item.value}
+            from={props.from}
+          />
+        )}
+        {item.value.type === 'SONG' && (
+          <SongModal
+            item={item.value}
+            isDownload={props.isDownload}
+            fullscreen={props.fullscreen}
+          />
+        )}
+        {item.value.type === 'PPT' && (
+          <NSpin show={pptLoading.value}>
+            <iframe
+              class={styles.pptBox}
+              src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
+                item.value.content
+              )}`}
+              onLoad={() => {
+                console.log('loading end');
+                pptLoading.value = false;
+              }}
+              width="100%"
+              height="100%"
+              frameborder="1"></iframe>
+          </NSpin>
+        )}
+        {item.value.type === 'RHYTHM' && (
+          <RhythmModal class={styles.musicPreview} item={item.value} />
+        )}
 
-          {item.value.type === 'LISTEN' && (
-            <ListenModal class={styles.musicPreview} item={item.value} />
-          )}
+        {item.value.type === 'LISTEN' && (
+          <ListenModal class={styles.musicPreview} item={item.value} />
+        )}
 
-          {(item.value.type === 'INSTRUMENT' ||
-            item.value.type === 'MUSICIAN') && (
-            <div class={styles.instrumentGroup}>
-              <InstruemntDetail
-                type="modal"
-                contentType={item.value.type}
-                id={item.value.content}
-              />
-            </div>
-          )}
-          {item.value.type === 'MUSIC_WIKI' && (
-            <div class={styles.instrumentGroup}>
-              <MusicDetail
-                type="modal"
-                contentType={item.value.type}
-                id={item.value.content}
-              />
-            </div>
-          )}
+        {(item.value.type === 'INSTRUMENT' ||
+          item.value.type === 'MUSICIAN') && (
+          <div class={styles.instrumentGroup}>
+            <InstruemntDetail
+              type="modal"
+              contentType={item.value.type}
+              id={item.value.content}
+            />
+          </div>
+        )}
+        {item.value.type === 'MUSIC_WIKI' && (
+          <div class={styles.instrumentGroup}>
+            <MusicDetail
+              type="modal"
+              contentType={item.value.type}
+              id={item.value.content}
+            />
+          </div>
+        )}
 
-          {item.value.type === 'THEORY' && (
-            <div>
-              <TheoryDetail type="modal" id={item.value.content} />
-            </div>
-          )}
+        {item.value.type === 'THEORY' && (
+          <div>
+            <TheoryDetail type="modal" id={item.value.content} />
+          </div>
+        )}
 
-          {/* LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家) */}
-          {/*  VIDEO("视频"),
+        {/* LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家) */}
+        {/*  VIDEO("视频"),
     MUSIC("曲目"),
     IMG("图片"),
     SONG("音频"),
@@ -197,21 +196,20 @@ export default defineComponent({
     MUSIC_WIKI("名曲鉴赏"),
     INSTRUMENT("乐器"),
     MUSICIAN("音乐家"), */}
-          {![
-            'VIDEO',
-            'MUSIC',
-            'SONG',
-            'PPT',
-            'RHYTHM',
-            'INSTRUMENT',
-            'THEORY',
-            'MUSICIAN',
-            'MUSIC_WIKI',
-            'LISTEN'
-          ].includes(item.value.type) && <TheEmpty />}
-          {props.from === 'class' && <Dragbom></Dragbom>}
-        </NModal>
-      </>
+        {![
+          'VIDEO',
+          'MUSIC',
+          'SONG',
+          'PPT',
+          'RHYTHM',
+          'INSTRUMENT',
+          'THEORY',
+          'MUSICIAN',
+          'MUSIC_WIKI',
+          'LISTEN'
+        ].includes(item.value.type) && <TheEmpty />}
+        {props.from === 'class' && <Dragbom></Dragbom>}
+      </NModal>
     );
   }
 });

+ 0 - 3
src/components/card-preview/video-modal/index.tsx

@@ -1,13 +1,10 @@
 import {
   defineComponent,
-  nextTick,
   onMounted,
   onUnmounted,
   reactive,
   toRefs
 } from 'vue';
-// import 'plyr/dist/plyr.css';
-// import Plyr from 'plyr';
 import { ref } from 'vue';
 import TCPlayer from 'tcplayer.js';
 import 'tcplayer.js/dist/tcplayer.min.css';

+ 106 - 0
src/components/layout/guide-section/driver.ts

@@ -0,0 +1,106 @@
+import { Config, DriveStep, PopoverDOM, State, driver } from 'driver.js';
+import 'driver.js/dist/driver.css';
+import { defineComponent, nextTick, ref } from 'vue';
+
+const endGuide = (guideInfo: any) => {
+  try {
+    // setGuidance({ guideTag: "guideInfo", guideValue: JSON.stringify(guideInfo) });
+    localStorage.setItem('teacher-guideInfo', JSON.stringify(guideInfo));
+  } catch (e) {
+    console.log(e);
+  }
+};
+
+/** 练习模式 */
+export const GuideDriver = defineComponent({
+  name: 'GuideDriver',
+  // props: {
+  //   // 按钮状态
+  //   statusAll: {
+  //     type: Object as PropType<ButtonStatus>,
+  //     default: () => {},
+  //   },
+  // },
+  setup(props) {
+    let driverObj: any;
+    const driverOptions: Config = {
+      showProgress: false,
+      allowClose: true,
+      // popoverOffset: 3,
+      disableActiveInteraction: true,
+      onCloseClick: () => {
+        onDriverClose();
+      },
+      onHighlightStarted: () => {
+        // driverNextStatus.value = true;
+      },
+      onHighlighted: () => {
+        // driverNextStatus.value = false;
+      },
+      steps: [
+        {
+          element: '.home-1',
+          popover: {
+            title: '操作指南',
+            description: '点击这里可以查看界面的功能说明和操作指南',
+            popoverClass: 'popoverClass popoverClassF1',
+            align: 'center',
+            side: 'bottom',
+            nextBtnText: `我知道了`,
+            showButtons: ['next', 'close'],
+            onPopoverRender: (
+              popover: PopoverDOM,
+              options: { config: Config; state: State }
+            ) => {
+              options.config.stageRadius = 5;
+              options.config.stagePadding = 0;
+            },
+            onCloseClick: () => {
+              onDriverClose()
+            },
+            onNextClick: () => {
+              onDriverClose();
+            },
+          }
+        }
+      ]
+    };
+    const guideInfo = ref({} as any);
+    const getAllGuidance = async () => {
+      try {
+        const res = localStorage.getItem('teacher-guideInfo');
+        if (res) {
+          guideInfo.value = JSON.parse(res) || null;
+        } else {
+          guideInfo.value = {};
+        }
+
+        if (!(guideInfo.value && guideInfo.value.homeGuide)) {
+          nextTick(() => {
+            driverObj = driver(driverOptions);
+            driverObj.drive(0);
+          });
+        }
+      } catch (e) {
+        console.log(e);
+      }
+    };
+
+    getAllGuidance();
+
+    // 结束关闭弹窗
+    const onDriverClose = () => {
+      if (!guideInfo.value) {
+        guideInfo.value = { homeGuide: true };
+      } else {
+        guideInfo.value.homeGuide = true;
+      }
+      endGuide(guideInfo.value);
+      driverObj.destroy();
+      // document.querySelector('.driver-popover-close-btn-custom')?.remove();
+      // document.removeEventListener('click', handleClickOutside, true);
+      // state.hasDriverPop = false;
+    };
+    // return () => <div>1212</div>
+  }
+});

+ 574 - 0
src/components/layout/guide-section/guide-drag.ts

@@ -0,0 +1,574 @@
+// 弹窗拖动
+import { ref, Ref, watch, nextTick, computed, reactive } from 'vue';
+
+type posType = {
+  top: number;
+  left: number;
+};
+
+type directionType =
+  | 'TOP'
+  | 'RIGHT'
+  | 'BOTTOM'
+  | 'LEFT'
+  | 'TOP_RIGHT'
+  | 'BOTTOM_RIGHT'
+  | 'BOTTOM_LEFT'
+  | 'TOP_LEFT';
+
+type baseSizeType = {
+  /**
+   * 允许拖动方向 上/上右/右/下右/下/下左/左/上左
+   */
+  resizeDirection: boolean[];
+  layoutTopHeight: number;
+  windowHeight: number;
+  windowWidth: number;
+  // 窗口模式的尺寸
+  winWidth: number;
+  winHeight: number;
+  minWidth: number;
+  minHeight: number;
+  maxHeight: number;
+  maxWidth: number;
+  borderRadius: number;
+  transformX: number;
+  transformY: number;
+  width: number;
+  height: number;
+};
+
+/***
+ * 初始化默认弹窗位置
+ */
+const initPos = {
+  right: 14,
+  top: 60
+};
+
+const getSizeToUnit = (num: number, unit = 'px') => {
+  return num > 0 ? num + unit : num + '';
+};
+
+/**
+ * @params classList  可拖动地方的class值,也为唯一值
+ * @params boxClass  容器class值必须为唯一值,这个class和useid拼接 作为缓存主键
+ * @params dragShow  弹窗是否显示
+ * @params userId    当前用户id
+ */
+export default function useDrag(
+  classList: string[],
+  boxClass: string,
+  dragShow: Ref<boolean>
+) {
+  const windowInfo = reactive({
+    // 小窗口 侧边大窗口
+    currentType: 'SMALL' as 'SMALL' | 'LARGE',
+    // 弹窗,还是还原
+    windowType: 'SMALL' as 'SMALL' | 'LARGE',
+    // showScreen: false, // 是否全屏显示
+    showType: 'MENU' as 'MENU' | 'CONTENT' // 当前显示哪一部分 - 如果是全屏显示则无效
+  });
+  const pos = ref<posType>({
+    top: -1, // -1 为初始值 代表没有缓存 默认居中
+    left: -1
+  });
+
+  watch(
+    () => windowInfo.windowType,
+    () => {
+      if (windowInfo.windowType === 'LARGE') {
+        baseSize.resizeDirection = [
+          true,
+          true,
+          true,
+          true,
+          true,
+          true,
+          true,
+          true
+        ];
+      } else if (windowInfo.windowType === 'SMALL') {
+        baseSize.resizeDirection = [
+          true,
+          false,
+          false,
+          false,
+          true,
+          false,
+          false,
+          false
+        ];
+      }
+      const dragDirectionPoints = document.querySelectorAll(
+        `.${boxClass} .dragDirectionPoint`
+      );
+      dragDirectionPoints.forEach((element: any, index) => {
+        if (baseSize.resizeDirection[index]) {
+          element.style.pointerEvents = 'all';
+        } else {
+          element.style.pointerEvents = 'none';
+        }
+      });
+    }
+  );
+
+  const styleDrag = computed(() => {
+    return {
+      ...dragStyles,
+      width: getSizeToUnit(baseSize.width),
+      height: getSizeToUnit(baseSize.height),
+      transform: `translate(${baseSize.transformX}px, ${baseSize.transformY}px)`
+    };
+  });
+
+  const baseSize = reactive<baseSizeType>({
+    resizeDirection: [true, false, false, false, true, false, false, false],
+    layoutTopHeight: 0,
+    windowHeight: window.innerHeight,
+    windowWidth: window.innerWidth,
+    // 窗口模式的尺寸
+    winWidth: 1010,
+    winHeight: 650,
+    minWidth: 400,
+    minHeight: 640,
+    maxHeight: window.innerHeight,
+    maxWidth: window.innerWidth > 1024 ? 1024 : window.innerWidth,
+    borderRadius: 12,
+    transformX: window.innerWidth - 400 - initPos.right,
+    transformY: (window.innerHeight - 640) / 2,
+    height: 640,
+    width: 400
+  });
+  const dragStyles = reactive({
+    // width: getSizeToUnit(baseSize.minWidth),
+    // height: getSizeToUnit(baseSize.minHeight),
+    maxHeight: getSizeToUnit(baseSize.maxHeight),
+    minWidth: getSizeToUnit(baseSize.minWidth),
+    minHeight: getSizeToUnit(baseSize.minHeight),
+    borderRadius: '0px'
+  });
+  nextTick(() => {
+    const layoutTopHeight =
+      document.querySelector('.layoutTop')?.clientHeight || 0;
+    baseSize.layoutTopHeight = Math.ceil(layoutTopHeight);
+    baseSize.windowHeight = window.innerHeight - layoutTopHeight;
+    baseSize.maxHeight = window.innerHeight - layoutTopHeight;
+
+    const translateY = (baseSize.windowHeight - baseSize.minHeight) / 2;
+    baseSize.transformX =
+      baseSize.windowWidth - baseSize.minWidth - initPos.right;
+    baseSize.transformY = translateY;
+    dragStyles.maxHeight = getSizeToUnit(baseSize.maxHeight);
+  });
+  // watch(dragShow, () => {
+  //   if (dragShow.value) {
+  //     // 初始化pos值
+  //     // initPos();
+  //     // window.addEventListener('resize', refreshPos);
+  //     nextTick(() => {
+  //       const boxClassDom = document.querySelector(
+  //         `.${boxClass}`
+  //       ) as HTMLElement;
+  //       if (!boxClassDom) {
+  //         return;
+  //       }
+  //       console.log(boxClassDom, 'boxClassDom');
+  //       classList.map((className: string) => {
+  //         const classDom = document.querySelector(
+  //           `.${className}`
+  //         ) as HTMLElement;
+  //         if (classDom) {
+  //           classDom.style.cursor = 'move';
+  //           drag(classDom, boxClassDom, baseSize);
+  //         }
+  //       });
+  //     });
+  //   } else {
+  //     // window.removeEventListener('resize', refreshPos);
+  //   }
+  // });
+
+  nextTick(() => {
+    const boxClassDom = document.querySelector(`.${boxClass}`) as HTMLElement;
+    if (!boxClassDom) {
+      return;
+    }
+    addReSizeDom(boxClassDom, baseSize.resizeDirection);
+
+    classList.map((className: string) => {
+      const classDom = document.querySelector(`.${className}`) as HTMLElement;
+      if (classDom) {
+        classDom.style.cursor = 'move';
+        drag(classDom, boxClassDom, baseSize);
+      }
+    });
+  });
+
+  /**
+   * 添加功能放大缩小操作DOM
+   * @param parentElement {添加拖动父级元素}
+   * @param direction {允许拖动的位置 上/上右/右/下右/下/下左/左/上左}
+   */
+  function addReSizeDom(
+    parentElement: HTMLElement,
+    direction: boolean[] = [
+      true,
+      false,
+      false,
+      false,
+      true,
+      false,
+      false,
+      false
+    ]
+  ) {
+    function addResizeDirection(params: {
+      width: string;
+      height: string;
+      direction: directionType;
+      top?: string | any;
+      right?: string | any;
+      bottom?: string | any;
+      left?: string | any;
+      cursor: string;
+      zIndex?: string;
+      pointerEvents: string;
+    }) {
+      const dom = document.createElement('div');
+      dom.className = 'dragDirectionPoint';
+      dom.style.position = 'absolute';
+      dom.style.userSelect = 'none';
+      dom.style.width = params.width;
+      dom.style.height = params.height;
+      dom.style.left = params.left;
+      dom.style.top = params.top;
+      dom.style.bottom = params.bottom;
+      dom.style.right = params.right;
+      dom.style.zIndex = params.zIndex || '0';
+      dom.style.cursor = params.cursor;
+      dom.style.pointerEvents = params.pointerEvents;
+      parentElement.appendChild(dom);
+      drag(dom, parentElement, baseSize, 'RESIZE', params.direction);
+    }
+    // 上
+    addResizeDirection({
+      width: '100%',
+      height: '10px',
+      left: '0',
+      top: '-5px',
+      cursor: 'row-resize',
+      direction: 'TOP',
+      pointerEvents: direction[0] ? 'all' : 'none'
+    });
+
+    // 上右
+    addResizeDirection({
+      width: '20px',
+      height: '20px',
+      right: '-10px',
+      top: '-10px',
+      zIndex: '1',
+      cursor: 'ne-resize',
+      direction: 'TOP_RIGHT',
+      pointerEvents: direction[1] ? 'all' : 'none'
+    });
+
+    // 右
+    addResizeDirection({
+      width: '10px',
+      height: '100%',
+      top: '0',
+      right: '-5px',
+      cursor: 'col-resize',
+      direction: 'RIGHT',
+      pointerEvents: direction[2] ? 'all' : 'none'
+    });
+
+    // 下右
+    addResizeDirection({
+      width: '20px',
+      height: '20px',
+      right: '-10px',
+      bottom: '-10px',
+      cursor: 'se-resize',
+      zIndex: '1',
+      direction: 'BOTTOM_RIGHT',
+      pointerEvents: direction[3] ? 'all' : 'none'
+    });
+
+    // 下
+    addResizeDirection({
+      width: '100%',
+      height: '10px',
+      left: '0',
+      bottom: '-5px',
+      cursor: 'row-resize',
+      direction: 'BOTTOM',
+      pointerEvents: direction[4] ? 'all' : 'none'
+    });
+
+    // 下左
+    addResizeDirection({
+      width: '20px',
+      height: '20px',
+      left: '-10px',
+      bottom: '-10px',
+      cursor: 'sw-resize',
+      zIndex: '1',
+      direction: 'BOTTOM_LEFT',
+      pointerEvents: direction[5] ? 'all' : 'none'
+    });
+
+    // 左
+    addResizeDirection({
+      width: '10px',
+      height: '100%',
+      top: '0',
+      left: '-5px',
+      cursor: 'col-resize',
+      direction: 'LEFT',
+      pointerEvents: direction[6] ? 'all' : 'none'
+    });
+
+    // 上左
+    addResizeDirection({
+      width: '20px',
+      height: '20px',
+      left: '-10px',
+      top: '-10px',
+      cursor: 'nw-resize',
+      zIndex: '1',
+      direction: 'TOP_LEFT',
+      pointerEvents: direction[7] ? 'all' : 'none'
+    });
+  }
+
+  function refreshPos() {
+    if (pos.value.left === -1 && pos.value.top === -1) {
+      return;
+    }
+    const boxClassDom = document.querySelector(`.${boxClass}`) as HTMLElement;
+    if (!boxClassDom) return;
+    const parentElementRect = boxClassDom.getBoundingClientRect();
+    const clientWidth = document.documentElement.clientWidth;
+    const clientHeight = document.documentElement.clientHeight;
+    const { top, left } = pos.value;
+    const maxLeft = clientWidth - parentElementRect.width;
+    const maxTop = clientHeight - parentElementRect.height;
+    let moveX = left;
+    let moveY = top;
+    const minLeft = 0;
+    const minTop = 0;
+    moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
+    moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
+    pos.value = {
+      top: moveY,
+      left: moveX
+    };
+  }
+
+  /** 切换窗口 */
+  function onScreen() {
+    if (windowInfo.windowType === 'SMALL') {
+      windowInfo.windowType = 'LARGE';
+      baseSize.transformX = (baseSize.windowWidth - baseSize.winWidth) / 2;
+      baseSize.transformY =
+        (baseSize.windowHeight - baseSize.winHeight) / 2 -
+        baseSize.layoutTopHeight / 2;
+      baseSize.width = baseSize.winWidth;
+      baseSize.height = baseSize.winHeight;
+      dragStyles.borderRadius = getSizeToUnit(baseSize.borderRadius);
+    } else if (windowInfo.windowType === 'LARGE') {
+      windowInfo.windowType = 'SMALL';
+      const translateY = (baseSize.windowHeight - baseSize.minHeight) / 2;
+      baseSize.transformX =
+        baseSize.windowWidth - baseSize.minWidth - initPos.right;
+      baseSize.transformY =
+        translateY > initPos.top
+          ? translateY + (translateY - initPos.top)
+          : translateY;
+      baseSize.width = baseSize.minWidth;
+      baseSize.height = baseSize.minHeight;
+      dragStyles.borderRadius = '0';
+    }
+  }
+
+  /** 格式化尺寸 */
+  function onResize() {
+    if (windowInfo.currentType === 'SMALL') {
+      windowInfo.currentType = 'LARGE';
+      windowInfo.windowType = 'SMALL';
+      baseSize.transformX = baseSize.windowWidth - baseSize.minWidth;
+      baseSize.transformY = 0;
+      baseSize.width = baseSize.minWidth;
+      baseSize.height = baseSize.maxHeight;
+      dragStyles.borderRadius = '0';
+    } else if (windowInfo.currentType === 'LARGE') {
+      windowInfo.currentType = 'SMALL';
+      windowInfo.windowType = 'SMALL';
+      baseSize.transformX =
+        baseSize.windowWidth - baseSize.minWidth - initPos.right;
+      baseSize.transformY =
+        baseSize.windowHeight - baseSize.minHeight - initPos.top;
+      baseSize.width = baseSize.minWidth;
+      baseSize.height = baseSize.minHeight;
+      dragStyles.borderRadius = '0';
+    }
+  }
+
+  return {
+    pos,
+    baseSize,
+    windowInfo,
+    styleDrag,
+    onScreen,
+    onResize
+  };
+}
+
+// 拖动
+function drag(
+  el: HTMLElement,
+  parentElement: HTMLElement,
+  baseSize: baseSizeType,
+  type = 'MOVE' as 'MOVE' | 'RESIZE',
+  direction?: directionType
+) {
+  function onDown(e: MouseEvent | TouchEvent) {
+    const isTouchEv = isTouchEvent(e);
+    const event = isTouchEv ? e.touches[0] : e;
+    const parentElementRect = parentElement.getBoundingClientRect();
+    const downX = event.clientX;
+    const downY = event.clientY;
+    const clientWidth = document.documentElement.clientWidth;
+    const clientHeight = document.documentElement.clientHeight;
+    const maxLeft = clientWidth - parentElementRect.width;
+    const maxTop =
+      clientHeight - parentElementRect.height - baseSize.layoutTopHeight;
+
+    const maxResizeLeft =
+      clientWidth - baseSize.minWidth - (clientWidth - parentElementRect.right);
+    const maxResizeTop =
+      clientHeight - baseSize.minHeight - baseSize.layoutTopHeight;
+    const minLeft = 0;
+    const minTop = 0;
+    const baseHeight = JSON.parse(JSON.stringify(baseSize.height));
+    const baseWidth = JSON.parse(JSON.stringify(baseSize.width));
+
+    function onTop(moveY: number) {
+      const maxSuffix =
+        parentElementRect.bottom -
+        baseSize.minHeight -
+        baseSize.layoutTopHeight;
+      moveY = moveY > maxSuffix ? maxSuffix : moveY;
+      const suffix = baseSize.transformY - moveY;
+      if (suffix > 0 || baseSize.height > baseSize.minHeight) {
+        baseSize.transformY = moveY;
+        baseSize.height = baseSize.height + suffix;
+      }
+    }
+    function onRight(moveX: number) {
+      moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
+
+      const suffix = Math.ceil(
+        baseWidth + moveX - (baseSize.width + baseSize.transformX)
+      );
+      if (baseSize.maxWidth > baseSize.width) {
+        baseSize.width =
+          baseSize.width + suffix > baseSize.maxWidth
+            ? baseSize.maxWidth
+            : baseSize.width + suffix;
+      } else {
+        baseSize.width =
+          baseSize.width + suffix <= baseSize.minWidth
+            ? baseSize.minWidth
+            : baseSize.width + suffix;
+      }
+    }
+    function onBottom(moveY: number) {
+      if (baseSize.maxHeight > baseSize.height) {
+        const suffix = Math.ceil(
+          baseHeight + moveY - (baseSize.height + baseSize.transformY)
+        );
+        baseSize.height = baseSize.height + suffix;
+      }
+    }
+    function onLeft(moveX: number) {
+      moveX =
+        moveX < minLeft
+          ? minLeft
+          : moveX > maxResizeLeft
+          ? maxResizeLeft
+          : moveX;
+      const suffix = baseSize.transformX - moveX;
+      if (suffix > 0 || baseSize.width > baseSize.minWidth) {
+        if (baseSize.width + suffix <= baseSize.maxWidth) {
+          baseSize.transformX = moveX;
+          baseSize.width = baseSize.width + suffix;
+        }
+      }
+    }
+    function onMove(e: MouseEvent | TouchEvent) {
+      const event = isTouchEvent(e) ? e.touches[0] : e;
+      if (type === 'MOVE') {
+        let moveX = parentElementRect.left + (event.clientX - downX);
+        let moveY =
+          parentElementRect.top -
+          baseSize.layoutTopHeight +
+          (event.clientY - downY);
+        moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
+        moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
+        // 移动
+        baseSize.transformY = moveY;
+        baseSize.transformX = moveX;
+      } else if (type === 'RESIZE') {
+        let moveY =
+          parentElementRect.top -
+          baseSize.layoutTopHeight +
+          (event.clientY - downY);
+        moveY =
+          moveY < minTop ? minTop : moveY > maxResizeTop ? maxResizeTop : moveY;
+
+        const moveX = parentElementRect.left + (event.clientX - downX);
+
+        // 拖动
+        if (direction === 'TOP') {
+          onTop(moveY);
+        } else if (direction === 'RIGHT') {
+          onRight(moveX);
+        } else if (direction === 'BOTTOM') {
+          onBottom(moveY);
+        } else if (direction === 'LEFT') {
+          onLeft(moveX);
+        } else if (direction === 'TOP_RIGHT') {
+          onTop(moveY);
+          onRight(moveX);
+        } else if (direction === 'BOTTOM_RIGHT') {
+          onBottom(moveY);
+          onRight(moveX);
+        } else if (direction === 'BOTTOM_LEFT') {
+          onBottom(moveY);
+          onLeft(moveX);
+        } else if (direction === 'TOP_LEFT') {
+          onTop(moveY);
+          onLeft(moveX);
+        }
+      }
+    }
+    function onUp() {
+      document.removeEventListener(
+        isTouchEv ? 'touchmove' : 'mousemove',
+        onMove
+      );
+      document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
+    }
+    document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove);
+    document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
+  }
+  el.addEventListener('mousedown', onDown);
+  el.addEventListener('touchstart', onDown);
+}
+function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
+  return window.TouchEvent && e instanceof window.TouchEvent;
+}

BIN
src/components/layout/guide-section/images/arrow-down.png


BIN
src/components/layout/guide-section/images/arrow-line-left.png


BIN
src/components/layout/guide-section/images/arrow.png


BIN
src/components/layout/guide-section/images/close.png


BIN
src/components/layout/guide-section/images/resize.png


BIN
src/components/layout/guide-section/images/screen-small.png


BIN
src/components/layout/guide-section/images/screen.png


BIN
src/components/layout/guide-section/images/video-center.png


+ 246 - 0
src/components/layout/guide-section/index.module.less

@@ -0,0 +1,246 @@
+.drag-wrapper-draggable {
+  transform: translate(0Px, 0Px);
+  position: fixed;
+  right: 0Px;
+  top: var(--layoutTopHeight, 64Px);
+  width: 100%;
+  z-index: 99;
+
+  &.draggleClose {
+    display: none;
+    opacity: 0;
+  }
+}
+
+.guideSection {
+  position: absolute;
+  user-select: auto;
+  touch-action: none;
+  // width: 400Px;
+  // height: 621Px;
+  display: inline-block;
+  top: 0Px;
+  left: 0Px;
+  cursor: auto;
+  pointer-events: all;
+  z-index: 101;
+  // transform: translate(354Px, 132Px);
+  // max-width: 1024Px;
+  box-sizing: border-box;
+  background: #ffffff;
+  box-shadow: -2Px 5Px 12Px 0Px rgba(0, 0, 0, 0.12);
+  border: 1Px solid #e8e8e8;
+  // border-radius: 16Px;
+  overflow: hidden;
+}
+
+.guideCenter {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.guideTitle {
+  // border-radius: 16Px 16Px 0 0;
+  cursor: move;
+  background: #f7f8fa;
+  padding: 16Px 24Px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  pointer-events: auto;
+  .name {
+    font-weight: 600;
+    font-size: 16Px;
+    color: #131415;
+    line-height: 22Px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+
+    .back {
+      display: inline-block;
+      width: 14Px;
+      height: 14Px;
+      background: url('./images/arrow-line-left.png') no-repeat center;
+      background-size: contain;
+      margin-right: 8Px;
+    }
+  }
+
+  .operation {
+    display: flex;
+    align-items: center;
+
+    i {
+      width: 22Px;
+      height: 22Px;
+      margin-left: 12Px;
+      cursor: pointer;
+    }
+    .screen {
+      background: url('./images/screen.png') no-repeat center;
+      background-size: contain;
+    }
+    .screenSmall {
+      background: url('./images/screen-small.png') no-repeat center;
+      background-size: contain;
+    }
+    .resize {
+      background: url('./images/resize.png') no-repeat center;
+      background-size: contain;
+    }
+    .close {
+      background: url('./images/close.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+}
+
+.container {
+  display: flex;
+  box-sizing: border-box;
+  height: calc(100% - 54Px);
+
+  &.windowContainer {
+    .leftGuide {
+      width: 300Px;
+      padding-right: 16Px;
+      position: sticky !important;
+      top: 0;
+      position: relative;
+      > div {
+        overflow-y: auto;
+        &::-webkit-scrollbar {
+          display: none;
+        }
+        height: 100%;
+        &::before {
+          position: absolute;
+          right: 0;
+          top: 0;
+          bottom: 0;
+          content: ' ';
+          display: inline-block;
+          border-left: 1Px solid #e8e8e8;
+        }
+      }
+    }
+    .rightGuide {
+      width: auto;
+      flex: 1;
+      padding-left: 16Px;
+    }
+  }
+
+  .leftGuide,
+  .rightGuide {
+    padding-left: 24Px;
+    padding-right: 24Px;
+    height: 100%;
+    overflow-y: auto;
+  }
+}
+.searchContainer {
+  background-color: #fff;
+  position: sticky;
+  top: 0;
+  left: 0;
+  padding: 16Px 0;
+  z-index: 1;
+  // :global {
+  //   .TheSearch {
+  //     border-radius: 6Px !important;
+  //   }
+  // }
+}
+
+.leftGuide {
+  width: 100%;
+  :global {
+    .n-collapse {
+      --n-title-font-weight: 600 !important;
+      --n-title-font-size: 16Px !important;
+      --n-title-text-color: #131415 !important;
+    }
+    .n-collapse-item {
+      border-top: none !important;
+      margin-top: 0;
+      .n-collapse-item__header {
+        padding: 9Px 0;
+      }
+    }
+    // .n-collapse .n-collapse-item:not(:first-child)
+    .n-collapse-item__header-main {
+      line-height: 22Px;
+    }
+    .n-collapse-item__content-inner {
+      padding-top: 0 !important;
+    }
+  }
+  .arrow {
+    width: 16Px;
+    height: 16Px;
+  }
+
+  .childItem {
+    padding: 10Px 20Px 10Px 40Px;
+    font-size: 14Px;
+    color: #333333;
+    line-height: 20Px;
+    // margin-top: 8Px;
+    cursor: pointer;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    &:hover {
+      color: #097bec;
+      background: #e8f4ff;
+      border-radius: 6Px;
+    }
+  }
+
+  .emptyDiv {
+    height: 80%;
+  }
+}
+
+.rightGuide {
+  word-break: break-all;
+  width: 100%;
+  padding: 12Px 0;
+  :global {
+    .n-spin-container {
+      height: 100%;
+    }
+
+    .n-space {
+      font-size: 0;
+    }
+
+    video,
+    img {
+      max-width: 100%;
+      cursor: pointer;
+    }
+
+    .videoSection {
+      position: relative;
+      display: inline-block;
+
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 50Px;
+        height: 50Px;
+        background: url('./images/video-center.png') no-repeat center;
+        background-size: contain;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%);
+        pointer-events: none;
+      }
+    }
+  }
+}

+ 344 - 0
src/components/layout/guide-section/index.tsx

@@ -0,0 +1,344 @@
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  reactive,
+  ref,
+  Teleport,
+  toRef
+} from 'vue';
+import styles from './index.module.less';
+import {
+  NCollapse,
+  NCollapseItem,
+  NImage,
+  NImageGroup,
+  NModal,
+  NSpace,
+  NSpin
+} from 'naive-ui';
+import arrow from './images/arrow.png';
+import TheSearch from '../../TheSearch';
+import {
+  api_sysTeacherManual_detail,
+  api_sysTeacherManualPage
+} from '/src/api/user';
+import TheEmpty from '../../TheEmpty';
+import VideoModal from '../../card-preview/video-modal';
+import { modalClickMask } from '/src/state';
+import useDrag from './guide-drag';
+import { onBeforeRouteUpdate, useRoute } from 'vue-router';
+
+/** 功能引导 */
+export default defineComponent({
+  name: 'guide-section',
+  setup(props, { expose }) {
+    const route = useRoute();
+    // 操作记录
+    const opInfo = reactive({
+      showGuide: false, // 是否显示
+      routePath: route.path === '/' ? '/Home' : route.path,
+      collapseId: [], // 默认展开哪个
+      dataList: [] as any[], // 手册列表
+      manualDetail: {} as any, // 详情
+      detailLoading: false // 加载详情
+    });
+    const previewShow = ref(false);
+    const previewUrl = ref('');
+    const previewImgList = ref<string[]>([]);
+
+    const guideBoxClass = 'guideBoxClass_drag';
+    const { styleDrag, windowInfo, onScreen, onResize } = useDrag(
+      [`${guideBoxClass} .guideTitle`],
+      guideBoxClass,
+      toRef(opInfo, 'showGuide')
+    );
+
+    // useDrag(
+    //   [`${guideBoxClass} .guideTitle`],
+    //   guideBoxClass,
+    //   toRef(opInfo, 'showGuide')
+    // );
+
+    const titleName = computed(() => {
+      let name = '操作手册';
+      if (
+        windowInfo.showType === 'CONTENT' &&
+        windowInfo.windowType === 'SMALL'
+      ) {
+        name = '返回';
+      }
+      return name;
+    });
+
+    /** 操作窗口状态 */
+    const onToggle = () => {
+      opInfo.showGuide = !opInfo.showGuide;
+    };
+
+    const onClickItem = async (item: any) => {
+      opInfo.detailLoading = true;
+      try {
+        windowInfo.showType = 'CONTENT';
+        await getTeacherManualDetail(item.id);
+      } catch {
+        //
+      }
+      opInfo.detailLoading = false;
+    };
+
+    // 点击菜单
+    const onClickMenu = () => {
+      if (windowInfo.windowType === 'LARGE') return;
+      windowInfo.showType = 'MENU';
+    };
+    /** 获取操作手册 */
+    const getTeacherManual = async (keyword = '') => {
+      try {
+        const { data } = await api_sysTeacherManualPage({
+          keyword,
+          permission: opInfo.routePath
+        });
+        opInfo.dataList = data || [];
+        opInfo.manualDetail = {};
+        const id: any = opInfo.dataList[0]?.id;
+        if (id) {
+          opInfo.collapseId = id;
+
+          const firstChildId = opInfo.dataList[0]?.children[0]?.id;
+          getTeacherManualDetail(firstChildId);
+        }
+      } catch {
+        //
+      }
+    };
+
+    /** 获取详情 */
+    const getTeacherManualDetail = async (id: string) => {
+      try {
+        if (id === opInfo.manualDetail?.id) return;
+
+        const { data } = await api_sysTeacherManual_detail(id);
+
+        let opFlow = data.opFlow || '';
+
+        opFlow = opFlow.replace(
+          /<img/gi,
+          '<img class="manualImg" onClick="onLookImg(this)"'
+        );
+        opFlow = opFlow.replace(
+          /<video/gi,
+          '<div class="videoSection"><video class="manualVideo" onClick="onLookVideo(this)" '
+        );
+        opFlow = opFlow.replace(/<\/video>/gi, '</video></div>');
+        opFlow = opFlow.replace(/controls/gi, '');
+        data.opFlow = opFlow;
+
+        opInfo.manualDetail = data || {};
+
+        nextTick(() => {
+          //   const manualImg = document.querySelector('.manualImg')
+          const domList = document.querySelectorAll('.manualImg');
+          const imgList: any[] = [];
+          domList.forEach((item: any) => {
+            imgList.push(item.src);
+          });
+          previewImgList.value = imgList;
+        });
+      } catch {
+        //
+      }
+    };
+
+    getTeacherManual();
+
+    onBeforeRouteUpdate((route: any) => {
+      if (route.path !== opInfo.routePath) {
+        opInfo.showGuide = false;
+        opInfo.routePath = route.path === '/' ? '/Home' : route.path;
+        windowInfo.showType = 'MENU';
+        getTeacherManual();
+      }
+    });
+
+    expose({
+      onToggle
+    });
+
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    window.onLookImg = (target: any) => {
+      const index = previewImgList.value.findIndex(
+        (src: string) => target.src === src
+      );
+
+      const nImage = document.querySelectorAll('.rightGuide .n-image')[index];
+      const img: any = nImage.querySelector('img');
+      img.click();
+    };
+
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    window.onLookVideo = (target: any) => {
+      const sourceElement = target.querySelector('source');
+      const videoSrc = sourceElement ? sourceElement.src : null;
+      previewUrl.value = videoSrc;
+      previewShow.value = true;
+    };
+
+    return () => (
+      <Teleport to={'body'}>
+        <div
+          class={[
+            styles['drag-wrapper-draggable'],
+            !opInfo.showGuide ? styles.draggleClose : ''
+          ]}>
+          <div
+            class={[styles.guideSection, guideBoxClass]}
+            style={{ ...styleDrag.value }}>
+            <div class={styles.guideCenter}>
+              <div class={[styles.guideTitle, 'guideTitle']}>
+                <div class={styles.name} onClick={onClickMenu}>
+                  {windowInfo.showType === 'CONTENT' &&
+                    windowInfo.windowType === 'SMALL' && (
+                      <i class={styles.back}></i>
+                    )}
+                  {titleName.value}
+                </div>
+                <div class={styles.operation}>
+                  <i
+                    class={[
+                      styles.screen,
+                      windowInfo.windowType === 'LARGE'
+                        ? styles.screenSmall
+                        : ''
+                    ]}
+                    onClick={onScreen}></i>
+                  <i class={styles.resize} onClick={onResize}></i>
+                  <i class={styles.close} onClick={onToggle}></i>
+                </div>
+              </div>
+              <div
+                class={[
+                  styles.container,
+                  windowInfo.windowType === 'LARGE'
+                    ? styles.windowContainer
+                    : ''
+                ]}>
+                <div
+                  class={styles.leftGuide}
+                  style={{
+                    display:
+                      windowInfo.showType === 'MENU' ||
+                      windowInfo.windowType === 'LARGE'
+                        ? 'block'
+                        : 'none'
+                  }}>
+                  <div
+                    style={{
+                      height: opInfo.dataList.length <= 0 ? '100%' : 'auto'
+                    }}>
+                    <div class={styles.searchContainer}>
+                      <TheSearch
+                        round={false}
+                        onSearch={async (val: any) => {
+                          await getTeacherManual(val);
+                          if (windowInfo.windowType === 'LARGE') {
+                            const id = opInfo.dataList[0]?.children[0]?.id;
+                            id && getTeacherManualDetail(id);
+                          }
+                        }}
+                      />
+                    </div>
+
+                    <NCollapse
+                      v-model:expandedNames={opInfo.collapseId}
+                      accordion>
+                      {{
+                        arrow: () => <img class={styles.arrow} src={arrow} />,
+                        default: () =>
+                          opInfo.dataList.map((item: any) => (
+                            <NCollapseItem title={item.name} name={item.id}>
+                              <div class={styles.childList}>
+                                {item.children &&
+                                  item.children.map((child: any) => (
+                                    <div
+                                      class={styles.childItem}
+                                      onClick={() => onClickItem(child)}>
+                                      {child.name}
+                                    </div>
+                                  ))}
+                              </div>
+                            </NCollapseItem>
+                          ))
+                      }}
+                    </NCollapse>
+
+                    {opInfo.dataList.length <= 0 && (
+                      <div class={styles.emptyDiv}>
+                        <TheEmpty />
+                      </div>
+                    )}
+                  </div>
+                </div>
+                <div
+                  class={[styles.rightGuide, 'rightGuide']}
+                  style={{
+                    display:
+                      windowInfo.showType === 'CONTENT' ||
+                      windowInfo.windowType === 'LARGE'
+                        ? 'block'
+                        : 'none'
+                  }}>
+                  {previewImgList.value.length > 0 && (
+                    <NImageGroup>
+                      <NSpace>
+                        {previewImgList.value.map((src: string) => (
+                          <NImage
+                            renderToolbar={({ nodes }: any) => {
+                              return [
+                                nodes.prev,
+                                nodes.next,
+                                nodes.rotateCounterclockwise,
+                                nodes.rotateClockwise,
+                                nodes.resizeToOriginalSize,
+                                nodes.zoomOut,
+                                nodes.close
+                              ];
+                            }}
+                            width="0"
+                            src={src}></NImage>
+                        ))}
+                      </NSpace>
+                    </NImageGroup>
+                  )}
+                  <NSpin show={opInfo.detailLoading}>
+                    <div
+                      v-html={opInfo.manualDetail.opFlow}
+                      class-="html-to-dom"></div>
+                  </NSpin>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <NModal
+            maskClosable={modalClickMask}
+            v-model:show={previewShow.value}
+            preset="card"
+            showIcon={false}
+            class={['modalTitle background cardPreviewGuide']}
+            title="预览"
+            blockScroll={false}>
+            <VideoModal
+              title="预览"
+              src={previewUrl.value}
+              isDownload
+              fullscreen
+            />
+          </NModal>
+        </div>
+      </Teleport>
+    );
+  }
+});

+ 1 - 1
src/components/layout/index.module.less

@@ -113,7 +113,7 @@
   flex: 1;
 
   .layoutTop {
-    height: 64px;
+    height: var(--layoutTopHeight, 64px);
     background-color: #fff;
     line-height: 64px;
     padding: 0 50px 0 32px;

+ 0 - 1
src/components/layout/layoutSilder.tsx

@@ -138,7 +138,6 @@ export default defineComponent({
         now.isActive = false;
         if (now.id == item.id) {
           now.isActive = true;
-          // console.log(item.path);
           item.path && router.push(item.path);
         }
       });

+ 35 - 33
src/components/layout/layoutTop.tsx

@@ -1,12 +1,4 @@
-import {
-  defineComponent,
-  ref,
-  onMounted,
-  nextTick,
-  onUnmounted,
-  reactive,
-  computed
-} from 'vue';
+import { defineComponent, ref, onMounted, nextTick, onUnmounted } from 'vue';
 import styles from './index.module.less';
 import { NImage, NBadge, NPopover, NIcon, NModal, NTooltip } from 'naive-ui';
 import styles2 from './modals/suggestion-option.module.less';
@@ -24,7 +16,7 @@ import inBack from './images/inBack.png';
 import submitBtn from './images/submitBtn.png';
 import sealing from './images/sealing.png';
 import boxBg from './images/boxBg.png';
-import { useRouter, useRoute } from 'vue-router';
+import { useRouter } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import opinionIcon from './images/opinionIcon.png';
 // import inviteIcon from './images/invite_student_icon.png';
@@ -38,10 +30,12 @@ import ClassModal from '/src/views/home/modals/class-modal';
 import { suggestMessageUnread } from '/src/api/user';
 import { eventGlobal } from '/src/utils';
 import { usePrepareStore } from '/src/store/modules/prepareLessons';
-import { schoolDetail } from '/src/views/studentList/api';
+// import { schoolDetail } from '/src/views/studentList/api';
 // import AddStudentModel from '/src/views/studentList/modals/addStudentModel';
 import { modalClickMask } from '/src/state';
 import { teacherJobType } from '/src/utils/contants';
+import GuideSection from './guide-section';
+import { GuideDriver } from './guide-section/driver';
 
 export default defineComponent({
   name: 'layoutTop',
@@ -58,10 +52,11 @@ export default defineComponent({
     const userInfoStatus = ref(false);
     const classRecordStatus = ref(false);
     const prepareStore = usePrepareStore();
-    const state = reactive({
-      addStudentVisible: false,
-      activeRow: {} as any
-    });
+    const guideSectionRef = ref();
+    // const state = reactive({
+    //   addStudentVisible: false,
+    //   activeRow: {} as any
+    // });
 
     const oncheckEditStatus = (callBack: any) => {
       showHeadFlag.value = false;
@@ -98,24 +93,23 @@ export default defineComponent({
       if (suggestionOptionRef.value) {
         suggestionOptionRef.value.onReset();
       }
-      console.log(suggestionOptionRef.value, 'suggestionOptionRef');
     };
 
     // 邀请学生二维码
-    const showInviteQrcode = async () => {
-      try {
-        const { schoolInfos } = users.getUserInfo;
-        const schoolId = schoolInfos.length > 0 ? schoolInfos[0].id : null;
-        if (schoolId) {
-          const { data } = await schoolDetail({ id: schoolId });
-          state.activeRow = data;
+    // const showInviteQrcode = async () => {
+    //   try {
+    //     const { schoolInfos } = users.getUserInfo;
+    //     const schoolId = schoolInfos.length > 0 ? schoolInfos[0].id : null;
+    //     if (schoolId) {
+    //       const { data } = await schoolDetail({ id: schoolId });
+    //       state.activeRow = data;
 
-          state.addStudentVisible = true;
-        }
-      } catch {
-        //
-      }
-    };
+    //       state.addStudentVisible = true;
+    //     }
+    //   } catch {
+    //     //
+    //   }
+    // };
 
     const suggestionStatus = ref(false);
     const getSuggestMessageUnread = async () => {
@@ -177,7 +171,7 @@ export default defineComponent({
       window.removeEventListener('message', onImMessage);
     });
 
-    const imglist = [inFront, inBack, submitBtn, sealing, boxBg];
+    const imgList = [inFront, inBack, submitBtn, sealing, boxBg];
     const loadImg = (imgList: any) => {
       for (let i = 0; i < imgList.length; i++) {
         const img = new Image();
@@ -191,10 +185,10 @@ export default defineComponent({
         };
       }
     };
-    loadImg(imglist);
+    loadImg(imgList);
 
     // 功能引导
-    const route = useRoute();
+    // const route = useRoute();
     // const helpNoteList = reactive({
     //   baseListTab: ''
     // });
@@ -219,7 +213,7 @@ export default defineComponent({
     //   }
     // });
     return () => (
-      <div class={styles.layoutTop}>
+      <div class={[styles.layoutTop, 'layoutTop']}>
         <div class={styles.layoutLeft}>
           <NImage
             src={schoolIcon}
@@ -236,6 +230,7 @@ export default defineComponent({
                 <div
                   class={[
                     styles.optons,
+                    'home-1'
                     // !helpNoteStatus.value && styles.booxToolDisabled
                   ]}
                   id="home-1"
@@ -245,6 +240,8 @@ export default defineComponent({
                     // document.querySelector('#WrapcoreViewWrap')?.scrollTo(0, 0);
                     // console.log(route.name, 'guideInfo');
                     // eventGlobal.emit('teacher-guideInfo', route.name);
+
+                    guideSectionRef.value?.onToggle();
                   }}>
                   <NImage src={gnydIcon} previewDisabled></NImage>
                 </div>
@@ -485,6 +482,11 @@ export default defineComponent({
               (showSuggestionViseble.value = false)
             }></SuggestionOption>
         </NModal>
+
+        {/* 操作手册 */}
+        <GuideSection ref={guideSectionRef} />
+
+        <GuideDriver />
       </div>
     );
   }

+ 180 - 58
src/styles/index.less

@@ -15,24 +15,24 @@
   -moz-osx-font-smoothing: grayscale;
   color: #333;
   min-height: 100vh;
-
 }
 
 body {
   user-select: none;
   background-color: #f1f5ff;
   overflow: hidden;
+  --layoutTopHeight: 64px;
 }
 
-body>.n-drawer-container-relative {
+body > .n-drawer-container-relative {
   position: relative !important;
 }
 
 // 搜索框前面放大镜样式重置
 .icon-search-input {
   display: inline-block;
-  width: max(16px, 14Px);
-  height: max(16px, 14Px);
+  width: max(16px, 14px);
+  height: max(16px, 14px);
   background: url('../common/images/icon_search.png') no-repeat center;
   background-size: contain;
 }
@@ -50,11 +50,11 @@ body>.n-drawer-container-relative {
 // }
 
 .n-base-select-menu .n-base-select-option {
-  font-size: max(15px, 12Px);
+  font-size: max(15px, 12px);
 }
 
 .n-popselect-menu {
-  --n-option-height: 38Px !important;
+  --n-option-height: 38px !important;
 }
 
 @font-face {
@@ -123,24 +123,26 @@ body>.n-drawer-container-relative {
   --n-padding: 0 28px !important;
 }
 
-.searchDate, .searchDateDefault {
-  font-size: max(18px, 13Px);
-  --n-height: max(40px, 36Px) !important;
-  
+.searchDate,
+.searchDateDefault {
+  font-size: max(18px, 13px);
+  --n-height: max(40px, 36px) !important;
+
   --n-padding: 0 28px !important;
-  background: linear-gradient( 312deg, #1B7AF8 0%, #3CBBFF 100%);
+  background: linear-gradient(312deg, #1b7af8 0%, #3cbbff 100%);
   border-radius: 8px;
   // line-height: 41px;
   font-weight: 600 !important;
 
-  .n-button__state-border, .n-button__border {
+  .n-button__state-border,
+  .n-button__border {
     border: none !important;
   }
 }
 
 .searchDateDefault {
-  background: #F1F2F6;
-  color: #1E2022;
+  background: #f1f2f6;
+  color: #1e2022;
 }
 
 // .n-data-table {
@@ -185,19 +187,16 @@ body>.n-drawer-container-relative {
   .n-button {
     border-radius: 8px;
   }
-
-
 }
 
 .n-cascader-submenu-wrapper {
-
-  .n-scrollbar>.n-scrollbar-rail.n-scrollbar-rail--vertical,
-  .n-scrollbar+.n-scrollbar-rail.n-scrollbar-rail--vertical {
-    right: 2Px;
+  .n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical,
+  .n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical {
+    right: 2px;
   }
 
   .n-cascader-option__suffix {
-    padding-right: 12Px !important;
+    padding-right: 12px !important;
   }
 }
 
@@ -265,7 +264,7 @@ body>.n-drawer-container-relative {
 .favitor-enter-active,
 .favitor-leave-active {
   // transition: all 0.5s cubic-bezier(0.18, 0.89, 0, 1.29);
-  transition: all .3s ease-in-out;
+  transition: all 0.3s ease-in-out;
 }
 
 .favitor-enter-from,
@@ -370,14 +369,12 @@ body>.n-drawer-container-relative {
   transition: transform 0s;
 }
 
-
 .n-data-table .n-data-table-th {
-
-  background: #F7F7F8;
+  background: #f7f7f8;
   color: rgba(113, 113, 114, 1) !important;
   border: none;
   min-height: 54px;
-  font-size: max(15px, 12Px);
+  font-size: max(15px, 12px);
 }
 
 .n-data-table.n-data-table--bordered .n-data-table-wrapper {
@@ -387,7 +384,7 @@ body>.n-drawer-container-relative {
 .n-data-table-tr .n-data-table-td .n-button__content,
 .n-data-table .n-data-table-td {
   font-weight: bold;
-  font-size: max(15px, 12Px);
+  font-size: max(15px, 12px);
 }
 
 .n-tooltip {
@@ -395,23 +392,21 @@ body>.n-drawer-container-relative {
   --n-border-radius: 6px !important;
 
   .n-popover__content {
-    font-size: max(14px, 12Px);
+    font-size: max(14px, 12px);
   }
 }
 
-
 .n-base-close:not(.n-base-close--disabled):active::before,
 .n-base-close:not(.n-base-close--disabled):focus::before {
   background-color: transparent !important;
 }
 
-
 .body .n-modal-mask {
   background-color: transparent !important;
 }
 
-.n-modal-mask{
-  background-color: rgba(0, 0, 0, .6);
+.n-modal-mask {
+  background-color: rgba(0, 0, 0, 0.6);
 }
 
 // 设置图片弹窗工具预览
@@ -421,30 +416,32 @@ body>.n-drawer-container-relative {
 }
 
 .n-breadcrumb .n-breadcrumb-item {
-  font-size: max(16px, 12Px) !important;
+  font-size: max(16px, 12px) !important;
 }
 
 .n-base-selection,
 .n-input,
 .n-input-group-label {
-  --n-height: max(40px, 36Px) !important;
+  --n-height: max(40px, 36px) !important;
   --n-border-radius: 8px !important;
-  font-size: max(15px, 13Px) !important;
+  font-size: max(15px, 13px) !important;
 }
 
 .n-button {
-  font-size: max(18px, 13Px);
-  --n-height: max(40px, 36Px) !important;
+  font-size: max(18px, 13px);
+  --n-height: max(40px, 36px) !important;
 }
 
 .n-base-selection-input,
 .n-input .n-input__input-el,
 .n-input .n-input__textarea-el {
-  font-size: max(15px, 13Px) !important;
+  font-size: max(15px, 13px) !important;
 }
 
-.n-base-selection .n-base-selection-label .n-base-selection-label__render-label {
-  font-size: max(15px, 13Px) !important;
+.n-base-selection
+  .n-base-selection-label
+  .n-base-selection-label__render-label {
+  font-size: max(15px, 13px) !important;
 }
 
 .n-select-menu .n-button {
@@ -452,15 +449,14 @@ body>.n-drawer-container-relative {
 }
 
 .n-form-item-label__text {
-  font-size: max(15px, 13Px);
+  font-size: max(15px, 13px);
 }
 
 .n-date-panel {
-
   .n-date-panel-actions__suffix,
   .n-time-picker-panel {
     .n-button {
-      font-size: 12Px;
+      font-size: 12px;
       height: 32px !important;
       line-height: 32px;
       padding: 0 13px !important;
@@ -473,22 +469,148 @@ body>.n-drawer-container-relative {
   all: revert;
 }
 .html-to-dom {
-  div, span, applet, object, iframe,
-  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-  a, abbr, acronym, address, big, cite, code,
-  del, dfn, em, img, ins, kbd, q, s, samp,
-  small, strike, strong, sub, sup, tt, var,
-  b, u, i, center,
-  dl, dt, dd, ol, ul, li,
-  fieldset, form, label, legend,
-  table, caption, tbody, tfoot, thead, tr, th, td,
-  article, aside, canvas, details, embed, 
-  figure, figcaption, footer, header, hgroup, 
-  menu, nav, output, ruby, section, summary,
-  time, mark, audio, video, hr {
+  div,
+  span,
+  applet,
+  object,
+  iframe,
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  p,
+  blockquote,
+  pre,
+  a,
+  abbr,
+  acronym,
+  address,
+  big,
+  cite,
+  code,
+  del,
+  dfn,
+  em,
+  img,
+  ins,
+  kbd,
+  q,
+  s,
+  samp,
+  small,
+  strike,
+  strong,
+  sub,
+  sup,
+  tt,
+  var,
+  b,
+  u,
+  i,
+  center,
+  dl,
+  dt,
+  dd,
+  ol,
+  ul,
+  li,
+  fieldset,
+  form,
+  label,
+  legend,
+  table,
+  caption,
+  tbody,
+  tfoot,
+  thead,
+  tr,
+  th,
+  td,
+  article,
+  aside,
+  canvas,
+  details,
+  embed,
+  figure,
+  figcaption,
+  footer,
+  header,
+  hgroup,
+  menu,
+  nav,
+  output,
+  ruby,
+  section,
+  summary,
+  time,
+  mark,
+  audio,
+  video,
+  hr {
     all: revert;
   }
   hr {
-    border-top: 1px solid #D2D2D2;
+    border-top: 1px solid #d2d2d2;
   }
-}
+}
+
+.cardPreviewGuide {
+  width: 920px;
+  overflow: hidden;
+
+  :global {
+    .n-card__content {
+      height: 517px;
+      overflow: hidden;
+    }
+
+    .n-card-header__main {
+      max-width: 60%;
+      margin: 0 auto;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}
+
+.popoverClassF1 {
+  border-radius: 12Px !important;
+  width: 320Px !important;
+  max-width: 320Px !important;
+  .driver-popover-close-btn {
+    font-size: 22Px;
+    height: auto;
+    margin-right: 5Px;
+    font-size: 400 !important;
+  }
+
+  .driver-popover-arrow {
+    left: 50%;
+    margin-left: -5px;
+    right: revert !important;
+  }
+
+  .driver-popover-title {
+    font-size: 16Px;
+  }
+
+  .driver-popover-description {
+    padding-top: 8Px;
+    font-size: 14Px;
+    color: #777777;
+  }
+
+  .driver-popover-next-btn {
+    background: linear-gradient(312deg, #1b7af8 0%, #3cbbff 100%);
+    border-radius: 6Px;
+    font-weight: 600;
+    font-size: 14Px;
+    color: #FFFFFF;
+    padding: 6Px 12Px;
+    text-shadow: none !important;
+    border: 0 !important;
+  }
+}

+ 4 - 0
src/views/home/index.tsx

@@ -37,6 +37,7 @@ import PreviewWindow from '../preview-window';
 import { modalClickMask, state } from '/src/state';
 import SubjectModal from './modals/subject-modal';
 import { vaildMusicScoreUrl } from '/src/utils/urlUtils';
+import GuideSection from '/src/components/layout/guide-section';
 // import HomeGuide from '/src/custom-plugins/guide-page/home-guide';
 // import { state } from '/src/state';
 export const formatDateToDay = () => {
@@ -328,6 +329,9 @@ export default defineComponent({
             }}
           />
         </NModal>
+
+        {/* 操作手册 */}
+        {/* <GuideSection  /> */}
       </div>
     );
   }

+ 1 - 1
vite.config.ts

@@ -23,7 +23,7 @@ function resolve(dir: string) {
 }
 // https://vitejs.dev/config/
 // https://github.com/vitejs/vite/issues/1930 .env
-const proxyUrl = 'https://test.kt.colexiu.com/';
+const proxyUrl = 'https://dev.kt.colexiu.com/';
 // const proxyUrl = 'https://test.kt.colexiu.com';
 // const proxyUrl = 'http://192.168.3.14:7989';
 const now = new Date().getTime();