knowledge-tree-node.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <template>
  2. <view class="knowledge-tree-node">
  3. <view class="" @click.stop="handleClick">
  4. <view class="flex items-center border-0 border-b border-solid border-[#E6E6E6] py-20">
  5. <view class="flex-1 min-w-1 flex items-center">
  6. <uv-icon v-if="!nodeData.isLeaf" name="arrow-right" size="14" color="#888"
  7. :custom-class="['mr-16 transition-transform duration-300', nodeData.isExpanded ? 'rotate-90' : '']" />
  8. <view>
  9. <view class="block text-28 text-fore-title font-bold ellipsis-1">{{ nodeData.name }}</view>
  10. <view class="mt-8 text-24 text-fore-light flex items-center">
  11. <progress class="w-100 rounded-full overflow-hidden" :percent="getProgressPercent(nodeData)"
  12. :show-text="false" activeColor="#31a0fc" backgroundColor="#efefef" />
  13. <text class="ml-10 text-primary">{{ nodeData.finishedCount }}</text>
  14. <text>/{{ nodeData.questionCount }}道</text>
  15. <text class="ml-10">正确率</text>
  16. <text class="ml-10 text-primary font-bold">{{ getCorrectRate(nodeData.finishedRatio) }}%</text>
  17. </view>
  18. </view>
  19. </view>
  20. <view v-if="nodeData.isLeaf"
  21. class="px-20 py-8 border border-solid border-primary rounded-full text-24 text-primary"
  22. @click.stop="handleStartPractice">开始练习</view>
  23. </view>
  24. </view>
  25. <!-- 子节点容器 -->
  26. <view v-if="nodeData.children && nodeData.children.length > 0" ref="childrenRef" class="knowledge-children"
  27. :id="`knowledge-children-${nodeData.id}`"
  28. :class="['ml-80 overflow-hidden transition-[height] duration-300', { 'is-measuring': isMeasuringHeight }]"
  29. :style="{ height: measuredHeight + 'px' }">
  30. <knowledge-tree-node v-for="child in nodeData.children" :key="child.name" :node-data="child"
  31. :parent-data="nodeData" @start-practice="handleChildStartPractice">
  32. </knowledge-tree-node>
  33. </view>
  34. </view>
  35. </template>
  36. <script lang="ts" setup>
  37. import KnowledgeTreeNode from './knowledge-tree-node.vue';
  38. import * as Study from '@/types/study';
  39. import { getCurrentInstance } from 'vue';
  40. const instance = getCurrentInstance();
  41. // 定义 props
  42. const props = defineProps({
  43. nodeData: {
  44. type: Object as PropType<Study.KnowledgeNode>,
  45. required: true
  46. },
  47. parentData: {
  48. type: Object as PropType<Study.KnowledgeNode>,
  49. default: null
  50. }
  51. });
  52. const isMeasuringHeight = ref(false);
  53. const childrenRef = ref();
  54. const measuredHeight = ref(0);
  55. // 定义 emits
  56. const emit = defineEmits(['startPractice']);
  57. const getRect = (selector: string) => {
  58. return new Promise((resolve: (rect: { top: number, height: number }) => void) => {
  59. const query = uni.createSelectorQuery().in(instance?.proxy);
  60. query.select(selector).boundingClientRect(function (rect) {
  61. resolve(rect as { top: number, height: number });
  62. }).exec();
  63. });
  64. }
  65. const measureHeight = () => {
  66. isMeasuringHeight.value = true;
  67. setTimeout(() => {
  68. nextTick(() => {
  69. getRect(`#knowledge-children-${props.nodeData.id}`).then((res: any) => {
  70. isMeasuringHeight.value = false;
  71. setTimeout(() => {
  72. nextTick(() => {
  73. measuredHeight.value = res?.height ?? 0;
  74. });
  75. }, 50);
  76. });
  77. });
  78. }, 50);
  79. }
  80. // 处理节点点击
  81. const handleClick = () => {
  82. // 切换展开状态
  83. props.nodeData.isExpanded = !props.nodeData.isExpanded;
  84. if (props.nodeData.isExpanded) {
  85. measureHeight();
  86. } else {
  87. measuredHeight.value = 0;
  88. }
  89. };
  90. // 处理开始练习事件
  91. const handleStartPractice = () => {
  92. emit('startPractice', props.nodeData);
  93. };
  94. // 处理子节点开始练习事件
  95. const handleChildStartPractice = (nodeData: Study.KnowledgeNode) => {
  96. emit('startPractice', nodeData);
  97. };
  98. const getCorrectRate = (rate: number): number => {
  99. if (!rate) {
  100. return 0;
  101. }
  102. if (rate > 0 && rate < 0.1) {
  103. return 0.1;
  104. }
  105. return rate;
  106. };
  107. const getProgressPercent = (nodeData: Study.KnowledgeNode): number => {
  108. const { finishedCount, questionCount } = nodeData;
  109. if (!finishedCount || !questionCount) {
  110. return 0;
  111. }
  112. return Math.min(finishedCount / questionCount, 1) * 100;
  113. };
  114. </script>
  115. <style lang="scss" scoped>
  116. .is-measuring {
  117. opacity: 0;
  118. position: fixed;
  119. z-index: -1000;
  120. transform: translateX(0);
  121. height: auto !important;
  122. }
  123. </style>