4人参与 • 2025-04-24 • Javascript
<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"> <!-- 右上区 (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> <!-- 左上区 (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"> <!-- 右下区 (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> <!-- 左下区 (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> <!-- 中线标识 --> <line :x1="viewboxwidth / 2" y1="50" :x2="viewboxwidth / 2" :y2="viewboxheight - 50" stroke="#78909c" stroke-width="1" stroke-dasharray="5,5" /> <!-- 分区标识 --> <text :x="viewboxwidth / 4" y="30" class="quadrant-label">右上区 (1-8)</text> <text :x="viewboxwidth * 3 / 4" y="30" class="quadrant-label">左上区 (9-16)</text> <text :x="viewboxwidth / 4" :y="viewboxheight - 20" class="quadrant-label">右下区 (17-24)</text> <text :x="viewboxwidth * 3 / 4" :y="viewboxheight - 20" class="quadrant-label">左下区 (25-32)</text> </svg> </div> <!-- 备注区域 --> <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"> 请点击牙齿进行选择 </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' }, // 新增的尺寸相关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 }) }) // 获取所有牙齿数据 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 '' } // 标准牙位布局数据 - 基于viewbox动态计算 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 } ] // 切换牙齿选择状态 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] }) } // 监听notes变化 watch(notes, () => { updatemodelvalue() }) // 监听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">提交数据</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(`已提交数据:\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>
到此这篇关于vue3 实现牙位图选择器的文章就介绍到这了,更多相关vue3 选择器内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论