knowledge-tree-node.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. <template>
  2. <view class="knowledge-tree-node">
  3. <view class="min-h-[68px]" @click.stop="handleClick">
  4. <view class="flex items-center border-0 border-b border-solid border-[#E6E6E6] py-26">
  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-4 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. <slot>
  21. <view v-if="nodeData.isLeaf"
  22. class="px-20 py-8 border border-solid border-primary rounded-full text-24 text-primary"
  23. @click.stop="handleStartPractice">开始练习</view>
  24. </slot>
  25. </view>
  26. </view>
  27. <!-- 子节点容器 -->
  28. <view v-if="nodeData.children && nodeData.children.length > 0"
  29. :class="['ml-40 overflow-hidden transition-all duration-300 h-0']"
  30. :style="{ height: nodeData.actualHeight + 'px' }">
  31. <knowledge-tree-node v-for="child in nodeData.children" :key="child.name" :node-data="child"
  32. :parent-data="nodeData" @node-click="handleNodeClick" @update-height="handleUpdateHeight"
  33. @start-practice="handleChildStartPractice">
  34. <template #default>
  35. <slot></slot>
  36. </template>
  37. </knowledge-tree-node>
  38. </view>
  39. </view>
  40. </template>
  41. <script lang="ts" setup>
  42. import * as Study from '@/types/study';
  43. // 定义 props
  44. const props = defineProps({
  45. nodeData: {
  46. type: Object as PropType<Study.KnowledgeNode>,
  47. required: true
  48. },
  49. parentData: {
  50. type: Object as PropType<Study.KnowledgeNode>,
  51. default: null
  52. }
  53. });
  54. // 定义 emits
  55. const emit = defineEmits(['nodeClick', 'updateHeight', 'startPractice']);
  56. // 计算子节点高度
  57. const calculateChildrenHeight = (node: Study.KnowledgeNode): number => {
  58. if (!node.children || node.children.length === 0) {
  59. return 0;
  60. }
  61. let height = 0;
  62. node.children.forEach(child => {
  63. // 每个子节点的基础高度
  64. height += 68;
  65. // 如果子节点已展开,递归计算其子节点高度
  66. if (child.isExpanded && child.children && child.children.length > 0) {
  67. height += calculateChildrenHeight(child);
  68. }
  69. });
  70. return height;
  71. };
  72. // 处理节点点击
  73. const handleClick = () => {
  74. // 切换展开状态
  75. props.nodeData.isExpanded = !props.nodeData.isExpanded;
  76. // 计算并设置实际高度
  77. if (props.nodeData.isExpanded) {
  78. props.nodeData.actualHeight = calculateChildrenHeight(props.nodeData);
  79. } else {
  80. props.nodeData.actualHeight = 0;
  81. }
  82. // 通知父组件节点被点击
  83. emit('nodeClick', {
  84. node: props.nodeData,
  85. parent: props.parentData
  86. });
  87. // 如果有父节点,通知父节点更新高度
  88. if (props.parentData) {
  89. emit('updateHeight', props.parentData);
  90. }
  91. };
  92. // 处理开始练习事件
  93. const handleStartPractice = () => {
  94. emit('startPractice', props.nodeData);
  95. };
  96. // 处理子节点开始练习事件
  97. const handleChildStartPractice = (nodeData: Study.KnowledgeNode) => {
  98. emit('startPractice', nodeData);
  99. };
  100. // 处理子节点点击事件
  101. const handleNodeClick = (eventData: { node: Study.KnowledgeNode; parent: Study.KnowledgeNode }) => {
  102. // 向上传递点击事件
  103. emit('nodeClick', eventData);
  104. };
  105. // 处理高度更新事件
  106. const handleUpdateHeight = (parentNode: Study.KnowledgeNode) => {
  107. // 重新计算父节点高度
  108. if (parentNode.isExpanded) {
  109. parentNode.actualHeight = calculateChildrenHeight(parentNode);
  110. }
  111. // 继续向上传递高度更新事件
  112. if (props.parentData) {
  113. emit('updateHeight', props.parentData);
  114. }
  115. };
  116. const getCorrectRate = (rate: number): number => {
  117. if (!rate) {
  118. return 0;
  119. }
  120. if (rate > 0 && rate < 0.1) {
  121. return 0.1;
  122. }
  123. return rate;
  124. };
  125. const getProgressPercent = (nodeData: Study.KnowledgeNode): number => {
  126. const { finishedCount, questionCount } = nodeData;
  127. if (!finishedCount || !questionCount) {
  128. return 0;
  129. }
  130. return Math.min(finishedCount / questionCount, 1);
  131. };
  132. </script>
  133. <style lang="scss" scoped></style>