From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/components/MagicCubeEditor/index.vue |  270 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 270 insertions(+), 0 deletions(-)

diff --git a/src/components/MagicCubeEditor/index.vue b/src/components/MagicCubeEditor/index.vue
new file mode 100644
index 0000000..6af4ca4
--- /dev/null
+++ b/src/components/MagicCubeEditor/index.vue
@@ -0,0 +1,270 @@
+<template>
+  <div class="relative">
+    <table class="cube-table">
+      <!-- 搴曞眰锛氶瓟鏂圭煩闃� -->
+      <tbody>
+        <tr v-for="(rowCubes, row) in cubes" :key="row">
+          <td
+            v-for="(cube, col) in rowCubes"
+            :key="col"
+            :class="['cube', { active: cube.active }]"
+            :style="{
+              width: `${cubeSize}px`,
+              height: `${cubeSize}px`
+            }"
+            @click="handleCubeClick(row, col)"
+            @mouseenter="handleCellHover(row, col)"
+          >
+            <Icon icon="ep-plus" />
+          </td>
+        </tr>
+      </tbody>
+      <!-- 椤跺眰锛氱儹鍖� -->
+      <div
+        v-for="(hotArea, index) in hotAreas"
+        :key="index"
+        class="hot-area"
+        :style="{
+          top: `${cubeSize * hotArea.top}px`,
+          left: `${cubeSize * hotArea.left}px`,
+          height: `${cubeSize * hotArea.height}px`,
+          width: `${cubeSize * hotArea.width}px`
+        }"
+        @click="handleHotAreaSelected(hotArea, index)"
+        @mouseover="exitHotAreaSelectMode"
+      >
+        <!-- 鍙充笂瑙掔儹鍖哄垹闄ゆ寜閽� -->
+        <div
+          v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
+          class="btn-delete"
+          @click="handleDeleteHotArea(index)"
+        >
+          <Icon icon="ep:circle-close-filled" />
+        </div>
+        <span v-if="hotArea.width">{{ `${hotArea.width}脳${hotArea.height}` }}</span>
+      </div>
+    </table>
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import * as vueTypes from 'vue-types'
+import { Point, Rect, isContains, isOverlap, createRect } from './util'
+
+// 榄旀柟缂栬緫鍣�
+// 鏈変袱閮ㄥ垎缁勬垚锛�
+// 1. 榄旀柟鐭╅樀锛氫綅浜庡簳灞傦紝鐢辨柟鍧楃粍浠剁殑浜岀淮琛ㄦ牸锛岀敤浜庡垱寤虹儹鍖�
+//    鎿嶄綔鏂规硶锛�
+//    1.1 鐐瑰嚮鍏朵腑涓�涓柟鍧楀氨浼氳繘鍏ョ儹鍖洪�夋嫨妯″紡
+//    1.2 鍐嶆鐐瑰嚮鍙﹀涓�涓柟鍧楁椂锛岀粨鏉熺儹鍖洪�夋嫨妯″紡
+//    1.3 鍦ㄤ袱涓柟鍧椾腑闂寸殑鍖哄煙鍒涘缓鐑尯
+//    濡傛灉涓ゆ鐐瑰嚮鐨勯兘鏄悓涓�鏂瑰潡锛屽氨鍙垱寤轰竴涓牸瀛愮殑鐑尯
+// 2. 鐑尯锛氫綅浜庨《灞傦紝閲囩敤缁濆瀹氫綅锛岃鐩栧湪榄旀柟鐭╅樀涓婇潰銆�
+defineOptions({ name: 'MagicCubeEditor' })
+
+/**
+ * 鏂瑰潡
+ * @property active 鏄惁婵�娲�
+ */
+type Cube = Point & { active: boolean }
+
+// 瀹氫箟灞炴��
+const props = defineProps({
+  // 鐑尯鍒楄〃
+  modelValue: vueTypes.array<any>().isRequired,
+  // 琛屾暟锛岄粯璁� 4 琛�
+  rows: propTypes.number.def(4),
+  // 鍒楁暟锛岄粯璁� 4 鍒�
+  cols: propTypes.number.def(4),
+  // 鏂瑰潡澶у皬锛屽崟浣峱x锛岄粯璁�75px
+  cubeSize: propTypes.number.def(75)
+})
+
+// 榄旀柟鐭╅樀锛氭墍鏈夌殑鏂瑰潡
+const cubes = ref<Cube[][]>([])
+// 鐩戝惉琛屾暟銆佸垪鏁板彉鍖�
+watch(
+  () => [props.rows, props.cols],
+  () => {
+    // 娓呯┖榄旀柟
+    cubes.value = []
+    if (!props.rows || !props.cols) return
+
+    // 鍒濆鍖栭瓟鏂�
+    for (let row = 0; row < props.rows; row++) {
+      cubes.value[row] = []
+      for (let col = 0; col < props.cols; col++) {
+        cubes.value[row].push({ x: col, y: row, active: false })
+      }
+    }
+  },
+  { immediate: true }
+)
+
+// 鐑尯鍒楄〃
+const hotAreas = ref<Rect[]>([])
+// 鍒濆鍖栫儹鍖�
+watch(
+  () => props.modelValue,
+  () => (hotAreas.value = props.modelValue || []),
+  { immediate: true }
+)
+
+// 鐑尯璧峰鏂瑰潡
+const hotAreaBeginCube = ref<Cube>()
+// 鏄惁寮�鍚簡鐑尯閫夋嫨妯″紡
+const isHotAreaSelectMode = () => !!hotAreaBeginCube.value
+/**
+ * 澶勭悊榧犳爣鐐瑰嚮鏂瑰潡
+ *
+ * @param currentRow 褰撳墠琛屽彿
+ * @param currentCol 褰撳墠鍒楀彿
+ */
+const handleCubeClick = (currentRow: number, currentCol: number) => {
+  const currentCube = cubes.value[currentRow][currentCol]
+  // 鎯呭喌1锛氳繘鍏ョ儹鍖洪�夋嫨妯″紡
+  if (!isHotAreaSelectMode()) {
+    hotAreaBeginCube.value = currentCube
+    hotAreaBeginCube.value.active = true
+    return
+  }
+
+  // 鎯呭喌2锛氱粨鏉熺儹鍖洪�夋嫨妯″紡
+  hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube))
+  // 缁撴潫鐑尯閫夋嫨妯″紡
+  exitHotAreaSelectMode()
+  // 鍒涘缓鍚庡氨閫変腑鐑尯
+  let hotAreaIndex = hotAreas.value.length - 1
+  handleHotAreaSelected(hotAreas.value[hotAreaIndex], hotAreaIndex)
+  // 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+  emitUpdateModelValue()
+}
+/**
+ * 澶勭悊榧犳爣缁忚繃鏂瑰潡
+ *
+ * @param currentRow 褰撳墠琛屽彿
+ * @param currentCol 褰撳墠鍒楀彿
+ */
+const handleCellHover = (currentRow: number, currentCol: number) => {
+  // 褰撳墠娌℃湁杩涘叆鐑尯閫夋嫨妯″紡
+  if (!isHotAreaSelectMode()) return
+
+  // 褰撳墠宸查�夌殑鍖哄煙
+  const currentSelectedArea = createRect(
+    hotAreaBeginCube.value!,
+    cubes.value[currentRow][currentCol]
+  )
+  // 鐑尯涓嶅厑璁搁噸鍙�
+  for (const hotArea of hotAreas.value) {
+    // 妫�鏌ユ槸鍚﹂噸鍙�
+    if (isOverlap(hotArea, currentSelectedArea)) {
+      // 缁撴潫鐑尯閫夋嫨妯″紡
+      exitHotAreaSelectMode()
+
+      return
+    }
+  }
+
+  // 婵�娲婚�変腑鍖哄煙鍐呴儴鐨勬柟鍧�
+  eachCube((_, __, cube) => {
+    cube.active = isContains(currentSelectedArea, cube)
+  })
+}
+/**
+ * 澶勭悊鐑尯鍒犻櫎
+ *
+ * @param index 鐑尯绱㈠紩
+ */
+const handleDeleteHotArea = (index: number) => {
+  hotAreas.value.splice(index, 1)
+  // 缁撴潫鐑尯閫夋嫨妯″紡
+  exitHotAreaSelectMode()
+  // 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+  emitUpdateModelValue()
+}
+
+// 鍙戦�佹ā鍨嬫洿鏂�
+const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
+// 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
+
+// 鐑尯閫変腑
+const selectedHotAreaIndex = ref(0)
+const handleHotAreaSelected = (hotArea: Rect, index: number) => {
+  selectedHotAreaIndex.value = index
+  emit('hotAreaSelected', hotArea, index)
+}
+
+/**
+ * 缁撴潫鐑尯閫夋嫨妯″紡
+ */
+function exitHotAreaSelectMode() {
+  // 绉婚櫎鏂瑰潡婵�娲绘爣璁�
+  eachCube((_, __, cube) => {
+    if (cube.active) {
+      cube.active = false
+    }
+  })
+
+  // 娓呴櫎璧风偣
+  hotAreaBeginCube.value = undefined
+}
+
+/**
+ * 杩唬榄旀柟鐭╅樀
+ * @param callback 鍥炶皟
+ */
+const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
+  for (let x = 0; x < cubes.value.length; x++) {
+    for (let y = 0; y < cubes.value[x].length; y++) {
+      callback(x, y, cubes.value[x][y])
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.cube-table {
+  position: relative;
+  border-spacing: 0;
+  border-collapse: collapse;
+
+  .cube {
+    border: 1px solid var(--el-border-color);
+    text-align: center;
+    color: var(--el-text-color-secondary);
+    cursor: pointer;
+    box-sizing: border-box;
+    &.active {
+      background: var(--el-color-primary-light-9);
+    }
+  }
+
+  .hot-area {
+    position: absolute;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border: 1px solid var(--el-color-primary);
+    background: var(--el-color-primary-light-8);
+    color: var(--el-color-primary);
+    box-sizing: border-box;
+    border-spacing: 0;
+    border-collapse: collapse;
+    cursor: pointer;
+
+    .btn-delete {
+      z-index: 1;
+      position: absolute;
+      top: -8px;
+      right: -8px;
+      height: 16px;
+      width: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 50%;
+      background-color: #fff;
+    }
+  }
+}
+</style>

--
Gitblit v1.8.0