unknown
2024-06-25 6c377a85f76fc4e15e1c86cecea4bf8fd2513532
Merge branch 'master' of http://182.92.203.7:2001/r/testbookLayout
1个文件已修改
4个文件已添加
462 ■■■■■ 已修改文件
src/books/mathBook/view/components/chapter001.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/components/brushSize.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/components/colorPicker.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/components/toolBtns.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/graffiti/index.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/books/mathBook/view/components/chapter001.vue
@@ -575,9 +575,10 @@
        </div>
      </div>
    </div>
    <!-- <div class="page-box padding-116" page="10">
      <drag :question="dragQuestion" :page="10"/>
    </div> -->
    <div class="page-box padding-116" page="10">
      <!-- <drag :question="dragQuestion" :page="10"/> -->
       <graffiti style="width:100%" />
    </div>
    <!-- 函数控件弹窗 -->
    <el-dialog
      :visible.sync="dialogVisible"
@@ -823,9 +824,10 @@
import { getResourcePath } from "@/assets/methods/resources";
import { getCollectResource,setCollectResource } from "@/assets/methods/resources";
import drag from '@/components/dragQuestion/index.vue'
import graffiti from "@/components/graffiti/index.vue"
export default {
  name: "chapter-one",
  components: { examinations,drag },
  components: { examinations,drag,graffiti },
  props: {
    showPageList: {
      type: Array,
src/components/graffiti/components/brushSize.vue
New file
@@ -0,0 +1,83 @@
<template>
  <div class="wrap-range">
    <!-- 为了不在子组件中变更值,不用v-model -->
    <input
      type="range"
      :value="brushSize"
      min="1"
      max="30"
      title="调整笔刷粗细"
      @change="(event) => $emit('change-size', +event.target.value)"
    />
  </div>
</template>
<script>
export default {
  props: {
    size: {
      type: Number,
      default: 5,
    },
  },
  computed:{
    brushSize() {
      return this.size
    }
  }
};
// const brushSize = computed(() => props.size);
</script>
<style scoped>
.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/colorPicker.vue
New file
@@ -0,0 +1,61 @@
<template>
  <div>
    <span
      v-for="(color, index) of colorList"
      class="color-item"
      :class="{ active: colorSelected === color }"
      :style="{ backgroundColor: color }"
      :key="index"
      @click="onChangeColor(color)"
    ></span>
  </div>
</template>
<script>
export default {
  props: {
    color: {
      type: String,
    },
  },
  computed:{
    colorSelected() {
      return this.color
    }
  },
  data() {
    return {
      colorList: [
        "#000000",
        "#808080",
        "#FF3333",
        "#0066FF",
        "#FFFF33",
        "#33CC66",
      ],
    };
  },
  methods: {
    onChangeColor(color) {
      this.$emit("change-color", color);
    },
  },
};
</script>
<style scoped>
.color-item {
  display: inline-block;
  width: 32px;
  height: 32px;
  margin: 0 4px;
  box-sizing: border-box;
  border: 4px solid white;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
  cursor: pointer;
  transition: 0.3s;
}
.color-item.active {
  box-shadow: 0 0 15px #00ccff;
}
</style>
src/components/graffiti/components/toolBtns.vue
New file
@@ -0,0 +1,74 @@
<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)"
    >
      <i :class="['iconfont', item.icon]"></i>
    </button>
  </div>
</template>
<script>
export default {
  props: {
    tool: {
      type: String,
      default: "brush",
    },
  },
  computed:{
    toolSelected() {
      return this.tool
    }
  },
  data() {
    return {
      toolList: [
        { name: "brush", title: "画笔", icon: "icon-qianbi" },
        { name: "eraser", title: "橡皮擦", icon: "icon-xiangpi" },
        { name: "clear", title: "清空", icon: "icon-qingchu" },
        { name: "undo", title: "撤销", icon: "icon-chexiao" },
        { name: "save", title: "保存", icon: "icon-fuzhi" },
      ],
    };
  },
  methods: {
    onChangeTool(tool) {
      this.$emit("change-tool", tool);
    },
  },
};
</script>
<style scoped>
.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;
}
</style>
src/components/graffiti/index.vue
New file
@@ -0,0 +1,234 @@
<template>
  <div class="page">
    <div class="main">
      <div id="canvas_panel">
        <canvas
          id="canvas"
          :style="{
            backgroundImage: `url(${backgroundImage})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center',
          }"
          >当前浏览器不支持canvas。</canvas
        >
      </div>
    </div>
    <div class="footer">
      <BrushSize :size="brushSize" @change-size="onChangeSize" />
      <ColorPicker :color="brushColor" @change-color="onChangeColor" />
      <ToolBtns :tool="brushTool" @change-tool="onChangeTool" />
    </div>
  </div>
</template>
<script>
import BrushSize from "./components/brushSize.vue";
import ColorPicker from "./components/colorPicker.vue";
import ToolBtns from "./components/toolBtns.vue";
export default {
  name: "graffiti",
  components: { BrushSize, ColorPicker, ToolBtns },
  data() {
    return {
      canvas: null,
      context: null,
      painting: false,
      historyData: [], // 存储历史数据,用于撤销
      brushSize: 5, // 笔刷大小
      brushColor: "#000000", // 笔刷颜色
      brushTool: "brush",
      canvasOffset: {
        left: 0,
        top: 0,
      },
      backgroundImage:
        "https://t7.baidu.com/it/u=1819248061,230866778&fm=193&f=GIF",
    };
  },
  mounted() {
    this.canvas = document.getElementById("canvas");
    if (this.canvas.getContext) {
      this.context = this.canvas.getContext("2d", { willReadFrequently: true });
      this.initCanvas();
      // window.addEventListener('resize', updateCanvasPosition);
      window.addEventListener("scroll", this.updateCanvasOffset); // 添加滚动条滚动事件监听器
      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);
    }
    this.toolClear();
  },
  methods: {
    changeBackground(imgUrl) {
      this.backgroundImage = imgUrl;
    },
    initCanvas() {
      const that = this
      function resetCanvas() {
        const elPanel = document.getElementById("canvas_panel");
        that.canvas.width = elPanel.clientWidth;
        that.canvas.height = elPanel.clientHeight;
        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;
      }
    },
    toolClear() {
      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.resetToolActive();
    },
    toolSave() {
      const imageDataUrl = this.canvas.toDataURL("image/png");
      console.log(imageDataUrl);
      // const imgUrl = canvas.toDataURL('image/png');
      // const el = document.createElement('a');
      // el.setAttribute('href', imgUrl);
      // el.setAttribute('target', '_blank');
      // el.setAttribute('download', `graffiti-${Date.now()}`);
      // document.body.appendChild(el);
      // el.click();
      // document.body.removeChild(el);
      // resetToolActive();
    },
    toolUndo() {
      if (this.historyData.length <= 0) {
        this.resetToolActive();
        return;
      }
      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: 1038px;
  height: 866px;
}
.main {
  flex: 1;
}
.footer {
  display: flex;
  justify-content: space-around;
  align-items: center;
  height: 88px;
  background-color: #fff;
}
#canvas_panel {
  margin: 12px;
  height: calc(100% - 24px);
  /* 消除空格影响 */
  font-size: 0;
  background-color: #fff;
  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
}
#canvas {
  cursor: crosshair;
  /* background: url('https://t7.baidu.com/it/u=1819248061,230866778&fm=193&f=GIF') no-repeat !important; */
}
</style>