| | |
| | | <!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> |
| | |
| | | }, |
| | | "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", |
| | |
| | | "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", |
| | |
| | | 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 |
| | |
| | | .icon-tabler-arrow-bar-to-left,.icon-tabler-arrow-bar-to-right{ |
| | | color:#707070 !important; |
| | | } |
| | | |
| | | body { |
| | | --keyboard-zindex: 999999; |
| | | } |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <!-- 涂色连线题控件 --> |
| | | <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> |
| | |
| | | import './child.ts' |
| | | import { loginCtx } from '@/assets/js/config.ts' |
| | | |
| | | // 公式输入 |
| | | import { MathfieldElement } from "mathlive" |
| | | // 公式解析 |
| | | import VueLatex from 'vatex' |
| | | |
| | | |
| | | const handleGetToken = () => { |
| | | return localStorage.getItem('token') |
| | | } |
| | |
| | | |
| | | const app = createApp(App) |
| | | |
| | | |
| | | app.provide('toolClass', toolClass) |
| | | app.provide('MG', MG) |
| | | app.use(VueLatex) |
| | | app.use(router) |
| | | app.use(ElementPlus) |
| | | app.use(pinia) |
New file |
| | |
| | | <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="[]"></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 handleInput = (...data) => { |
| | | console.log(data); |
| | | } |
| | | |
| | | 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> |
| | |
| | | <wrongQuestion /> |
| | | </div> |
| | | </el-dialog> |
| | | <el-dialog |
| | | title="公式编辑" |
| | | align-center |
| | | v-model="formulaDialog" |
| | | class="myDialogs" |
| | | > |
| | | <div class="wendabox"> |
| | | <formula /> |
| | | </div> |
| | | </el-dialog> |
| | | <!-- 答题器 --> |
| | | <examination |
| | | ref="examinationRef" |
| | |
| | | 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' |
| | |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const formulaDialog = ref(false) |
| | | const openFormulaDialog = () => { |
| | | formulaDialog.value = true |
| | | } |
| | | </script> |
| | | |
| | | <style lang="less"> |
| | |
| | | |
| | | 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' |
| | |
| | | additionalData: '@import "./src/assets/style/global.less";' |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | }) |