vue3 canvas 选择器 Canvas 增加页面性能

发布于:2025-07-15 ⋅ 阅读:(12) ⋅ 点赞:(0)

Vue3 选择器 Canvas 增加页面性能

基于Vue3 Composition API和Canvas实现的交互式选择器,支持PC端和移动端的拖动选择、多选取消选择功能

在这里插入图片描述

vue3组件封装

<script lang="ts" setup>
import { onMounted, reactive, watch } from 'vue';
import { CheckList } from '/@/types';
const props = defineProps({
  list: {
    type: Array as PropType<CheckList[]>,
    default: () => [],
  },
});
const emit = defineEmits(['changeValue']);

// 正确类型定义
const canvas: Ref<HTMLCanvasElement | null> = ref(null);
const ctx: Ref<CanvasRenderingContext2D | null> = ref(null);
// 网格配置
const rows = 8;
const cols = 12;
const rowLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
const colLabels = Array.from({ length: 12 }, (_, i) => i + 1);
// 类型定义
type Position = {
  x: number;
  y: number;
};
// 状态管理
const isSelecting = ref(false);
const startPos = ref<Position>({ x: 0, y: 0 });
const endPos = ref<Position>({ x: 0, y: 0 });
const selectionMode = ref('add'); // 'add' 或 'remove'

// 选项状态 (96个选项)
// const options = ref(
//   Array(rows * cols)
//     .fill()
//     .map((_, i) => ({
//       id: `${rowLabels[Math.floor(i / cols)]}${colLabels[i % cols]}`,
//       selected: false,
//     })),
// );
const options = ref([...props.list]);

// 计算属性
const selectedItems = computed(() => options.value.filter((opt) => opt.selected).map((opt) => opt.id));

const selectedCount = computed(() => options.value.filter((opt) => opt.selected).length);

// 初始化Canvas
const initCanvas = () => {
  if (canvas.value == null) return;

  const canvasEl: HTMLCanvasElement = canvas.value;

  ctx.value = canvasEl.getContext('2d');

  // 设置Canvas尺寸
  canvasEl.width = canvasEl.clientWidth;
  canvasEl.height = canvasEl.clientHeight;

  drawGrid();
};

// 绘制网格和选项
const drawGrid = () => {
  if (options.value.length == 0 || !canvas.value || !ctx.value) return;
  const canvasEl = canvas.value;

  ctx.value.clearRect(0, 0, canvasEl.width, canvasEl.height);

  // 计算每个选项的尺寸
  const cellWidth = canvasEl.width / cols;
  const cellHeight = canvasEl.height / rows;

  // 绘制网格和选项
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      const x = col * cellWidth;
      const y = row * cellHeight;
      const index = row * cols + col;
      const isSelected = options.value[index].selected;

      // 绘制选项背景
      ctx.value.fillStyle = isSelected ? '#00a9bb' : '#ffffff';
      ctx.value.fillRect(x, y, cellWidth, cellHeight);

      // 绘制边框
      ctx.value.strokeStyle = isSelected ? '#eeeeee' : '#cccccc';
      ctx.value.lineWidth = isSelected ? 3 : 1;
      ctx.value.strokeRect(x, y, cellWidth, cellHeight);

      // 绘制选项文本
      ctx.value.fillStyle = isSelected ? '#fff' : '#000000';
      ctx.value.font = `bold ${cellHeight * 0.3}px Arial`;
      ctx.value.textAlign = 'center';
      ctx.value.textBaseline = 'middle';
      ctx.value.fillText(options.value[index].id, x + cellWidth / 2, y + cellHeight / 2);
    }
  }

  // 绘制行标签 (1-12)
  // ctx.value.fillStyle = '#f00';
  // ctx.value.font = `${12}px`;
  // for (let col = 0; col < cols; col++) {
  //   ctx.value.fillText(colLabels[col], (col + 0.5) * cellWidth, cellHeight * 0.2);
  // }

  // // 绘制列标签 (A-H)
  // for (let row = 0; row < rows; row++) {
  //   ctx.value.fillText(rowLabels[row].toString(), cellWidth * 0.1, (row + 0.5) * cellHeight);
  // }

  // 绘制选择框
  if (isSelecting.value) {
    const x = Math.min(startPos.value.x, endPos.value.x);
    const y = Math.min(startPos.value.y, endPos.value.y);
    const width = Math.abs(endPos.value.x - startPos.value.x);
    const height = Math.abs(endPos.value.y - startPos.value.y);

    ctx.value.fillStyle = selectionMode.value === 'add' ? 'rgba(100, 200, 255, 0.2)' : 'rgba(255, 100, 100, 0.2)';
    ctx.value.fillRect(x, y, width, height);

    ctx.value.strokeStyle = selectionMode.value === 'add' ? 'rgba(100, 200, 255, 0.8)' : 'rgba(255, 100, 100, 0.8)';
    ctx.value.lineWidth = 2;
    ctx.value.strokeRect(x, y, width, height);
  }
};

