From 4e201651d4a5ca76b66faba9e00f5ee2f9ae484f Mon Sep 17 00:00:00 2001 From: YM <479443481@qq.com> Date: 星期三, 20 十一月 2024 15:27:54 +0800 Subject: [PATCH] 1 --- src/assets/images/graffiti/rubber.png | 0 src/assets/main.css | 4 src/assets/images/graffiti/save.png | 0 src/assets/images/graffiti/scrub.png | 0 src/main.ts | 8 vite.config.ts | 12 src/assets/js/middleGround/api/app.js | 27 + src/components/graffiti/components/toolBtns.vue | 106 +++++++++ src/views/readerPages/webHome.vue | 17 + src/assets/images/graffiti/revoke.png | 0 src/components/graffiti/index.vue | 243 ++++++++++++++++++++++ index.html | 27 +- package.json | 8 src/components/graffiti/components/brushSize.vue | 113 ++++++++++ src/views/components/formula.vue | 58 +++++ src/assets/images/graffiti/brush.png | 0 16 files changed, 597 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index a9a6a0f..465a048 100644 --- a/index.html +++ b/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> \ No newline at end of file diff --git a/package.json b/package.json index 9f95fbc..1c1586a 100644 --- a/package.json +++ b/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", diff --git a/src/assets/images/graffiti/brush.png b/src/assets/images/graffiti/brush.png new file mode 100644 index 0000000..5f23599 --- /dev/null +++ b/src/assets/images/graffiti/brush.png Binary files differ diff --git a/src/assets/images/graffiti/revoke.png b/src/assets/images/graffiti/revoke.png new file mode 100644 index 0000000..97c4e20 --- /dev/null +++ b/src/assets/images/graffiti/revoke.png Binary files differ diff --git a/src/assets/images/graffiti/rubber.png b/src/assets/images/graffiti/rubber.png new file mode 100644 index 0000000..bf9955c --- /dev/null +++ b/src/assets/images/graffiti/rubber.png Binary files differ diff --git a/src/assets/images/graffiti/save.png b/src/assets/images/graffiti/save.png new file mode 100644 index 0000000..f7b2421 --- /dev/null +++ b/src/assets/images/graffiti/save.png Binary files differ diff --git a/src/assets/images/graffiti/scrub.png b/src/assets/images/graffiti/scrub.png new file mode 100644 index 0000000..2ef004c --- /dev/null +++ b/src/assets/images/graffiti/scrub.png Binary files differ diff --git a/src/assets/js/middleGround/api/app.js b/src/assets/js/middleGround/api/app.js index f7edc50..cf11cec 100644 --- a/src/assets/js/middleGround/api/app.js +++ b/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 diff --git a/src/assets/main.css b/src/assets/main.css index 659e1b2..422cb91 100644 --- a/src/assets/main.css +++ b/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; +} \ No newline at end of file diff --git a/src/components/graffiti/components/brushSize.vue b/src/components/graffiti/components/brushSize.vue new file mode 100644 index 0000000..ec5043a --- /dev/null +++ b/src/components/graffiti/components/brushSize.vue @@ -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> diff --git a/src/components/graffiti/components/toolBtns.vue b/src/components/graffiti/components/toolBtns.vue new file mode 100644 index 0000000..9f608cd --- /dev/null +++ b/src/components/graffiti/components/toolBtns.vue @@ -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> \ No newline at end of file diff --git a/src/components/graffiti/index.vue b/src/components/graffiti/index.vue new file mode 100644 index 0000000..4c8034a --- /dev/null +++ b/src/components/graffiti/index.vue @@ -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(); + // 鐩戝惉绐楀彛澶у皬 锛岀獥鍙f敼鍙橀噸鏂版覆鏌撶敾甯� + 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); + }, + + // 璁$畻褰撳墠榧犳爣鐩稿浜巆anvas鐨勫潗鏍� + 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; + } + // 灏嗙敾鐨勪笂涓�姝ユ暟鎹啓鍏anvas 閲嶆柊娓叉煋 + 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> \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index de5d30d..43ec77a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,12 @@ import './child.ts' import { loginCtx } from '@/assets/js/config.ts' +// 鍏紡杈撳叆 +import { MathfieldElement } from "mathlive" +// 鍏紡瑙f瀽 +import VueLatex from 'vatex' + + const handleGetToken = () => { return localStorage.getItem('token') } @@ -77,8 +83,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) diff --git a/src/views/components/formula.vue b/src/views/components/formula.vue new file mode 100644 index 0000000..9e3b39f --- /dev/null +++ b/src/views/components/formula.vue @@ -0,0 +1,58 @@ +<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> \ No newline at end of file diff --git a/src/views/readerPages/webHome.vue b/src/views/readerPages/webHome.vue index e4f1f31..3ba1680 100644 --- a/src/views/readerPages/webHome.vue +++ b/src/views/readerPages/webHome.vue @@ -8,6 +8,7 @@ <div class="userName" v-if="userInfo.name">{{ userInfo.name }}</div> <div v-if="token"><div class="layout hover" @click="layoutBtn">閫�鍑�</div></div> <div v-else><div class="layout hover" @click="goLogin">鐧诲綍</div></div> + <div @click="openFormulaDialog">鍏紡</div> </div> </div> <div class="contentBox"> @@ -1238,6 +1239,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" @@ -1271,6 +1282,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 { ElMessage, ElMessageBox, valueEquals } from 'element-plus' @@ -4028,6 +4040,11 @@ }) } } + +const formulaDialog = ref(false) +const openFormulaDialog = () => { + formulaDialog.value = true +} </script> <style lang="less"> diff --git a/vite.config.ts b/vite.config.ts index 2f5b6f7..ecb6c57 100644 --- a/vite.config.ts +++ b/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";' } } - } + }, }) -- Gitblit v1.9.1