mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-30 17:34:51 +08:00
Harden AI optimize modals with safer DOM and a11y
This commit is contained in:
+1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+78
-39
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user