Harden AI optimize modals with safer DOM and a11y

This commit is contained in:
sligter
2026-07-29 19:14:18 +08:00
parent 7e283784c6
commit a6d73b83a7
5 changed files with 302 additions and 157 deletions
@@ -1279,6 +1279,7 @@
font-size: 0.8rem;
color: var(--text-primary);
cursor: pointer;
font-family: inherit;
transition: transform 0.2s ease, background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
@@ -12,10 +12,13 @@
// 创建美化的AI优化需求输入弹窗
function showAIOptimizeModal(config) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
// 创建模态框遮罩
const modal = document.createElement('div');
modal.className = 'ai-optimize-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-label', String(config.title || 'AI优化'));
// 创建弹窗内容
const content = document.createElement('div');
@@ -35,11 +38,11 @@
<div class="ai-optimize-modal__header-content">
<div>
<h3 class="ai-optimize-modal__title">
<i class="fas fa-magic"></i>${config.title}
<i class="fas fa-magic"></i><span class="ai-optimize-title"></span>
</h3>
<p class="ai-optimize-modal__subtitle">${config.subtitle}</p>
<p class="ai-optimize-modal__subtitle"></p>
</div>
<button type="button" class="ai-optimize-modal__close" onclick="this.closest('.ai-optimize-modal').remove()" aria-label="关闭">
<button type="button" class="ai-optimize-modal__close" aria-label="关闭">
<span aria-hidden="true">×</span>
</button>
</div>
@@ -49,15 +52,15 @@
<div class="current-info">
<div>
<strong><i class="fas fa-info-circle"></i> 当前内容</strong><br>
${config.currentInfo}
<span class="ai-optimize-current-info" style="white-space: pre-line;"></span>
</div>
</div>
<div class="input-group">
<label class="input-label" for="aiOptimizeInput">
<label class="input-label">
<i class="fas fa-edit"></i> 请描述您的优化需求
</label>
<textarea id="aiOptimizeInput" class="input-textarea" placeholder="详细描述您希望如何优化此内容...
<textarea class="input-textarea ai-optimize-input" aria-label="优化需求" placeholder="详细描述您希望如何优化此内容...
例如:
- 增加更多技术细节
@@ -69,9 +72,7 @@
<label class="suggestion-label">
<i class="fas fa-lightbulb"></i> 点击快捷建议快速填充
</label>
<div class="suggestion-list">
${suggestions.map(s => `<span class="suggestion-tag" onclick="document.getElementById('aiOptimizeInput').value = '${s}'">${s}</span>`).join('')}
</div>
<div class="suggestion-list"></div>
</div>
</div>
@@ -80,47 +81,85 @@
<i class="fas fa-robot"></i> AI将根据您的需求智能优化内容
</div>
<div class="footer-actions">
<button type="button" class="outline-modal-btn" onclick="this.closest('.ai-optimize-modal').remove()">
<button type="button" class="outline-modal-btn ai-optimize-cancel">
<i class="fas fa-times"></i><span>取消</span>
</button>
<button type="button" id="confirmOptimizeBtn" class="outline-modal-btn outline-modal-btn--solid">
<button type="button" class="outline-modal-btn outline-modal-btn--solid ai-optimize-confirm">
<i class="fas fa-magic"></i><span>开始优化</span>
</button>
</div>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
const previousFocus = document.activeElement;
const input = content.querySelector('.ai-optimize-input');
const confirmBtn = content.querySelector('.ai-optimize-confirm');
const currentInfoElement = content.querySelector('.ai-optimize-current-info');
content.querySelector('.ai-optimize-title').textContent = String(config.title || '');
content.querySelector('.ai-optimize-modal__subtitle').textContent = String(config.subtitle || '');
currentInfoElement.textContent = String(config.currentInfo || '');
// 聚焦输入框
setTimeout(() => {
const input = document.getElementById('aiOptimizeInput');
if (input) input.focus();
}, 100);
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
reject('用户取消');
}
const suggestionList = content.querySelector('.suggestion-list');
suggestions.forEach((suggestion) => {
const suggestionButton = document.createElement('button');
suggestionButton.type = 'button';
suggestionButton.className = 'suggestion-tag';
suggestionButton.textContent = String(suggestion);
suggestionButton.addEventListener('click', () => {
input.value = String(suggestion);
input.focus();
});
suggestionList.appendChild(suggestionButton);
});
// 确认按钮
const confirmBtn = document.getElementById('confirmOptimizeBtn');
confirmBtn.onclick = () => {
const input = document.getElementById('aiOptimizeInput');
const value = input?.value.trim();
if (!value) {
// 输入框抖动动画(通过 class 触发,动画定义在 quickEdit.css
input.classList.add('shake');
setTimeout(() => { input.classList.remove('shake'); }, 500);
let closed = false;
function closeModal(value = null) {
if (closed) return;
closed = true;
document.removeEventListener('keydown', handleKeydown);
modal.remove();
previousFocus?.focus?.();
resolve(value);
}
function handleKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
closeModal();
return;
}
modal.remove();
resolve(value);
};
if (event.key !== 'Tab') return;
const focusable = content.querySelectorAll('button:not([disabled]), textarea:not([disabled])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
content.querySelector('.ai-optimize-modal__close').addEventListener('click', () => closeModal());
content.querySelector('.ai-optimize-cancel').addEventListener('click', () => closeModal());
modal.addEventListener('click', (event) => {
if (event.target === modal) closeModal();
});
confirmBtn.addEventListener('click', () => {
const value = input.value.trim();
if (!value) {
input.classList.add('shake');
setTimeout(() => input.classList.remove('shake'), 500);
return;
}
closeModal(value);
});
modal.appendChild(content);
document.body.appendChild(modal);
document.addEventListener('keydown', handleKeydown);
requestAnimationFrame(() => input.focus());
});
}
@@ -153,7 +192,7 @@
userRequest = await showAIOptimizeModal({
title: `AI优化 - 第${currentSlideIndex + 1}`,
subtitle: '让AI帮助您优化这一页的内容',
currentInfo: `<strong>标题:</strong>${title}<br><strong>类型:</strong>${slideType}<br><strong>内容要点:</strong>${contentPoints.length}`,
currentInfo: `标题:${title}\n类型:${slideType}\n内容要点:${contentPoints.length}`,
suggestions: [
'增加更多技术细节和实例',
'简化内容,突出核心要点',
@@ -661,13 +661,17 @@
// 创建美化的AI优化需求输入弹窗
function showAIOptimizeModal(config) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
// 创建模态框遮罩
const modal = document.createElement('div');
modal.className = 'ai-optimize-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-label', String(config.title || 'AI优化'));
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 255, 255, 0.98);
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
z-index: 10000; display: flex; align-items: center; justify-content: center;
padding: 20px; animation: fadeIn 0.3s ease;
`;
@@ -770,19 +774,19 @@
align-items: center;
gap: 8px;
}
.input-group {
.ai-optimize-modal .input-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.input-label {
.ai-optimize-modal .input-label {
display: block;
color: var(--text-primary);
font-weight: 600;
font-size: 0.95rem;
letter-spacing: 0.01em;
}
.input-textarea {
.ai-optimize-modal .input-textarea {
width: 100%;
min-height: 120px;
padding: 16px 18px;
@@ -795,33 +799,41 @@
color: var(--text-primary);
background: var(--surface);
}
.input-textarea::placeholder {
.ai-optimize-modal .input-textarea::placeholder {
color: var(--text-muted);
opacity: 0.9;
}
.input-textarea:focus {
.ai-optimize-modal .input-textarea:focus {
outline: none;
border-color: var(--surface-contrast);
box-shadow: 0 0 0 3px rgba(17, 17, 17, 0.08);
}
.suggestions {
.ai-optimize-modal .input-textarea.shake {
animation: aiOptimizeShake 0.5s;
}
@keyframes aiOptimizeShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.ai-optimize-modal .suggestions {
display: flex;
flex-direction: column;
gap: 12px;
}
.suggestion-label {
.ai-optimize-modal .suggestion-label {
display: block;
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.02em;
}
.suggestion-list {
.ai-optimize-modal .suggestion-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.suggestion-tag {
.ai-optimize-modal .suggestion-tag {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -833,8 +845,9 @@
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.suggestion-tag:hover {
.ai-optimize-modal .suggestion-tag:hover {
transform: translateY(-2px);
background: var(--surface-contrast);
border-color: var(--surface-contrast);
@@ -850,27 +863,27 @@
justify-content: space-between;
gap: 16px;
}
.footer-hint {
.ai-optimize-modal .footer-hint {
color: var(--text-secondary);
font-size: 0.85rem;
display: inline-flex;
align-items: center;
gap: 8px;
}
.footer-actions {
.ai-optimize-modal .footer-actions {
display: flex;
gap: 12px;
}
.footer-actions .btn {
.ai-optimize-modal .footer-actions .btn {
min-width: 110px;
justify-content: center;
}
.modal-cancel {
.ai-optimize-modal .ai-optimize-cancel {
background: var(--surface-subtle);
border-color: var(--border-color);
color: var(--text-primary);
}
.modal-cancel:hover {
.ai-optimize-modal .ai-optimize-cancel:hover {
background: var(--surface);
}
@media (max-width: 520px) {
@@ -880,11 +893,11 @@
padding-left: 20px;
padding-right: 20px;
}
.footer-actions {
.ai-optimize-modal .footer-actions {
width: 100%;
flex-direction: column;
}
.footer-actions .btn {
.ai-optimize-modal .footer-actions .btn {
width: 100%;
}
}
@@ -894,11 +907,11 @@
<div class="ai-optimize-modal__header-content">
<div>
<h3 class="ai-optimize-modal__title">
<i class="fas fa-magic"></i>${config.title}
<i class="fas fa-magic"></i><span class="ai-optimize-title"></span>
</h3>
<p class="ai-optimize-modal__subtitle">${config.subtitle}</p>
<p class="ai-optimize-modal__subtitle"></p>
</div>
<button type="button" class="ai-optimize-modal__close" onclick="this.closest('.ai-optimize-modal').remove()" aria-label="关闭">
<button type="button" class="ai-optimize-modal__close" aria-label="关闭">
<span aria-hidden="true">×</span>
</button>
</div>
@@ -908,15 +921,15 @@
<div class="current-info">
<div>
<strong><i class="fas fa-info-circle"></i> 当前内容</strong><br>
${config.currentInfo}
<span class="ai-optimize-current-info" style="white-space: pre-line;"></span>
</div>
</div>
<div class="input-group">
<label class="input-label" for="aiOptimizeInput">
<label class="input-label">
<i class="fas fa-edit"></i> 请描述您的优化需求
</label>
<textarea id="aiOptimizeInput" class="input-textarea" placeholder="详细描述您希望如何优化此内容...
<textarea class="input-textarea ai-optimize-input" aria-label="优化需求" placeholder="详细描述您希望如何优化此内容...
例如:
- 增加更多技术细节
@@ -928,9 +941,7 @@
<label class="suggestion-label">
<i class="fas fa-lightbulb"></i> 点击快捷建议快速填充
</label>
<div class="suggestion-list">
${suggestions.map(s => `<span class="suggestion-tag" onclick="document.getElementById('aiOptimizeInput').value = '${s}'">${s}</span>`).join('')}
</div>
<div class="suggestion-list"></div>
</div>
</div>
@@ -939,58 +950,85 @@
<i class="fas fa-robot"></i> AI将根据您的需求智能优化内容
</div>
<div class="footer-actions">
<button type="button" class="btn btn-sm modal-cancel" onclick="this.closest('.ai-optimize-modal').remove()">
<button type="button" class="btn btn-sm ai-optimize-cancel">
取消
</button>
<button type="button" id="confirmOptimizeBtn" class="btn btn-sm btn-primary">
<button type="button" class="btn btn-sm btn-primary ai-optimize-confirm">
开始优化
</button>
</div>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
const previousFocus = document.activeElement;
const input = content.querySelector('.ai-optimize-input');
const confirmBtn = content.querySelector('.ai-optimize-confirm');
const currentInfoElement = content.querySelector('.ai-optimize-current-info');
content.querySelector('.ai-optimize-title').textContent = String(config.title || '');
content.querySelector('.ai-optimize-modal__subtitle').textContent = String(config.subtitle || '');
currentInfoElement.textContent = String(config.currentInfo || '');
// 聚焦输入框
setTimeout(() => {
const input = document.getElementById('aiOptimizeInput');
if (input) input.focus();
}, 100);
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
reject('用户取消');
}
const suggestionList = content.querySelector('.suggestion-list');
suggestions.forEach((suggestion) => {
const suggestionButton = document.createElement('button');
suggestionButton.type = 'button';
suggestionButton.className = 'suggestion-tag';
suggestionButton.textContent = String(suggestion);
suggestionButton.addEventListener('click', () => {
input.value = String(suggestion);
input.focus();
});
suggestionList.appendChild(suggestionButton);
});
// 确认按钮
const confirmBtn = document.getElementById('confirmOptimizeBtn');
confirmBtn.onclick = () => {
const input = document.getElementById('aiOptimizeInput');
const value = input?.value.trim();
if (!value) {
// 输入框抖动动画
input.style.animation = 'shake 0.5s';
setTimeout(() => { input.style.animation = ''; }, 500);
let closed = false;
function closeModal(value = null) {
if (closed) return;
closed = true;
document.removeEventListener('keydown', handleKeydown);
modal.remove();
previousFocus?.focus?.();
resolve(value);
}
function handleKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
closeModal();
return;
}
modal.remove();
resolve(value);
};
if (event.key !== 'Tab') return;
const focusable = content.querySelectorAll('button:not([disabled]), textarea:not([disabled])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
// 添加shake动画
const style = document.createElement('style');
style.textContent = `
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
`;
document.head.appendChild(style);
content.querySelector('.ai-optimize-modal__close').addEventListener('click', () => closeModal());
content.querySelector('.ai-optimize-cancel').addEventListener('click', () => closeModal());
modal.addEventListener('click', (event) => {
if (event.target === modal) closeModal();
});
confirmBtn.addEventListener('click', () => {
const value = input.value.trim();
if (!value) {
input.classList.add('shake');
setTimeout(() => input.classList.remove('shake'), 500);
return;
}
closeModal(value);
});
modal.appendChild(content);
document.body.appendChild(modal);
document.addEventListener('keydown', handleKeydown);
requestAnimationFrame(() => input.focus());
});
}
@@ -1030,7 +1068,7 @@
userRequest = await showAIOptimizeModal({
title: '优化大纲',
subtitle: '让AI帮助您优化整个PPT大纲结构和内容',
currentInfo: `<strong>大纲标题:</strong>${parsedOutline.title}<br><strong>幻灯片数量:</strong>${parsedOutline.slides ? parsedOutline.slides.length : 0} 页<br><strong>场景:</strong>${parsedOutline.metadata?.scenario || '通用'}`,
currentInfo: `大纲标题:${parsedOutline.title || ''}\n幻灯片数量:${parsedOutline.slides ? parsedOutline.slides.length : 0} 页\n场景:${parsedOutline.metadata?.scenario || '通用'}`,
suggestions: [
'增加更多文字说明和技术细节',
'重新组织结构,优化逻辑流程',
@@ -1146,7 +1184,7 @@
userRequest = await showAIOptimizeModal({
title: `AI优化 - 第${slideIndex + 1}页`,
subtitle: '让AI帮助您优化这一页的内容',
currentInfo: `<strong>标题:</strong>${title}<br>${subtitle ? `<strong>副标题:</strong>${subtitle}<br>` : ''}<strong>类型:</strong>${slideType}<br><strong>内容要点:</strong>${contentPoints.length}个`,
currentInfo: `标题:${title}\n${subtitle ? `副标题:${subtitle}\n` : ''}类型:${slideType}\n内容要点:${contentPoints.length}个`,
suggestions: [
'增加更多技术细节和实例',
'简化内容,突出核心要点',
@@ -5423,13 +5423,17 @@
// 创建美化的AI优化需求输入弹窗
function showAIOptimizeModal(config) {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
// 创建模态框遮罩
const modal = document.createElement('div');
modal.className = 'ai-optimize-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-label', String(config.title || 'AI优化'));
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 255, 255, 0.98);
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
z-index: 10000; display: flex; align-items: center; justify-content: center;
padding: 20px; animation: fadeIn 0.3s ease;
`;
@@ -5475,6 +5479,7 @@
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid #111;
font-family: inherit;
}
.ai-optimize-modal .suggestion-tag:hover {
transform: translateY(-2px);
@@ -5486,6 +5491,14 @@
border-color: #111;
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
}
.ai-optimize-modal textarea.shake {
animation: aiOptimizeShake 0.5s;
}
@keyframes aiOptimizeShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
</style>
<!-- 头部 -->
@@ -5494,11 +5507,11 @@
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin: 0 0 8px 0; font-size: 1.3em; font-weight: 700;">
<i class="fas fa-magic" style="margin-right: 10px;"></i>${config.title}
<i class="fas fa-magic" style="margin-right: 10px;"></i><span class="ai-optimize-title"></span>
</h3>
<p style="margin: 0; opacity: 0.9; font-size: 0.9em;">${config.subtitle}</p>
<p class="ai-optimize-subtitle" style="margin: 0; opacity: 0.9; font-size: 0.9em;"></p>
</div>
<button onclick="this.closest('.ai-optimize-modal').remove()" style="background: transparent; color: #fff; border: 1px solid #fff; border-radius: 50%; width: 36px; height: 36px; cursor: pointer; font-size: 20px; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='transparent'">
<button type="button" class="ai-optimize-modal__close" aria-label="关闭" style="background: transparent; color: #fff; border: 1px solid #fff; border-radius: 50%; width: 36px; height: 36px; cursor: pointer; font-size: 20px; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='transparent'">
×
</button>
</div>
@@ -5511,7 +5524,7 @@
<div style="background: #f5f5f5; border-left: 4px solid #111; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div style="color: #111; font-size: 0.9em; line-height: 1.6;">
<strong style="color: #111;"><i class="fas fa-info-circle"></i> 当前内容</strong><br>
${config.currentInfo}
<span class="ai-optimize-current-info" style="white-space: pre-line;"></span>
</div>
</div>
@@ -5520,7 +5533,7 @@
<label style="display: block; margin-bottom: 10px; color: #111; font-weight: 600; font-size: 0.95em;">
<i class="fas fa-edit"></i> 请描述您的优化需求
</label>
<textarea id="aiOptimizeInput" style="width: 100%; min-height: 120px; padding: 15px; border: 2px solid #d9d9d9; border-radius: 10px; font-size: 14px; resize: vertical; font-family: inherit; transition: all 0.3s ease; background: #fff; color: #111;" placeholder="详细描述您希望如何优化此内容...\n\n例如:\n- 增加更多技术细节\n- 重新组织逻辑结构\n- 添加案例分析"></textarea>
<textarea class="ai-optimize-input" aria-label="优化需求" style="width: 100%; min-height: 120px; padding: 15px; border: 2px solid #d9d9d9; border-radius: 10px; font-size: 14px; resize: vertical; font-family: inherit; transition: all 0.3s ease; background: #fff; color: #111;" placeholder="详细描述您希望如何优化此内容...\n\n例如:\n- 增加更多技术细节\n- 重新组织逻辑结构\n- 添加案例分析"></textarea>
</div>
<!-- 快捷建议 -->
@@ -5528,9 +5541,7 @@
<label style="display: block; margin-bottom: 10px; color: #555; font-size: 0.85em;">
<i class="fas fa-lightbulb"></i> 点击快捷建议快速填充
</label>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
${suggestions.map(s => `<span class="suggestion-tag" onclick="document.getElementById('aiOptimizeInput').value = '${s}'">${s}</span>`).join('')}
</div>
<div class="ai-optimize-suggestion-list" style="display: flex; flex-wrap: wrap; gap: 8px;"></div>
</div>
</div>
@@ -5540,58 +5551,85 @@
<i class="fas fa-robot"></i> AI将根据您的需求智能优化内容
</div>
<div style="display: flex; gap: 10px;">
<button onclick="this.closest('.ai-optimize-modal').remove()" class="btn btn-sm" style="background: #fff; color: #111; padding: 10px 20px; border: 1px solid #111;">
<button type="button" class="btn btn-sm ai-optimize-cancel" style="background: #fff; color: #111; padding: 10px 20px; border: 1px solid #111;">
<i class="fas fa-times"></i> 取消
</button>
<button id="confirmOptimizeBtn" class="btn btn-sm" style="background: #111; color: #fff; padding: 10px 24px; border: 1px solid #111;">
<button type="button" class="btn btn-sm ai-optimize-confirm" style="background: #111; color: #fff; padding: 10px 24px; border: 1px solid #111;">
<i class="fas fa-magic"></i> 开始优化
</button>
</div>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
const previousFocus = document.activeElement;
const input = content.querySelector('.ai-optimize-input');
const confirmBtn = content.querySelector('.ai-optimize-confirm');
const currentInfoElement = content.querySelector('.ai-optimize-current-info');
content.querySelector('.ai-optimize-title').textContent = String(config.title || '');
content.querySelector('.ai-optimize-subtitle').textContent = String(config.subtitle || '');
currentInfoElement.textContent = String(config.currentInfo || '');
// 聚焦输入框
setTimeout(() => {
const input = document.getElementById('aiOptimizeInput');
if (input) input.focus();
}, 100);
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
reject('用户取消');
}
const suggestionList = content.querySelector('.ai-optimize-suggestion-list');
suggestions.forEach((suggestion) => {
const suggestionButton = document.createElement('button');
suggestionButton.type = 'button';
suggestionButton.className = 'suggestion-tag';
suggestionButton.textContent = String(suggestion);
suggestionButton.addEventListener('click', () => {
input.value = String(suggestion);
input.focus();
});
suggestionList.appendChild(suggestionButton);
});
// 确认按钮
const confirmBtn = document.getElementById('confirmOptimizeBtn');
confirmBtn.onclick = () => {
const input = document.getElementById('aiOptimizeInput');
const value = input?.value.trim();
if (!value) {
// 输入框抖动动画
input.style.animation = 'shake 0.5s';
setTimeout(() => { input.style.animation = ''; }, 500);
let closed = false;
function closeModal(value = null) {
if (closed) return;
closed = true;
document.removeEventListener('keydown', handleKeydown);
modal.remove();
previousFocus?.focus?.();
resolve(value);
}
function handleKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
closeModal();
return;
}
modal.remove();
resolve(value);
};
// 添加shake动画
const style = document.createElement('style');
style.textContent = `
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
if (event.key !== 'Tab') return;
const focusable = content.querySelectorAll('button:not([disabled]), textarea:not([disabled])');
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
`;
document.head.appendChild(style);
content.querySelector('.ai-optimize-modal__close').addEventListener('click', () => closeModal());
content.querySelector('.ai-optimize-cancel').addEventListener('click', () => closeModal());
modal.addEventListener('click', (event) => {
if (event.target === modal) closeModal();
});
confirmBtn.addEventListener('click', () => {
const value = input.value.trim();
if (!value) {
input.classList.add('shake');
setTimeout(() => input.classList.remove('shake'), 500);
return;
}
closeModal(value);
});
modal.appendChild(content);
document.body.appendChild(modal);
document.addEventListener('keydown', handleKeydown);
requestAnimationFrame(() => input.focus());
});
}
@@ -5633,7 +5671,7 @@
userRequest = await showAIOptimizeModal({
title: `AI优化 - 第${slideIndex + 1}页`,
subtitle: '让AI帮助您优化这一页的内容',
currentInfo: `<strong>标题:</strong>${escapeHtml(title)}<br>${subtitle ? `<strong>副标题:</strong>${escapeHtml(subtitle)}<br>` : ''}<strong>类型:</strong>${escapeHtml(slideType)}<br><strong>内容要点:</strong>${contentPoints.length}个`,
currentInfo: `标题:${title}\n${subtitle ? `副标题:${subtitle}\n` : ''}类型:${slideType}\n内容要点:${contentPoints.length}个`,
suggestions: [
'增加更多技术细节和实例',
'简化内容,突出核心要点',
@@ -5792,7 +5830,7 @@
userRequest = await showAIOptimizeModal({
title: '优化大纲',
subtitle: '让AI帮助您优化整个PPT大纲结构和内容',
currentInfo: `<strong>大纲标题:</strong>${escapeHtml(parsedOutline.title)}<br><strong>幻灯片数量:</strong>${parsedOutline.slides ? parsedOutline.slides.length : 0} 页<br><strong>场景:</strong>${escapeHtml(parsedOutline.metadata?.scenario) || '通用'}`,
currentInfo: `大纲标题:${parsedOutline.title || ''}\n幻灯片数量:${parsedOutline.slides ? parsedOutline.slides.length : 0} 页\n场景:${parsedOutline.metadata?.scenario || '通用'}`,
suggestions: [
'增加更多文字说明',
'重新组织结构,优化逻辑流程',
@@ -335,3 +335,32 @@ def test_editor_sidebar_thumbnail_refresh_recalculates_scale_without_overriding_
assert "aspect-ratio: 16 / 9;" in css
assert "height: 95px" not in css
assert "scale(0.1875)" in css
def test_ai_optimize_modals_settle_safely_and_use_scoped_dom():
sources = [
_read("src/landppt/web/templates/components/project/todo_board/extra_js_1.html"),
_read("src/landppt/web/templates/components/project/detail/extra_js_1.html"),
_read(
"src/landppt/web/static/js/pages/project/slides_editor/"
"projectSlidesEditor.aiOptimize.js"
),
]
for source in sources:
assert "function closeModal(value = null)" in source
assert "content.querySelector('.ai-optimize-input')" in source
assert "suggestionButton.textContent = String(suggestion)" in source
assert "currentInfoElement.textContent = String(config.currentInfo || '')" in source
assert "document.addEventListener('keydown', handleKeydown)" in source
assert ".ai-optimize-modal__close').addEventListener('click', () => closeModal())" in source
assert ".ai-optimize-cancel').addEventListener('click', () => closeModal())" in source
assert "if (event.target === modal) closeModal()" in source
assert 'id="aiOptimizeInput"' not in source
assert 'id="confirmOptimizeBtn"' not in source
assert "${suggestions.map" not in source
assert "${config.currentInfo}" not in source
assert "onclick=\"this.closest('.ai-optimize-modal').remove()\"" not in source
assert "document.head.appendChild(style)" not in sources[0]
assert "document.head.appendChild(style)" not in sources[1]