重构: ChatInput 拆分(提取 SkillMention+ImageInput, 1183->1155行)
- 提取 SkillMention.vue(/ 技能联想浮层+已选 chip) - 提取 ImageInput.vue(待发送图片预览+移除) - ChatInput 模板内联技能浮层替换为子组件
This commit is contained in:
@@ -1,28 +1,18 @@
|
||||
<template>
|
||||
<div class="ai-input-area">
|
||||
<!-- 已选技能 chip(含参数格式提示 argument_hint) -->
|
||||
<div v-if="pendingSkill" class="ai-skill-chip">
|
||||
<span class="ai-skill-chip-main">
|
||||
<span class="ai-skill-chip-name">/{{ pendingSkill.name }}</span>
|
||||
<span v-if="pendingSkill.description" class="ai-skill-chip-desc">{{ pendingSkill.description }}</span>
|
||||
<code v-if="pendingSkill.argument_hint" class="ai-skill-chip-hint">{{ pendingSkill.argument_hint }}</code>
|
||||
</span>
|
||||
<button class="ai-skill-chip-x" @click="clearSkill" :title="$t('aiChat.clearSkill')" :aria-label="$t('aiChat.clearSkill')">×</button>
|
||||
</div>
|
||||
<SkillMention
|
||||
:skill-open="skillOpen"
|
||||
:filtered-skills="filteredSkills"
|
||||
:skill-index="skillIndex"
|
||||
:pending-skill="pendingSkill"
|
||||
:skill-query="skillQuery"
|
||||
@select="selectSkill"
|
||||
@clear="clearSkill"
|
||||
@update:skill-index="skillIndex = $event"
|
||||
/>
|
||||
<div class="ai-input-wrap">
|
||||
<!-- F-260614-05 Phase 2b: 待发送图片预览(粘贴/拖拽加入,发送前可移除) -->
|
||||
<div v-if="pendingImages.length" class="ai-img-preview-row">
|
||||
<div v-for="img in pendingImages" :key="img.id" class="ai-img-preview">
|
||||
<img :src="img.dataUrl" :alt="$t('aiChat.imagePreviewAlt')" class="ai-img-preview-thumb" />
|
||||
<button
|
||||
type="button"
|
||||
class="ai-img-preview-x"
|
||||
:title="$t('aiChat.removeImage')"
|
||||
:aria-label="$t('aiChat.removeImage')"
|
||||
@click="removePendingImage(img.id)"
|
||||
>×</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- F-260614-05 Phase 2b: 待发送图片预览 -->
|
||||
<ImageInput :pending-images="pendingImages" @remove="removePendingImage" />
|
||||
<textarea
|
||||
ref="inputEl"
|
||||
v-model="inputText"
|
||||
@@ -38,27 +28,7 @@
|
||||
@dragover="onDragOver"
|
||||
@drop="onDrop"
|
||||
></textarea>
|
||||
<!-- 技能联想浮层 -->
|
||||
<div v-if="skillOpen" class="ai-skill-popover">
|
||||
<div v-if="!filteredSkills.length" class="ai-skill-empty">
|
||||
{{ store.state.skills.length ? $t('aiChat.skillNoMatch') : $t('aiChat.skillNotLoaded') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(s, i) in filteredSkills"
|
||||
:key="s.path"
|
||||
class="ai-skill-item"
|
||||
:class="{ 'ai-skill-item--active': i === skillIndex }"
|
||||
@click="selectSkill(s)"
|
||||
@mouseenter="skillIndex = i"
|
||||
>
|
||||
<div class="ai-skill-item-row">
|
||||
<span class="ai-skill-item-name">/{{ s.name }}</span>
|
||||
<span class="ai-skill-item-desc">{{ s.description }}</span>
|
||||
<span class="ai-skill-item-src">{{ s.source }}</span>
|
||||
</div>
|
||||
<code v-if="s.argument_hint" class="ai-skill-item-hint">{{ s.argument_hint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:名] 标记,架构复用技能 popover) -->
|
||||
<div v-if="mentionOpen" class="ai-skill-popover ai-mention-popover">
|
||||
<div v-if="!mentionItems.length" class="ai-skill-empty">
|
||||
@@ -158,6 +128,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import SkillMention from './SkillMention.vue'
|
||||
import ImageInput from './ImageInput.vue'
|
||||
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
33
src/components/ai/ImageInput.vue
Normal file
33
src/components/ai/ImageInput.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
//! 图片输入预览 — 待发送图片缩略图列表,从 ChatInput.vue 提取
|
||||
//!
|
||||
//! 职责:显示待发送图片缩略图、移除按钮。不含图片读取/粘贴/拖拽逻辑。
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
/** 待发送图片列表 */
|
||||
pendingImages: { id: string; dataUrl: string; mediaType: string }[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', id: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="pendingImages.length" class="ai-img-preview-row">
|
||||
<div v-for="img in pendingImages" :key="img.id" class="ai-img-preview">
|
||||
<img :src="img.dataUrl" :alt="t('aiChat.imagePreviewAlt')" class="ai-img-preview-thumb" />
|
||||
<button
|
||||
type="button"
|
||||
class="ai-img-preview-x"
|
||||
:title="t('aiChat.removeImage')"
|
||||
:aria-label="t('aiChat.removeImage')"
|
||||
@click="emit('remove', img.id)"
|
||||
>×</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
63
src/components/ai/SkillMention.vue
Normal file
63
src/components/ai/SkillMention.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
//! 技能联想浮层 — / 触发技能选择,从 ChatInput.vue 提取
|
||||
//!
|
||||
//! 职责:显示 /@ 联想浮层(Filtered skill list)、选中/清除技能、关联 i18n
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SkillInfo } from '../../api/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
/** 联想浮层是否打开 */
|
||||
skillOpen: boolean
|
||||
/** 过滤后的技能列表 */
|
||||
filteredSkills: SkillInfo[]
|
||||
/** 当前高亮索引 */
|
||||
skillIndex: number
|
||||
/** 已选技能(用于顶部 chip 展示) */
|
||||
pendingSkill: SkillInfo | null
|
||||
/** 技能查询文本 */
|
||||
skillQuery: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', skill: SkillInfo): void
|
||||
(e: 'clear'): void
|
||||
(e: 'update:skillIndex', index: number): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 已选技能 chip -->
|
||||
<div v-if="pendingSkill" class="ai-skill-chip">
|
||||
<span class="ai-skill-chip-main">
|
||||
<span class="ai-skill-chip-name">/{{ pendingSkill.name }}</span>
|
||||
<span v-if="pendingSkill.description" class="ai-skill-chip-desc">{{ pendingSkill.description }}</span>
|
||||
<code v-if="pendingSkill.argument_hint" class="ai-skill-chip-hint">{{ pendingSkill.argument_hint }}</code>
|
||||
</span>
|
||||
<button class="ai-skill-chip-x" @click="emit('clear')" :title="t('aiChat.clearSkill')" :aria-label="t('aiChat.clearSkill')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 技能联想浮层 -->
|
||||
<div v-if="skillOpen" class="ai-skill-popover">
|
||||
<div v-if="!filteredSkills.length" class="ai-skill-empty">
|
||||
{{ skillQuery.length ? t('aiChat.skillNoMatch') : t('aiChat.skillNotLoaded') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(s, i) in filteredSkills"
|
||||
:key="s.path"
|
||||
class="ai-skill-item"
|
||||
:class="{ 'ai-skill-item--active': i === skillIndex }"
|
||||
@click="emit('select', s)"
|
||||
@mouseenter="emit('update:skillIndex', i)"
|
||||
>
|
||||
<div class="ai-skill-item-row">
|
||||
<span class="ai-skill-item-name">/{{ s.name }}</span>
|
||||
<span class="ai-skill-item-desc">{{ s.description }}</span>
|
||||
<span class="ai-skill-item-src">{{ s.source }}</span>
|
||||
</div>
|
||||
<code v-if="s.argument_hint" class="ai-skill-item-hint">{{ s.argument_hint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user