litian
2024-11-20 621b71bb1cc0dc383db1e4b89c9413bb9925b231
Merge branch 'master' of http://182.92.203.7:2001/r/TextbookReader
7个文件已修改
9个文件已添加
628 ■■■■■ 已修改文件
index.html 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/graffiti/brush.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/graffiti/revoke.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/graffiti/rubber.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/graffiti/save.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/graffiti/scrub.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/js/middleGround/api/app.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/main.css 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/components/brushSize.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/components/toolBtns.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/index.vue 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/components/formula.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/readerPages/webHome.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html
@@ -1,14 +1,17 @@
<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';"> -->
    <title>数字教材阅读器</title>
  </head>
  <body>
    <div id="parentApp"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline';"> -->
  <title>数字教材阅读器</title>
</head>
<body>
  <div id="parentApp"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>
package.json
@@ -18,6 +18,7 @@
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "@vitejs/plugin-vue-jsx": "^4.0.0",
    "@vue-office/docx": "^1.6.1",
    "axios": "^1.6.2",
    "cross-env": "^7.0.3",
@@ -28,21 +29,24 @@
    "element-plus": "^2.4.3",
    "fabric": "^5.3.0",
    "js-web-screen-shot": "^1.9.9-rc.18",
    "katex": "^0.16.11",
    "less": "^4.2.0",
    "less-loader": "^11.1.3",
    "mathlive": "^0.101.0",
    "moment": "^2.30.1",
    "node-xlsx": "^0.23.0",
    "pinia": "^2.1.7",
    "qiankun": "^2.10.16",
    "spark-md5": "^3.0.2",
    "style-resources-loader": "^1.5.0",
    "vatex": "^0.1.0",
    "viewerjs": "^1.11.6",
    "vite-plugin-electron": "^0.15.5",
    "vue": "^3.3.10",
    "vue-cli-plugin-style-resources-loader": "^0.1.5",
    "vue-clipboard3": "^2.0.0",
    "vue-demi": "^0.14.7",
    "vue-router": "^4.2.5",
    "viewerjs": "^1.11.6"
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.3.3",
src/assets/images/graffiti/brush.png
src/assets/images/graffiti/revoke.png
src/assets/images/graffiti/rubber.png
src/assets/images/graffiti/save.png
src/assets/images/graffiti/scrub.png
src/assets/js/middleGround/api/app.js
@@ -1,22 +1,29 @@
import request from "@/plugin/axios/index.ts";
import request from '@/plugin/axios/index.ts'
const appApi = {
  // 获取用户消息列表
  getAppMessageList(data) {
    return request({
      url: "/app/api/ApiGetAppMessageList",
      method: "post",
      url: '/app/api/ApiGetAppMessageList',
      method: 'post',
      data
    });
    })
  },
  // 获取用户消息详情
  getMessage(data) {
    return request({
      url: "/app/api/ApiGetMessage",
      method: "post",
      url: '/app/api/ApiGetMessage',
      method: 'post',
      data
    });
    })
  },
  // ai识别图片中的公式
  getLatexFormulaFromImage(data) {
    return request({
      url: '/ai/api/GetLatexFormulaFromImage',
      method: 'post',
      data
    })
  }
}
};
export default appApi;
export default appApi
src/assets/main.css
@@ -311,3 +311,7 @@
.icon-tabler-arrow-bar-to-left,.icon-tabler-arrow-bar-to-right{
  color:#707070 !important;
}
body {
  --keyboard-zindex: 999999;
}
src/components/graffiti/components/brushSize.vue
New file
@@ -0,0 +1,113 @@
<template>
  <div class="brushSize">
    <!-- 为了不在子组件中变更值,不用v-model -->
     <div class="wrap-range">
      <input
      type="range"
      :value="brushSize"
      min="1"
      max="30"
      title="调整笔刷粗细"
      class="input-brush"
      @change="(event) => $emit('change-size', +event.target.value)"
    />
     </div>
    <!-- <el-color-picker v-model="checkColor" @change="onChangeColor"></el-color-picker> -->
     <div>
       <input type="color" v-model="checkColor" @input="onChangeColor">
     </div>
  </div>
