| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- <template>
- <div v-if="hasSuggestions" class="ai-suggest">
- <div class="ai-suggest__label">{{ hasGroups ? '下一步分析方向' : '继续追问' }}</div>
- <!-- 有合法方向分组:每个方向一行「方向名小标签 + 该组追问 chips」。
- 后端保证有 directions 时其 questions 并集 == 扁平 questions(漏方向的句子会兜底进「继续分析」组),
- 故分组模式只渲染 groups,不再从扁平列表补漏 -->
- <div v-if="hasGroups" class="ai-suggest__groups">
- <div v-for="(group, gIdx) in groups" :key="gIdx" class="ai-suggest__group">
- <span class="ai-suggest__dir" :title="group.name">{{ group.name }}</span>
- <div class="ai-suggest__list">
- <button
- v-for="(q, qIdx) in group.questions"
- :key="qIdx"
- class="ai-suggest__chip"
- type="button"
- @click="emit('pick', q)"
- >
- {{ q }}
- </button>
- </div>
- </div>
- </div>
- <!-- 无方向分组(旧数据 / 分组非法):保持原平铺样式 -->
- <div v-else class="ai-suggest__list">
- <button
- v-for="(q, idx) in flatQuestions"
- :key="idx"
- class="ai-suggest__chip"
- type="button"
- @click="emit('pick', q)"
- >
- {{ q }}
- </button>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- /** 下一步引导建议(后端 FollowupMiddleware 产出,可能带方向分组) */
- import { computed } from 'vue';
- import type { SseSuggestionDirection } from '../api/types';
- /** 总展示条数上限(跨方向合并计数,与旧版平铺上限一致) */
- const MAX_CHIPS = 5;
- const props = defineProps<{
- /** 扁平追问列表(旧字段,后端始终下发) */
- questions?: string[];
- /** 可选方向分组;有合法分组时按方向渲染,否则回退平铺 */
- directions?: SseSuggestionDirection[];
- }>();
- const emit = defineEmits<{ pick: [question: string] }>();
- /** 清洗追问:剔除空串/非字符串并去首尾空白(避免 chip 文字带空白、点击带脏值) */
- function normalizeQuestions(list: unknown): string[] {
- if (!Array.isArray(list)) return [];
- return list.filter((q): q is string => typeof q === 'string' && !!q.trim()).map((q) => q.trim());
- }
- /**
- * 合法分组:方向名非空 + 至少一条有效追问;按总上限跨组截断。
- * 后端保证有 directions 时分组 questions 并集 == 扁平 questions,故分组模式不再补漏。
- */
- const groups = computed<SseSuggestionDirection[]>(() => {
- const result: SseSuggestionDirection[] = [];
- if (!Array.isArray(props.directions)) return result;
- let remain = MAX_CHIPS;
- props.directions.forEach((direction) => {
- if (remain <= 0) return;
- const name = typeof direction?.name === 'string' ? direction.name.trim() : '';
- const items = normalizeQuestions(direction?.questions);
- // 空方向名 / 空问题列表:整组丢弃(不占名额,其它合法分组照常渲染)
- if (!name || !items.length) return;
- const visible = items.slice(0, remain);
- result.push({ name, questions: visible });
- remain -= visible.length;
- });
- return result;
- });
- /** 旧版平铺列表(无合法分组时使用) */
- const flatQuestions = computed<string[]>(() => normalizeQuestions(props.questions).slice(0, MAX_CHIPS));
- const hasGroups = computed(() => groups.value.length > 0);
- /** 分组与平铺都为空时不渲染(父级 v-if 已兜底,这里防御组件被单独使用) */
- const hasSuggestions = computed(() => hasGroups.value || flatQuestions.value.length > 0);
- </script>
- <style lang="less" scoped>
- .ai-suggest {
- margin-top: var(--ai-sp-3);
- padding-top: var(--ai-sp-3);
- border-top: 1px dashed var(--ai-border);
- &__label {
- margin-bottom: var(--ai-sp-2);
- font-size: 11px;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--ai-text-2);
- }
- /* 方向分组容器:每组一行,组间用紧间距分隔 */
- &__groups {
- display: flex;
- flex-direction: column;
- gap: var(--ai-sp-2);
- }
- &__group {
- display: flex;
- align-items: flex-start;
- gap: var(--ai-sp-2);
- .ai-suggest__list {
- flex: 1;
- min-width: 0;
- }
- }
- /* 方向名小标签:主色淡底;超长名截断,完整名由 title 兜底,避免挤占 chips 行 */
- &__dir {
- flex: 0 0 auto;
- max-width: 40%;
- margin-top: var(--ai-sp-1);
- padding: 1px 8px;
- overflow: hidden;
- font-size: 11px;
- line-height: 17px;
- color: var(--ai-primary);
- white-space: nowrap;
- text-overflow: ellipsis;
- background: var(--ai-primary-soft);
- border-radius: 999px;
- }
- &__list {
- display: flex;
- flex-wrap: wrap;
- gap: var(--ai-sp-2);
- }
- &__chip {
- max-width: 100%;
- padding: 5px 12px;
- font-size: 12.5px;
- line-height: 1.5;
- text-align: left;
- color: var(--ai-text);
- background: var(--ai-bg);
- border: 1px solid var(--ai-border);
- border-radius: 999px;
- cursor: pointer;
- transition:
- color 0.15s ease-out,
- border-color 0.15s ease-out,
- background 0.15s ease-out;
- &:hover {
- color: var(--ai-primary);
- border-color: fade(@primary-color, 45%);
- background: var(--ai-primary-softer);
- }
- }
- }
- </style>
|