效果图
封装组件: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">
- <!-- 右上区 (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 选择器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/javascript/339853q77.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |