• 设为首页
  • 收藏本站
  • 积分充值
  • VIP赞助
  • 手机版
  • 微博
  • 微信
    微信公众号 添加方式:
    1:搜索微信号(888888
    2:扫描左侧二维码
  • 快捷导航
    福建二哥 门户 查看主题

    vue3 实现牙位图选择器的实例代码

    发布者: 涵韵 | 发布时间: 2025-6-16 07:41| 查看数: 60| 评论数: 0|帖子模式

    效果图



    封装组件:ToothChart.vue
    1. <template>
    2.   <div class="tooth-chart-container">
    3.     <div class="tooth-chart">
    4.       <svg :width="computedWidth" :height="computedHeight" :viewBox="`0 0 ${viewBoxWidth} ${viewBoxHeight}`"
    5.         xmlns="http://www.w3.org/2000/svg">
    6.         <!-- 上颌牙齿 -->
    7.         <g class="upper-jaw">
    8.           <!-- 右上区 (1-8) -->
    9.           <g v-for="tooth in upperRightTeeth" :key="tooth.number">
    10.             <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius"
    11.               :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]"
    12.               @click="toggleTooth(tooth.number)" />
    13.             <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number"
    14.               @click="toggleTooth(tooth.number)">
    15.               {{ tooth.number }}
    16.             </text>
    17.           </g>
    18.           <!-- 左上区 (9-16) -->
    19.           <g v-for="tooth in upperLeftTeeth" :key="tooth.number">
    20.             <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius"
    21.               :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]"
    22.               @click="toggleTooth(tooth.number)" />
    23.             <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number"
    24.               @click="toggleTooth(tooth.number)">
    25.               {{ tooth.number }}
    26.             </text>
    27.           </g>
    28.         </g>
    29.         <!-- 下颌牙齿 -->
    30.         <g class="lower-jaw">
    31.           <!-- 右下区 (17-24) -->
    32.           <g v-for="tooth in lowerRightTeeth" :key="tooth.number">
    33.             <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius"
    34.               :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]"
    35.               @click="toggleTooth(tooth.number)" />
    36.             <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number"
    37.               @click="toggleTooth(tooth.number)">
    38.               {{ tooth.number }}
    39.             </text>
    40.           </g>
    41.           <!-- 左下区 (25-32) -->
    42.           <g v-for="tooth in lowerLeftTeeth" :key="tooth.number">
    43.             <rect :x="tooth.x" :y="tooth.y" :width="toothWidth" :height="toothHeight" :rx="toothRadius"
    44.               :class="['tooth', { selected: selectedTeeth.includes(tooth.number) }]"
    45.               @click="toggleTooth(tooth.number)" />
    46.             <text :x="tooth.x + toothWidth / 2" :y="tooth.y + toothHeight / 2 + 5" class="tooth-number"
    47.               @click="toggleTooth(tooth.number)">
    48.               {{ tooth.number }}
    49.             </text>
    50.           </g>
    51.         </g>
    52.         <!-- 中线标识 -->
    53.         <line :x1="viewBoxWidth / 2" y1="50" :x2="viewBoxWidth / 2" :y2="viewBoxHeight - 50" stroke="#78909C" stroke-width="1"
    54.           stroke-dasharray="5,5" />
    55.         <!-- 分区标识 -->
    56.         <text :x="viewBoxWidth / 4" y="30" class="quadrant-label">右上区 (1-8)</text>
    57.         <text :x="viewBoxWidth * 3 / 4" y="30" class="quadrant-label">左上区 (9-16)</text>
    58.         <text :x="viewBoxWidth / 4" :y="viewBoxHeight - 20" class="quadrant-label">右下区 (17-24)</text>
    59.         <text :x="viewBoxWidth * 3 / 4" :y="viewBoxHeight - 20" class="quadrant-label">左下区 (25-32)</text>
    60.       </svg>
    61.     </div>
    62.     <!-- 备注区域 -->
    63.     <div class="notes-section">
    64.       <div v-if="selectedTeeth.length > 0">
    65.         <h3>选中牙齿: {{ selectedTeethWithPosition.join(', ') }}</h3>
    66.         <textarea v-model="notes" placeholder="请输入治疗备注..." class="notes-textarea"></textarea>
    67.       </div>
    68.       <div v-else class="no-selection">
    69.         请点击牙齿进行选择
    70.       </div>
    71.     </div>
    72.   </div>
    73. </template>
    74. <script setup>
    75. import { ref, watch, computed } from 'vue'
    76. const props = defineProps({
    77.   modelValue: {
    78.     type: Object,
    79.     default: () => ({
    80.       selectedTeeth: [],
    81.       notes: ''
    82.     })
    83.   },
    84.   width: {
    85.     type: [Number, String],
    86.     default: '100%'
    87.   },
    88.   height: {
    89.     type: [Number, String],
    90.     default: '600'
    91.   },
    92.   // 新增的尺寸相关props
    93.   viewBoxWidth: {
    94.     type: Number,
    95.     default: 1000
    96.   },
    97.   viewBoxHeight: {
    98.     type: Number,
    99.     default: 600
    100.   },
    101.   toothWidth: {
    102.     type: Number,
    103.     default: 40
    104.   },
    105.   toothHeight: {
    106.     type: Number,
    107.     default: 60
    108.   },
    109.   toothRadius: {
    110.     type: Number,
    111.     default: 5
    112.   }
    113. })
    114. const emit = defineEmits(['update:modelValue'])
    115. const selectedTeeth = ref([...props.modelValue.selectedTeeth])
    116. const notes = ref(props.modelValue.notes)
    117. // 计算属性
    118. const computedWidth = computed(() => typeof props.width === 'number' ? `${props.width}px` : props.width)
    119. const computedHeight = computed(() => typeof props.height === 'number' ? `${props.height}px` : props.height)
    120. // 计算选中牙齿及其位置信息
    121. const selectedTeethWithPosition = computed(() => {
    122.   return selectedTeeth.value.map(num => {
    123.     const tooth = getAllTeeth().find(t => t.number === num)
    124.     return tooth ? `${num}(${getPositionName(num)})` : num
    125.   })
    126. })
    127. // 获取所有牙齿数据
    128. const getAllTeeth = () => [...upperRightTeeth, ...upperLeftTeeth, ...lowerRightTeeth, ...lowerLeftTeeth]
    129. // 获取牙齿位置名称
    130. const getPositionName = (toothNumber) => {
    131.   if (toothNumber >= 1 && toothNumber <= 8) return '右上'
    132.   if (toothNumber >= 9 && toothNumber <= 16) return '左上'
    133.   if (toothNumber >= 17 && toothNumber <= 24) return '右下'
    134.   if (toothNumber >= 25 && toothNumber <= 32) return '左下'
    135.   return ''
    136. }
    137. // 标准牙位布局数据 - 基于viewBox动态计算
    138. const upperRightTeeth = [
    139.   { number: 1, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 },
    140.   { number: 2, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 },
    141.   { number: 3, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: 50 },
    142.   { number: 4, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 + props.toothHeight + 10 },
    143.   { number: 5, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 + props.toothHeight + 10 },
    144.   { number: 6, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: 50 + props.toothHeight + 10 },
    145.   { number: 7, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: 50 + (props.toothHeight + 10) * 2 },
    146.   { number: 8, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: 50 + (props.toothHeight + 10) * 2 }
    147. ]
    148. const upperLeftTeeth = [
    149.   { number: 9, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 + (props.toothHeight + 10) * 2 },
    150.   { number: 10, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 + (props.toothHeight + 10) * 2 },
    151.   { number: 11, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 + props.toothHeight + 10 },
    152.   { number: 12, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 + props.toothHeight + 10 },
    153.   { number: 13, x: props.viewBoxWidth / 2 + props.toothWidth, y: 50 + props.toothHeight + 10 },
    154.   { number: 14, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: 50 },
    155.   { number: 15, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: 50 },
    156.   { number: 16, x: props.viewBoxWidth / 2 + props.toothWidth, y: 50 }
    157. ]
    158. const lowerRightTeeth = [
    159.   { number: 17, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 },
    160.   { number: 18, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 },
    161.   { number: 19, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 },
    162.   { number: 20, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    163.   { number: 21, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    164.   { number: 22, x: props.viewBoxWidth / 2 - props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    165.   { number: 23, x: props.viewBoxWidth / 2 - props.toothWidth * 4, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 },
    166.   { number: 24, x: props.viewBoxWidth / 2 - props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 }
    167. ]
    168. const lowerLeftTeeth = [
    169.   { number: 25, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 },
    170.   { number: 26, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + (props.toothHeight + 10) * 2 },
    171.   { number: 27, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    172.   { number: 28, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    173.   { number: 29, x: props.viewBoxWidth / 2 + props.toothWidth, y: props.viewBoxHeight / 2 + 20 + props.toothHeight + 10 },
    174.   { number: 30, x: props.viewBoxWidth / 2 + props.toothWidth * 3, y: props.viewBoxHeight / 2 + 20 },
    175.   { number: 31, x: props.viewBoxWidth / 2 + props.toothWidth * 2, y: props.viewBoxHeight / 2 + 20 },
    176.   { number: 32, x: props.viewBoxWidth / 2 + props.toothWidth, y: props.viewBoxHeight / 2 + 20 }
    177. ]
    178. // 切换牙齿选择状态
    179. const toggleTooth = (toothNumber) => {
    180.   const index = selectedTeeth.value.indexOf(toothNumber)
    181.   if (index === -1) {
    182.     selectedTeeth.value.push(toothNumber)
    183.   } else {
    184.     selectedTeeth.value.splice(index, 1)
    185.   }
    186.   updateModelValue()
    187. }
    188. // 更新模型值
    189. const updateModelValue = () => {
    190.   emit('update:modelValue', {
    191.     selectedTeeth: [...selectedTeeth.value],
    192.     notes: notes.value,
    193.     selectedTeethWithPosition: [...selectedTeethWithPosition.value]
    194.   })
    195. }
    196. // 监听notes变化
    197. watch(notes, () => {
    198.   updateModelValue()
    199. })
    200. // 监听props变化
    201. watch(() => props.modelValue, (newVal) => {
    202.   if (JSON.stringify(newVal.selectedTeeth) !== JSON.stringify(selectedTeeth.value)) {
    203.     selectedTeeth.value = [...newVal.selectedTeeth]
    204.   }
    205.   if (newVal.notes !== notes.value) {
    206.     notes.value = newVal.notes
    207.   }
    208. }, { deep: true })
    209. // 暴露方法
    210. defineExpose({
    211.   clearSelection: () => {
    212.     selectedTeeth.value = []
    213.     notes.value = ''
    214.     updateModelValue()
    215.   },
    216.   getSelectedTeeth: () => [...selectedTeeth.value],
    217.   getSelectedTeethWithPosition: () => [...selectedTeethWithPosition.value],
    218.   getNotes: () => notes.value
    219. })
    220. </script>
    221. <style scoped>
    222. .tooth-chart-container {
    223.   display: flex;
    224.   flex-direction: column;
    225.   gap: 20px;
    226.   font-family: 'Arial', sans-serif;
    227.   max-width: 100%;
    228.   margin: 0 auto;
    229. }
    230. .tooth-chart {
    231.   border: 1px solid #e0e0e0;
    232.   border-radius: 12px;
    233.   overflow: hidden;
    234.   background-color: #f8f9fa;
    235.   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
    236. }
    237. .tooth {
    238.   fill: #ffffff;
    239.   stroke: #90a4ae;
    240.   stroke-width: 1.5;
    241.   cursor: pointer;
    242.   transition: all 0.3s ease;
    243. }
    244. .tooth:hover {
    245.   fill: #e3f2fd;
    246.   stroke: #42a5f5;
    247. }
    248. .tooth.selected {
    249.   fill: #bbdefb;
    250.   stroke: #1e88e5;
    251.   stroke-width: 2;
    252.   filter: drop-shadow(0 0 4px rgba(30, 136, 229, 0.4));
    253. }
    254. .tooth-number {
    255.   font-size: 22px;
    256.   font-weight: 600;
    257.   text-anchor: middle;
    258.   cursor: pointer;
    259.   user-select: none;
    260.   fill: #37474f;
    261. }
    262. .quadrant-label {
    263.   font-size: 26px;
    264.   fill: #78909C;
    265.   text-anchor: middle;
    266.   font-weight: 500;
    267. }
    268. .notes-section {
    269.   padding: 20px;
    270.   border: 1px solid #e0e0e0;
    271.   border-radius: 12px;
    272.   background-color: #f8f9fa;
    273.   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
    274. }
    275. .notes-section h3 {
    276.   margin-top: 0;
    277.   margin-bottom: 15px;
    278.   color: #263238;
    279.   font-size: 18px;
    280. }
    281. .notes-textarea {
    282.   width: 100%;
    283.   min-height: 120px;
    284.   padding: 12px;
    285.   border: 1px solid #cfd8dc;
    286.   border-radius: 8px;
    287.   resize: vertical;
    288.   font-family: inherit;
    289.   font-size: 14px;
    290.   line-height: 1.5;
    291.   transition: border-color 0.3s;
    292. }
    293. .notes-textarea:focus {
    294.   outline: none;
    295.   border-color: #42a5f5;
    296.   box-shadow: 0 0 0 2px rgba(66, 165, 245, 0.2);
    297. }
    298. .no-selection {
    299.   color: #90a4ae;
    300.   text-align: center;
    301.   padding: 30px;
    302.   font-size: 16px;
    303. }
    304. </style>
    复制代码
    使用示例:
    1. <template>
    2.   <div class="demo-container">
    3.     <h1>牙位图选择器</h1>
    4.     <ToothChart v-model="toothData" :width="chartWidth" :height="chartHeight" :tooth-width="toothWidth"
    5.       :tooth-height="toothHeight" />
    6.     <div class="actions">
    7.       <button @click="clearSelection">清除选择</button>
    8.       <button @click="submitData">提交数据</button>
    9.     </div>
    10.   </div>
    11. </template>
    12. <script setup>
    13. import { ref } from 'vue'
    14. import ToothChart from '@/components/ToothChart.vue';
    15. const toothData = ref({
    16.   selectedTeeth: [],
    17.   notes: '',
    18.   selectedTeethWithPosition: []
    19. })
    20. const chartWidth = ref('100%')
    21. const chartHeight = ref('500px')
    22. const toothWidth = ref(40)
    23. const toothHeight = ref(60)
    24. const clearSelection = () => {
    25.   toothData.value = {
    26.     selectedTeeth: [],
    27.     notes: '',
    28.     selectedTeethWithPosition: []
    29.   }
    30. }
    31. const submitData = () => {
    32.   alert(`已提交数据:\n选中牙齿: ${toothData.value.selectedTeethWithPosition.join(', ')}\n备注: ${toothData.value.notes}`)
    33. }
    34. </script>
    35. <style scoped lang="scss">
    36. .demo-container {
    37.   max-width: 1000px;
    38.   margin: 0 auto;
    39.   padding: 20px;
    40.   font-family: Arial, sans-serif;
    41. }
    42. .actions {
    43.   display: flex;
    44.   gap: 10px;
    45.   margin: 20px 0;
    46. }
    47. .actions button {
    48.   padding: 8px 16px;
    49.   background: #42a5f5;
    50.   color: white;
    51.   border: none;
    52.   border-radius: 4px;
    53.   cursor: pointer;
    54.   transition: background 0.3s;
    55. }
    56. .actions button:hover {
    57.   background: #1e88e5;
    58. }
    59. </style>
    复制代码
    到此这篇关于vue3 实现牙位图选择器的文章就介绍到这了,更多相关vue3 选择器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    来源:https://www.jb51.net/javascript/339853q77.htm
    免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    ×

    最新评论

    QQ Archiver 手机版 小黑屋 福建二哥 ( 闽ICP备2022004717号|闽公网安备35052402000345号 )

    Powered by Discuz! X3.5 © 2001-2023

    快速回复 返回顶部 返回列表