</template>
<script>
export default {
  props: {
    size: {
      type: Number,
      default: 5,
    },
  },
  computed:{
    brushSize() {
      return this.size
    }
  },
  data() {
    return  {
      checkColor:"#000000"
    }
  },
  methods:{
    onChangeColor(e) {
      this.$emit("change-color", e.srcElement.value);
    },
  }
};
// const brushSize = computed(() => props.size);
</script>
<style lang="less" scoped>
.brushSize {
  display: flex;
}
.wrap-range {
  display: flex;
  align-items: center;
  margin-right: 10px;
  .el-color-picker {
    margin-left: 20px;
  }
}
.wrap-range input {
  width: 150px;
  height: 20px;
  margin: 0;
  transform-origin: 75px 75px;
  border-radius: 15px;
  -webkit-appearance: none;
  appearance: none;
  outline: none;
  position: relative;
}
.wrap-range input::after {
  display: block;
  content: "";
  width: 0;
  height: 0;
  border: 5px solid transparent;
  border-right: 150px solid #00ccff;
  border-left-width: 0;
  position: absolute;
  left: 0;
  top: 5px;
  border-radius: 15px;
  z-index: 0;
}
.wrap-range input[type="range"]::-webkit-slider-thumb,
.wrap-range input[type="range"]::-moz-range-thumb {
  -webkit-appearance: none;
}
.wrap-range input[type="range"]::-webkit-slider-runnable-track,
.wrap-range input[type="range"]::-moz-range-track {
  height: 10px;
  border-radius: 10px;
  box-shadow: none;
}
.wrap-range input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: 20px;
  width: 20px;
  margin-top: -1px;
  background: #ffffff;
  border-radius: 50%;
  box-shadow: 0 0 8px #00ccff;
  position: relative;
  z-index: 999;
}
</style>
src/components/graffiti/components/toolBtns.vue
New file
@@ -0,0 +1,106 @@
<template>
  <div class="tools">
    <button v-for="(item, index) of toolList" :key="index" :class="{ active: toolSelected === item.name }"
      :title="item.title" @click="onChangeTool(item.name, index)"
      :style="{ boxShadow: index == num ? '0 0 15px #00ccff' : '' }">
      <img :src="item.icon" alt="" class="giaffiti-btn" :style="{ width: index == 0 ? '18px' : '' }" />
    </button>
  </div>
</template>
<script>
  import brush from '@/assets/images/graffiti/brush.png'
  import rubber from '@/assets/images/graffiti/rubber.png'
  import scrub from '@/assets/images/graffiti/scrub.png'
  import revoke from '@/assets/images/graffiti/revoke.png'
  import save from '@/assets/images/graffiti/save.png'
  export default {
    props: {
      tool: {
        type: String,
        default: "brush",
      },
    },
    computed: {
      toolSelected() {
        return this.tool;
      },
    },
    data() {
      return {
        toolList: [
          {
            name: "brush",
            title: "画笔",
            icon: brush,
          },
          {
            name: "eraser",
            title: "橡皮擦",
            icon: rubber,
          },
          {
            name: "clear",
            title: "清空",
            icon: scrub,
          },
          {
            name: "undo",
            title: "撤销",
            icon: revoke,
          },
          {
            name: "save",
            title: "保存",
            icon: save,
          },
        ],
        num: 0,
      };
    },
    methods: {
      onChangeTool(tool, index) {
        if (index == 0 || index == 1) this.num = index;
        this.$emit("change-tool", tool);
      },
    },
  };
</script>
<style scoped>
  .tools {
    display: flex;
    align-items: center;
  }
  .tools button {
    /* border-radius: 50%; */
    width: 32px;
    height: 32px;
    background-color: rgba(255, 255, 255, 0.7);
    border: 1px solid #eee;
    outline: none;
    cursor: pointer;
    box-sizing: border-box;
    margin: 0 8px;
    padding: 0;
    text-align: center;
    color: #ccc;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
    transition: 0.3s;
  }
  .tools button.active,
  .tools button:active {
    /* box-shadow: 0 0 15px #00CCFF; */
    color: #00ccff;
  }
  .tools button i {
    font-size: 20px;
  }
  .giaffiti-btn {
    width: 24px;
  }
</style>
src/components/graffiti/index.vue
New file
@@ -0,0 +1,243 @@
<!-- 涂色连线题控件 -->
<template>
  <div class="page">
    <div class="main">
      <div id="canvas_panel">
        <canvas id="canvas" :style="{
            backgroundSize: 'cover',
            backgroundPosition: 'center',
          }">当前浏览器不支持canvas。</canvas>
      </div>
    </div>
    <div class="footer">
      <BrushSize :size="brushSize" @change-size="onChangeSize" @change-color="onChangeColor" />
      <ToolBtns :tool="brushTool" @change-tool="onChangeTool" />
    </div>
  </div>
