cl-select-seat.uvue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. <template>
  2. <view class="cl-select-seat" :style="{ width: width + 'px', height: height + 'px' }">
  3. <view
  4. class="cl-select-seat__index"
  5. :style="{
  6. transform: `translateY(${translateY}px)`
  7. }"
  8. >
  9. <view
  10. class="cl-select-seat__index-item"
  11. v-for="i in rows"
  12. :key="i"
  13. :style="{
  14. height: seatSize * scale + 'px',
  15. marginTop: i == 1 ? 0 : seatGap * scale + 'px'
  16. }"
  17. >
  18. <cl-text
  19. color="white"
  20. :pt="{
  21. className: 'text-xs'
  22. }"
  23. >{{ i }}</cl-text
  24. >
  25. </view>
  26. </view>
  27. <canvas
  28. id="seatCanvas"
  29. class="cl-select-seat__canvas"
  30. @touchstart.stop.prevent="onTouchStart"
  31. @touchmove.stop.prevent="onTouchMove"
  32. @touchend.stop.prevent="onTouchEnd"
  33. ></canvas>
  34. </view>
  35. </template>
  36. <script setup lang="ts">
  37. import { assign, getColor, getDevicePixelRatio, isDark, isH5, isMp } from "@/cool";
  38. import { computed, getCurrentInstance, onMounted, ref, watch } from "vue";
  39. import { getIcon } from "../cl-icon/utils";
  40. import type { PropType } from "vue";
  41. import type { ClSelectSeatItem, ClSelectSeatValue } from "../../types";
  42. defineOptions({
  43. name: "cl-select-seat"
  44. });
  45. type TouchPoint = {
  46. x: number;
  47. y: number;
  48. identifier: number;
  49. };
  50. const props = defineProps({
  51. modelValue: {
  52. type: Array as PropType<ClSelectSeatValue[]>,
  53. default: () => []
  54. },
  55. rows: {
  56. type: Number,
  57. default: 0
  58. },
  59. cols: {
  60. type: Number,
  61. default: 0
  62. },
  63. seatGap: {
  64. type: Number,
  65. default: 8
  66. },
  67. borderRadius: {
  68. type: Number,
  69. default: 8
  70. },
  71. borderWidth: {
  72. type: Number,
  73. default: 1
  74. },
  75. minScale: {
  76. type: Number,
  77. default: 1
  78. },
  79. maxScale: {
  80. type: Number,
  81. default: 3
  82. },
  83. color: {
  84. type: String,
  85. default: () => getColor("surface-300")
  86. },
  87. darkColor: {
  88. type: String,
  89. default: () => getColor("surface-500")
  90. },
  91. bgColor: {
  92. type: String,
  93. default: () => "#ffffff"
  94. },
  95. darkBgColor: {
  96. type: String,
  97. default: () => getColor("surface-800")
  98. },
  99. borderColor: {
  100. type: String,
  101. default: () => getColor("surface-200")
  102. },
  103. darkBorderColor: {
  104. type: String,
  105. default: () => getColor("surface-600")
  106. },
  107. selectedBgColor: {
  108. type: String,
  109. default: () => getColor("primary-500")
  110. },
  111. selectedColor: {
  112. type: String,
  113. default: () => "#ffffff"
  114. },
  115. selectedIcon: {
  116. type: String,
  117. default: "check-line"
  118. },
  119. selectedImage: {
  120. type: String,
  121. default: ""
  122. },
  123. image: {
  124. type: String,
  125. default: ""
  126. },
  127. width: {
  128. type: Number,
  129. default: 0
  130. },
  131. height: {
  132. type: Number,
  133. default: 0
  134. },
  135. seats: {
  136. type: Array as PropType<ClSelectSeatItem[]>,
  137. default: () => []
  138. }
  139. });
  140. const emit = defineEmits(["update:modelValue", "seatClick", "move"]);
  141. const { proxy } = getCurrentInstance()!;
  142. // 画布渲染上下文
  143. let renderingContext: CanvasRenderingContext2D | null = null;
  144. let canvasContext: CanvasContext | null = null;
  145. // 画布偏移顶部
  146. let offsetTop = 0;
  147. // 画布偏移左侧
  148. let offsetLeft = 0;
  149. // 左侧边距
  150. let leftPadding = 0;
  151. // 座位数据
  152. let seats: ClSelectSeatItem[] = [];
  153. // 图片缓存
  154. const imageCache = new Map<string, Image>();
  155. // 画布变换参数
  156. const scale = ref(1);
  157. const translateX = ref(0);
  158. const translateY = ref(0);
  159. // 触摸状态
  160. let touchPoints: TouchPoint[] = [];
  161. let lastDistance = 0;
  162. let lastCenterX = 0;
  163. let lastCenterY = 0;
  164. // 是否发生过拖动或缩放,用于防止误触选座
  165. let hasMoved = false;
  166. // 起始触摸点,用于判断是否发生拖动
  167. let startTouchX = 0;
  168. let startTouchY = 0;
  169. // 拖动阈值(像素)
  170. const moveThreshold = 10;
  171. // 根据视图大小计算座位大小
  172. const seatSize = computed(() => {
  173. const availableWidth =
  174. props.width - (props.cols - 1) * props.seatGap - props.borderWidth * props.cols;
  175. const seatWidth = availableWidth / props.cols;
  176. const availableHeight =
  177. props.height - (props.rows - 1) * props.seatGap - props.borderWidth * props.rows;
  178. const seatHeight = availableHeight / props.rows;
  179. return Math.min(seatWidth, seatHeight);
  180. });
  181. // 是否选中
  182. function isSelected(row: number, col: number): boolean {
  183. return props.modelValue.some((item) => item.row == row && item.col == col);
  184. }
  185. // 初始化座位数据
  186. function initSeats() {
  187. seats = [];
  188. if (props.seats.length > 0) {
  189. props.seats.forEach((e) => {
  190. seats.push({
  191. row: e.row,
  192. col: e.col,
  193. disabled: e.disabled,
  194. empty: e.empty,
  195. bgColor: e.bgColor,
  196. borderColor: e.borderColor,
  197. selectedBgColor: e.selectedBgColor,
  198. selectedColor: e.selectedColor,
  199. selectedIcon: e.selectedIcon,
  200. selectedImage: e.selectedImage,
  201. icon: e.icon,
  202. image: e.image,
  203. color: e.color
  204. } as ClSelectSeatItem);
  205. });
  206. } else {
  207. for (let row = 0; row < props.rows; row++) {
  208. for (let col = 0; col < props.cols; col++) {
  209. seats.push({
  210. row,
  211. col
  212. } as ClSelectSeatItem);
  213. }
  214. }
  215. }
  216. }
  217. // 加载图片
  218. function loadImage(src: string): Promise<Image> {
  219. return new Promise((resolve, reject) => {
  220. if (imageCache.has(src)) {
  221. resolve(imageCache.get(src)!);
  222. return;
  223. }
  224. // 创建图片
  225. let img: Image;
  226. // 微信小程序环境创建图片
  227. // #ifdef MP-WEIXIN || APP-HARMONY
  228. img = canvasContext!.createImage();
  229. // #endif
  230. // 其他环境创建图片
  231. // #ifndef MP-WEIXIN || APP-HARMONY
  232. img = new Image();
  233. // #endif
  234. img.src = src;
  235. img.onload = () => {
  236. imageCache.set(src, img);
  237. resolve(img);
  238. };
  239. img.onerror = () => {
  240. reject(new Error("Image load failed"));
  241. };
  242. });
  243. }
  244. // 预加载所有图片
  245. async function preloadImages() {
  246. const imagesToLoad: string[] = [];
  247. // 收集所有需要加载的图片
  248. if (props.image != "") imagesToLoad.push(props.image);
  249. if (props.selectedImage != "") imagesToLoad.push(props.selectedImage);
  250. seats.forEach((seat) => {
  251. if (seat.image != null) imagesToLoad.push(seat.image);
  252. if (seat.selectedImage != null) imagesToLoad.push(seat.selectedImage);
  253. });
  254. // 去重并加载
  255. const uniqueImages = [...new Set(imagesToLoad)];
  256. await Promise.all(uniqueImages.map((src) => loadImage(src).catch(() => {})));
  257. }
  258. // 居中显示
  259. function centerView() {
  260. if (renderingContext == null) return;
  261. const contentWidth = props.cols * (seatSize.value + props.seatGap) - props.seatGap;
  262. const contentHeight = props.rows * (seatSize.value + props.seatGap) - props.seatGap;
  263. translateX.value = (renderingContext!.canvas.offsetWidth - contentWidth) / 2;
  264. translateY.value = (renderingContext!.canvas.offsetHeight - contentHeight) / 2;
  265. leftPadding = translateX.value;
  266. }
  267. // 限制平移范围
  268. function constrainTranslate() {
  269. if (renderingContext == null) return;
  270. // 计算内容区(座位区域)宽高
  271. const contentWidth = props.cols * (seatSize.value + props.seatGap) - props.seatGap;
  272. const contentHeight = props.rows * (seatSize.value + props.seatGap) - props.seatGap;
  273. // 获取画布的显示区域宽高
  274. const viewWidth = renderingContext!.canvas.offsetWidth;
  275. const viewHeight = renderingContext!.canvas.offsetHeight;
  276. // 计算缩放后的实际宽高
  277. const scaledWidth = contentWidth * scale.value;
  278. const scaledHeight = contentHeight * scale.value;
  279. // 允许的最大空白比例(视图的 20%)
  280. const marginRatio = 0.2;
  281. const marginX = viewWidth * marginRatio;
  282. const marginY = viewHeight * marginRatio;
  283. // 水平方向边界限制:内容左边缘最多在视图左侧 20% 处,右边缘最多在视图右侧 80% 处
  284. const minTranslateX = viewWidth * (1 - marginRatio) - scaledWidth;
  285. const maxTranslateX = marginX;
  286. translateX.value = Math.max(minTranslateX, Math.min(maxTranslateX, translateX.value));
  287. // 垂直方向边界限制:内容上边缘最多在视图顶部 20% 处,下边缘最多在视图底部 80% 处
  288. const minTranslateY = viewHeight * (1 - marginRatio) - scaledHeight;
  289. const maxTranslateY = marginY;
  290. translateY.value = Math.max(minTranslateY, Math.min(maxTranslateY, translateY.value));
  291. }
  292. // 绘制圆角矩形
  293. function drawRoundRect(
  294. ctx: CanvasRenderingContext2D,
  295. x: number,
  296. y: number,
  297. width: number,
  298. height: number,
  299. radius: number
  300. ) {
  301. ctx.beginPath();
  302. ctx.moveTo(x + radius, y);
  303. ctx.lineTo(x + width - radius, y);
  304. ctx.arc(x + width - radius, y + radius, radius, Math.PI * 1.5, Math.PI * 2);
  305. ctx.lineTo(x + width, y + height - radius);
  306. ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI * 0.5);
  307. ctx.lineTo(x + radius, y + height);
  308. ctx.arc(x + radius, y + height - radius, radius, Math.PI * 0.5, Math.PI);
  309. ctx.lineTo(x, y + radius);
  310. ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
  311. ctx.closePath();
  312. }
  313. // 绘制单个座位
  314. function drawSeat(seat: ClSelectSeatItem) {
  315. if (renderingContext == null) return;
  316. // 如果是空位,不渲染但保留位置
  317. if (seat.empty == true) return;
  318. const x = seat.col * (seatSize.value + props.seatGap);
  319. const y = seat.row * (seatSize.value + props.seatGap);
  320. // 计算图标中心位置(使用整数避免亚像素渲染问题)
  321. const centerX = Math.round(x + seatSize.value / 2);
  322. const centerY = Math.round(y + seatSize.value / 2);
  323. const fontSize = Math.round(seatSize.value * 0.6);
  324. if (isSelected(seat.row, seat.col)) {
  325. // 优先使用图片,否则使用背景+图标
  326. const selectedImageSrc = seat.selectedImage ?? props.selectedImage;
  327. if (selectedImageSrc != "" && imageCache.has(selectedImageSrc)) {
  328. // 使用圆角裁剪绘制图片
  329. renderingContext!.save();
  330. drawRoundRect(renderingContext!, x, y, seatSize.value, seatSize.value, props.borderRadius);
  331. renderingContext!.clip();
  332. const img = imageCache.get(selectedImageSrc)!;
  333. renderingContext!.drawImage(img, x, y, seatSize.value, seatSize.value);
  334. renderingContext!.restore();
  335. } else {
  336. // 绘制选中背景
  337. renderingContext!.fillStyle = seat.selectedBgColor ?? props.selectedBgColor;
  338. drawRoundRect(renderingContext!, x, y, seatSize.value, seatSize.value, props.borderRadius);
  339. renderingContext!.fill();
  340. // 绘制选中图标
  341. const { text, font } = getIcon(seat.selectedIcon ?? props.selectedIcon);
  342. if (text != "") {
  343. renderingContext!.fillStyle = seat.selectedColor ?? props.selectedColor;
  344. renderingContext!.font = `${fontSize}px ${font}`;
  345. renderingContext!.textAlign = "center";
  346. renderingContext!.textBaseline = "middle";
  347. renderingContext!.fillText(text, centerX, centerY);
  348. }
  349. }
  350. } else {
  351. // 优先使用图片,否则使用背景+边框+图标
  352. const imageSrc = seat.image ?? props.image;
  353. if (imageSrc != "" && imageCache.has(imageSrc)) {
  354. // 使用圆角裁剪绘制图片
  355. renderingContext!.save();
  356. drawRoundRect(renderingContext!, x, y, seatSize.value, seatSize.value, props.borderRadius);
  357. renderingContext!.clip();
  358. const img = imageCache.get(imageSrc)!;
  359. renderingContext!.drawImage(img, x, y, seatSize.value, seatSize.value);
  360. renderingContext!.restore();
  361. } else {
  362. // 绘制未选中背景
  363. const bgColor = seat.bgColor ?? (isDark.value ? props.darkBgColor : props.bgColor);
  364. renderingContext!.fillStyle = bgColor;
  365. drawRoundRect(renderingContext!, x, y, seatSize.value, seatSize.value, props.borderRadius);
  366. renderingContext!.fill();
  367. // 绘制边框
  368. renderingContext!.strokeStyle =
  369. seat.borderColor ?? (isDark.value ? props.darkBorderColor : props.borderColor);
  370. renderingContext!.lineWidth = props.borderWidth;
  371. drawRoundRect(renderingContext!, x, y, seatSize.value, seatSize.value, props.borderRadius);
  372. renderingContext!.stroke();
  373. // 绘制默认图标
  374. if (seat.icon != null) {
  375. const { text, font } = getIcon(seat.icon);
  376. if (text != "") {
  377. renderingContext!.fillStyle =
  378. seat.color ?? (isDark.value ? props.darkColor : props.color);
  379. renderingContext!.font = `${fontSize}px ${font}`;
  380. renderingContext!.textAlign = "center";
  381. renderingContext!.textBaseline = "middle";
  382. renderingContext!.fillText(text, centerX, centerY);
  383. }
  384. }
  385. }
  386. }
  387. }
  388. // 绘制画布
  389. function draw() {
  390. if (renderingContext == null) return;
  391. // 清空画布
  392. renderingContext!.save();
  393. renderingContext!.setTransform(1, 0, 0, 1, 0, 0);
  394. renderingContext!.clearRect(
  395. 0,
  396. 0,
  397. renderingContext!.canvas.width,
  398. renderingContext!.canvas.height
  399. );
  400. renderingContext!.restore();
  401. // 应用变换
  402. renderingContext!.save();
  403. renderingContext!.translate(translateX.value, translateY.value);
  404. renderingContext!.scale(scale.value, scale.value);
  405. // 绘制座位
  406. seats.forEach((seat) => {
  407. drawSeat(seat);
  408. });
  409. renderingContext!.restore();
  410. }
  411. // 转换触摸点数据
  412. function getTouches(touches: UniTouch[]): TouchPoint[] {
  413. const result: TouchPoint[] = [];
  414. for (let i = 0; i < touches.length; i++) {
  415. const touch = touches[i];
  416. result.push({
  417. x: touch.clientX,
  418. y: touch.clientY,
  419. identifier: touch.identifier
  420. } as TouchPoint);
  421. }
  422. return result;
  423. }
  424. // 更新画布偏移量
  425. function updateOffset() {
  426. uni.createSelectorQuery()
  427. .in(proxy)
  428. .select("#seatCanvas")
  429. .boundingClientRect((rect) => {
  430. offsetTop = (rect as NodeInfo).top ?? 0;
  431. offsetLeft = (rect as NodeInfo).left ?? 0;
  432. })
  433. .exec();
  434. }
  435. // 计算两点距离
  436. function getTouchDistance(p1: TouchPoint, p2: TouchPoint): number {
  437. const dx = p2.x - p1.x;
  438. const dy = p2.y - p1.y;
  439. return Math.sqrt(dx * dx + dy * dy);
  440. }
  441. // 计算两点中心
  442. function getTouchCenter(p1: TouchPoint, p2: TouchPoint): TouchPoint {
  443. return {
  444. x: (p1.x + p2.x) / 2,
  445. y: (p1.y + p2.y) / 2,
  446. identifier: 0
  447. };
  448. }
  449. // 根据坐标获取座位
  450. function getSeatAtPoint(screenX: number, screenY: number): ClSelectSeatItem | null {
  451. // 转换为相对坐标
  452. const relativeX = screenX - offsetLeft;
  453. const relativeY = screenY - offsetTop - (isH5() ? 45 : 0);
  454. // 转换为画布坐标
  455. const canvasX = (relativeX - translateX.value) / scale.value;
  456. const canvasY = (relativeY - translateY.value) / scale.value;
  457. // 计算行列
  458. const col = Math.floor(canvasX / (seatSize.value + props.seatGap));
  459. const row = Math.floor(canvasY / (seatSize.value + props.seatGap));
  460. // 检查有效性
  461. if (row >= 0 && row < props.rows && col >= 0 && col < props.cols) {
  462. const localX = canvasX - col * (seatSize.value + props.seatGap);
  463. const localY = canvasY - row * (seatSize.value + props.seatGap);
  464. // 检查是否在座位内(排除间隙)
  465. if (localX >= 0 && localX <= seatSize.value && localY >= 0 && localY <= seatSize.value) {
  466. const index = row * props.cols + col;
  467. return seats[index];
  468. }
  469. }
  470. return null;
  471. }
  472. // 设置座位
  473. function setSeat(row: number, col: number, data: UTSJSONObject) {
  474. const index = row * props.cols + col;
  475. if (index >= 0 && index < seats.length) {
  476. assign(seats[index], data);
  477. draw();
  478. }
  479. }
  480. // 获取所有座位
  481. function getSeats(): ClSelectSeatItem[] {
  482. return seats;
  483. }
  484. // 初始化 Canvas
  485. function initCanvas() {
  486. uni.createCanvasContextAsync({
  487. id: "seatCanvas",
  488. component: proxy,
  489. success: (context: CanvasContext) => {
  490. canvasContext = context;
  491. renderingContext = context.getContext("2d")!;
  492. // 适配高清屏
  493. const dpr = getDevicePixelRatio();
  494. renderingContext!.canvas.width = renderingContext!.canvas.offsetWidth * dpr;
  495. renderingContext!.canvas.height = renderingContext!.canvas.offsetHeight * dpr;
  496. renderingContext!.scale(dpr, dpr);
  497. initSeats();
  498. centerView();
  499. // 预加载图片后再绘制
  500. preloadImages().finally(() => {
  501. draw();
  502. updateOffset();
  503. });
  504. }
  505. });
  506. }
  507. // 触摸开始
  508. function onTouchStart(e: UniTouchEvent) {
  509. updateOffset();
  510. touchPoints = getTouches(e.touches);
  511. hasMoved = false;
  512. // 记录起始触摸点
  513. if (touchPoints.length == 1) {
  514. startTouchX = touchPoints[0].x;
  515. startTouchY = touchPoints[0].y;
  516. }
  517. // 记录双指初始状态
  518. if (touchPoints.length == 2) {
  519. hasMoved = true; // 双指操作直接标记为已移动
  520. lastDistance = getTouchDistance(touchPoints[0], touchPoints[1]);
  521. const center = getTouchCenter(touchPoints[0], touchPoints[1]);
  522. lastCenterX = center.x;
  523. lastCenterY = center.y;
  524. }
  525. }
  526. // 触摸移动
  527. function onTouchMove(e: UniTouchEvent) {
  528. const currentTouches = getTouches(e.touches);
  529. // 双指缩放
  530. if (currentTouches.length == 2 && touchPoints.length == 2) {
  531. hasMoved = true;
  532. const currentDistance = getTouchDistance(currentTouches[0], currentTouches[1]);
  533. const currentCenter = getTouchCenter(currentTouches[0], currentTouches[1]);
  534. // 计算缩放
  535. const scaleChange = currentDistance / lastDistance;
  536. const newScale = Math.max(
  537. props.minScale,
  538. Math.min(props.maxScale, scale.value * scaleChange)
  539. );
  540. // 以触摸中心为基准缩放
  541. const scaleDiff = newScale - scale.value;
  542. translateX.value -= (currentCenter.x - translateX.value) * (scaleDiff / scale.value);
  543. translateY.value -= (currentCenter.y - translateY.value) * (scaleDiff / scale.value);
  544. scale.value = newScale;
  545. lastDistance = currentDistance;
  546. // 计算平移
  547. const dx = currentCenter.x - lastCenterX;
  548. const dy = currentCenter.y - lastCenterY;
  549. translateX.value += dx;
  550. translateY.value += dy;
  551. lastCenterX = currentCenter.x;
  552. lastCenterY = currentCenter.y;
  553. } else if (currentTouches.length == 1 && touchPoints.length == 1) {
  554. // 单指拖动
  555. const dx = currentTouches[0].x - touchPoints[0].x;
  556. const dy = currentTouches[0].y - touchPoints[0].y;
  557. translateX.value += dx;
  558. translateY.value += dy;
  559. // 判断是否超过拖动阈值
  560. const totalDx = currentTouches[0].x - startTouchX;
  561. const totalDy = currentTouches[0].y - startTouchY;
  562. if (Math.abs(totalDx) > moveThreshold || Math.abs(totalDy) > moveThreshold) {
  563. hasMoved = true;
  564. }
  565. }
  566. // 限制平移范围
  567. constrainTranslate();
  568. // 绘制
  569. draw();
  570. // 触发移动事件
  571. emit("move", {
  572. translateX: translateX.value,
  573. translateY: translateY.value,
  574. scale: scale.value,
  575. screenTranslateX: translateX.value - leftPadding + (props.width * (scale.value - 1)) / 2
  576. });
  577. // 更新触摸点
  578. touchPoints = currentTouches;
  579. }
  580. // 触摸结束
  581. function onTouchEnd(e: UniTouchEvent) {
  582. const changedTouches = getTouches(e.changedTouches);
  583. // 单击选座(未发生拖动或缩放时才触发)
  584. if (changedTouches.length == 1 && touchPoints.length == 1 && !hasMoved) {
  585. const touch = changedTouches[0];
  586. const seat = getSeatAtPoint(touch.x, touch.y);
  587. if (seat != null && seat.disabled != true && seat.empty != true) {
  588. let value: ClSelectSeatValue[] = [];
  589. if (isSelected(seat.row, seat.col)) {
  590. value = props.modelValue.filter(
  591. (item) => !(item.row == seat.row && item.col == seat.col)
  592. );
  593. } else {
  594. value = [
  595. ...props.modelValue,
  596. { row: seat.row, col: seat.col } as ClSelectSeatValue
  597. ];
  598. }
  599. emit("update:modelValue", value);
  600. emit("seatClick", seat);
  601. }
  602. }
  603. touchPoints = getTouches(e.touches);
  604. // 所有手指抬起后重置状态
  605. if (touchPoints.length == 0) {
  606. hasMoved = false;
  607. }
  608. }
  609. // 监听选中变化
  610. watch(
  611. computed<ClSelectSeatValue[]>(() => props.modelValue),
  612. () => {
  613. draw();
  614. },
  615. { deep: true }
  616. );
  617. // 监听暗色模式变化
  618. watch(
  619. isDark,
  620. () => {
  621. draw();
  622. },
  623. { deep: true }
  624. );
  625. // 监听座位数据变化
  626. watch(
  627. computed<ClSelectSeatItem[]>(() => props.seats),
  628. () => {
  629. initSeats();
  630. draw();
  631. },
  632. { deep: true }
  633. );
  634. onMounted(() => {
  635. initCanvas();
  636. });
  637. defineExpose({
  638. setSeat,
  639. getSeats,
  640. draw
  641. });
  642. </script>
  643. <style lang="scss" scoped>
  644. .cl-select-seat {
  645. @apply relative;
  646. &__canvas {
  647. @apply h-full w-full;
  648. }
  649. &__index {
  650. @apply absolute top-0 left-1 z-10 rounded-xl;
  651. background-color: rgba(0, 0, 0, 0.4);
  652. &-item {
  653. @apply flex flex-col items-center justify-center;
  654. width: 36rpx;
  655. }
  656. }
  657. }
  658. </style>