| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- <template>
- <!-- 思考级别选择器:关闭 / 高 / 最高 -->
- <div class="btn-think-wrapper" ref="wrapperRef">
- <a-tooltip :title="props.disabled ? '思考级别(只读:仅管理员可修改)' : '思考级别'">
- <div class="btn-think" :class="{ 'is-readonly': props.disabled }" @click="togglePopup">
- <div class="btn-icon think-icon"></div>
- <span class="btn-text">{{ selected.label }}</span>
- <div class="btn-arrow"></div>
- </div>
- </a-tooltip>
- <div v-if="showPopup" class="think-popup" @click.stop>
- <div v-for="item in options" :key="item.key" class="think-popup-item" :class="{ active: selected.key === item.key }" @click="selectItem(item)">
- <div class="think-item-main">
- <span class="think-item-label">{{ item.label }}</span>
- <span class="think-item-desc">{{ item.desc }}</span>
- </div>
- <div class="think-item-check" :class="{ visible: selected.key === item.key }"></div>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, watch } from 'vue';
- import { onClickOutside } from '@vueuse/core';
- interface ThinkLevelOption {
- key: string;
- label: string;
- desc: string;
- }
- /**
- * 思考级别选择器。选中项是本组件状态(仅供展示),切换时通过 change 事件通知父组件调接口。
- *
- * disabled 用于「非管理员只读」:控件仍然显示(当前级别本身是需要被看到的状态),
- * 但不展开菜单、不派发 change,因此不会触发切换接口。
- */
- const props = withDefaults(defineProps<{ initialLevel?: string; disabled?: boolean }>(), {
- initialLevel: 'off',
- disabled: false,
- });
- const emit = defineEmits<{
- (e: 'change', key: string): void;
- }>();
- const options: ThinkLevelOption[] = [
- { key: 'off', label: '关闭', desc: '不使用深度思考,响应最快' },
- { key: 'high', label: '高', desc: '深度思考,平衡速度与质量' },
- { key: 'highest', label: '最高', desc: '极致思考,质量最高但较慢' },
- ];
- const selected = ref<ThinkLevelOption>(options[1]);
- const showPopup = ref(false);
- const wrapperRef = ref<HTMLElement>();
- watch(
- () => props.initialLevel,
- (level) => {
- const found = options.find((opt) => opt.key === level);
- if (found) selected.value = found;
- },
- { immediate: true }
- );
- const togglePopup = () => {
- // 只读:不展开菜单(展示当前级别即可,不允许改)
- if (props.disabled) return;
- showPopup.value = !showPopup.value;
- };
- const selectItem = (item: ThinkLevelOption) => {
- // 只读:不派发 change,避免触发切换接口
- if (props.disabled) return;
- selected.value = item;
- showPopup.value = false;
- emit('change', item.key);
- };
- onClickOutside(wrapperRef, () => {
- showPopup.value = false;
- });
- </script>
- <style scoped lang="less">
- /* 以下 .btn-icon/.btn-text/.btn-arrow/.think-icon 与原输入区内联按钮原语一致:
- 按钮下移到组件后,父组件的 scoped 样式不再作用于这里的后代元素,故随组件复制一份。 */
- .btn-icon {
- width: 14px;
- height: 14px;
- background-repeat: no-repeat;
- background-size: 100% 100%;
- flex-shrink: 0;
- }
- .think-icon {
- background-image: var(--img-chat-thinking-icon);
- }
- .btn-text {
- font-size: 12px;
- color: #e0e6ed;
- white-space: nowrap;
- }
- .btn-arrow {
- width: 0;
- height: 0;
- border-left: 4px solid transparent;
- border-right: 4px solid transparent;
- border-top: 4px solid #8b949e;
- flex-shrink: 0;
- }
- .btn-think-wrapper {
- position: relative;
- display: flex;
- align-items: center;
- }
- .btn-think {
- height: 28px;
- padding: 0 10px;
- border-radius: 6px;
- display: flex;
- align-items: center;
- gap: 4px;
- cursor: pointer;
- transition: all 0.2s;
- &:hover {
- border-color: #3a8fd4;
- background: rgba(10, 132, 255, 0.1);
- }
- /* 只读态:保留可读的当前级别,但不给出可点击的反馈 */
- &.is-readonly {
- cursor: not-allowed;
- opacity: 0.6;
- &:hover {
- border-color: transparent;
- background: transparent;
- }
- }
- }
- .think-popup {
- position: absolute;
- bottom: 38px;
- left: 50%;
- transform: translateX(-50%);
- background: #0a2a3f;
- border: 1px solid #1a4a6f;
- border-radius: 8px;
- min-width: 200px;
- z-index: 1000;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
- .think-popup-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 12px;
- cursor: pointer;
- transition: background 0.2s;
- border-radius: 6px;
- &:hover {
- background: rgba(10, 132, 255, 0.15);
- }
- &.active {
- background: rgba(10, 132, 255, 0.1);
- }
- .think-item-main {
- display: flex;
- flex-direction: column;
- gap: 2px;
- .think-item-label {
- font-size: 13px;
- color: #e0e6ed;
- white-space: nowrap;
- }
- .think-item-desc {
- font-size: 11px;
- color: #8b949e;
- white-space: nowrap;
- }
- }
- .think-item-check {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- visibility: hidden;
- &.visible {
- visibility: visible;
- }
- &::after {
- content: '✓';
- font-size: 12px;
- color: #3a8fd4;
- }
- }
- }
- }
- </style>
|