diff --git a/web/static/css/style.css b/web/static/css/style.css index 3de7152..75485e0 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1,3 +1,9 @@ +/* + * 精简版样式表 + * 注意:此站点现在主要使用 Tailwind CSS。 + * 这个文件只包含基本的重置样式,以支持可能不使用 Tailwind 的旧页面。 + */ + /* 基本样式重置 */ * { box-sizing: border-box; @@ -5,319 +11,12 @@ padding: 0; } -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - line-height: 1.6; - color: #333; - background-color: #f5f7fa; - padding: 20px; -} - -/* 容器 */ -.container { - max-width: 1000px; - margin: 0 auto; -} - -/* 页眉 */ -header { - text-align: center; - margin-bottom: 30px; - padding: 20px; -} - -header h1 { - font-size: 2.5rem; - margin-bottom: 10px; - color: #2c3e50; -} - -header p { - font-size: 1.2rem; - color: #7f8c8d; - margin-bottom: 20px; -} - -/* 导航 */ -nav { - display: flex; - justify-content: center; - margin-top: 20px; -} - -nav a { - text-decoration: none; - color: #3498db; - margin: 0 15px; - padding: 5px 10px; - border-radius: 5px; - transition: all 0.3s ease; -} - -nav a:hover { - background-color: #3498db; - color: #fff; -} - -nav a.active { - background-color: #3498db; - color: #fff; -} - -/* 卡片 */ -.card { - background-color: #fff; - border-radius: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - padding: 25px; - margin-bottom: 25px; -} - -/* 标题 */ -h2 { - color: #2c3e50; - margin-bottom: 20px; - border-bottom: 1px solid #ecf0f1; - padding-bottom: 10px; -} - -h3 { - color: #3498db; - margin: 20px 0 10px; -} - -/* 输入区域 */ -.input-group { - position: relative; - margin-bottom: 20px; -} - -textarea { - width: 100%; - padding: 15px; - border: 1px solid #ddd; - border-radius: 5px; - resize: none; - font-size: 1rem; - font-family: inherit; -} - -textarea:focus { - outline: none; - border-color: #3498db; - box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); -} - -.char-counter { - position: absolute; - bottom: 10px; - right: 10px; - font-size: 0.8rem; - color: #7f8c8d; -} - -/* API Key 输入框样式 */ -#api-key-group { - margin-bottom: 15px; - background-color: #f8f9fa; - padding: 12px 15px; - border-radius: 6px; - border-left: 4px solid #3498db; -} - -.api-key-container { - display: flex; - align-items: center; -} - -.api-key-container label { - width: 80px; /* 增加标签宽度 */ - flex-shrink: 0; /* 防止标签被压缩 */ - margin-bottom: 0; /* 覆盖默认的底部边距 */ -} - -#api-key { - width: 100%; - padding: 8px 10px; - border: 1px solid #ddd; - border-radius: 5px; - font-family: inherit; - font-size: 1rem; -} - -#api-key:focus { - outline: none; - border-color: #3498db; - box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); -} - -#api-key::placeholder { - color: #aaa; - font-size: 0.9rem; -} - -/* 设置区域 */ -.settings { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin-bottom: 20px; -} - -.setting-group { - display: flex; - flex-direction: column; -} - -label { - margin-bottom: 5px; - font-weight: bold; - color: #2c3e50; -} - -select, input[type="range"] { - padding: 8px; - border: 1px solid #ddd; - border-radius: 5px; - background-color: #fff; -} - -select:focus { - outline: none; - border-color: #3498db; -} - -/* 按钮 */ -.actions { - display: flex; - justify-content: center; - margin-top: 20px; -} - -button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 1rem; - transition: all 0.3s ease; -} - -.primary-button { - background-color: #3498db; - color: #fff; -} - -.primary-button:hover { - background-color: #2980b9; -} - -.secondary-button { - background-color: #ecf0f1; - color: #2c3e50; - margin: 0 5px; -} - -.secondary-button:hover { - background-color: #bdc3c7; -} - -/* 音频播放器 */ -.audio-player { - display: flex; - flex-direction: column; - align-items: center; +/* 针对旧浏览器的代码高亮支持 */ +pre { + overflow-x: auto; } +/* 确保音频播放器响应式适应 */ audio { width: 100%; - margin-bottom: 15px; -} - -.audio-controls { - display: flex; - justify-content: center; -} - -/* 表格 */ -table { - width: 100%; - border-collapse: collapse; - margin: 20px 0; -} - -th, td { - padding: 12px 15px; - text-align: left; - border-bottom: 1px solid #ddd; -} - -th { - background-color: #f8f9fa; - font-weight: bold; -} - -/* 代码 */ -code, pre { - font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; - background-color: #f8f9fa; - border-radius: 3px; - padding: 2px 5px; - font-size: 0.9rem; -} - -pre { - padding: 15px; - overflow-x: auto; - margin: 15px 0; -} - -pre code { - padding: 0; - background-color: transparent; -} - -/* 页脚 */ -footer { - text-align: center; - margin-top: 40px; - padding: 20px; - color: #7f8c8d; - font-size: 0.9rem; -} - -footer a { - color: #3498db; - text-decoration: none; -} - -footer a:hover { - text-decoration: underline; -} - -/* 响应式调整 */ -@media (max-width: 768px) { - .settings { - grid-template-columns: 1fr; - } - - header h1 { - font-size: 2rem; - } - - .card { - padding: 15px; - } -} - -.settings-row { - display: flex; - flex-direction: row; - justify-content: space-between; - gap: 20px; - width: 100%; -} - -.half-width { - width: 48%; } \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js index 736e044..e1fb737 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -9,6 +9,7 @@ document.addEventListener('DOMContentLoaded', function() { const pitchValue = document.getElementById('pitchValue'); const apiKeyInput = document.getElementById('api-key'); const apiKeyGroup = document.getElementById('api-key-group'); + const apiKeyButtons = document.getElementById('api-key-buttons'); const speakButton = document.getElementById('speak'); const downloadButton = document.getElementById('download'); const copyLinkButton = document.getElementById('copyLink'); @@ -16,33 +17,34 @@ document.addEventListener('DOMContentLoaded', function() { const audioPlayer = document.getElementById('audioPlayer'); const resultSection = document.getElementById('resultSection'); const charCount = document.getElementById('charCount'); - + // 保存最后一个音频URL let lastAudioUrl = ''; // 存储语音数据 let voicesData = []; - + // 初始化 initVoicesList(); initEventListeners(); - + loadApiKeyFromLocalStorage(); // 加载API Key + // 更新字符计数 textInput.addEventListener('input', function() { charCount.textContent = this.value.length; }); - + // 更新语速值显示 rateInput.addEventListener('input', function() { const value = this.value; rateValue.textContent = value + '%'; }); - + // 更新语调值显示 pitchInput.addEventListener('input', function() { const value = this.value; pitchValue.textContent = value + '%'; }); - + // 语音选择变化时更新可用风格 voiceSelect.addEventListener('change', function() { updateStyleOptions(); @@ -53,40 +55,40 @@ document.addEventListener('DOMContentLoaded', function() { try { const response = await fetch(`${config.basePath}/voices`); if (!response.ok) throw new Error('获取语音列表失败'); - + voicesData = await response.json(); - + // 清空并重建选项 voiceSelect.innerHTML = ''; - + // 按语言和名称分组 const voicesByLocale = {}; - + voicesData.forEach(voice => { if (!voicesByLocale[voice.locale]) { voicesByLocale[voice.locale] = []; } voicesByLocale[voice.locale].push(voice); }); - + // 创建选项组 for (const locale in voicesByLocale) { const optgroup = document.createElement('optgroup'); optgroup.label = voicesByLocale[locale][0].locale_name; - + voicesByLocale[locale].forEach(voice => { const option = document.createElement('option'); option.value = voice.short_name; option.textContent = `${voice.local_name || voice.display_name} (${voice.gender})`; - + // 如果是默认语音则选中 if (voice.short_name === config.defaultVoice) { option.selected = true; } - + optgroup.appendChild(option); }); - + voiceSelect.appendChild(optgroup); } @@ -97,7 +99,7 @@ document.addEventListener('DOMContentLoaded', function() { voiceSelect.innerHTML = ''; } } - + // 更新风格选项 function updateStyleOptions() { // 清空风格选择 @@ -119,8 +121,8 @@ document.addEventListener('DOMContentLoaded', function() { // 添加可用风格选项 voiceData.style_list.forEach(style => { const option = document.createElement('option'); - option.value = style - option.textContent = style + option.value = style; + option.textContent = style; // 如果是默认风格则选中 if (style === config.defaultStyle || @@ -136,7 +138,7 @@ document.addEventListener('DOMContentLoaded', function() { function initEventListeners() { // 转换按钮点击事件 speakButton.addEventListener('click', generateSpeech); - + // 下载按钮点击事件 downloadButton.addEventListener('click', function() { if (lastAudioUrl) { @@ -148,7 +150,7 @@ document.addEventListener('DOMContentLoaded', function() { document.body.removeChild(a); } }); - + // 复制链接按钮点击事件 copyLinkButton.addEventListener('click', function() { if (lastAudioUrl) { @@ -173,7 +175,7 @@ document.addEventListener('DOMContentLoaded', function() { const apiKey = apiKeyInput.value.trim(); // 构建HttpTTS链接 - let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t={{java.encodeURI(speakText)}}&v=${voice}&r={{speakSpeed*4}}&p=${pitch}&s=${style}`; + let httpTtsLink = `${window.location.origin}${config.basePath}/tts?t=${encodeURIComponent(text)}&v=${voice}&r=${rate}&p=${pitch}&s=${style}`; // 添加API Key参数(如果有) if (apiKey) { @@ -182,8 +184,93 @@ document.addEventListener('DOMContentLoaded', function() { copyToClipboard(httpTtsLink); }); + + // 保存API Key按钮点击事件 + const saveApiKeyButton = document.createElement('button'); + saveApiKeyButton.textContent = '保存'; + saveApiKeyButton.className = 'flex-1 h-10 px-4 bg-gray-100 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-opacity-50 flex items-center justify-center'; + saveApiKeyButton.innerHTML = ` + + + + 保存 + `; + saveApiKeyButton.addEventListener('click', function() { + saveApiKeyToLocalStorage(apiKeyInput.value); + + // 显示保存成功提示 + const successNotice = document.createElement('div'); + successNotice.className = 'fixed top-4 right-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded shadow-md flex items-center z-50'; + successNotice.innerHTML = ` + + + + API Key 已保存 + `; + document.body.appendChild(successNotice); + + // 2秒后自动移除提示 + setTimeout(() => { + document.body.removeChild(successNotice); + }, 2000); + }); + apiKeyButtons.appendChild(saveApiKeyButton); + + // 清除API Key按钮点击事件 + const clearApiKeyButton = document.createElement('button'); + clearApiKeyButton.textContent = '清除'; + clearApiKeyButton.className = 'flex-1 h-10 px-4 bg-gray-100 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-opacity-50 flex items-center justify-center'; + clearApiKeyButton.innerHTML = ` + + + + 清除 + `; + clearApiKeyButton.addEventListener('click', function() { + localStorage.removeItem('apiKey'); + apiKeyInput.value = ''; + + // 显示清除成功提示 + const successNotice = document.createElement('div'); + successNotice.className = 'fixed top-4 right-4 bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded shadow-md flex items-center z-50'; + successNotice.innerHTML = ` + + + + API Key 已清除 + `; + document.body.appendChild(successNotice); + + // 2秒后自动移除提示 + setTimeout(() => { + document.body.removeChild(successNotice); + }, 2000); + }); + apiKeyButtons.appendChild(clearApiKeyButton); } - + + // 增加密码显示/隐藏功能 + const togglePasswordButton = document.getElementById('toggle-password'); + if (togglePasswordButton) { + togglePasswordButton.addEventListener('click', function() { + const apiKeyInput = document.getElementById('api-key'); + const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password'; + apiKeyInput.setAttribute('type', type); + + // 更新图标 + if (type === 'password') { + this.innerHTML = ` + + + `; + } else { + this.innerHTML = ` + + `; + } + }); + } + // 复制内容到剪贴板的通用函数 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { @@ -215,17 +302,17 @@ document.addEventListener('DOMContentLoaded', function() { alert('请输入要转换的文本'); return; } - + const voice = voiceSelect.value; const style = styleSelect.value; const rate = rateInput.value; const pitch = pitchInput.value; const apiKey = apiKeyInput.value.trim(); - + // 禁用按钮,显示加载状态 speakButton.disabled = true; speakButton.textContent = '生成中...'; - + try { // 构建URL参数 const params = new URLSearchParams({ @@ -235,14 +322,14 @@ document.addEventListener('DOMContentLoaded', function() { r: rate, p: pitch }); - + // 添加API Key参数(如果有) if (apiKey) { params.append('api_key', apiKey); } const url = `${config.basePath}/tts?${params.toString()}`; - + // 使用fetch发送请求以便捕获HTTP状态码 const response = await fetch(url); @@ -264,10 +351,10 @@ document.addEventListener('DOMContentLoaded', function() { // 更新音频播放器 audioPlayer.src = audioUrl; lastAudioUrl = url; // 保存原始URL用于下载和复制链接 - + // 显示结果区域 - resultSection.style.display = 'block'; - + resultSection.classList.remove('hidden'); + // 播放音频 audioPlayer.play(); } catch (error) { @@ -281,4 +368,21 @@ document.addEventListener('DOMContentLoaded', function() { speakButton.textContent = '转换为语音'; } } -}); \ No newline at end of file + + // 保存API Key到localStorage + function saveApiKeyToLocalStorage(apiKey) { + if (apiKey) { + localStorage.setItem('apiKey', apiKey); + } else { + localStorage.removeItem('apiKey'); + } + } + + // 从localStorage加载API Key + function loadApiKeyFromLocalStorage() { + const apiKey = localStorage.getItem('apiKey'); + if (apiKey) { + apiKeyInput.value = apiKey; + } + } +}); diff --git a/web/templates/api-doc.html b/web/templates/api-doc.html index 696958c..9b74f62 100644 --- a/web/templates/api-doc.html +++ b/web/templates/api-doc.html @@ -4,156 +4,164 @@ API文档 - TTS服务 - + - -
-
-

TTS服务 API文档

-

快速、高质量的文本转语音API服务

-