ThinkLevelPopup.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <template>
  2. <!-- 思考级别选择器:关闭 / 高 / 最高 -->
  3. <div class="btn-think-wrapper" ref="wrapperRef">
  4. <a-tooltip :title="props.disabled ? '思考级别(只读:仅管理员可修改)' : '思考级别'">
  5. <div class="btn-think" :class="{ 'is-readonly': props.disabled }" @click="togglePopup">
  6. <div class="btn-icon think-icon"></div>
  7. <span class="btn-text">{{ selected.label }}</span>
  8. <div class="btn-arrow"></div>
  9. </div>
  10. </a-tooltip>
  11. <div v-if="showPopup" class="think-popup" @click.stop>
  12. <div v-for="item in options" :key="item.key" class="think-popup-item" :class="{ active: selected.key === item.key }" @click="selectItem(item)">
  13. <div class="think-item-main">
  14. <span class="think-item-label">{{ item.label }}</span>
  15. <span class="think-item-desc">{{ item.desc }}</span>
  16. </div>
  17. <div class="think-item-check" :class="{ visible: selected.key === item.key }"></div>
  18. </div>
  19. </div>
  20. </div>
  21. </template>
  22. <script setup lang="ts">
  23. import { ref, watch } from 'vue';
  24. import { onClickOutside } from '@vueuse/core';
  25. interface ThinkLevelOption {
  26. key: string;
  27. label: string;
  28. desc: string;
  29. }
  30. /**
  31. * 思考级别选择器。选中项是本组件状态(仅供展示),切换时通过 change 事件通知父组件调接口。
  32. *
  33. * disabled 用于「非管理员只读」:控件仍然显示(当前级别本身是需要被看到的状态),
  34. * 但不展开菜单、不派发 change,因此不会触发切换接口。
  35. */
  36. const props = withDefaults(defineProps<{ initialLevel?: string; disabled?: boolean }>(), {
  37. initialLevel: 'off',
  38. disabled: false,
  39. });
  40. const emit = defineEmits<{
  41. (e: 'change', key: string): void;
  42. }>();
  43. const options: ThinkLevelOption[] = [
  44. { key: 'off', label: '关闭', desc: '不使用深度思考,响应最快' },
  45. { key: 'high', label: '高', desc: '深度思考,平衡速度与质量' },
  46. { key: 'highest', label: '最高', desc: '极致思考,质量最高但较慢' },
  47. ];
  48. const selected = ref<ThinkLevelOption>(options[1]);
  49. const showPopup = ref(false);
  50. const wrapperRef = ref<HTMLElement>();
  51. watch(
  52. () => props.initialLevel,
  53. (level) => {
  54. const found = options.find((opt) => opt.key === level);
  55. if (found) selected.value = found;
  56. },
  57. { immediate: true }
  58. );
  59. const togglePopup = () => {
  60. // 只读:不展开菜单(展示当前级别即可,不允许改)
  61. if (props.disabled) return;
  62. showPopup.value = !showPopup.value;
  63. };
  64. const selectItem = (item: ThinkLevelOption) => {
  65. // 只读:不派发 change,避免触发切换接口
  66. if (props.disabled) return;
  67. selected.value = item;
  68. showPopup.value = false;
  69. emit('change', item.key);
  70. };
  71. onClickOutside(wrapperRef, () => {
  72. showPopup.value = false;
  73. });
  74. </script>
  75. <style scoped lang="less">
  76. /* 以下 .btn-icon/.btn-text/.btn-arrow/.think-icon 与原输入区内联按钮原语一致:
  77. 按钮下移到组件后,父组件的 scoped 样式不再作用于这里的后代元素,故随组件复制一份。 */
  78. .btn-icon {
  79. width: 14px;
  80. height: 14px;
  81. background-repeat: no-repeat;
  82. background-size: 100% 100%;
  83. flex-shrink: 0;
  84. }
  85. .think-icon {
  86. background-image: var(--img-chat-thinking-icon);
  87. }
  88. .btn-text {
  89. font-size: 12px;
  90. color: #e0e6ed;
  91. white-space: nowrap;
  92. }
  93. .btn-arrow {
  94. width: 0;
  95. height: 0;
  96. border-left: 4px solid transparent;
  97. border-right: 4px solid transparent;
  98. border-top: 4px solid #8b949e;
  99. flex-shrink: 0;
  100. }
  101. .btn-think-wrapper {
  102. position: relative;
  103. display: flex;
  104. align-items: center;
  105. }
  106. .btn-think {
  107. height: 28px;
  108. padding: 0 10px;
  109. border-radius: 6px;
  110. display: flex;
  111. align-items: center;
  112. gap: 4px;
  113. cursor: pointer;
  114. transition: all 0.2s;
  115. &:hover {
  116. border-color: #3a8fd4;
  117. background: rgba(10, 132, 255, 0.1);
  118. }
  119. /* 只读态:保留可读的当前级别,但不给出可点击的反馈 */
  120. &.is-readonly {
  121. cursor: not-allowed;
  122. opacity: 0.6;
  123. &:hover {
  124. border-color: transparent;
  125. background: transparent;
  126. }
  127. }
  128. }
  129. .think-popup {
  130. position: absolute;
  131. bottom: 38px;
  132. left: 50%;
  133. transform: translateX(-50%);
  134. background: #0a2a3f;
  135. border: 1px solid #1a4a6f;
  136. border-radius: 8px;
  137. min-width: 200px;
  138. z-index: 1000;
  139. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  140. .think-popup-item {
  141. display: flex;
  142. align-items: center;
  143. justify-content: space-between;
  144. padding: 10px 12px;
  145. cursor: pointer;
  146. transition: background 0.2s;
  147. border-radius: 6px;
  148. &:hover {
  149. background: rgba(10, 132, 255, 0.15);
  150. }
  151. &.active {
  152. background: rgba(10, 132, 255, 0.1);
  153. }
  154. .think-item-main {
  155. display: flex;
  156. flex-direction: column;
  157. gap: 2px;
  158. .think-item-label {
  159. font-size: 13px;
  160. color: #e0e6ed;
  161. white-space: nowrap;
  162. }
  163. .think-item-desc {
  164. font-size: 11px;
  165. color: #8b949e;
  166. white-space: nowrap;
  167. }
  168. }
  169. .think-item-check {
  170. width: 16px;
  171. height: 16px;
  172. flex-shrink: 0;
  173. display: flex;
  174. align-items: center;
  175. justify-content: center;
  176. visibility: hidden;
  177. &.visible {
  178. visibility: visible;
  179. }
  180. &::after {
  181. content: '✓';
  182. font-size: 12px;
  183. color: #3a8fd4;
  184. }
  185. }
  186. }
  187. }
  188. </style>