// 获取Canvas坐标
const getCanvasPos = (event: MouseEvent | TouchEvent) => {
  if (canvas.value == null) return;
  const canvasEl: HTMLCanvasElement = canvas.value;
  const rect = canvasEl.getBoundingClientRect();

  let clientX: number, clientY: number;
  // if (event.type.includes('touch')) {
  if ('touches' in event) {
    clientX = event.touches[0].clientX;
    clientY = event.touches[0].clientY;
  } else {
    clientX = event.clientX;
    clientY = event.clientY;
  }

  return {
    x: clientX - rect.left,
    y: clientY - rect.top,
  };
};

// 开始选择
const startSelection = (event: MouseEvent | TouchEvent) => {
  event.preventDefault();
  const pos: any = getCanvasPos(event);

  startPos.value = { ...pos };
  endPos.value = { ...pos };
  isSelecting.value = true;

  // 确定选择模式(添加或移除)
  if (canvas.value == null) return;
  const canvasEl: HTMLCanvasElement = canvas.value;
  const cellWidth = canvasEl.width / cols;
  const cellHeight = canvasEl.height / rows;

  const colIndex = Math.floor(pos.x / cellWidth);
  const rowIndex = Math.floor(pos.y / cellHeight);
  const index = rowIndex * cols + colIndex;

  if (index >= 0 && index < options.value.length) {
    // 如果点击的选项已选择,则设为移除模式,否则设为添加模式
    selectionMode.value = options.value[index].selected ? 'remove' : 'add';
    // 切换点击的选项状态
    options.value[index].selected = !options.value[index].selected;
  } else {
    selectionMode.value = 'add';
  }

  drawGrid();
};

// 更新选择
const updateSelection = (event: MouseEvent | TouchEvent) => {
  if (!isSelecting.value) return;
  event.preventDefault();

  const pos: any = getCanvasPos(event);
  endPos.value = { ...pos };

  updateSelectedOptions();
  drawGrid();
};

// 结束选择
const endSelection = (event: MouseEvent | TouchEvent) => {
  if (!isSelecting.value) return;
  event.preventDefault();

  isSelecting.value = false;
  drawGrid();
};

// 更新被选中的选项
const updateSelectedOptions = () => {
  if (canvas.value == null) return;

  const canvasEl: HTMLCanvasElement = canvas.value;

  const cellWidth = canvasEl.width / cols;
  const cellHeight = canvasEl.height / rows;

  // 计算选择框覆盖的区域
  const minX = Math.min(startPos.value.x, endPos.value.x);
  const maxX = Math.max(startPos.value.x, endPos.value.x);
  const minY = Math.min(startPos.value.y, endPos.value.y);
  const maxY = Math.max(startPos.value.y, endPos.value.y);

  // 计算覆盖的网格范围
  const startCol = Math.max(0, Math.floor(minX / cellWidth));
  const endCol = Math.min(cols - 1, Math.floor(maxX / cellWidth));
  const startRow = Math.max(0, Math.floor(minY / cellHeight));
  const endRow = Math.min(rows - 1, Math.floor(maxY / cellHeight));

  // 更新选项状态
  for (let row = startRow; row <= endRow; row++) {
    for (let col = startCol; col <= endCol; col++) {
      const index = row * cols + col;
      if (selectionMode.value === 'add') {
        options.value[index].selected = true;
      } else {
        options.value[index].selected = false;
      }
    }
  }
};
const radioValue = ref('1');
const handleRadio = (e: any) => {
  if (e.target.value === '1') {
    selectAll();
  } else if (e.target.value === '2') {
    clearSelection();
  }
};
// 全选
const selectAll = () => {
  options.value.forEach((opt) => (opt.selected = true));
  drawGrid();
};

// 清空选择
const clearSelection = () => {
  options.value.forEach((opt) => (opt.selected = false));
  drawGrid();
};