</template>
<script>
  import BrushSize from "./components/brushSize.vue";
  import ToolBtns from "./components/toolBtns.vue";
  export default {
    name: "graffiti",
    components: { BrushSize, ToolBtns },
    props: {
      save: {
        type: Function,
      },
    },
    data() {
      return {
        canvas: null,
        context: null,
        painting: false, // 记录状态,鼠标是否在按下状态
        historyData: [], // 存储历史数据,用于撤销
        brushSize: 2, // 笔刷大小
        brushColor: "#000000", // 笔刷颜色
        brushTool: "brush",
        canvasOffset: {
          left: 0,
          top: 0,
        },
      };
    },
    mounted() {
      this.canvas = (this.container ? this.container : document).getElementById(
        "canvas"
      );
      if (this.canvas.getContext) {
        this.context = this.canvas.getContext("2d", { willReadFrequently: true });
        // window.addEventListener('resize', updateCanvasPosition);
        (this.container ? this.container : document).addEventListener(
          "scroll",
          this.updateCanvasOffset,
          true
        ); // 添加滚动条滚动事件监听器
        this.getCanvasOffset();
        this.context.lineGap = "round";
        this.context.lineJoin = "round";
        this.canvas.addEventListener("mousedown", this.downCallback);
        this.canvas.addEventListener("mousemove", this.moveCallback);
        this.canvas.addEventListener("mouseup", this.closePaint);
        this.canvas.addEventListener("mouseleave", this.closePaint);
        setTimeout(() => {
          this.initCanvas();
        }, 300);
      }
      this.toolClear();
    },
    methods: {
      // 初始化 画布,设置大小背景色
      initCanvas() {
        const that = this;
        const resetCanvas = () => {
          const elPanel = (
            this.container ? this.container : document
          ).getElementById("canvas_panel");
          console.log("clientWidth"+elPanel.clientWidth);
          console.log("clientWidth"+elPanel.clientHeight);
          try {
            that.canvas.width = elPanel.clientWidth;
            that.canvas.height = elPanel.clientHeight;
          } catch (error) { }
          that.context = that.canvas.getContext("2d", {
            willReadFrequently: true,
          }); // 添加这一行
          that.context.fillStyle = "white";
          that.context.fillRect(0, 0, that.canvas.width, that.canvas.height);
          that.context.fillStyle = "black";
          that.getCanvasOffset(); // 更新画布位置
        };
        resetCanvas();
        // 监听窗口大小 ,窗口改变重新渲染画布
        window.addEventListener("resize", resetCanvas);
      },
      // 获取canvas的偏移值
      getCanvasOffset() {
        const rect = this.canvas.getBoundingClientRect();
        this.canvasOffset.left = rect.left * (this.canvas.width / rect.width); // 兼容缩放场景
        this.canvasOffset.top = rect.top * (this.canvas.height / rect.height);
      },
      // 计算当前鼠标相对于canvas的坐标
      calcRelativeCoordinate(x, y) {
        return {
          x: x - this.canvasOffset.left,
          y: y - this.canvasOffset.top,
        };
      },
      // 鼠标抬起方法
      downCallback(event) {
        // 先保存之前的数据,用于撤销时恢复(绘制前保存,不是绘制后再保存)
        const data = this.context.getImageData(
          0,
          0,
          this.canvas.width,
          this.canvas.height
        );
        this.saveData(data);
        const { clientX, clientY } = event;
        const { x, y } = this.calcRelativeCoordinate(clientX, clientY);
        this.context.beginPath();
        this.context.moveTo(x, y);
        this.context.lineWidth = this.brushSize;
        this.context.strokeStyle =
          this.brushTool === "eraser" ? "#FFFFFF" : this.brushColor;
        this.painting = true;
      },
      // 鼠标移动方法(计算坐标并渲染轨迹)
      moveCallback(event) {
        if (!this.painting) {
          return;
        }
        const { clientX, clientY } = event;
        const { x, y } = this.calcRelativeCoordinate(clientX, clientY);
        this.context.lineTo(x, y);
        this.context.stroke();
      },
      closePaint() {
        this.painting = false;
      },
      // 重新计算画布的偏移值
      updateCanvasOffset() {
        this.getCanvasOffset();
      },
      // 改变笔刷大小
      onChangeSize(size) {
        this.brushSize = size;
      },
      // 改变笔刷颜色
      onChangeColor(color) {
        this.brushColor = color;
      },
      // 保存,清空等按钮
      onChangeTool(tool) {
        this.brushTool = tool;
        switch (tool) {
          case "clear":
            this.toolClear();
            break;
          case "undo":
            this.toolUndo();
            break;
          case "save":
            this.toolSave();
            break;
        }
      },
      // 清空canvas所有内容(背景图除外)
      toolClear() {
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.resetToolActive();
      },
      // 保存画布背景和划线到本地方法
      toolSave() {
        var imgData = this.canvas.toDataURL('image/jpeg');
        console.log(imgData);
        if (this.save) {
          this.save(imgData);
        }
      },
      // 返回上一步方法(撤销)
      toolUndo() {
        if (this.historyData.length <= 0) {
          this.resetToolActive();
          return;
        }
        // 将画的上一步数据写入canvas 重新渲染
        const lastIndex = this.historyData.length - 1;
        this.context.putImageData(this.historyData[lastIndex], 0, 0);
        this.historyData.pop();
        this.resetToolActive();
      },
      // 存储数据
      saveData(data) {
        this.historyData.length >= 50 && this.historyData.shift(); // 设置储存上限为50步
        this.historyData.push(data);
      },
      // 清除、撤销、保存状态不需要保持,操作完后恢复笔刷状态
      resetToolActive() {
        setTimeout(() => {
          this.brushTool = "brush";
        }, 1000);
      },
    },
  };
