feat: implement API Key management features including save, clear, and test functionalities
This commit is contained in:
@@ -1,22 +0,0 @@
|
|||||||
/*
|
|
||||||
* 精简版样式表
|
|
||||||
* 注意:此站点现在主要使用 Tailwind CSS。
|
|
||||||
* 这个文件只包含基本的重置样式,以支持可能不使用 Tailwind 的旧页面。
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* 基本样式重置 */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 针对旧浏览器的代码高亮支持 */
|
|
||||||
pre {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保音频播放器响应式适应 */
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,29 @@
|
|||||||
|
function saveApiKey() {
|
||||||
|
console.log('Save API Key function called directly');
|
||||||
|
|
||||||
|
const apiKeyInput = document.getElementById('api-key');
|
||||||
|
const apiKeyGroup = document.getElementById('api-key-group');
|
||||||
|
|
||||||
|
if (apiKeyInput) {
|
||||||
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
if (apiKey) {
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('apiKey', apiKey);
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
showCustomAlert('API Key 已成功保存', 'success');
|
||||||
|
|
||||||
|
// 隐藏API Key输入区域
|
||||||
|
if (apiKeyGroup) {
|
||||||
|
apiKeyGroup.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCustomAlert('请输入有效的API Key', 'warning');
|
||||||
|
apiKeyInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 获取DOM元素
|
// 获取DOM元素
|
||||||
const textInput = document.getElementById('text');
|
const textInput = document.getElementById('text');
|
||||||
@@ -17,6 +43,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const audioPlayer = document.getElementById('audioPlayer');
|
const audioPlayer = document.getElementById('audioPlayer');
|
||||||
const resultSection = document.getElementById('resultSection');
|
const resultSection = document.getElementById('resultSection');
|
||||||
const charCount = document.getElementById('charCount');
|
const charCount = document.getElementById('charCount');
|
||||||
|
const toggleApiKeyButton = document.getElementById('toggle-api-key');
|
||||||
|
const apiKeyStatus = document.getElementById('api-key-status');
|
||||||
|
const apiKeySaveButton = document.getElementById('api-key-save');
|
||||||
|
const apiKeyClearButton = document.getElementById('api-key-clear');
|
||||||
|
const apiKeyTestButton = document.getElementById('api-key-test');
|
||||||
|
const togglePasswordButton = document.getElementById('toggle-password');
|
||||||
|
const apiKeyHelp = document.getElementById('api-key-help');
|
||||||
|
|
||||||
// 保存最后一个音频URL
|
// 保存最后一个音频URL
|
||||||
let lastAudioUrl = '';
|
let lastAudioUrl = '';
|
||||||
@@ -27,6 +60,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
initVoicesList();
|
initVoicesList();
|
||||||
initEventListeners();
|
initEventListeners();
|
||||||
loadApiKeyFromLocalStorage(); // 加载API Key
|
loadApiKeyFromLocalStorage(); // 加载API Key
|
||||||
|
updateApiKeyVisibility(); // 更新API Key区域的可见性
|
||||||
|
|
||||||
// 更新字符计数
|
// 更新字符计数
|
||||||
textInput.addEventListener('input', function() {
|
textInput.addEventListener('input', function() {
|
||||||
@@ -61,6 +95,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// 清空并重建选项
|
// 清空并重建选项
|
||||||
voiceSelect.innerHTML = '';
|
voiceSelect.innerHTML = '';
|
||||||
|
|
||||||
|
// 如果还在加载,使用带有动画的加载提示
|
||||||
|
if (voicesData.length === 0) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = "loading";
|
||||||
|
option.textContent = "加载中";
|
||||||
|
option.className = "loading-text";
|
||||||
|
voiceSelect.appendChild(option);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 按语言和名称分组
|
// 按语言和名称分组
|
||||||
const voicesByLocale = {};
|
const voicesByLocale = {};
|
||||||
|
|
||||||
@@ -96,7 +140,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
updateStyleOptions();
|
updateStyleOptions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取语音列表失败:', error);
|
console.error('获取语音列表失败:', error);
|
||||||
voiceSelect.innerHTML = '<option value="">无法加载语音列表</option>';
|
voiceSelect.innerHTML = '<option value="" class="text-red-500">无法加载语音列表</option>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +208,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
copyHttpTtsLinkButton.addEventListener('click', function() {
|
copyHttpTtsLinkButton.addEventListener('click', function() {
|
||||||
const text = textInput.value.trim();
|
const text = textInput.value.trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
alert('请输入要转换的文本');
|
showCustomAlert('请输入要转换的文本', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,121 +229,128 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
copyToClipboard(httpTtsLink);
|
copyToClipboard(httpTtsLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存API Key按钮点击事件
|
// 显示/隐藏API Key区域的按钮事件
|
||||||
const saveApiKeyButton = document.createElement('button');
|
if (toggleApiKeyButton) {
|
||||||
saveApiKeyButton.textContent = '保存';
|
toggleApiKeyButton.addEventListener('click', function() {
|
||||||
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';
|
console.log('API Key button clicked'); // 添加调试日志
|
||||||
saveApiKeyButton.innerHTML = `
|
if (apiKeyGroup) {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
apiKeyGroup.classList.toggle('hidden');
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
|
||||||
</svg>
|
|
||||||
保存
|
|
||||||
`;
|
|
||||||
saveApiKeyButton.addEventListener('click', function() {
|
|
||||||
saveApiKeyToLocalStorage(apiKeyInput.value);
|
|
||||||
|
|
||||||
// 显示保存成功提示
|
// 如果是显示操作,聚焦到输入框
|
||||||
const successNotice = document.createElement('div');
|
if (!apiKeyGroup.classList.contains('hidden') && apiKeyInput) {
|
||||||
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';
|
apiKeyInput.focus();
|
||||||
successNotice.innerHTML = `
|
}
|
||||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
});
|
||||||
</svg>
|
}
|
||||||
API Key 已保存
|
|
||||||
`;
|
|
||||||
document.body.appendChild(successNotice);
|
|
||||||
|
|
||||||
// 2秒后自动移除提示
|
// API Key显示/隐藏功能
|
||||||
setTimeout(() => {
|
if (togglePasswordButton) {
|
||||||
document.body.removeChild(successNotice);
|
togglePasswordButton.addEventListener('click', function() {
|
||||||
}, 2000);
|
const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
});
|
apiKeyInput.setAttribute('type', type);
|
||||||
apiKeyButtons.appendChild(saveApiKeyButton);
|
|
||||||
|
|
||||||
// 清除API Key按钮点击事件
|
// 更新图标
|
||||||
const clearApiKeyButton = document.createElement('button');
|
if (type === 'password') {
|
||||||
clearApiKeyButton.textContent = '清除';
|
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
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';
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
clearApiKeyButton.innerHTML = `
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</svg>`;
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
} else {
|
||||||
</svg>
|
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
清除
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18" />
|
||||||
`;
|
</svg>`;
|
||||||
clearApiKeyButton.addEventListener('click', function() {
|
}
|
||||||
localStorage.removeItem('apiKey');
|
});
|
||||||
apiKeyInput.value = '';
|
}
|
||||||
|
|
||||||
// 显示清除成功提示
|
// 删除原有的保存API Key事件绑定,使用HTML onclick
|
||||||
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 = `
|
|
||||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
||||||
</svg>
|
|
||||||
API Key 已清除
|
|
||||||
`;
|
|
||||||
document.body.appendChild(successNotice);
|
|
||||||
|
|
||||||
// 2秒后自动移除提示
|
// 按Enter键保存API Key
|
||||||
setTimeout(() => {
|
if (apiKeyInput) {
|
||||||
document.body.removeChild(successNotice);
|
apiKeyInput.addEventListener('keydown', function(event) {
|
||||||
}, 2000);
|
if (event.key === 'Enter') {
|
||||||
});
|
event.preventDefault();
|
||||||
apiKeyButtons.appendChild(clearApiKeyButton);
|
saveApiKey(); // 直接调用全局保存函数
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 增加密码显示/隐藏功能
|
// 清除API Key
|
||||||
const togglePasswordButton = document.getElementById('toggle-password');
|
if (apiKeyClearButton) {
|
||||||
if (togglePasswordButton) {
|
apiKeyClearButton.addEventListener('click', function() {
|
||||||
togglePasswordButton.addEventListener('click', function() {
|
apiKeyInput.value = '';
|
||||||
const apiKeyInput = document.getElementById('api-key');
|
localStorage.removeItem('apiKey');
|
||||||
const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
showNotification('API Key 已清除', 'success');
|
||||||
apiKeyInput.setAttribute('type', type);
|
|
||||||
|
|
||||||
// 更新图标
|
// 更新状态提示
|
||||||
if (type === 'password') {
|
apiKeyStatus.textContent = '';
|
||||||
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
apiKeyStatus.className = 'api-key-status hidden';
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
});
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
}
|
||||||
</svg>`;
|
|
||||||
} else {
|
|
||||||
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18" />
|
|
||||||
</svg>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制内容到剪贴板的通用函数
|
// 测试API Key
|
||||||
function copyToClipboard(text) {
|
if (apiKeyTestButton) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
apiKeyTestButton.addEventListener('click', function() {
|
||||||
alert('链接已复制到剪贴板');
|
const apiKey = apiKeyInput.value.trim();
|
||||||
}).catch(err => {
|
if (!apiKey) {
|
||||||
console.error('复制失败:', err);
|
showNotification('请先输入API Key再测试', 'warning');
|
||||||
// 兼容处理
|
apiKeyInput.focus();
|
||||||
const textArea = document.createElement('textarea');
|
return;
|
||||||
textArea.value = text;
|
}
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
|
|
||||||
try {
|
apiKeyTestButton.disabled = true;
|
||||||
document.execCommand('copy');
|
apiKeyTestButton.innerHTML = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> 测试中...';
|
||||||
alert('链接已复制到剪贴板');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
// 使用一个简单请求测试API Key
|
||||||
});
|
fetch(`${config.basePath}/voices?limit=1`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
apiKeyStatus.textContent = 'API Key 有效';
|
||||||
|
apiKeyStatus.className = 'api-key-status valid';
|
||||||
|
apiKeyStatus.classList.remove('hidden');
|
||||||
|
showNotification('API Key 验证成功', 'success');
|
||||||
|
} else {
|
||||||
|
apiKeyStatus.textContent = 'API Key 无效';
|
||||||
|
apiKeyStatus.className = 'api-key-status invalid';
|
||||||
|
apiKeyStatus.classList.remove('hidden');
|
||||||
|
showNotification('API Key 验证失败', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('测试API Key时出错:', error);
|
||||||
|
apiKeyStatus.textContent = '测试失败,请检查网络';
|
||||||
|
apiKeyStatus.className = 'api-key-status invalid';
|
||||||
|
apiKeyStatus.classList.remove('hidden');
|
||||||
|
showNotification('无法完成API Key测试', 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
apiKeyTestButton.disabled = false;
|
||||||
|
apiKeyTestButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> 测试';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示帮助信息
|
||||||
|
if (apiKeyHelp) {
|
||||||
|
apiKeyHelp.addEventListener('click', function() {
|
||||||
|
showNotification('Azure API Key 可在Microsoft Azure门户中的认知服务资源下找到。更多信息请访问帮助文档。', 'info', 8000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增强音频播放器
|
||||||
|
enhanceAudioPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成语音
|
// 生成语音
|
||||||
async function generateSpeech() {
|
async function generateSpeech() {
|
||||||
const text = textInput.value.trim();
|
const text = textInput.value.trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
alert('请输入要转换的文本');
|
showCustomAlert('请输入要转换的文本', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,8 +386,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// 显示API Key输入框
|
// 显示API Key输入框
|
||||||
apiKeyGroup.style.display = 'flex';
|
apiKeyGroup.classList.remove('hidden');
|
||||||
alert('请输入有效的API Key以继续操作');
|
showCustomAlert('请输入有效的API Key以继续操作', 'error');
|
||||||
throw new Error('需要API Key授权');
|
throw new Error('需要API Key授权');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +411,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('生成语音失败:', error);
|
console.error('生成语音失败:', error);
|
||||||
if (error.message !== '需要API Key授权') {
|
if (error.message !== '需要API Key授权') {
|
||||||
alert('生成语音失败,请重试');
|
showCustomAlert('生成语音失败,请重试', 'error');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 恢复按钮状态
|
// 恢复按钮状态
|
||||||
@@ -371,18 +422,290 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// 保存API Key到localStorage
|
// 保存API Key到localStorage
|
||||||
function saveApiKeyToLocalStorage(apiKey) {
|
function saveApiKeyToLocalStorage(apiKey) {
|
||||||
|
console.log('Saving API Key to localStorage'); // 添加调试日志
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
localStorage.setItem('apiKey', apiKey);
|
localStorage.setItem('apiKey', apiKey);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('apiKey');
|
localStorage.removeItem('apiKey');
|
||||||
|
// 如果清除了API Key,显示输入区域
|
||||||
|
if (apiKeyGroup) {
|
||||||
|
apiKeyGroup.classList.remove('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从localStorage加载API Key
|
// 从localStorage加载API Key
|
||||||
function loadApiKeyFromLocalStorage() {
|
function loadApiKeyFromLocalStorage() {
|
||||||
const apiKey = localStorage.getItem('apiKey');
|
const apiKey = localStorage.getItem('apiKey');
|
||||||
if (apiKey) {
|
|
||||||
|
if (apiKey && apiKeyInput) {
|
||||||
apiKeyInput.value = apiKey;
|
apiKeyInput.value = apiKey;
|
||||||
|
|
||||||
|
// 显示已保存状态
|
||||||
|
if (apiKeyStatus) {
|
||||||
|
apiKeyStatus.textContent = 'API Key 已保存';
|
||||||
|
apiKeyStatus.className = 'api-key-status valid';
|
||||||
|
apiKeyStatus.classList.remove('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新API Key区域的可见性
|
||||||
|
function updateApiKeyVisibility() {
|
||||||
|
// 确保元素存在
|
||||||
|
if (!apiKeyGroup) return;
|
||||||
|
|
||||||
|
// 如果有保存的API Key,则默认隐藏API Key输入区域
|
||||||
|
const apiKey = localStorage.getItem('apiKey');
|
||||||
|
if (apiKey && apiKey.trim() !== '') {
|
||||||
|
apiKeyGroup.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
apiKeyGroup.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义音频播放器增强
|
||||||
|
function enhanceAudioPlayer() {
|
||||||
|
// 监听音频播放器出现在DOM中
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.addedNodes.length) {
|
||||||
|
const audioPlayer = document.getElementById('audioPlayer');
|
||||||
|
if (audioPlayer && !audioPlayer.dataset.enhanced) {
|
||||||
|
// 标记为已增强,避免重复处理
|
||||||
|
audioPlayer.dataset.enhanced = 'true';
|
||||||
|
|
||||||
|
// 为音频播放器添加额外样式
|
||||||
|
audioPlayer.addEventListener('play', () => {
|
||||||
|
audioPlayer.classList.add('playing');
|
||||||
|
// 可以添加播放时的视觉反馈
|
||||||
|
resultSection.classList.add('active-playback');
|
||||||
|
});
|
||||||
|
|
||||||
|
audioPlayer.addEventListener('pause', () => {
|
||||||
|
audioPlayer.classList.remove('playing');
|
||||||
|
resultSection.classList.remove('active-playback');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开始观察DOM变化
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制内容到剪贴板的通用函数
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
showCustomAlert('链接已复制到剪贴板', 'success');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
// 兼容处理
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showCustomAlert('链接已复制到剪贴板', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加通知函数
|
||||||
|
function showNotification(message, type = 'info', duration = 3000) {
|
||||||
|
// 移除任何现有通知
|
||||||
|
const existingNotifications = document.querySelectorAll('.api-key-notification');
|
||||||
|
existingNotifications.forEach(notification => {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建通知元素
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `api-key-notification ${type}`;
|
||||||
|
|
||||||
|
// 添加图标
|
||||||
|
let icon = '';
|
||||||
|
switch(type) {
|
||||||
|
case 'success':
|
||||||
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="#10b981"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="#ef4444"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /></svg>';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="#f59e0b"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="#3b82f6"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `${icon}<span>${message}</span>`;
|
||||||
|
|
||||||
|
// 添加到页面
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示动画
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 设置自动关闭
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加特定的初始化函数来确保API Key按钮功能正常
|
||||||
|
function ensureApiKeyFunctionality() {
|
||||||
|
const saveApiKeyBtn = document.getElementById('api-key-save');
|
||||||
|
if (saveApiKeyBtn && !saveApiKeyBtn.hasEventListener) {
|
||||||
|
saveApiKeyBtn.hasEventListener = true;
|
||||||
|
saveApiKeyBtn.addEventListener('click', function() {
|
||||||
|
console.log('Save API Key button clicked (fallback)');
|
||||||
|
|
||||||
|
const apiKeyInput = document.getElementById('api-key');
|
||||||
|
if (apiKeyInput) {
|
||||||
|
const apiKey = apiKeyInput.value.trim();
|
||||||
|
if (apiKey) {
|
||||||
|
localStorage.setItem('apiKey', apiKey);
|
||||||
|
showCustomAlert('API Key 已成功保存', 'success');
|
||||||
|
|
||||||
|
const apiKeyGroup = document.getElementById('api-key-group');
|
||||||
|
if (apiKeyGroup) {
|
||||||
|
apiKeyGroup.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showCustomAlert('请输入有效的API Key', 'warning');
|
||||||
|
apiKeyInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保API Key功能在DOM加载后初始化
|
||||||
|
setTimeout(ensureApiKeyFunctionality, 500);
|
||||||
|
|
||||||
|
// 添加自定义alert函数到全局范围
|
||||||
|
window.showCustomAlert = showCustomAlert;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自定义alert函数
|
||||||
|
function showCustomAlert(message, type = 'info', title = '', duration = 3000) {
|
||||||
|
// 获取或创建通知容器
|
||||||
|
let container = document.getElementById('custom-alert-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = 'custom-alert-container';
|
||||||
|
container.className = 'custom-alert-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建通知元素
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `custom-alert ${type}`;
|
||||||
|
|
||||||
|
// 根据类型设置图标
|
||||||
|
let iconSvg = '';
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-.997-6l7.07-7.071-1.414-1.414-5.656 5.657-2.829-2.829-1.414 1.414L11.003 16z"/></svg>';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>';
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>';
|
||||||
|
break;
|
||||||
|
default: // info
|
||||||
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建通知内容
|
||||||
|
alert.innerHTML = `
|
||||||
|
<div class="custom-alert-icon">
|
||||||
|
${iconSvg}
|
||||||
|
</div>
|
||||||
|
<div class="custom-alert-content">
|
||||||
|
${title ? `<h4>${title}</h4>` : ''}
|
||||||
|
<p class="custom-alert-message">${message}</p>
|
||||||
|
</div>
|
||||||
|
<button class="custom-alert-close" aria-label="关闭">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="custom-alert-progress"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加到容器
|
||||||
|
container.appendChild(alert);
|
||||||
|
|
||||||
|
// 添加关闭事件
|
||||||
|
const closeBtn = alert.querySelector('.custom-alert-close');
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
removeAlert(alert);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
setTimeout(() => {
|
||||||
|
alert.classList.add('show');
|
||||||
|
|
||||||
|
// 进度条动画
|
||||||
|
const progress = alert.querySelector('.custom-alert-progress::after');
|
||||||
|
if (progress) {
|
||||||
|
progress.style.animation = `progress ${duration}ms linear forwards`;
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 自动关闭
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
removeAlert(alert);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
// 清除函数
|
||||||
|
function removeAlert(element) {
|
||||||
|
element.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (element.parentNode) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回alert对象,以便可以手动控制
|
||||||
|
return {
|
||||||
|
element: alert,
|
||||||
|
close: () => removeAlert(alert)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换全局的alert函数(可选,谨慎使用)
|
||||||
|
const originalAlert = window.alert;
|
||||||
|
window.alert = function(message) {
|
||||||
|
showCustomAlert(message, 'info');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在页面加载后,添加一次性检查以确保按钮可点击
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
console.log('Window loaded, checking save button...');
|
||||||
|
const saveBtn = document.getElementById('api-key-save');
|
||||||
|
if (saveBtn) {
|
||||||
|
console.log('Save button found, adding fallback click handler');
|
||||||
|
saveBtn.onclick = saveApiKey;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" class="bg-[#0f1a2f]">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -7,161 +7,502 @@
|
|||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/static/icons/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/static/icons/favicon.svg">
|
||||||
<meta name="description" content="TTS服务API文档">
|
<meta name="description" content="TTS服务API文档">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
background-color: #0f1a2f; /* 确保即使在滚动时也有深色背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定渐变背景,移除动画效果 */
|
||||||
|
.bg-gradient-animated {
|
||||||
|
background: linear-gradient(-45deg, #0f1a2f, #191e3a, #1a365d, #0d2f62);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激光效果 */
|
||||||
|
.laser-container {
|
||||||
|
position: fixed; /* 改为fixed定位,使激光效果固定在视口中 */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw; /* 使用视口宽度单位而非百分比 */
|
||||||
|
height: 100vh; /* 使用视口高度单位 */
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none; /* 确保不会拦截鼠标事件 */
|
||||||
|
overflow: hidden; /* 防止激光光束溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-beam {
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(to right, transparent, rgba(0, 195, 255, 0.5), transparent);
|
||||||
|
height: 2px;
|
||||||
|
width: 150vw; /* 增加宽度确保激光覆盖旋转后的视口 */
|
||||||
|
transform-origin: 0 0;
|
||||||
|
left: -25vw; /* 偏移,确保即使旋转也能覆盖整个屏幕 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-beam:nth-child(2n) {
|
||||||
|
background: linear-gradient(to right, transparent, rgba(255, 0, 200, 0.5), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-beam:nth-child(3n) {
|
||||||
|
background: linear-gradient(to right, transparent, rgba(0, 255, 170, 0.5), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.laser-beam:nth-child(5n) {
|
||||||
|
background: linear-gradient(to right, transparent, rgba(255, 250, 0, 0.3), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除霓虹光效果动画 */
|
||||||
|
.glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 60%;
|
||||||
|
height: 60%;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(80px);
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow:nth-child(1) {
|
||||||
|
top: -30%;
|
||||||
|
left: -10%;
|
||||||
|
background: linear-gradient(45deg, #4158D0, #0084ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow:nth-child(2) {
|
||||||
|
bottom: -30%;
|
||||||
|
right: -10%;
|
||||||
|
background: linear-gradient(-45deg, #00f2ff, #4158D0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.content-area {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 毛玻璃面板效果 */
|
||||||
|
.panel-frosted {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块毛玻璃效果 */
|
||||||
|
pre {
|
||||||
|
background: rgba(45, 55, 72, 0.8) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid rgba(74, 85, 104, 0.3);
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap; /* 确保长字符串可以换行 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格毛玻璃效果 */
|
||||||
|
table {
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
table-layout: fixed; /* 控制表格布局算法 */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: rgba(247, 250, 252, 0.8) !重要;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接效果强化 */
|
||||||
|
a.text-blue-500 {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.text-blue-500:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
background: currentColor;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.text-blue-500:hover:after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复表格响应式问题 */
|
||||||
|
.overflow-x-auto {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式修复 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
pre code {
|
||||||
|
word-break: break-all; /* 在移动设备上更激进地断行 */
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
word-break: break-word; /* 允许在任何字符处断行 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 重新设计的按钮基础样式 */
|
||||||
|
.btn-neo {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(13, 47, 98, 0.6);
|
||||||
|
color: #dbeafe;
|
||||||
|
border: none;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 8px -2px rgba(0, 0, 0, 0.2),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0 -1px 0 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮边框发光效果 */
|
||||||
|
.btn-neo::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(147, 197, 253, 0.3), rgba(59, 130, 246, 0.1));
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮悬浮效果 */
|
||||||
|
.btn-neo:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 16px -4px rgba(0, 0, 0, 0.3),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
|
||||||
|
0 0 10px 2px rgba(59, 130, 246, 0.4);
|
||||||
|
background: rgba(26, 86, 219, 0.7);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新设计的Tab栏样式 - 更加和谐 */
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(13, 47, 98, 0.15); /* 更轻微的背景色 */
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 4px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.15),
|
||||||
|
0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
max-width: 380px;
|
||||||
|
margin: 0 auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-bar::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.2); /* 更微妙的边框 */
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
position: relative;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 18px;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
border-radius: 6px;
|
||||||
|
z-index: 2;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
color: #1a365d; /* 更深的文字颜色,与主题对比 */
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.9); /* 纯白色指示器 */
|
||||||
|
border-radius: 6px;
|
||||||
|
height: calc(100% - 8px);
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab中的图标样式 - 简化 */
|
||||||
|
.tab-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 6px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover .tab-icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-icon {
|
||||||
|
color: #1a365d; /* 图标颜色与文本匹配 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简化下方线条效果 */
|
||||||
|
.tab-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
bottom: -2px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) scaleX(0);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active::after {
|
||||||
|
opacity: 0; /* 移除活动状态下的额外线条,保持简洁 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-gray-800 p-4 font-sans">
|
<body class="bg-gradient-animated text-gray-800 font-sans min-h-screen">
|
||||||
<div class="max-w-4xl mx-auto">
|
<!-- 激光效果 -->
|
||||||
<header class="text-center mb-8 py-4">
|
<div class="laser-container">
|
||||||
<h1 class="text-4xl font-bold mb-2 text-gray-800">TTS服务 API文档</h1>
|
<div class="glow"></div>
|
||||||
<p class="text-xl text-gray-600 mb-4">快速、高质量的文本转语音API服务</p>
|
<div class="glow"></div>
|
||||||
<nav class="flex justify-center mt-4">
|
<div class="laser-beam" style="top: 10%; animation-delay: 0.5s;"></div>
|
||||||
<a href="{{.BasePath}}/" class="mx-2 px-3 py-1 rounded text-blue-500 hover:bg-blue-500 hover:text-white transition-colors">主页</a>
|
<div class="laser-beam" style="top: 20%; animation-delay: 2s;"></div>
|
||||||
<a href="{{.BasePath}}/api-doc" class="mx-2 px-3 py-1 rounded bg-blue-500 text-white">API文档</a>
|
<div class="laser-beam" style="top: 30%; animation-delay: 1s;"></div>
|
||||||
</nav>
|
<div class="laser-beam" style="top: 45%; animation-delay: 3.5s;"></div>
|
||||||
</header>
|
<div class="laser-beam" style="top: 60%; animation-delay: 2.5s;"></div>
|
||||||
|
<div class="laser-beam" style="top: 75%; animation-delay: 0s;"></div>
|
||||||
|
<div class="laser-beam" style="top: 90%; animation-delay: 4s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main>
|
<div class="max-w-4xl mx-auto p-4 relative">
|
||||||
<section class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div class="content-area p-6 shadow-xl my-8">
|
||||||
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">API概述</h2>
|
<header class="text-center mb-8 py-4">
|
||||||
<p class="mb-3">TTS服务API提供了简单而强大的方式将文本转换为自然语音。我们支持多种语言和声音,并允许您调节语速、语调以适应不同场景需求。</p>
|
<h1 class="text-4xl font-bold mb-2 text-gray-800">TTS服务 API文档</h1>
|
||||||
<p class="mb-3">基础URL: <code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{{.BasePath}}</code></p>
|
<p class="text-xl text-gray-600 mb-4">快速、高质量的文本转语音API服务</p>
|
||||||
<p class="mb-3">所有API请求均使用HTTP协议,返回标准HTTP状态码表示请求结果。</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<!-- 新Tab栏设计 -->
|
||||||
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">文本转语音 API</h2>
|
<div class="tab-bar mt-6">
|
||||||
|
<span class="tab-indicator" style="width: 50%; transform: translateX(50%);"></span>
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">端点</h3>
|
<a href="{{.BasePath}}/" class="tab-item">
|
||||||
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">GET {{.BasePath}}/tts</code>
|
<span class="tab-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">参数</h3>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
<div class="overflow-x-auto">
|
</svg>
|
||||||
<table class="min-w-full bg-white mb-6">
|
</span>
|
||||||
<thead>
|
主页
|
||||||
<tr>
|
</a>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
<a href="{{.BasePath}}/api-doc" class="tab-item active">
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
<span class="tab-icon">
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||||
</tr>
|
</svg>
|
||||||
</thead>
|
</span>
|
||||||
<tbody>
|
API文档
|
||||||
<tr>
|
</a>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">t</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">是</td>
|
|
||||||
<td class="py-2 px-4 border-b">要转换的文本(需要进行URL编码)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">v</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">语音名称,使用short_name格式,默认: {{.DefaultVoice}}。可通过/voices接口获取所有可用语音</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">r</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">语速调整,范围: -100%到100%,默认: {{.DefaultRate}}。正值加快语速,负值减慢语速</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">p</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">语调调整,范围: -100%到100%,默认: {{.DefaultPitch}}。正值提高语调,负值降低语调</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">o</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">输出音频格式,默认: {{.DefaultFormat}}。详见下方支持的格式列表</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">s</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">情感风格,可用值取决于所选语音的style_list属性。例如:"cheerful"、"sad"等</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
<main>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/tts?t=%E4%BD%A0%E5%A5%BD%EF%BC%8C%E4%B8%96%E7%95%8C&v=zh-CN-XiaoxiaoNeural&r=0%25&p=0%25"</code></pre>
|
<section class="panel-frosted bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">API概述</h2>
|
||||||
|
<p class="mb-3">TTS服务API提供了简单而强大的方式将文本转换为自然语音。我们支持多种语言和声音,并允许您调节语速、语调以适应不同场景需求。</p>
|
||||||
|
<p class="mb-3">基础URL: <code class="bg-gray-100 px-2 py-1 rounded text-sm font-mono">{{.BasePath}}</code></p>
|
||||||
|
<p class="mb-3">所有API请求均使用HTTP协议,返回标准HTTP状态码表示请求结果。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">另一个示例(带情感风格)</h3>
|
<!-- 为所有 section 添加毛玻璃面板效果 -->
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/tts?t=%E4%BB%8A%E5%A4%A9%E5%A4%A9%E6%B0%94%E7%9C%9F%E5%A5%BD&v=zh-CN-XiaoxiaoNeural&s=cheerful"</code></pre>
|
<section class="panel-frosted bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">文本转语音 API</h2>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">端点</h3>
|
||||||
<p class="mb-3">返回音频文件,内容类型取决于请求的输出格式。正常响应状态码为200。</p>
|
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">GET {{.BasePath}}/tts</code>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">错误响应</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">参数</h3>
|
||||||
<p class="mb-3">如果请求参数有误或服务出现问题,将返回对应的HTTP错误码和错误消息。</p>
|
<div class="overflow-x-auto">
|
||||||
<div class="overflow-x-auto">
|
<table class="min-w-full bg-white mb-6">
|
||||||
<table class="min-w-full bg-white mb-6">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">状态码</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
||||||
</tr>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td class="py-2 px-4 border-b">400</td>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b">参数错误或缺失必要参数</td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">t</code></td>
|
||||||
</tr>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
<tr>
|
<td class="py-2 px-4 border-b">是</td>
|
||||||
<td class="py-2 px-4 border-b">404</td>
|
<td class="py-2 px-4 border-b">要转换的文本(需要进行URL编码)</td>
|
||||||
<td class="py-2 px-4 border-b">请求的资源不存在</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">v</code></td>
|
||||||
<td class="py-2 px-4 border-b">500</td>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
<td class="py-2 px-4 border-b">服务器内部错误</td>
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
</tr>
|
<td class="py-2 px-4 border-b">语音名称,使用short_name格式,默认: {{.DefaultVoice}}。可通过/voices接口获取所有可用语音</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
<tr>
|
||||||
</div>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">r</code></td>
|
||||||
</section>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">语速调整,范围: -100%到100%,默认: {{.DefaultRate}}。正值加快语速,负值减慢语速</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">p</code></td>
|
||||||
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">语调调整,范围: -100%到100%,默认: {{.DefaultPitch}}。正值提高语调,负值降低语调</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">o</code></td>
|
||||||
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">输出音频格式,默认: {{.DefaultFormat}}。详见下方支持的格式列表</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">s</code></td>
|
||||||
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">情感风格,可用值取决于所选语音的style_list属性。例如:"cheerful"、"sad"等</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
||||||
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">获取可用语音 API</h2>
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/tts?t=%E4%BD%A0%E5%A5%BD%EF%BC%8C%E4%B8%96%E7%95%8C&v=zh-CN-XiaoxiaoNeural&r=0%25&p=0%25"</code></pre>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">端点</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">另一个示例(带情感风格)</h3>
|
||||||
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">GET {{.BasePath}}/voices</code>
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/tts?t=%E4%BB%8A%E5%A4%A9%E5%A4%A9%E6%B0%94%E7%9C%9F%E5%A5%BD&v=zh-CN-XiaoxiaoNeural&s=cheerful"</code></pre>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">参数</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
||||||
<div class="overflow-x-auto">
|
<p class="mb-3">返回音频文件,内容类型取决于请求的输出格式。正常响应状态码为200。</p>
|
||||||
<table class="min-w-full bg-white mb-6">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">locale</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">筛选特定语言的语音,例如:zh-CN(中文)、en-US(英文)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">gender</code></td>
|
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
|
||||||
<td class="py-2 px-4 border-b">筛选特定性别的语音,可选值:Male(男性)、Female(女性)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">错误响应</h3>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/voices?locale=zh-CN&gender=Female"</code></pre>
|
<p class="mb-3">如果请求参数有误或服务出现问题,将返回对应的HTTP错误码和错误消息。</p>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full bg-white mb-6">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">状态码</th>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b">400</td>
|
||||||
|
<td class="py-2 px-4 border-b">参数错误或缺失必要参数</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b">404</td>
|
||||||
|
<td class="py-2 px-4 border-b">请求的资源不存在</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b">500</td>
|
||||||
|
<td class="py-2 px-4 border-b">服务器内部错误</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
<section class="panel-frosted bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<p class="mb-3">返回JSON格式的可用语音列表:</p>
|
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">获取可用语音 API</h2>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>[
|
|
||||||
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">端点</h3>
|
||||||
|
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">GET {{.BasePath}}/voices</code>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">参数</h3>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full bg-white mb-6">
|
||||||
|
<thead></thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
||||||
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">locale</code></td>
|
||||||
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">筛选特定语言的语音,例如:zh-CN(中文)、en-US(英文)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">gender</code></td>
|
||||||
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
|
<td class="py-2 px-4 border-b">筛选特定性别的语音,可选值:Male(男性)、Female(女性)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
||||||
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl "{{.BasePath}}/voices?locale=zh-CN&gender=Female"</code></pre>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
||||||
|
<p class="mb-3">返回JSON格式的可用语音列表:</p>
|
||||||
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>[
|
||||||
{
|
{
|
||||||
"name": "Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)",
|
"name": "Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)",
|
||||||
"display_name": "Xiaoxiao",
|
"display_name": "Xiaoxiao",
|
||||||
@@ -174,67 +515,67 @@
|
|||||||
},
|
},
|
||||||
...
|
...
|
||||||
]</code></pre>
|
]</code></pre>
|
||||||
<p class="mb-3">响应字段说明:</p>
|
<p class="mb-3">响应字段说明:</p>
|
||||||
<ul class="list-disc pl-5 mb-4 space-y-1">
|
<ul class="list-disc pl-5 mb-4 space-y-1">
|
||||||
<li><span class="font-semibold">name</span>:语音的完整名称</li>
|
<li><span class="font-semibold">name</span>:语音的完整名称</li>
|
||||||
<li><span class="font-semibold">display_name</span>:显示用名称(拉丁字符)</li>
|
<li><span class="font-semibold">display_name</span>:显示用名称(拉丁字符)</li>
|
||||||
<li><span class="font-semibold">local_name</span>:本地化名称</li>
|
<li><span class="font-semibold">local_name</span>:本地化名称</li>
|
||||||
<li><span class="font-semibold">short_name</span>:简短名称(用于API调用的v参数)</li>
|
<li><span class="font-semibold">short_name</span>:简短名称(用于API调用的v参数)</li>
|
||||||
<li><span class="font-semibold">gender</span>:性别(Male或Female)</li>
|
<li><span class="font-semibold">gender</span>:性别(Male或Female)</li>
|
||||||
<li><span class="font-semibold">locale</span>:语言代码</li>
|
<li><span class="font-semibold">locale</span>:语言代码</li>
|
||||||
<li><span class="font-semibold">locale_name</span>:语言本地化名称</li>
|
<li><span class="font-semibold">locale_name</span>:语言本地化名称</li>
|
||||||
<li><span class="font-semibold">style_list</span>:支持的情感风格列表(如有)</li>
|
<li><span class="font-semibold">style_list</span>:支持的情感风格列表(如有)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<section class="panel-frosted bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">兼容OpenAI接口 API</h2>
|
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">兼容OpenAI接口 API</h2>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">语音合成</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">语音合成</h3>
|
||||||
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">POST {{.BasePath}}/v1/audio/speech</code>
|
<code class="block bg-gray-100 p-3 rounded text-sm font-mono mb-4">POST {{.BasePath}}/v1/audio/speech</code>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">请求体 (JSON)</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">请求体 (JSON)</h3>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full bg-white mb-6">
|
<table class="min-w-full bg-white mb-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">参数</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">类型</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">必选</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">model</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">model</code></td>
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
<td class="py-2 px-4 border-b">是</td>
|
<td class="py-2 px-4 border-b">是</td>
|
||||||
<td class="py-2 px-4 border-b">当前仅支持值: "tts-1"</td>
|
<td class="py-2 px-4 border-b">当前仅支持值: "tts-1"</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">input</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">input</code></td>
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
<td class="py-2 px-4 border-b">是</td>
|
<td class="py-2 px-4 border-b">是</td>
|
||||||
<td class="py-2 px-4 border-b">要转换的文本内容</td>
|
<td class="py-2 px-4 border-b">要转换的文本内容</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">voice</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">voice</code></td>
|
||||||
<td class="py-2 px-4 border-b">string</td>
|
<td class="py-2 px-4 border-b">string</td>
|
||||||
<td class="py-2 px-4 border-b">是</td>
|
<td class="py-2 px-4 border-b">是</td>
|
||||||
<td class="py-2 px-4 border-b">声音名称,使用Microsoft语音格式,例如:ja-JP-KeitaNeural、zh-CN-XiaoxiaoNeural</td>
|
<td class="py-2 px-4 border-b">声音名称,使用Microsoft语音格式,例如:ja-JP-KeitaNeural、zh-CN-XiaoxiaoNeural</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">speed</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">speed</code></td>
|
||||||
<td class="py-2 px-4 border-b">number</td>
|
<td class="py-2 px-4 border-b">number</td>
|
||||||
<td class="py-2 px-4 border-b">否</td>
|
<td class="py-2 px-4 border-b">否</td>
|
||||||
<td class="py-2 px-4 border-b">语速调整,范围: 0.5到2.0,默认: 1.0</td>
|
<td class="py-2 px-4 border-b">语速调整,范围: 0.5到2.0,默认: 1.0</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">示例请求</h3>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl -X POST "{{.BasePath}}/v1/audio/speech" \
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl -X POST "{{.BasePath}}/v1/audio/speech" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "tts-1",
|
"model": "tts-1",
|
||||||
@@ -242,8 +583,8 @@
|
|||||||
"voice": "zh-CN-XiaoxiaoNeural"
|
"voice": "zh-CN-XiaoxiaoNeural"
|
||||||
}'</code></pre>
|
}'</code></pre>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">另一个示例(带速度调整)</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">另一个示例(带速度调整)</h3>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl -X POST "{{.BasePath}}/v1/audio/speech" \
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>curl -X POST "{{.BasePath}}/v1/audio/speech" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "tts-1",
|
"model": "tts-1",
|
||||||
@@ -252,72 +593,145 @@
|
|||||||
"speed": 1.2
|
"speed": 1.2
|
||||||
}'</code></pre>
|
}'</code></pre>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">响应</h3>
|
||||||
<p class="mb-3">返回音频文件,内容类型取决于请求的输出格式。正常响应状态码为200。</p>
|
<p class="mb-3">返回音频文件,内容类型取决于请求的输出格式。正常响应状态码为200。</p>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">错误响应</h3>
|
<h3 class="text-xl font-semibold text-blue-600 mt-6 mb-2">错误响应</h3>
|
||||||
<p class="mb-3">如果请求有误,将返回JSON格式的错误信息:</p>
|
<p class="mb-3">如果请求有误,将返回JSON格式的错误信息:</p>
|
||||||
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>{
|
<pre class="bg-gray-800 text-gray-100 p-4 rounded overflow-x-auto mb-4"><code>{
|
||||||
"error": {
|
"error": {
|
||||||
"message": "错误信息描述",
|
"message": "错误信息描述",
|
||||||
"type": "错误类型",
|
"type": "错误类型",
|
||||||
"code": "错误代码"
|
"code": "错误代码"
|
||||||
}
|
}
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-white rounded-lg shadow-md p-6 mb-6">
|
<section class="panel-frosted bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">支持的输出格式</h2>
|
<h2 class="text-2xl font-bold mb-4 pb-2 border-b border-gray-200 text-gray-800">支持的输出格式</h2>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full bg-white mb-6">
|
<table class="min-w-full bg-white mb-6">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">格式名称</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">格式名称</th>
|
||||||
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
<th class="py-3 px-4 bg-gray-100 font-semibold text-sm text-left border-b">描述</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-32kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-32kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,16kHz, 32kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,16kHz, 32kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-64kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-64kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,16kHz, 64kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,16kHz, 64kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-128kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-16khz-128kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,16kHz, 128kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,16kHz, 128kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-48kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-48kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,24kHz, 48kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,24kHz, 48kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-96kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-96kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,24kHz, 96kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,24kHz, 96kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-160kbitrate-mono-mp3</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">audio-24khz-160kbitrate-mono-mp3</code></td>
|
||||||
<td class="py-2 px-4 border-b">MP3格式,24kHz, 160kbps</td>
|
<td class="py-2 px-4 border-b">MP3格式,24kHz, 160kbps</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">riff-16khz-16bit-mono-pcm</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">riff-16khz-16bit-mono-pcm</code></td>
|
||||||
<td class="py-2 px-4 border-b">WAV格式,16kHz</td>
|
<td class="py-2 px-4 border-b">WAV格式,16kHz</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">riff-24khz-16bit-mono-pcm</code></td>
|
<td class="py-2 px-4 border-b"><code class="bg-gray-100 px-1 rounded text-sm font-mono">riff-24khz-16bit-mono-pcm</code></td>
|
||||||
<td class="py-2 px-4 border-b">WAV格式,24kHz</td>
|
<td class="py-2 px-4 border-b">WAV格式,24kHz</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="text-center mt-10 py-4 text-gray-600 text-sm">
|
<footer class="text-center mt-10 py-4 text-gray-600 text-sm">
|
||||||
<p>© 2025 TTS服务 | <a href="{{.BasePath}}/" class="text-blue-500 hover:underline">返回主页</a></p>
|
<p>© 2025 TTS服务 | <a href="{{.BasePath}}/" class="text-blue-500 hover:underline">返回主页</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 添加动态激光效果
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const laserContainer = document.querySelector('.laser-container');
|
||||||
|
|
||||||
|
// 清除任何现有的激光束
|
||||||
|
while (laserContainer.lastChild) {
|
||||||
|
laserContainer.removeChild(laserContainer.lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留发光效果
|
||||||
|
const glow1 = document.createElement('div');
|
||||||
|
glow1.className = 'glow';
|
||||||
|
glow1.style.top = '-30%';
|
||||||
|
glow1.style.left = '-10%';
|
||||||
|
laserContainer.appendChild(glow1);
|
||||||
|
|
||||||
|
const glow2 = document.createElement('div');
|
||||||
|
glow2.className = 'glow';
|
||||||
|
glow2.style.bottom = '-30%';
|
||||||
|
glow2.style.right = '-10%';
|
||||||
|
laserContainer.appendChild(glow2);
|
||||||
|
|
||||||
|
// 添加更随机方向的激光束,同时确保不会导致水平溢出
|
||||||
|
const laserCount = 8;
|
||||||
|
for (let i = 0; i < laserCount; i++) {
|
||||||
|
const laser = document.createElement('div');
|
||||||
|
laser.className = 'laser-beam';
|
||||||
|
|
||||||
|
// 随机高度
|
||||||
|
const height = Math.random() * 2 + 1;
|
||||||
|
laser.style.height = `${height}px`;
|
||||||
|
|
||||||
|
// 随机位置
|
||||||
|
laser.style.top = `${Math.random() * 100}%`;
|
||||||
|
|
||||||
|
// 随机角度 (限制角度范围,防止水平溢出)
|
||||||
|
const angle = Math.random() * 160 + 10; // 10-170度,避免接近水平的角度
|
||||||
|
|
||||||
|
// 静态定位,不使用动画
|
||||||
|
laser.style.transform = `rotate(${angle}deg)`;
|
||||||
|
|
||||||
|
// 随机不透明度
|
||||||
|
laser.style.opacity = Math.random() * 0.5 + 0.3;
|
||||||
|
|
||||||
|
laserContainer.appendChild(laser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tab栏交互效果
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 初始化Tab指示器的位置
|
||||||
|
const tabBar = document.querySelector('.tab-bar');
|
||||||
|
const activeTab = tabBar.querySelector('.active');
|
||||||
|
const indicator = tabBar.querySelector('.tab-indicator');
|
||||||
|
|
||||||
|
// 设置指示器初始位置和宽度来匹配活动tab
|
||||||
|
if (indicator && activeTab) {
|
||||||
|
indicator.style.width = `${activeTab.offsetWidth}px`;
|
||||||
|
indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当窗口大小改变时调整指示器
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (indicator && activeTab) {
|
||||||
|
indicator.style.width = `${activeTab.offsetWidth}px`;
|
||||||
|
indicator.style.transform = `translateX(${activeTab.offsetLeft}px)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user