// 生命周期钩子
onMounted(() => {
  initCanvas();
  window.addEventListener('resize', initCanvas);
});

watch(
  options,
  (newVal) => {
    if (newVal.every((item) => item.selected)) {
      radioValue.value = '1';
    } else if (newVal.every((item) => !item.selected)) {
      radioValue.value = '2';
    } else {
      radioValue.value = '3';
    }
    emit('changeValue', newVal);
  },
  {
    deep: true,
  },
);
</script>

<template>
  <div class="box">
    <canvas
      ref="canvas"
      @mousedown="startSelection"
      @mousemove="updateSelection"
      @mouseup="endSelection"
      @mouseleave="endSelection"
      @touchstart="startSelection"
      @touchmove="updateSelection"
      @touchend="endSelection"
    ></canvas>
    <div class="mt-20 pl-26">
      <a-radio-group v-model:value="radioValue" @change="handleRadio" name="radioGroup">
        <a-radio value="1">全满</a-radio>
        <a-radio value="2">全空</a-radio>
      </a-radio-group>
      <div mt-10>注:单击带蓝绿色表示有,单击显白色表示无</div>
    </div>
  </div>
</template>

<style lang="less" scoped>
canvas {
  width: 450px;
  height: 300px;
  background: rgba(10, 15, 30, 0.7);
  // border-radius: 10px;
  display: block;
  cursor: pointer;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
</style>

html代码


<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3 Canvas 选择器组件</title>
  <script src="https://unpkg.com/vue@3.2.47/dist/vue.global.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }
    body {
      background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
      color: #fff;
    }
    #app {
      max-width: 1200px;
      width: 100%;
    }
    .container {
      display: flex;
      flex-direction: column;
      gap: 30px;
    }
    header {
      text-align: center;
      padding: 30px 20px;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 20px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
      box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
    }
    h1 {
      font-size: 2.8rem;
      margin-bottom: 15px;
      background: linear-gradient(to right, #4facfe, #00f2fe);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }
    .subtitle {
      font-size: 1.2rem;
      opacity: 0.85;
      max-width: 800px;
      margin: 0 auto;
      line-height: 1.6;
    }
    .content {
      display: flex;
      gap: 30px;
      flex-wrap: wrap;
    }
    .canvas-container {
      flex: 1;
      min-width: 300px;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 20px;
      padding: 25px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
      box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
    }
    canvas {
      width: 100%;
      height: 600px;
      background: rgba(10, 15, 30, 0.8);
      border-radius: 15px;
      display: block;
      cursor: pointer;
      box-shadow: 0 5px 25px rgba(0, 0, 0, 0.5);
    }
    .instructions {
      font-size: 0.9rem;
      text-align: center;
      margin-top: 15px;
      opacity: 0.8;
    }
    .info-panel {
      width: 320px;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 20px;
      padding: 30px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
      box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
    }
    .info-panel h2 {
      font-size: 1.8rem;
      margin-bottom: 25px;
      color: #00f2fe;
      text-align: center;
      background: linear-gradient(to right, #4facfe, #00f2fe);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }
    .stats {
      display: flex;
      justify-content: space-between;
      margin-bottom: 30px;
      padding-bottom: 20px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    }
    .stat {
      text-align: center;
      padding: 15px;
      background: rgba(0, 0, 0, 0.2);
      border-radius: 15px;
      flex: 1;
      margin: 0 10px;
    }
    .stat-value {
      font-size: 2.5rem;
      font-weight: bold;
      color: #4facfe;
      margin-bottom: 5px;
    }
    .stat-label {
      font-size: 0.95rem;
      opacity: 0.8;
    }
    .selected-items {
      max-height: 300px;
      overflow-y: auto;
      margin-top: 20px;
    }
    .selected-items h3 {
      margin-bottom: 20px;
      color: #00f2fe;
      text-align: center;
      font-size: 1.4rem;
    }
    .items-list {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      justify-content: center;
    }
    .item {
      background: linear-gradient(to right, rgba(79, 172, 254, 0.2), rgba(0, 242, 254, 0.2));
      padding: 8px 15px;
      border-radius: 25px;
      font-size: 1rem;
      border: 1px solid rgba(79, 172, 254, 0.4);
    }
    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      justify-content: center;
      margin-top: 30px;
    }
    button {
      background: linear-gradient(to right, #4facfe, #00f2fe);
      color: white;
      border: none;
      padding: 12px 30px;
      border-radius: 30px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s ease;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
      letter-spacing: 0.5px;
      min-width: 180px;
    }
    button:hover {
      transform: translateY(-3px);
      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
    }
    button:active {
      transform: translateY(1px);
    }
    .empty-message {
      text-align: center;
      opacity: 0.6;
      font-style: italic;
      margin: 25px 0;
      padding: 20px;
      background: rgba(0, 0, 0, 0.15);
      border-radius: 15px;
    }
    .mode-indicator {
      display: flex;
      justify-content: center;
      gap: 20px;
      margin-top: 15px;
    }
    .mode {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 16px;
      border-radius: 20px;
      background: rgba(0, 0, 0, 0.2);
    }
    .mode-color {
      width: 20px;
      height: 20px;
      border-radius: 50%;
    }
    .add-color {
      background: rgba(100, 200, 255, 0.8);
    }
    .remove-color {
      background: rgba(255, 100, 100, 0.8);
    }
    .active-mode {
      background: rgba(79, 172, 254, 0.3);
      border: 1px solid rgba(79, 172, 254, 0.6);
    }
    footer {
      text-align: center;
      padding: 25px;
      opacity: 0.7;
      font-size: 0.95rem;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 20px;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
      box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
    }
    
    @media (max-width: 768px) {
      .content {
        flex-direction: column;
      }
      .info-panel {
        width: 100%;
      }
      h1 {
        font-size: 2.2rem;
      }
      canvas {
        height: 500px;
      }
      .stat-value {
        font-size: 2rem;
      }
    }
    
    /* 滚动条样式 */
    .selected-items::-webkit-scrollbar {
      width: 8px;
    }
    .selected-items::-webkit-scrollbar-track {
      background: rgba(0, 0, 0, 0.1);
      border-radius: 4px;
    }
    .selected-items::-webkit-scrollbar-thumb {
      background: linear-gradient(to bottom, #4facfe, #00f2fe);
      border-radius: 4px;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="container">
      <header>
        <h1>Vue3 Canvas 选择器组件</h1>
        <p class="subtitle">基于Vue3 Composition API和Canvas实现的交互式选择器,支持PC端和移动端的拖动选择、多选取消选择功能</p>
      </header>
      
      <div class="content">
        <canvas-selector></canvas-selector>
      </div>
      
      <footer>
        <p>Vue3 + Canvas 实现 | 支持PC端和移动端 | 拖动选择多个选项 | 点击切换选择模式</p>
      </footer>
    </div>
  </div>

  <script>
    const { createApp, ref, onMounted, computed, defineComponent } = Vue;
    
    const CanvasSelector = defineComponent({
      setup() {
        // 引用Canvas元素
        const canvas = ref(null);
        const ctx = ref(null);
        
        // 网格配置
        const rows = 12;
        const cols = 8;
        const colLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
        const rowLabels = Array.from({ length: 12 }, (_, i) => i + 1);
        
        // 状态管理
        const isSelecting = ref(false);
        const startPos = ref({ x: 0, y: 0 });
        const endPos = ref({ x: 0, y: 0 });
        const selectionMode = ref('add'); // 'add' 或 'remove'
        
        // 选项状态 (96个选项)
        const options = ref(Array(rows * cols).fill().map((_, i) => ({
          id: `${colLabels[i % cols]}${rowLabels[Math.floor(i / cols)]}`,
          selected: false
        })));
        
        // 计算属性
        const selectedItems = computed(() => 
          options.value.filter(opt => opt.selected).map(opt => opt.id)
        );
        
        const selectedCount = computed(() => 
          options.value.filter(opt => opt.selected).length
        );
        
        // 初始化Canvas
        const initCanvas = () => {
          const canvasEl = canvas.value;
          ctx.value = canvasEl.getContext('2d');
          
          // 设置Canvas尺寸
          canvasEl.width = canvasEl.clientWidth;
          canvasEl.height = canvasEl.clientHeight;
          
          drawGrid();
        };
        
        // 绘制网格和选项
        const drawGrid = () => {
          const canvasEl = canvas.value;
          ctx.value.clearRect(0, 0, canvasEl.width, canvasEl.height);
          
          // 计算每个选项的尺寸
          const cellWidth = canvasEl.width / cols;
          const cellHeight = canvasEl.height / rows;
          
          // 绘制网格和选项
          for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
              const x = col * cellWidth;
              const y = row * cellHeight;
              const index = row * cols + col;
              const isSelected = options.value[index].selected;
              
              // 绘制选项背景
              ctx.value.fillStyle = isSelected ? 'rgba(79, 172, 254, 0.7)' : 'rgba(30, 35, 60, 0.8)';
              ctx.value.fillRect(x, y, cellWidth, cellHeight);
              
              // 绘制边框
              ctx.value.strokeStyle = isSelected ? 'rgba(0, 242, 254, 0.9)' : 'rgba(100, 150, 255, 0.3)';
              ctx.value.lineWidth = isSelected ? 3 : 1;
              ctx.value.strokeRect(x, y, cellWidth, cellHeight);
              
              // 绘制选项文本
              ctx.value.fillStyle = isSelected ? '#fff' : 'rgba(255, 255, 255, 0.7)';
              ctx.value.font = `bold ${cellHeight * 0.3}px Arial`;
              ctx.value.textAlign = 'center';
              ctx.value.textBaseline = 'middle';
              ctx.value.fillText(
                options.value[index].id, 
                x + cellWidth / 2, 
                y + cellHeight / 2
              );
            }
          }
          
          // 绘制列标签 (A-H)
          ctx.value.fillStyle = 'rgba(200, 220, 255, 0.9)';
          ctx.value.font = `bold ${cellHeight * 0.25}px Arial`;
          for (let col = 0; col < cols; col++) {
            ctx.value.fillText(
              colLabels[col],
              (col + 0.5) * cellWidth,
              cellHeight * 0.2
            );
          }
          
          // 绘制行标签 (1-12)
          for (let row = 0; row < rows; row++) {
            ctx.value.fillText(
              rowLabels[row].toString(),
              cellWidth * 0.2,
              (row + 0.5) * cellHeight
            );
          }
          
          // 绘制选择框
          if (isSelecting.value) {
            const x = Math.min(startPos.value.x, endPos.value.x);
            const y = Math.min(startPos.value.y, endPos.value.y);
            const width = Math.abs(endPos.value.x - startPos.value.x);
            const height = Math.abs(endPos.value.y - startPos.value.y);
            
            ctx.value.fillStyle = selectionMode.value === 'add' 
              ? 'rgba(100, 200, 255, 0.2)' 
              : 'rgba(255, 100, 100, 0.2)';
            ctx.value.fillRect(x, y, width, height);
            
            ctx.value.strokeStyle = selectionMode.value === 'add' 
              ? 'rgba(100, 200, 255, 0.8)' 
              : 'rgba(255, 100, 100, 0.8)';
            ctx.value.lineWidth = 2;
            ctx.value.setLineDash([5, 3]);
            ctx.value.strokeRect(x, y, width, height);
            ctx.value.setLineDash([]);
          }
        };
        
        // 获取Canvas坐标
        const getCanvasPos = (event) => {
          const canvasEl = canvas.value;
          const rect = canvasEl.getBoundingClientRect();
          
          let clientX, clientY;
          if (event.type.includes('touch')) {
            clientX = event.touches[0].clientX;
            clientY = event.touches[0].clientY;
          } else {
            clientX = event.clientX;
            clientY = event.clientY;
          }
          
          return {
            x: clientX - rect.left,
            y: clientY - rect.top
          };
        };
        
        // 开始选择
        const startSelection = (event) => {
          event.preventDefault();
          const pos = getCanvasPos(event);
          startPos.value = { ...pos };
          endPos.value = { ...pos };
          isSelecting.value = true;
          
          // 确定选择模式(添加或移除)
          const canvasEl = canvas.value;
          const cellWidth = canvasEl.width / cols;
          const cellHeight = canvasEl.height / rows;
          
          const colIndex = Math.floor(pos.x / cellWidth);
          const rowIndex = Math.floor(pos.y / cellHeight);
          const index = rowIndex * cols + colIndex;
          
          if (index >= 0 && index < options.value.length) {
            // 如果点击的选项已选择,则设为移除模式,否则设为添加模式
            selectionMode.value = options.value[index].selected ? 'remove' : 'add';
            // 切换点击的选项状态
            options.value[index].selected = !options.value[index].selected;
          } else {
            selectionMode.value = 'add';
          }
          
          drawGrid();
        };
        
        // 更新选择
        const updateSelection = (event) => {
          if (!isSelecting.value) return;
          event.preventDefault();
          
          const pos = getCanvasPos(event);
          endPos.value = { ...pos };
          
          updateSelectedOptions();
          drawGrid();
        };
        
        // 结束选择
        const endSelection = (event) => {
          if (!isSelecting.value) return;
          event.preventDefault();
          
          isSelecting.value = false;
          drawGrid();
        };
        
        // 更新被选中的选项
        const updateSelectedOptions = () => {
          const canvasEl = canvas.value;
          const cellWidth = canvasEl.width / cols;
          const cellHeight = canvasEl.height / rows;
          
          // 计算选择框覆盖的区域
          const minX = Math.min(startPos.value.x, endPos.value.x);
          const maxX = Math.max(startPos.value.x, endPos.value.x);
          const minY = Math.min(startPos.value.y, endPos.value.y);
          const maxY = Math.max(startPos.value.y, endPos.value.y);
          
          // 计算覆盖的网格范围
          const startCol = Math.max(0, Math.floor(minX / cellWidth));
          const endCol = Math.min(cols - 1, Math.floor(maxX / cellWidth));
          const startRow = Math.max(0, Math.floor(minY / cellHeight));
          const endRow = Math.min(rows - 1, Math.floor(maxY / cellHeight));
          
          // 更新选项状态
          for (let row = startRow; row <= endRow; row++) {
            for (let col = startCol; col <= endCol; col++) {
              const index = row * cols + col;
              if (selectionMode.value === 'add') {
                options.value[index].selected = true;
              } else {
                options.value[index].selected = false;
              }
            }
          }
        };
        
        // 全选
        const selectAll = () => {
          options.value.forEach(opt => opt.selected = true);
          drawGrid();
        };
        
        // 清空选择
        const clearSelection = () => {
          options.value.forEach(opt => opt.selected = false);
          drawGrid();
        };
        
        // 切换选择模式
        const toggleSelectionMode = () => {
          selectionMode.value = selectionMode.value === 'add' ? 'remove' : 'add';
        };
        
        // 生命周期钩子
        onMounted(() => {
          initCanvas();
          window.addEventListener('resize', initCanvas);
        });
        
        return {
          canvas,
          selectedItems,
          selectedCount,
          selectionMode,
          startSelection,
          updateSelection,
          endSelection,
          selectAll,
          clearSelection,
          toggleSelectionMode
        };
      },
      template: `
        <div class="canvas-container">
          <canvas ref="canvas" 
                  @mousedown="startSelection"
                  @mousemove="updateSelection"
                  @mouseup="endSelection"
                  @mouseleave="endSelection"
                  @touchstart="startSelection"
                  @touchmove="updateSelection"
                  @touchend="endSelection"></canvas>
          <p class="instructions">PC端:点击并拖动鼠标进行选择 | 移动端:触摸并滑动进行选择</p>
          
          <div class="mode-indicator">
            <div class="mode" :class="{ 'active-mode': selectionMode === 'add' }">
              <div class="mode-color add-color"></div>
              <span>添加模式</span>
            </div>
            <div class="mode" :class="{ 'active-mode': selectionMode === 'remove' }">
              <div class="mode-color remove-color"></div>
              <span>移除模式</span>
            </div>
          </div>
        </div>
        
        <div class="info-panel">
          <h2>选择信息面板</h2>
          <div class="stats">
            <div class="stat">
              <div class="stat-value">{{ selectedCount }}</div>
              <div class="stat-label">已选选项</div>
            </div>
            <div class="stat">
              <div class="stat-value">96</div>
              <div class="stat-label">总选项</div>
            </div>
          </div>
          
          <div class="selected-items">
            <h3>已选选项 ({{ selectedCount }})</h3>
            <div v-if="selectedItems.length > 0" class="items-list">
              <div v-for="item in selectedItems" :key="item" class="item">{{ item }}</div>
            </div>
            <div v-else class="empty-message">暂无选择,请在左侧区域进行选择</div>
          </div>
          
          <div class="controls">
            <button @click="selectAll">全选</button>
            <button @click="clearSelection">清空</button>
            <button @click="toggleSelectionMode">
              {{ selectionMode === 'add' ? '切换到移除模式' : '切换到添加模式' }}
            </button>
          </div>
        </div>
      `
    });
    
    createApp({
      components: {
        CanvasSelector
      }
    }).mount('#app');
  </script>
</body>
</html>

网站公告

今日签到

点亮在社区的每一天
去签到