</script>
<style scoped>
  .page {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
  }
  .main {
    flex: 1;
  }
  .footer {
    display: flex;
    justify-content: space-around;
    align-items: center;
    height: 88px;
  }
  #canvas_panel {
    width: 100%;
    height: 100%;
    margin-bottom: 12px;
    /* 消除空格影响 */
    font-size: 0;
    background-color: #fff;
    border-bottom: 1px solid #ccc;
  }
  #canvas {
    cursor: crosshair;
  }
</style>
src/main.ts
@@ -12,6 +12,12 @@
import './child.ts'
import { loginCtx } from '@/assets/js/config.ts'
// 公式输入
import { MathfieldElement } from "mathlive"
// 公式解析
import VueLatex from 'vatex'
const handleGetToken = () => {
  return localStorage.getItem('token')
}
@@ -74,8 +80,10 @@
const app = createApp(App)
app.provide('toolClass', toolClass)
app.provide('MG', MG)
app.use(VueLatex)
app.use(router)
app.use(ElementPlus)
app.use(pinia)
src/views/components/formula.vue
New file
@@ -0,0 +1,61 @@
<template>
  <div>
    <h2>手写识别</h2>
    <div class="box" style="width: 100%;height: 400px;">
      <graffiti :save="save" />
    </div>
    <div>
      <p>识别结果:</p>
      <div id="showData">
        <vue-latex :expression="content" display-mode />
      </div>
    </div>
    <h2>公式输入</h2>
    <math-field class="mathField" @input="handleInput" :menuItems="[]">{{valueData}}</math-field>
  </div>
</template>
<script setup lang="ts">
  import { ref, nextTick, reactive, watch, onMounted, onBeforeMount, onBeforeUnmount, inject } from 'vue'
  import { MathfieldElement } from "mathlive"
  import graffiti from '@/components/graffiti/index.vue'
  const commonsVariable: any = inject('commonsVariable')
  const MG: any = inject('MG')
  const valueData = ref("f(x) = \\frac{x}{2}")
  const handleInput = (data) => {
    console.log(data.target.value);
  }
  const content = ref('')
  const save = (data) => {
    console.log(data.split(",")[1]);
    MG.app
      .getLatexFormulaFromImage({
        base64Jpg: data.split(",")[1]
      })
      .then((res: any) => {
        content.value = res;
        nextTick(() => {
        })
      })
  }
</script>
<style lang="less">
  .mathField {
    width: 500px;
    margin-top: 10px;
  }
  #showData {
    border: 1px solid #ccc;
    padding: 20px;
    margin-top: 10px;
    margin-bottom: 50px;
  }
</style>
src/views/readerPages/webHome.vue
@@ -12,6 +12,9 @@
        <div v-else>
          <div class="layout hover" @click="goLogin">登录</div>
        </div>
        <!-- <div>
          <div class="layout hover" @click="openFormulaDialog">公式</div>
        </div> -->
      </div>
    </div>
    <div class="contentBox">
@@ -1279,6 +1282,16 @@
      <wrongQuestion />
    </div>
  </el-dialog>
  <el-dialog
    title="公式编辑"
    align-center
    v-model="formulaDialog"
    class="myDialogs"
  >
    <div class="wendabox">
      <formula />
    </div>
  </el-dialog>
  <!-- 答题器 -->
  <examination
    ref="examinationRef"
@@ -1309,6 +1322,7 @@
import moment from 'moment'
import dictionary from '@/views/components/dictionary.vue'
import newWord from '@/views/components/newWord.vue'
import formula from '@/views/components/formula.vue'
import wrongQuestion from '@/views/components/wrongQuestion.vue'
import voiceReader from '@/views/components/voiceReader.vue'
import logo from '@/assets/images/header/logo.png'
@@ -4131,6 +4145,11 @@
    })
  }
}
const formulaDialog = ref(false)
const openFormulaDialog = () => {
  formulaDialog.value = true
}
</script>
<style lang="less">
vite.config.ts
@@ -2,13 +2,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx';
import electron from 'vite-plugin-electron'
// https://vitejs.dev/config/
export default defineConfig({
  base:"./",
  plugins: [
    vue(),
    vueJsx(),
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.includes('math-field')
        }
      }
    }),
    // electron({
    //   // 配置 Electron 入口文件
    //   entry: 'electron-commonJS/main.js'
@@ -39,5 +47,5 @@
        additionalData: '@import "./src/assets/style/global.less";'
      }
    }
  }
  },
})