vue3 實現(xiàn)牙位圖選擇器的實例代碼
更新時間:2025年04月18日 09:19:33 作者:A_ugust__
這篇文章主要介紹了vue3 實現(xiàn)牙位圖選擇器的實例代碼,代碼簡單易懂,需要的朋友參考下吧
效果圖
封裝組件:ToothChart.vue
<template> <div class="tooth-chart-container"> <div class="tooth-chart"> <svg :width="computedWidth" :height="computedHeight" :viewBox="`0 0 ${viewBoxWidth} ${viewBoxHeight}`" xmlns="http://www.w3.org/2000/svg"> <!-- 上頜牙齒 --> <g class="upper-jaw"> <!-- 右上區(qū) (1-8) --> <g v-for="tooth in upperRightTeeth" :key="tooth.number"> <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius" :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]" @click="toggleTooth(tooth.number)" /> <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number" @click="toggleTooth(tooth.number)"> {{ tooth.number }} </text> </g> <!-- 左上區(qū) (9-16) --> <g v-for="tooth in upperLeftTeeth" :key="tooth.number"> <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius" :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]" @click="toggleTooth(tooth.number)" /> <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number" @click="toggleTooth(tooth.number)"> {{ tooth.number }} </text> </g> </g> <!-- 下頜牙齒 --> <g class="lower-jaw"> <!-- 右下區(qū) (17-24) --> <g v-for="tooth in lowerRightTeeth" :key="tooth.number"> <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius" :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]" @click="toggleTooth(tooth.number)" /> <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number" @click="toggleTooth(tooth.number)"> {{ tooth.number }} </text> </g> <!-- 左下區(qū) (25-32) --> <g v-for="tooth in lowerLeftTeeth" :key="tooth.number"> <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius" :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]" @click="toggleTooth(tooth.number)" /> <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number" @click="toggleTooth(tooth.number)"> {{ tooth.number }} </text> </g> </g> <!-- 中線標(biāo)識 --> <line :x1="viewBoxWidth / 2" y1="50" :x2="viewBoxWidth / 2" :y2="viewBoxHeight - 50" stroke="#78909C" stroke-width="1" stroke-dasharray="5,5" /> <!-- 分區(qū)標(biāo)識 --> <text :x="viewBoxWidth / 4" y="30" class="quadrant-label">右上區(qū) (1-8)</text> <text :x="viewBoxWidth * 3 / 4" y="30" class="quadrant-label">左上區(qū) (9-16)</text> <text :x="viewBoxWidth / 4" :y="viewBoxHeight - 20" class="quadrant-label">右下區(qū) (17-24)</text> <text :x="viewBoxWidth * 3 / 4" :y="viewBoxHeight - 20" class="quadrant-label">左下區(qū) (25-32)</text> </svg> </div> <!-- 備注區(qū)域 --> <div class="notes-section"> <div v-if="selectedTeeth.length > 0"> <h3>選中牙齒: {{ selectedTeethWithPosition.join(', ') }}</h3> <textarea v-model="notes" placeholder="請輸入治療備注..." class="notes-textarea"></textarea> </div> <div v-else class="no-selection"> 請點擊牙齒進(jìn)行選擇 </div> </div> </div> </template> <script setup> import { ref, watch, computed } from 'vue' const props = defineProps({ modelValue: { type: Object, default: () => ({ selectedTeeth: [], notes: '' }) }, width: { type: [Number, String], default: '100%' }, height: { type: [Number, String], default: '600' }, // 新增的尺寸相關(guān)props viewBoxWidth: { type: Number, default: 1000 }, viewBoxHeight: { type: Number, default: 600 }, toothWidth: { type: Number, default: 40 }, toothHeight: { type: Number, default: 60 }, toothRadius: { type: Number, default: 5 } }) const emit = defineEmits(['update:modelValue']) const selectedTeeth = ref([...props.modelValue.selectedTeeth]) const notes = ref(props.modelValue.notes) // 計算屬性 const computedWidth = computed(() => typeof props.width === 'number' ? `${props.width}px` : props.width) const computedHeight = computed(() => typeof props.height === 'number' ? `${props.height}px` : props.height) // 計算選中牙齒及其位置信息 const selectedTeethWithPosition = computed(() => { return selectedTeeth.value.map(num => { const tooth = getAllTeeth().find(t => t.number === num) return tooth ? `${num}(${getPositionName(num)})` : num }) }) // 獲取所有牙齒數(shù)據(jù) const getAllTeeth = () => [...upperRightTeeth, ...upperLeftTeeth, ...lowerRightTeeth, ...lowerLeftTeeth] // 獲取牙齒位置名稱 const getPositionName = (toothNumber) => { if (toothNumber >= 1 && toothNumber <= 8) return '右上' if (toothNumber >= 9 && toothNumber <= 16) return '左上' if (toothNumber >= 17 && toothNumber <= 24) return '右下' if (toothNumber >= 25 && toothNumber <= 32) return '左下' return '' } // 標(biāo)準(zhǔn)牙位布局?jǐn)?shù)據(jù) - 基于viewBox動態(tài)計算 const upperRightTeeth = [ { number: 1, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 }, { number: 2, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 }, { number: 3, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: 50 }, { number: 4, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 + props.toothHeight + 10 }, { number: 5, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 + props.toothHeight + 10 }, { number: 6, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: 50 + props.toothHeight + 10 }, { number: 7, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 + (props.toothHeight + 10) * 2 }, { number: 8, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 + (props.toothHeight + 10) * 2 } ] const upperLeftTeeth = [ { number: 9, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 + (props.toothHeight + 10) * 2 }, { number: 10, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 + (props.toothHeight + 10) * 2 }, { number: 11, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 + props.toothHeight + 10 }, { number: 12, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 + props.toothHeight + 10 }, { number: 13, x: props.viewBoxWidth / 2 + props.toothWidth, y: 50 + props.toothHeight + 10 }, { number: 14, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 }, { number: 15, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 }, { number: 16, x: props.viewBoxWidth / 2 + props.toothWidth, y: 50 } ] const lowerRightTeeth = [ { number: 17, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 }, { number: 18, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 }, { number: 19, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 }, { number: 20, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 21, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 22, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 23, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 }, { number: 24, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 } ] const lowerLeftTeeth = [ { number: 25, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 }, { number: 26, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 }, { number: 27, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 28, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 29, x: props.viewBoxWidth / 2 + props.toothWidth, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 }, { number: 30, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 }, { number: 31, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 }, { number: 32, x: props.viewBoxWidth / 2 + props.toothWidth, y: props.viewBoxHeight / 2 + 20 } ] // 切換牙齒選擇狀態(tài) const toggleTooth = (toothNumber) => { const index = selectedTeeth.value.indexOf(toothNumber) if (index === -1) { selectedTeeth.value.push(toothNumber) } else { selectedTeeth.value.splice(index, 1) } updateModelValue() } // 更新模型值 const updateModelValue = () => { emit('update:modelValue', { selectedTeeth: [...selectedTeeth.value], notes: notes.value, selectedTeethWithPosition: [...selectedTeethWithPosition.value] }) } // 監(jiān)聽notes變化 watch(notes, () => { updateModelValue() }) // 監(jiān)聽props變化 watch(() => props.modelValue, (newVal) => { if (JSON.stringify(newVal.selectedTeeth) !== JSON.stringify(selectedTeeth.value)) { selectedTeeth.value = [...newVal.selectedTeeth] } if (newVal.notes !== notes.value) { notes.value = newVal.notes } }, { deep: true }) // 暴露方法 defineExpose({ clearSelection: () => { selectedTeeth.value = [] notes.value = '' updateModelValue() }, getSelectedTeeth: () => [...selectedTeeth.value], getSelectedTeethWithPosition: () => [...selectedTeethWithPosition.value], getNotes: () => notes.value }) </script> <style scoped> .tooth-chart-container { display: flex; flex-direction: column; gap: 20px; font-family: 'Arial', sans-serif; max-width: 100%; margin: 0 auto; } .tooth-chart { border: 1px solid #e0e0e0; border-radius: 12px; overflow: hidden; background-color: #f8f9fa; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); } .tooth { fill: #ffffff; stroke: #90a4ae; stroke-width: 1.5; cursor: pointer; transition: all 0.3s ease; } .tooth:hover { fill: #e3f2fd; stroke: #42a5f5; } .tooth.selected { fill: #bbdefb; stroke: #1e88e5; stroke-width: 2; filter: drop-shadow(0 0 4px rgba(30, 136, 229, 0.4)); } .tooth-number { font-size: 22px; font-weight: 600; text-anchor: middle; cursor: pointer; user-select: none; fill: #37474f; } .quadrant-label { font-size: 26px; fill: #78909C; text-anchor: middle; font-weight: 500; } .notes-section { padding: 20px; border: 1px solid #e0e0e0; border-radius: 12px; background-color: #f8f9fa; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); } .notes-section h3 { margin-top: 0; margin-bottom: 15px; color: #263238; font-size: 18px; } .notes-textarea { width: 100%; min-height: 120px; padding: 12px; border: 1px solid #cfd8dc; border-radius: 8px; resize: vertical; font-family: inherit; font-size: 14px; line-height: 1.5; transition: border-color 0.3s; } .notes-textarea:focus { outline: none; border-color: #42a5f5; box-shadow: 0 0 0 2px rgba(66, 165, 245, 0.2); } .no-selection { color: #90a4ae; text-align: center; padding: 30px; font-size: 16px; } </style>
使用示例:
<template> <div class="demo-container"> <h1>牙位圖選擇器</h1> <ToothChart v-model="toothData" :width="chartWidth" :height="chartHeight" :tooth-width="toothWidth" :tooth-height="toothHeight" /> <div class="actions"> <button @click="clearSelection">清除選擇</button> <button @click="submitData">提交數(shù)據(jù)</button> </div> </div> </template> <script setup> import { ref } from 'vue' import ToothChart from '@/components/ToothChart.vue'; const toothData = ref({ selectedTeeth: [], notes: '', selectedTeethWithPosition: [] }) const chartWidth = ref('100%') const chartHeight = ref('500px') const toothWidth = ref(40) const toothHeight = ref(60) const clearSelection = () => { toothData.value = { selectedTeeth: [], notes: '', selectedTeethWithPosition: [] } } const submitData = () => { alert(`已提交數(shù)據(jù):\n選中牙齒: ${toothData.value.selectedTeethWithPosition.join(', ')}\n備注: ${toothData.value.notes}`) } </script> <style scoped lang="scss"> .demo-container { max-width: 1000px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; } .actions { display: flex; gap: 10px; margin: 20px 0; } .actions button { padding: 8px 16px; background: #42a5f5; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.3s; } .actions button:hover { background: #1e88e5; } </style>
到此這篇關(guān)于vue3 實現(xiàn)牙位圖選擇器的文章就介紹到這了,更多相關(guān)vue3 選擇器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue判斷數(shù)組內(nèi)是否存在某一項的兩種方法
這篇文章主要介紹了Vue判斷數(shù)組內(nèi)是否存在某一項,今天給大家分享兩種方法,分別是findIndex()和 indexOf()方法,本文結(jié)合實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07vue 組件內(nèi)獲取actions的response方式
今天小編就為大家分享一篇vue 組件內(nèi)獲取actions的response方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11Vue引入highCharts實現(xiàn)數(shù)據(jù)可視化
這篇文章主要為大家詳細(xì)介紹了Vue引入highCharts實現(xiàn)數(shù)據(jù)可視化,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03