127127 />
128128
129129 <!-- 原文模式 -->
130- <NInput v-else-if =" internalViewMode === 'source'"
131- :value =" content"
132- @input =" handleSourceInput"
133- :readonly =" mode !== 'editable' || streaming"
134- type =" textarea"
135- :placeholder =" placeholder"
136- :autosize =" { minRows: 10 }"
137- style =" height : 100% ; min-height : 0 ;"
138- />
130+ <template v-if =" internalViewMode === ' source' " >
131+ <!-- 🆕 Pro 模式:使用变量感知输入框 -->
132+ <VariableAwareInput
133+ v-if =" shouldEnableVariables && variableData"
134+ :model-value =" content"
135+ @update:model-value =" handleSourceInput"
136+ :readonly =" mode !== 'editable' || streaming"
137+ :placeholder =" placeholder"
138+ :autosize =" { minRows: 10, maxRows: 20 }"
139+ v-bind =" variableData"
140+ @variable-extracted =" handleVariableExtracted"
141+ @add-missing-variable =" handleAddMissingVariable"
142+ />
143+
144+ <!-- Basic/Image 模式:使用普通输入框 -->
145+ <NInput
146+ v-else
147+ :value =" content"
148+ @input =" handleSourceInput"
149+ :readonly =" mode !== 'editable' || streaming"
150+ type =" textarea"
151+ :placeholder =" placeholder"
152+ :autosize =" { minRows: 10 }"
153+ style =" height : 100% ; min-height : 0 ;"
154+ />
155+ </template >
139156
140157 <!-- 渲染模式(默认) -->
141158 <NFlex v-else
165182</template >
166183
167184<script setup lang="ts">
168- import { computed , ref , watch , nextTick , onMounted } from ' vue'
185+ import { computed , ref , watch , nextTick , onMounted , inject , type Ref } from ' vue'
169186
170187import { useI18n } from ' vue-i18n'
171188import {
@@ -178,6 +195,12 @@ import { useClipboard } from '../composables/ui/useClipboard'
178195import MarkdownRenderer from ' ./MarkdownRenderer.vue'
179196import TextDiffUI from ' ./TextDiff.vue'
180197import type { CompareResult } from ' @prompt-optimizer/core'
198+ import { VariableAwareInput } from ' ./variable-extraction'
199+ import { useFunctionMode } from ' ../composables/mode/useFunctionMode'
200+ import { useTemporaryVariables } from ' ../composables/variable/useTemporaryVariables'
201+ import { useVariableManager } from ' ../composables/prompt/useVariableManager'
202+ import type { AppServices } from ' ../types/services'
203+ import { platform } from ' ../utils/platform'
181204
182205type ActionName = ' fullscreen' | ' diff' | ' copy' | ' edit' | ' reasoning' | ' favorite'
183206
@@ -186,8 +209,8 @@ const { copyText } = useClipboard()
186209
187210const message = useToast ()
188211
189- // 服务注入(当前未使用,保留用于未来扩展 )
190- // const services = inject<Ref<AppServices | null> | null >('services', null)
212+ // 🆕 注入 services(用于变量管理 )
213+ const services = inject <Ref <AppServices | null >>(' services' , ref ( null ) )
191214
192215// 移除收藏状态管理(改由父组件处理)
193216
@@ -241,6 +264,134 @@ const emit = defineEmits<{
241264 ' save-favorite' : [data : { content: string ; originalContent? : string }]
242265}>()
243266
267+ // 🆕 变量管理功能(仅 Pro 模式)
268+ // ==================== 功能模式判断 ====================
269+ // ✅ 无条件调用,使用全局单例的 functionMode
270+ // ⚠️ 不主动初始化,避免在 services 未就绪时污染全局单例
271+ const { functionMode } = useFunctionMode (services )
272+
273+ // 判断是否启用变量功能(仅 Pro 模式)
274+ const shouldEnableVariables = computed (() => functionMode .value === ' pro' )
275+
276+ // ==================== 变量管理 Composables ====================
277+ // 临时变量管理器(全局单例)
278+ const tempVars = useTemporaryVariables ()
279+
280+ // ✅ 无条件调用,composable 内部会等待 services.preferenceService 准备就绪
281+ const globalVarsManager = useVariableManager (services )
282+
283+ // ==================== 变量数据计算 ====================
284+ /**
285+ * 计算纯预定义变量
286+ * allVariables = 预定义变量 + 自定义全局变量
287+ * 因此:预定义变量 = allVariables - customVariables
288+ */
289+ const purePredefinedVariables = computed (() => {
290+ const all = globalVarsManager .allVariables .value || {}
291+ const custom = globalVarsManager .customVariables .value || {}
292+
293+ const predefined: Record <string , string > = {}
294+ for (const [key, value] of Object .entries (all )) {
295+ // 只保留不在 customVariables 中的变量
296+ if (! (key in custom )) {
297+ predefined [key ] = value
298+ }
299+ }
300+
301+ return predefined
302+ })
303+
304+ const variableData = computed (() => {
305+ // 只在 Pro 模式下提供变量数据
306+ if (! shouldEnableVariables .value ) return null
307+
308+ // 🔒 如果全局变量管理器未就绪,返回 null 以禁用变量功能
309+ // 这样可以避免文本被替换但变量未保存的不一致状态
310+ if (! globalVarsManager .isReady .value ) return null
311+
312+ return {
313+ existingGlobalVariables: Object .keys (globalVarsManager .customVariables .value || {}),
314+ existingTemporaryVariables: Object .keys (tempVars .temporaryVariables .value || {}),
315+ predefinedVariables: Object .keys (purePredefinedVariables .value ),
316+ globalVariableValues: globalVarsManager .customVariables .value || {},
317+ temporaryVariableValues: tempVars .temporaryVariables .value || {},
318+ predefinedVariableValues: purePredefinedVariables .value
319+ }
320+ })
321+
322+ // ==================== 变量事件处理 ====================
323+ /**
324+ * 处理变量提取事件
325+ * 在 Pro 模式的原文编辑模式下,用户选中文本提取变量时触发
326+ *
327+ * ⚠️ 注意:此函数只会在 variableData 不为 null 时被调用
328+ * (即管理器已就绪且为 Pro 模式),因此不需要额外检查
329+ *
330+ * ⚠️ 数据一致性问题:
331+ * VariableAwareInput 在触发此事件前已完成文本替换({{varName}})
332+ * 如果保存失败,文本已被修改但变量未保存,需提示用户撤销操作
333+ */
334+ const handleVariableExtracted = (data : {
335+ variableName: string
336+ variableValue: string
337+ variableType: ' global' | ' temporary'
338+ }) => {
339+ if (data .variableType === ' global' ) {
340+ try {
341+ // 保存到全局变量
342+ globalVarsManager .addVariable (data .variableName , data .variableValue )
343+ message .success (
344+ t (' variableExtraction.savedToGlobal' , { name: data .variableName })
345+ )
346+ } catch (error ) {
347+ console .error (' [OutputDisplayCore] Failed to save global variable:' , error )
348+ // ⚠️ 保存失败但文本已被替换,提示用户需要撤销
349+ message .error (
350+ t (' variableExtraction.saveFailedWithUndo' , {
351+ name: data .variableName ,
352+ undo: platform .getUndoKey ()
353+ }),
354+ {
355+ duration: 8000 , // 延长显示时间,确保用户看到
356+ closable: true
357+ }
358+ )
359+ }
360+ } else {
361+ // 保存到临时变量(临时变量管理器是全局单例,始终可用)
362+ try {
363+ tempVars .setVariable (data .variableName , data .variableValue )
364+ message .success (
365+ t (' variableExtraction.savedToTemporary' , { name: data .variableName })
366+ )
367+ } catch (error ) {
368+ console .error (' [OutputDisplayCore] Failed to save temporary variable:' , error )
369+ // 临时变量保存失败的可能性极低,但仍需处理
370+ message .error (
371+ t (' variableExtraction.saveFailedWithUndo' , {
372+ name: data .variableName ,
373+ undo: platform .getUndoKey ()
374+ }),
375+ {
376+ duration: 8000 ,
377+ closable: true
378+ }
379+ )
380+ }
381+ }
382+ }
383+
384+ /**
385+ * 处理添加缺失变量事件
386+ * 当用户悬停在缺失变量上并点击快速添加时触发
387+ */
388+ const handleAddMissingVariable = (varName : string ) => {
389+ tempVars .setVariable (varName , ' ' )
390+ message .success (
391+ t (' variableDetection.addSuccess' , { name: varName })
392+ )
393+ }
394+
244395// 内部状态
245396const reasoningContentRef = ref <HTMLDivElement | null >(null )
246397const userHasManuallyToggledReasoning = ref (false )
@@ -462,6 +613,12 @@ const handleFavorite = () => {
462613
463614// 组件挂载时设置初始视图模式
464615onMounted (() => {
616+ // ⚠️ 不在此处初始化 functionMode
617+ // 原因:useFunctionMode 是全局单例,不应由单个组件控制初始化时机
618+ // - 如果 services 未就绪,初始化会失败但仍标记为已完成,导致永久卡在 'basic'
619+ // - 应该在应用级别统一初始化(如 App.vue)
620+ // - functionMode 有默认值 'basic',可以正常工作
621+
465622 // 如果是可编辑模式,默认显示原文
466623 if (props .mode === ' editable' ) {
467624 internalViewMode .value = ' source' ;
0 commit comments