feat: 修复内置模板语言切换后迭代页面不更新的问题,完善事件传播机制

This commit is contained in:
linshen committed 2025-07-06 11:31:25 +08:00
1 parent 94f4df0481
commit c0cc629542
10 files changed
+454 -8

No files matched your search

@@ -14,20 +14,23 @@
## 文档清单
- [x] `troubleshooting.md` - 模板管理故障排除清单
- [x] `event-propagation-fix.md` - 事件传播机制修复(内置模板语言切换bug)
- [ ] `design.md` - 模板管理功能设计
- [ ] `experience.md` - 开发经验总结(待从experience.md中提取)
## 相关代码变更
- 影响包:@prompt-optimizer/core, @prompt-optimizer/ui
- 影响包:@prompt-optimizer/core, @prompt-optimizer/ui, @prompt-optimizer/web, @prompt-optimizer/extension
- 主要变更:
- 模板管理功能实现
- 异步操作优化
- 错误处理改进
- **事件传播机制完善**:修复内置模板语言切换后迭代页面不更新的问题
## 已知问题和解决方案
- 模板删除错误"Template not found":异步方法调用缺少await关键字
- 模态框渲染问题:缺少v-if指令控制显示
- 模板管理器调用逻辑:优化模式选择与模板管理的关联
- **内置模板语言切换后迭代页面不更新**:事件传播机制缺失,需要建立完整的事件传播链
## 后续影响
- 提升模板管理用户体验
@@ -0,0 +1,209 @@
# 事件传播机制修复 - 内置模板语言切换bug
## 🎯 问题描述
### 核心问题
内置模板语言切换后,主界面的优化提示词下拉框正确更新,但迭代页面的模板选择显示旧语言的模板名称。
### 问题表现
1. **主界面正常**:优化提示词下拉框从"通用优化"正确切换到"General Optimization"
2. **迭代页面异常**:
- 当前选中项显示"通用迭代"(中文)
- 下拉列表显示"General Iteration"(英文)
- 用户需要手动重新选择才能使用英文模板
3. **实际请求正常**:发送请求时生效的是新语言(因为通过templateId重新获取)
### 用户体验影响
- 造成用户困惑:UI显示不一致
- 需要额外操作:用户必须手动重新选择模板
- 功能不完整:语言切换功能没有完全生效
## 🔍 根本原因分析
### 组件层级差异
**主界面的优化提示词下拉框(正常):**
```
App.vue
└── TemplateSelectUI (ref="templateSelectRef")
```
**迭代页面的模板下拉框(异常):**
```
App.vue
└── PromptPanelUI (ref="promptPanelRef")
└── TemplateSelect (ref="iterateTemplateSelectRef")
```
### 事件传播路径差异
**主界面的刷新机制:**
1. TemplateManager关闭时自动调用 `templateSelectRef?.refresh?.()`
2. 直接引用,事件传播路径短
3. 有完整的刷新机制
**迭代页面的问题:**
1. 语言切换事件无法传播到深层的TemplateSelect组件
2. 组件层级更深,需要额外的事件传播机制
3. 之前没有建立完整的事件传播链
### 技术细节
1. **事件源**:`BuiltinTemplateLanguageSwitch` 发出 `languageChanged` 事件
2. **处理层**:`TemplateManager` 处理事件并更新自身状态
3. **传播断点**:事件没有继续传播到App.vue层级
4. **影响范围**:只有TemplateManager内部的组件得到更新
## 🔧 解决方案
### 1. 建立事件传播链
**TemplateManager.vue** - 发出语言变化事件:
```javascript
const handleLanguageChanged = async (newLanguage: string) => {
// 重新加载模板列表以反映新的语言
await loadTemplates()
// 如果当前选中的模板是内置模板,需要重新选择以获取新语言版本
const currentSelected = selectedTemplate.value
if (currentSelected && currentSelected.isBuiltin) {
try {
const updatedTemplate = await getTemplateManager.value.getTemplate(currentSelected.id)
if (updatedTemplate) {
emit('select', updatedTemplate, getCurrentTemplateType());
}
} catch (error) {
// 错误处理逻辑...
}
}
// 🔑 关键修复:发出语言变化事件,通知父组件
emit('languageChanged', newLanguage)
}
```
**事件定义:**
```javascript
const emit = defineEmits(['close', 'select', 'update:show', 'languageChanged'])
```
### 2. App.vue处理事件并传播
**监听语言变化事件:**
```vue
<TemplateManagerUI
v-if="isReady"
v-model:show="templateManagerState.showTemplates"
:templateType="templateManagerState.currentType"
@close="() => templateManagerState.handleTemplateManagerClose(() => templateSelectRef?.refresh?.())"
@languageChanged="handleTemplateLanguageChanged"
/>
```
**处理语言变化:**
```javascript
// 处理模板语言变化
const handleTemplateLanguageChanged = (newLanguage: string) => {
console.log('[App] 模板语言已切换:', newLanguage)
// 刷新主界面的模板选择组件
if (templateSelectRef.value?.refresh) {
templateSelectRef.value.refresh()
}
// 🔑 关键修复:刷新迭代页面的模板选择组件
if (promptPanelRef.value?.refreshIterateTemplateSelect) {
promptPanelRef.value.refreshIterateTemplateSelect()
}
}
```
**添加组件引用:**
```javascript
const templateSelectRef = ref<{ refresh?: () => void } | null>(null)
const promptPanelRef = ref<{ refreshIterateTemplateSelect?: () => void } | null>(null)
```
### 3. PromptPanel暴露刷新方法
**添加迭代模板选择组件引用:**
```vue
<TemplateSelect
ref="iterateTemplateSelectRef"
:modelValue="selectedIterateTemplate"
@update:modelValue="$emit('update:selectedIterateTemplate', $event)"
:type="templateType"
:optimization-mode="optimizationMode"
:services="services"
@manage="$emit('openTemplateManager', templateType)"
/>
```
**暴露刷新方法:**
```javascript
const iterateTemplateSelectRef = ref<{ refresh?: () => void } | null>(null);
// 暴露刷新迭代模板选择的方法
const refreshIterateTemplateSelect = () => {
if (iterateTemplateSelectRef.value?.refresh) {
iterateTemplateSelectRef.value.refresh()
}
}
defineExpose({
refreshIterateTemplateSelect
})
```
## ✅ 修复验证
### 测试步骤
1. 打开应用,确认主界面显示中文模板
2. 点击"功能提示词"打开模板管理界面
3. 点击"中文"按钮切换到"English"
4. 确认主界面优化提示词下拉框更新为英文
5. 输入测试内容并执行优化
6. 点击"继续优化"打开迭代页面
7. **关键验证**:确认迭代页面的模板选择正确显示英文模板
### 验证结果
- [x] 语言切换事件正确传播到所有TemplateSelect组件
- [x] 迭代页面的下拉列表正确更新为新语言
- [x] 用户可以在迭代页面直接使用正确语言的模板
- [x] 主界面和迭代页面行为一致
- [x] 无需用户手动重新选择模板
## 💡 经验总结
### 架构设计原则
1. **事件传播完整性**:确保状态变化事件能传播到所有相关组件
2. **组件层级意识**:深层组件需要额外的事件传播机制
3. **统一响应机制**:相同功能的组件应该有相同的响应机制
4. **接口一致性**:所有相关组件都应该暴露统一的刷新接口
### 最佳实践
1. **建立完整的事件链**:从事件源到所有消费者的完整路径
2. **使用ref和defineExpose**:为深层组件提供外部访问接口
3. **统一刷新机制**:所有TemplateSelect组件都有refresh方法
4. **日志记录**:添加适当的日志帮助调试事件传播
### 避免的陷阱
1. **假设事件会自动传播**:Vue的事件系统不会自动向下传播
2. **忽略组件层级差异**:不同层级的组件需要不同的处理方式
3. **不完整的修复**:只修复部分组件而忽略其他相关组件
4. **缺乏验证**:没有完整测试所有相关功能
### 适用场景
这个修复模式适用于:
- 全局状态变化需要通知多个层级的组件
- 组件层级复杂的应用架构
- 需要统一响应机制的功能模块
- 事件传播路径不一致的问题
## 🔗 相关文档
- `112-desktop-ipc-fixes/language-switch-fix.md` - 语言切换按钮修复
- `106-template-management/troubleshooting.md` - 模板管理故障排除清单
## 📅 修复记录
- **发现时间**:2025-01-07
- **修复时间**:2025-01-07
- **影响范围**:Web和Extension环境
- **修复类型**:事件传播机制完善
- **重要程度**:高(影响用户体验的核心功能)
@@ -180,7 +180,87 @@
- [ ] 测试UI组件与服务层的交互
- [ ] 测试Electron环境下的IPC通信
### 9. 监控和调试
### 9. 内置模板语言切换后迭代页面模板选择不更新
**症状:**
- 在模板管理界面切换内置模板语言后,主界面的优化提示词下拉框正确更新
- 但执行优化后点击"继续优化",迭代页面的模板选择显示旧语言的模板名称
- 下拉列表已更新为新语言,但当前选中项还是旧语言
- 实际发送请求时生效的是新语言(因为通过templateId重新获取)
**根本原因:**
- **事件传播路径不同**:主界面和迭代页面的TemplateSelect组件在不同的层级
- **组件层级差异**:
- 主界面:`App.vue → TemplateSelectUI`(直接引用)
- 迭代页面:`App.vue → PromptPanelUI → TemplateSelect`(间接引用)
- **刷新机制缺失**:语言切换事件无法传播到深层的TemplateSelect组件
**详细分析:**
1. **主界面正常的原因**:
- 在TemplateManager关闭时会自动调用 `templateSelectRef?.refresh?.()`
- 组件层级简单,事件传播路径短
- 有直接的引用和刷新机制
2. **迭代页面异常的原因**:
- 迭代页面的TemplateSelect没有被包含在语言切换的刷新逻辑中
- 组件层级更深,需要额外的事件传播机制
- 之前没有建立完整的事件传播链
**解决方案:**
1. **建立事件传播链**:
```javascript
// TemplateManager.vue - 发出语言变化事件
const handleLanguageChanged = async (newLanguage: string) => {
// ... 现有逻辑 ...
// 发出语言变化事件,通知父组件
emit('languageChanged', newLanguage)
}
```
2. **App.vue处理事件并传播**:
```javascript
// 处理模板语言变化
const handleTemplateLanguageChanged = (newLanguage: string) => {
// 刷新主界面的模板选择组件
if (templateSelectRef.value?.refresh) {
templateSelectRef.value.refresh()
}
// 刷新迭代页面的模板选择组件
if (promptPanelRef.value?.refreshIterateTemplateSelect) {
promptPanelRef.value.refreshIterateTemplateSelect()
}
}
```
3. **PromptPanel暴露刷新方法**:
```javascript
// PromptPanel.vue - 暴露刷新迭代模板的方法
const refreshIterateTemplateSelect = () => {
if (iterateTemplateSelectRef.value?.refresh) {
iterateTemplateSelectRef.value.refresh()
}
}
defineExpose({
refreshIterateTemplateSelect
})
```
**修复验证:**
- [x] 语言切换事件正确传播到所有TemplateSelect组件
- [x] 迭代页面的下拉列表正确更新为新语言
- [x] 用户可以在迭代页面选择正确语言的模板
- [x] 主界面和迭代页面行为一致
**经验总结:**
1. **组件层级影响事件传播**:深层组件需要额外的事件传播机制
2. **统一刷新机制**:所有相关组件都应该有统一的刷新接口
3. **完整的事件链**:确保事件能够传播到所有需要响应的组件
4. **架构一致性**:相同功能的组件应该有相同的响应机制
### 10. 监控和调试
**日志记录:**
- [ ] 记录模板操作的开始和结束
@@ -84,10 +84,15 @@ ipcMain.handle('template-getCurrentBuiltinTemplateLanguage', async (event) => {
1. **接口一致性**: 跨环境的接口必须保持一致的异步性
2. **错误处理**: 让错误自然传播,便于问题定位
3. **类型安全**: 使用TypeScript确保接口实现的完整性
4. **事件传播**: 确保语言切换事件能传播到所有相关组件
### 最佳实践
1. **统一异步**: 所有跨环境接口都应该是异步的
2. **接口驱动**: 先定义接口,再实现具体类
3. **完整测试**: 在两种环境下都要验证功能
4. **事件链完整性**: 建立完整的事件传播机制,确保深层组件也能响应状态变化
### 相关问题
- **迭代页面模板选择不更新**: 语言切换后,由于组件层级差异和事件传播机制缺失,迭代页面的模板选择无法正确更新。解决方案是建立完整的事件传播链,确保所有TemplateSelect组件都能响应语言切换事件。详见 `106-template-management/troubleshooting.md` 第9节。
这个修复建立了完整的异步接口设计模式,为后续的IPC开发提供了标准。
+97
View File
@@ -84,6 +84,87 @@ defineOptions({
</script>
```
#### 深层组件事件传播机制
**问题**:当全局状态变化需要通知多层级嵌套的组件时,事件传播可能中断,导致深层组件无法及时更新。
**典型场景**:
- 语言切换后,主界面组件更新正常,但Modal内部的组件显示旧状态
- 组件层级差异:`App.vue → ComponentA`(直接引用)vs `App.vue → ComponentB → ComponentC`(间接引用)
**核心原因**:
1. **v-if条件渲染**:组件被销毁后ref失效,无法调用组件方法
2. **事件传播断点**:事件只传播到直接子组件,不会自动向下传播到深层组件
3. **组件生命周期差异**:不同层级的组件可能处于不同的生命周期阶段
**解决方案**:
1. **使用v-show替代v-if**:确保组件实例始终存在,ref保持有效
```vue
<!-- ❌ 问题方案:组件会被销毁 -->
<Modal v-if="showModal">
<TemplateSelect ref="templateRef" />
</Modal>
<!-- ✅ 推荐方案:组件始终渲染 -->
<Modal v-show="showModal">
<TemplateSelect ref="templateRef" />
</Modal>
```
2. **建立完整事件传播链**:从事件源到所有消费组件
```javascript
// 父组件:建立事件传播
const handleGlobalStateChange = (newState) => {
// 刷新直接子组件
if (directChildRef.value?.refresh) {
directChildRef.value.refresh()
}
// 刷新深层组件(通过中间组件的暴露方法)
if (intermediateRef.value?.refreshDeepChild) {
intermediateRef.value.refreshDeepChild()
}
}
// 中间组件:暴露深层组件的刷新方法
const deepChildRef = ref()
const refreshDeepChild = () => {
if (deepChildRef.value?.refresh) {
deepChildRef.value.refresh()
}
}
defineExpose({
refreshDeepChild
})
```
3. **统一刷新接口**:所有相关组件都暴露相同的刷新方法
```javascript
// 每个需要响应全局状态变化的组件都实现refresh方法
const refresh = () => {
// 重新加载数据或更新状态
}
defineExpose({
refresh
})
```
**最佳实践**:
- **架构设计**:在设计阶段考虑事件传播的完整路径
- **接口一致性**:定义标准的组件刷新接口(如`refresh()`方法)
- **文档记录**:为复杂的事件传播链建立清晰的架构图
- **测试验证**:确保在所有使用场景下事件都能正确传播
**适用场景**:
- 全局主题切换
- 语言切换
- 用户权限变更
- 模板/配置更新
> **详细案例**:参见 [106-template-management/event-propagation-fix.md](../archives/106-template-management/event-propagation-fix.md)
## ⚡ 快速问题排查
### 布局问题
@@ -96,6 +177,22 @@ defineOptions({
2. 确认高度约束是否从顶层正确传递
3. 验证滚动容器是否有正确的 `overflow-y: auto`
### 组件状态同步问题
1. **深层组件未更新**:
- 检查是否使用了 `v-if` 导致组件被销毁
- 确认事件传播链是否完整(父→中间→目标组件)
- 验证目标组件是否暴露了刷新方法
2. **Modal内组件状态异常**:
- 检查Modal是否使用 `v-show` 而非 `v-if`
- 确认组件ref在Modal关闭时是否仍然有效
- 验证全局状态变化事件是否传播到Modal内部
3. **组件ref调用失败**:
- 确认组件是否已完成挂载(`nextTick`)
- 检查条件渲染是否导致组件不存在
- 验证ref绑定的组件是否暴露了对应方法
### API调用问题
1. 检查环境变量是否正确设置(`VITE_` 前缀)
2. 确认参数是否过度设置默认值
+18
View File
@@ -99,6 +99,7 @@
<div class="flex-1 min-h-0">
<template v-if="services && services.templateManager">
<PromptPanelUI
ref="promptPanelRef"
v-model:optimized-prompt="optimizer.optimizedPrompt"
:reasoning="optimizer.optimizedReasoning"
:original-prompt="optimizer.prompt"
@@ -139,6 +140,7 @@
v-model:show="templateManagerState.showTemplates"
:templateType="templateManagerState.currentType"
@close="() => templateManagerState.handleTemplateManagerClose(() => templateSelectRef?.refresh?.())"
@languageChanged="handleTemplateLanguageChanged"
/>
<HistoryDrawerUI
v-if="isReady"
@@ -218,6 +220,7 @@ const showDataManager = ref(false)
const optimizeModelSelect = ref(null)
const testPanelRef = ref(null)
const templateSelectRef = ref<{ refresh?: () => void } | null>(null)
const promptPanelRef = ref<{ refreshIterateTemplateSelect?: () => void } | null>(null)
const templateSelectType = computed<'optimize' | 'userOptimize' | 'iterate'>(() => {
return selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize';
@@ -340,6 +343,21 @@ const handleOptimizationModeChange = (mode: OptimizationMode) => {
selectedOptimizationMode.value = mode
}
// 处理模板语言变化
const handleTemplateLanguageChanged = (newLanguage: string) => {
console.log('[App] 模板语言已切换:', newLanguage)
// 刷新主界面的模板选择组件
if (templateSelectRef.value?.refresh) {
templateSelectRef.value.refresh()
}
// 刷新迭代页面的模板选择组件
if (promptPanelRef.value?.refreshIterateTemplateSelect) {
promptPanelRef.value.refreshIterateTemplateSelect()
}
}
// 处理历史记录使用 - 智能模式切换
const handleHistoryReuse = async (context: { record: any, chainId: string, rootPrompt: string, chain: any }) => {
const { chain } = context
+1 -1
View File
@@ -9,7 +9,7 @@
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="modelValue" class="fixed inset-0 z-30 overflow-y-auto" @click="handleBackdropClick">
<div v-show="modelValue" class="fixed inset-0 z-30 overflow-y-auto" @click="handleBackdropClick">
<div class="flex min-h-full items-center justify-center p-4">
<!-- 背景遮罩 -->
<div class="fixed inset-0 theme-mask"></div>
@@ -65,6 +65,7 @@
<div>
<h4 class="theme-label mb-2">{{ templateSelectText }}</h4>
<TemplateSelect
ref="iterateTemplateSelectRef"
:modelValue="selectedIterateTemplate"
@update:modelValue="$emit('update:selectedIterateTemplate', $event)"
:type="templateType"
@@ -185,6 +186,7 @@ const iterateInput = ref('')
const templateType = ref<'optimize' | 'iterate'>('iterate')
const outputDisplayRef = ref<InstanceType<typeof OutputDisplay> | null>(null);
const iterateTemplateSelectRef = ref<{ refresh?: () => void } | null>(null);
// 计算标题文本
const templateTitleText = computed(() => {
@@ -279,6 +281,17 @@ watch([() => props.isOptimizing, () => props.isIterating], ([newOptimizing, newI
}
}, { immediate: false })
// 暴露刷新迭代模板选择的方法
const refreshIterateTemplateSelect = () => {
if (iterateTemplateSelectRef.value?.refresh) {
iterateTemplateSelectRef.value.refresh()
}
}
defineExpose({
refreshIterateTemplateSelect
})
</script>
<style scoped>
@@ -671,7 +671,7 @@ const props = defineProps<{
show: boolean
}>()
const emit = defineEmits(['close', 'select', 'update:show'])
const emit = defineEmits(['close', 'select', 'update:show', 'languageChanged'])
const toast = useToast()
const templates = ref<Template[]>([])
@@ -1225,6 +1225,9 @@ const syntaxGuideMarkdown = computed(() => {
}
}
}
// 发出语言变化事件,通知父组件
emit('languageChanged', newLanguage)
}
// 监听 props.templateType 变化,更新当前分类
+22 -4
View File
@@ -99,6 +99,7 @@
<div class="flex-1 min-h-0">
<template v-if="services && services.templateManager">
<PromptPanelUI
ref="promptPanelRef"
v-model:optimized-prompt="optimizer.optimizedPrompt"
:reasoning="optimizer.optimizedReasoning"
:original-prompt="optimizer.prompt"
@@ -134,11 +135,12 @@
<!-- Modals and Drawers that are conditionally rendered -->
<ModelManagerUI v-if="isReady" v-model:show="modelManager.showConfig" />
<TemplateManagerUI
v-if="isReady"
v-model:show="templateManagerState.showTemplates"
:templateType="templateManagerState.currentType"
<TemplateManagerUI
v-if="isReady"
v-model:show="templateManagerState.showTemplates"
:templateType="templateManagerState.currentType"
@close="() => templateManagerState.handleTemplateManagerClose(() => templateSelectRef?.refresh?.())"
@languageChanged="handleTemplateLanguageChanged"
/>
<HistoryDrawerUI
v-if="isReady"
@@ -218,6 +220,7 @@ const showDataManager = ref(false)
const optimizeModelSelect = ref(null)
const testPanelRef = ref(null)
const templateSelectRef = ref<{ refresh?: () => void } | null>(null)
const promptPanelRef = ref<{ refreshIterateTemplateSelect?: () => void } | null>(null)
const templateSelectType = computed<'optimize' | 'userOptimize' | 'iterate'>(() => {
return selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize';
@@ -340,6 +343,21 @@ const handleOptimizationModeChange = (mode: OptimizationMode) => {
selectedOptimizationMode.value = mode
}
// 处理模板语言变化
const handleTemplateLanguageChanged = (newLanguage: string) => {
console.log('[App] 模板语言已切换:', newLanguage)
// 刷新主界面的模板选择组件
if (templateSelectRef.value?.refresh) {
templateSelectRef.value.refresh()
}
// 刷新迭代页面的模板选择组件
if (promptPanelRef.value?.refreshIterateTemplateSelect) {
promptPanelRef.value.refreshIterateTemplateSelect()
}
}
// 处理历史记录使用 - 智能模式切换
const handleHistoryReuse = async (context: { record: any, chainId: string, rootPrompt: string, chain: any }) => {
const { chain } = context