SkillPopup.vue 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <template>
  2. <!-- 技能引用选择弹层:由父组件用 $ 触发,单选;已选状态与高亮项都由父组件传入 -->
  3. <div ref="rootEl" class="skill-popup" @click.stop @pointerdown.stop>
  4. <div class="skill-popup-header">
  5. <span>选择技能</span>
  6. <span class="skill-count">0/1</span>
  7. </div>
  8. <div class="skill-list">
  9. <div
  10. v-for="(skill, index) in list"
  11. :key="skill.name"
  12. class="skill-item"
  13. :class="{
  14. active: index === activeIndex,
  15. selected: isSelected(skill.name),
  16. }"
  17. @click="emit('select', skill)"
  18. >
  19. <div class="skill-item-checkbox" :class="{ checked: isSelected(skill.name) }">
  20. <span v-if="isSelected(skill.name)" class="check-mark">✓</span>
  21. </div>
  22. <div class="skill-item-icon"></div>
  23. <span class="skill-item-name">{{ skill.display_name }}</span>
  24. </div>
  25. <div v-if="list.length === 0" class="skill-empty">{{ loadFailed ? '技能列表加载失败,请稍后重试' : '暂无匹配技能' }}</div>
  26. </div>
  27. <div class="skill-popup-footer">
  28. <span>点击选择技能(仅可选1个)</span>
  29. </div>
  30. </div>
  31. </template>
  32. <script setup lang="ts">
  33. import { ref } from 'vue';
  34. import { onClickOutside } from '@vueuse/core';
  35. interface SkillOption {
  36. name: string;
  37. display_name: string;
  38. }
  39. const props = defineProps<{
  40. list: SkillOption[];
  41. activeIndex: number;
  42. selectedName: string;
  43. /** 技能列表是否加载失败:失败时空态文案要与「确实没有技能」区分开 */
  44. loadFailed?: boolean;
  45. }>();
  46. const emit = defineEmits<{ (e: 'select', skill: SkillOption): void; (e: 'close'): void }>();
  47. const isSelected = (name: string) => props.selectedName === name;
  48. // 组件自己持有根元素,外部点击由这里判定后回调父组件
  49. const rootEl = ref<HTMLElement>();
  50. onClickOutside(rootEl, () => emit('close'));
  51. </script>
  52. <style scoped lang="less">
  53. .skill-popup {
  54. position: absolute;
  55. bottom: 100%;
  56. left: 0;
  57. right: 0;
  58. margin-bottom: 8px;
  59. background: #0a2a3f;
  60. border: 1px solid #1a4a6f;
  61. border-radius: 8px;
  62. max-height: 300px;
  63. display: flex;
  64. flex-direction: column;
  65. z-index: 1000;
  66. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  67. .skill-popup-header {
  68. display: flex;
  69. justify-content: space-between;
  70. align-items: center;
  71. padding: 10px 14px 8px;
  72. font-size: 13px;
  73. color: #e0e6ed;
  74. border-bottom: 1px solid rgba(63, 80, 106, 0.3);
  75. flex-shrink: 0;
  76. .skill-count {
  77. font-size: 12px;
  78. color: #3a8fd4;
  79. font-weight: 500;
  80. }
  81. }
  82. .skill-list {
  83. flex: 1;
  84. overflow-y: auto;
  85. padding: 6px;
  86. .skill-item {
  87. display: flex;
  88. align-items: center;
  89. gap: 10px;
  90. padding: 8px 10px;
  91. cursor: pointer;
  92. border-radius: 6px;
  93. transition: background 0.2s;
  94. &:hover {
  95. background: rgba(10, 132, 255, 0.15);
  96. }
  97. &.active {
  98. background: rgba(10, 132, 255, 0.2);
  99. }
  100. &.selected {
  101. background: rgba(10, 132, 255, 0.12);
  102. }
  103. .skill-item-checkbox {
  104. width: 16px;
  105. height: 16px;
  106. border: 1.5px solid rgba(255, 255, 255, 0.25);
  107. border-radius: 3px;
  108. flex-shrink: 0;
  109. display: flex;
  110. align-items: center;
  111. justify-content: center;
  112. transition: all 0.2s;
  113. &.checked {
  114. background: #3a8fd4;
  115. border-color: #3a8fd4;
  116. }
  117. .check-mark {
  118. font-size: 11px;
  119. color: #fff;
  120. line-height: 1;
  121. }
  122. }
  123. .skill-item-icon {
  124. width: 18px;
  125. height: 18px;
  126. background-image: var(--img-chat-options-skill-icon);
  127. background-repeat: no-repeat;
  128. background-size: 100% 100%;
  129. flex-shrink: 0;
  130. }
  131. .skill-item-name {
  132. font-size: 13px;
  133. color: #e0e6ed;
  134. overflow: hidden;
  135. text-overflow: ellipsis;
  136. white-space: nowrap;
  137. }
  138. }
  139. .skill-empty {
  140. text-align: center;
  141. color: #8b949e;
  142. font-size: 13px;
  143. padding: 20px 0;
  144. }
  145. }
  146. .skill-popup-footer {
  147. padding: 8px 14px;
  148. font-size: 12px;
  149. color: #8b949e;
  150. text-align: center;
  151. border-top: 1px solid rgba(63, 80, 106, 0.3);
  152. flex-shrink: 0;
  153. }
  154. }
  155. </style>