文章目錄 Vue3 選擇器 Canvas 增加頁面性能 基于Vue3 Composition API和Canvas實現的交互式選擇器,支持PC端和移動端的拖動選擇、多選取消選擇功能 vue3組件封裝 html代碼
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' ) ;
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) ;
const initCanvas = ( ) => { if ( canvas. value == null ) return ; const canvasEl: HTMLCanvasElement = canvas. value; ctx. value = canvasEl. getContext ( '2d' ) ; 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 ) ; } } 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) ; }
} ;
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 ( '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" > < canvasref= "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: 450 px; height: 300 px; background: rgba ( 10 , 15 , 30 , 0.7 ) ; display: block; cursor: pointer; box- shadow: 0 4 px 20 px 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 ( ) { 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' ) ; 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) ; const initCanvas = ( ) => { const canvasEl = canvas. value; ctx. value = canvasEl. getContext ( '2d' ) ; 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 ) ; } } 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 ) ; } 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 ( [ ] ) ; } } ; 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>