2026-01-17 16:34:39 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
|
|
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
|
|
|
|
<meta name="mobile-web-app-capable" content="yes">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<title>TTS 语音设置</title>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
:root {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
极简科技风配色 - 黑白灰 + 单色点缀
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 背景层次 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--bg-primary: #0a0a0c;
|
|
|
|
|
|
--bg-secondary: #111114;
|
|
|
|
|
|
--bg-tertiary: #18181c;
|
|
|
|
|
|
--bg-elevated: #1e1e24;
|
|
|
|
|
|
--bg-input: rgba(255, 255, 255, 0.04);
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 文字层次 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--text-primary: #f0f0f2;
|
|
|
|
|
|
--text-secondary: #a0a0a8;
|
|
|
|
|
|
--text-muted: #606068;
|
|
|
|
|
|
--text-dim: #404048;
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 边框 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--border: rgba(255, 255, 255, 0.08);
|
|
|
|
|
|
--border-light: rgba(255, 255, 255, 0.12);
|
|
|
|
|
|
--border-focus: rgba(140, 200, 255, 0.4);
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 唯一强调色 - 淡青蓝(科技感) */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--accent: #8cc8ff;
|
|
|
|
|
|
--accent-soft: rgba(140, 200, 255, 0.1);
|
|
|
|
|
|
--accent-glow: rgba(140, 200, 255, 0.15);
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 功能色 - 极低饱和度 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--success: #90d4a0;
|
|
|
|
|
|
--success-soft: rgba(144, 212, 160, 0.08);
|
|
|
|
|
|
--error: #e08080;
|
|
|
|
|
|
--error-soft: rgba(224, 128, 128, 0.08);
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 试用/鉴权 - 不用彩色,用明暗区分 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
--tag-free: rgba(255, 255, 255, 0.7);
|
|
|
|
|
|
--tag-free-bg: rgba(255, 255, 255, 0.06);
|
|
|
|
|
|
--tag-auth: var(--accent);
|
|
|
|
|
|
--tag-auth-bg: var(--accent-soft);
|
|
|
|
|
|
|
|
|
|
|
|
/* Safe Area */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
--safe-area-top: env(safe-area-inset-top, 0px);
|
|
|
|
|
|
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
|
|
|
|
|
|
--safe-area-left: env(safe-area-inset-left, 0px);
|
|
|
|
|
|
--safe-area-right: env(safe-area-inset-right, 0px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
html {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
height: -webkit-fill-available;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
|
min-height: -webkit-fill-available;
|
|
|
|
|
|
overflow-x: hidden;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 100vh;
|
2026-01-18 01:48:30 +08:00
|
|
|
|
min-height: 100dvh;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
min-height: -webkit-fill-available;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Header
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.app-header {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
padding-top: calc(12px + var(--safe-area-top));
|
|
|
|
|
|
padding-left: calc(20px + var(--safe-area-left));
|
|
|
|
|
|
padding-right: calc(20px + var(--safe-area-right));
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.header-logo {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-logo i {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-status {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 23:03:36 +08:00
|
|
|
|
.header-toggles {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
2026-01-18 23:20:04 +08:00
|
|
|
|
padding: 4px 8px;
|
2026-01-18 23:03:36 +08:00
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-toggle:hover {
|
|
|
|
|
|
border-color: var(--border-light);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-toggle input[type="checkbox"] {
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
accent-color: var(--accent);
|
|
|
|
|
|
cursor: pointer;
|
2026-01-18 23:20:04 +08:00
|
|
|
|
margin: 0;
|
2026-01-18 23:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.header-badge {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-badge i {
|
|
|
|
|
|
font-size: 5px;
|
|
|
|
|
|
opacity: 0.5;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.header-badge.active {
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
border-color: var(--border-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-badge.active i {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.header-spacer { flex: 1; min-width: 10px; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.header-close {
|
|
|
|
|
|
width: 36px; height: 36px; min-width: 36px;
|
2026-01-18 01:48:30 +08:00
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-close:hover {
|
|
|
|
|
|
background: rgba(255,255,255,0.05);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
border-color: var(--border-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Layout
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.app-body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-sidebar {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 200px;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border-right: 1px solid var(--border);
|
|
|
|
|
|
padding: 16px 8px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
padding-left: calc(8px + var(--safe-area-left));
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
padding-right: calc(24px + var(--safe-area-right));
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Navigation
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.nav-item {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-item:hover {
|
|
|
|
|
|
background: rgba(255,255,255,0.03);
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.nav-item.active {
|
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
font-weight: 500;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.nav-item i { width: 18px; text-align: center; }
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.nav-divider {
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: var(--border);
|
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Views
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.view {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.view.active {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
animation: viewIn 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes viewIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
|
}
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.view-header { margin-bottom: 20px; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.view-title {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.view-desc {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Cards
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.card {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.card-title {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.08em;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Forms
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.form-group { margin-bottom: 16px; }
|
|
|
|
|
|
.form-group:last-child { margin-bottom: 0; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-hint {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.input {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.input:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: var(--border-focus);
|
|
|
|
|
|
background: rgba(255,255,255,0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
.input::placeholder { color: var(--text-dim); }
|
|
|
|
|
|
textarea.input {
|
|
|
|
|
|
min-height: 80px;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
font-family: inherit;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
select.input { cursor: pointer; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.input-row { display: flex; gap: 8px; }
|
|
|
|
|
|
.input-row .input { flex: 1; min-width: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.checkbox-row {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.checkbox-row input[type="checkbox"] {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
accent-color: var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.checkbox-row label {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--text-secondary);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Buttons
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.btn {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn:hover {
|
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
border-color: var(--border-light);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.btn:active { transform: scale(0.98); }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
|
border-color: rgba(140, 200, 255, 0.2);
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
|
|
|
background: rgba(140, 200, 255, 0.15);
|
|
|
|
|
|
border-color: rgba(140, 200, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger {
|
|
|
|
|
|
color: var(--error);
|
|
|
|
|
|
border-color: rgba(224, 128, 128, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-danger:hover {
|
|
|
|
|
|
background: var(--error-soft);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.btn-icon { width: 40px; padding: 0; }
|
|
|
|
|
|
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
|
|
|
|
.btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }
|
|
|
|
|
|
.btn-xs { padding: 4px 8px; min-height: 28px; font-size: 11px; }
|
|
|
|
|
|
|
|
|
|
|
|
.btn.saving { pointer-events: none; opacity: 0.7; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.btn.save-success {
|
|
|
|
|
|
background: var(--success-soft) !important;
|
|
|
|
|
|
border-color: rgba(144, 212, 160, 0.3) !important;
|
|
|
|
|
|
color: var(--success) !important;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Slider
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.slider-row { display: flex; align-items: center; gap: 12px; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.slider-row input[type="range"] {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
accent-color: var(--accent);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
.slider-row input[type="range"]:hover { opacity: 1; }
|
|
|
|
|
|
.slider-row .slider-val {
|
|
|
|
|
|
min-width: 50px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Rules Editor
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.rules-editor {
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.rules-header {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.rules-header-title {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.rules-list { max-height: 200px; overflow-y: auto; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.rules-empty {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.rule-item {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.rule-item:last-child { border-bottom: none; }
|
|
|
|
|
|
.rule-item:hover { background: rgba(255,255,255,0.02); }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.rule-input {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-size: 12px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.rule-input:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: var(--border-focus);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.rule-input::placeholder { color: var(--text-dim); }
|
|
|
|
|
|
|
|
|
|
|
|
.rule-arrow {
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rule-delete {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.rule-delete:hover {
|
|
|
|
|
|
background: var(--error-soft);
|
|
|
|
|
|
color: var(--error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Current Voice Card - 极简版
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.current-voice-card {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.current-voice-card:hover {
|
|
|
|
|
|
border-color: var(--border-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-voice-label {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-voice-display {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.current-voice-icon {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.current-voice-card:hover .current-voice-icon {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
border-color: rgba(140, 200, 255, 0.2);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.current-voice-info { flex: 1; }
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.current-voice-name {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.current-voice-source {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Source Badge - 极简黑白版
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.source-badge {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
padding: 2px 7px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 试用 - 白色/浅色调 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.source-badge.trial {
|
|
|
|
|
|
background: var(--tag-free-bg);
|
|
|
|
|
|
color: var(--tag-free);
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* 鉴权 - 蓝色点缀 */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.source-badge.auth {
|
|
|
|
|
|
background: var(--tag-auth-bg);
|
|
|
|
|
|
color: var(--tag-auth);
|
|
|
|
|
|
border: 1px solid rgba(140, 200, 255, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Voice Tabs - 极简版
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-tabs {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
padding: 3px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
border: 1px solid var(--border);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-tab {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-tab:hover {
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-tab.active {
|
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-weight: 500;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-tab-count {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-tab.active .voice-tab-count {
|
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
|
color: var(--accent);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-panel { display: none; }
|
|
|
|
|
|
.voice-panel.active { display: block; }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Test Voice Box
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.test-voice-box {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-voice-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.test-voice-row .input { flex: 1; }
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.test-voice-status {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
min-height: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.test-voice-status.playing { color: var(--accent); }
|
|
|
|
|
|
.test-voice-status.error { color: var(--error); }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Voice Filters
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.voice-filters {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-filters select {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-filters select:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: var(--border-focus);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-search {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.voice-search .input { flex: 1; }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Voice List - 极简版
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.voice-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
max-height: 320px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-item {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item:hover {
|
|
|
|
|
|
border-color: var(--border-light);
|
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.selected {
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.in-my-list {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.disabled {
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.disabled:hover {
|
|
|
|
|
|
border-color: var(--border);
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-item-radio {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 2px solid var(--text-dim);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item:hover .voice-item-radio {
|
|
|
|
|
|
border-color: var(--text-muted);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.voice-item.selected .voice-item-radio {
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.selected .voice-item-radio::after {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
content: '✓';
|
2026-01-18 01:48:30 +08:00
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.voice-item-info { flex: 1; min-width: 0; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.voice-item-name {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-item-meta {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.voice-item-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Editing State */
|
|
|
|
|
|
.voice-item.editing {
|
|
|
|
|
|
background: var(--accent-soft);
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item-edit-form {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.editing .voice-item-edit-form {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item.editing .voice-item-info > *:not(.voice-item-edit-form) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-item-edit-form input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Add Form */
|
|
|
|
|
|
.voice-add-form {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
padding-top: 16px;
|
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-add-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-add-row .form-group {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-add-row .form-label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.preset-save-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.preset-save-row input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
API Status Box
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.api-status-box {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 10px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.api-status-icon {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
font-size: 14px;
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
|
border: 1px solid var(--border);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.api-status-box.configured .api-status-icon {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
border-color: rgba(140, 200, 255, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.api-status-box.not-configured .api-status-icon {
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.api-status-info { flex: 1; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.api-status-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.api-status-desc {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Stats Card
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.stats-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 32px;
|
|
|
|
|
|
}
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.stats-item { text-align: center; }
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.stats-value {
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-label {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
Tip Box - 极简版
|
|
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.tip-box {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tip-box i {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tip-box.warning {
|
|
|
|
|
|
border-left: 3px solid var(--accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.tip-box.warning i {
|
|
|
|
|
|
color: var(--accent);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Guide Box
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-box {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-box h3 {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.guide-box h3 i {
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
opacity: 0.8;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
.guide-box p {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.guide-box ol, .guide-box ul {
|
|
|
|
|
|
margin-left: 18px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.guide-box li {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-box a { color: var(--accent); }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-box code {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
border: 1px solid var(--border);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-box pre {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
line-height: 1.5;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-image {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
opacity: 0.9;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.guide-image:hover { opacity: 1; }
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.guide-link {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
|
background: var(--bg-input);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
word-break: break-all;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.guide-link a { color: var(--accent); }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Mobile Navigation
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.mobile-nav {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
height: calc(60px + var(--safe-area-bottom));
|
|
|
|
|
|
padding-bottom: var(--safe-area-bottom);
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
}
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.mobile-nav-inner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
padding-left: var(--safe-area-left);
|
|
|
|
|
|
padding-right: var(--safe-area-right);
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-nav-inner::-webkit-scrollbar { display: none; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.mobile-nav-item {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 60px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
color: var(--text-dim);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
transition: color 0.15s;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.mobile-nav-item i { font-size: 18px; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.mobile-nav-item:hover { color: var(--text-muted); }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
.mobile-nav-item.active { color: var(--accent); }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Responsive
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.app-sidebar { display: none; }
|
|
|
|
|
|
.mobile-nav { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
.app-body {
|
|
|
|
|
|
padding-bottom: calc(60px + var(--safe-area-bottom));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-main {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
padding-left: calc(16px + var(--safe-area-left));
|
|
|
|
|
|
padding-right: calc(16px + var(--safe-area-right));
|
|
|
|
|
|
padding-bottom: calc(80px + var(--safe-area-bottom));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
.view { padding-bottom: 20px; }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.app-header {
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
padding-top: calc(10px + var(--safe-area-top));
|
|
|
|
|
|
padding-left: calc(12px + var(--safe-area-left));
|
|
|
|
|
|
padding-right: calc(12px + var(--safe-area-right));
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-logo span { display: none; }
|
|
|
|
|
|
.header-badge span { display: none; }
|
|
|
|
|
|
.header-close { width: 32px; height: 32px; min-width: 32px; font-size: 14px; }
|
|
|
|
|
|
.view-title { font-size: 18px; }
|
|
|
|
|
|
.card { padding: 16px; }
|
|
|
|
|
|
.form-row { grid-template-columns: 1fr; }
|
|
|
|
|
|
.voice-filters select { min-width: calc(50% - 4px); }
|
|
|
|
|
|
.stats-card { flex-direction: column; align-items: stretch; }
|
|
|
|
|
|
.stats-group { justify-content: space-around; }
|
|
|
|
|
|
.voice-add-row { flex-wrap: wrap; }
|
|
|
|
|
|
.voice-add-row .form-group { min-width: calc(50% - 4px); }
|
|
|
|
|
|
.preset-save-row { flex-wrap: wrap; }
|
|
|
|
|
|
.preset-save-row input { min-width: 100%; margin-bottom: 8px; }
|
|
|
|
|
|
.voice-tabs { flex-wrap: wrap; }
|
|
|
|
|
|
.voice-tab { min-width: calc(33% - 3px); font-size: 11px; padding: 8px 6px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 400px) {
|
|
|
|
|
|
.app-header { padding: 8px 10px; }
|
|
|
|
|
|
.mobile-nav-item { min-width: 48px; font-size: 9px; }
|
|
|
|
|
|
.mobile-nav-item i { font-size: 16px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (hover: none) and (pointer: coarse) {
|
|
|
|
|
|
.btn { min-height: 44px; }
|
|
|
|
|
|
.input { min-height: 44px; padding: 12px; }
|
|
|
|
|
|
.nav-item { min-height: 44px; }
|
|
|
|
|
|
.header-close { width: 44px; height: 44px; min-width: 44px; }
|
|
|
|
|
|
.mobile-nav-item { min-height: 44px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
Scrollbar
|
2026-01-18 03:06:38 +08:00
|
|
|
|
═══════════════════════════════════════════════════════════════ */
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
|
|
|
|
::-webkit-scrollbar-track { background: transparent; }
|
2026-01-18 01:48:30 +08:00
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: rgba(255,255,255,0.08);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: rgba(255,255,255,0.15);
|
|
|
|
|
|
}
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="app-container">
|
|
|
|
|
|
|
|
|
|
|
|
<header class="app-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="header-logo"><i class="fa-solid fa-microphone"></i><span>TTS 语音</span></div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="header-status">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div id="badge_trial" class="header-badge active"><i class="fa-solid fa-circle"></i><span>试用</span></div>
|
|
|
|
|
|
<div id="badge_auth" class="header-badge"><i class="fa-solid fa-circle"></i><span>鉴权</span></div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
2026-01-18 23:20:04 +08:00
|
|
|
|
<div class="header-spacer"></div>
|
2026-01-18 23:03:36 +08:00
|
|
|
|
<div class="header-toggles">
|
|
|
|
|
|
<label class="header-toggle" title="消息楼层内显示播放器">
|
|
|
|
|
|
<input type="checkbox" id="showFloorButton" checked>
|
|
|
|
|
|
<span>楼层</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="header-toggle" title="屏幕固定位置显示播放器">
|
|
|
|
|
|
<input type="checkbox" id="showFloatingButton">
|
|
|
|
|
|
<span>悬浮</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button id="tts_close" class="header-close">✕</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-body">
|
|
|
|
|
|
<nav class="app-sidebar">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="nav-item active" data-view="config"><i class="fa-solid fa-sliders"></i>基础配置</div>
|
|
|
|
|
|
<div class="nav-item" data-view="voice"><i class="fa-solid fa-user-astronaut"></i>音色管理</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="nav-divider"></div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="nav-item" data-view="advanced"><i class="fa-solid fa-gear"></i>高级设置</div>
|
|
|
|
|
|
<div class="nav-item" data-view="cache"><i class="fa-solid fa-database"></i>缓存管理</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="nav-divider"></div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="nav-item" data-view="guide"><i class="fa-solid fa-circle-question"></i>使用说明</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="app-main">
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 基础配置 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="view-config" class="view active">
|
|
|
|
|
|
<div class="view-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h2 class="view-title">基础配置</h2>
|
|
|
|
|
|
<p class="view-desc">TTS 服务连接与朗读设置</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tip-box" style="margin-bottom: 16px;">
|
|
|
|
|
|
<i class="fa-solid fa-info-circle"></i>
|
|
|
|
|
|
<div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<strong>试用音色</strong> — 无需配置,使用插件服务器(11个音色)<br>
|
|
|
|
|
|
<strong>鉴权音色</strong> — 需配置火山引擎 API(200+ 音色 + 复刻)
|
2026-01-18 02:55:49 +08:00
|
|
|
|
</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">鉴权配置(可选)</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="apiStatusBox" class="api-status-box not-configured">
|
2026-01-18 01:48:30 +08:00
|
|
|
|
<div class="api-status-icon"><i class="fa-solid fa-link-slash"></i></div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="api-status-info">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="api-status-title">未配置</div>
|
|
|
|
|
|
<div class="api-status-desc">配置后可使用预设音色库和复刻音色</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">AppID</label>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="appId" class="input" placeholder="火山引擎 AppID">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">Access Token</label>
|
|
|
|
|
|
<div class="input-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="password" id="accessKey" class="input" placeholder="火山引擎 Access Token">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<button id="toggleKey" class="btn btn-icon"><i class="fa-solid fa-eye"></i></button>
|
|
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<p class="form-hint">获取方式见「使用说明」页</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">朗读设置</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="autoSpeak" checked>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="autoSpeak">AI 回复后自动朗读</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">语速</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="slider-row">
|
|
|
|
|
|
<input type="range" id="speechRate" min="0.5" max="2.0" step="0.1" value="1.0">
|
|
|
|
|
|
<span class="slider-val" id="speechRateValue">1.0x</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">文本过滤</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">跳过区间</label>
|
|
|
|
|
|
<p class="form-hint" style="margin-bottom: 8px;">遇到「起始」后跳过,直到「结束」。起始或结束可单独留空。</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="rules-editor">
|
|
|
|
|
|
<div class="rules-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<span class="rules-header-title">当前规则</span>
|
|
|
|
|
|
<button class="btn btn-sm" id="addSkipRule"><i class="fa-solid fa-plus"></i> 添加</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rules-list" id="skipRulesList"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<div class="checkbox-row" style="margin-bottom: 8px;">
|
|
|
|
|
|
<input type="checkbox" id="readRangesEnabled">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="readRangesEnabled">启用只读区间(仅朗读匹配内容)</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<p class="form-hint" style="margin-bottom: 8px;">起始或结束可单独留空。</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="rules-editor">
|
|
|
|
|
|
<div class="rules-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<span class="rules-header-title">只读规则</span>
|
|
|
|
|
|
<button class="btn btn-sm" id="addReadRule"><i class="fa-solid fa-plus"></i> 添加</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rules-list" id="readRulesList"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button id="saveConfigBtn" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> 保存配置</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 音色管理 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="view-voice" class="view">
|
|
|
|
|
|
<div class="view-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h2 class="view-title">音色管理</h2>
|
|
|
|
|
|
<p class="view-desc">将喜欢的音色重命名加入【我的音色】</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="current-voice-card" id="currentVoiceCard">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="current-voice-label">当前默认音色</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="current-voice-display">
|
|
|
|
|
|
<div class="current-voice-icon"><i class="fa-solid fa-microphone-lines"></i></div>
|
|
|
|
|
|
<div class="current-voice-info">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="current-voice-name" id="currentVoiceName">未选择</div>
|
|
|
|
|
|
<div class="current-voice-source" id="currentVoiceSource">请在下方选择音色</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="voice-tabs">
|
|
|
|
|
|
<button class="voice-tab active" data-panel="myVoice">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<i class="fa-solid fa-star"></i> 我的
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<span class="voice-tab-count" id="myVoiceCount">0</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="voice-tab" data-panel="trialVoice">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<i class="fa-solid fa-flask"></i> 试用
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button class="voice-tab" data-panel="authVoice">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<i class="fa-solid fa-list"></i> 预设库
|
2026-01-18 02:55:49 +08:00
|
|
|
|
</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 我的音色面板 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-panel active" id="panel-myVoice">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="test-voice-box">
|
|
|
|
|
|
<div class="test-voice-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="testTextMy" class="input" value="♪ 我能想到最浪漫的事~♪" placeholder="输入测试文本">
|
|
|
|
|
|
<button class="btn btn-primary" id="testMyVoiceBtn"><i class="fa-solid fa-play"></i> 试听</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-voice-status" id="testMyStatus"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="form-hint" style="margin-bottom: 12px;">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
点击选中设为默认。<span class="source-badge trial">试用</span> 无需配置,<span class="source-badge auth">鉴权</span> 需配置 API
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<div class="voice-list" id="myVoiceList"></div>
|
|
|
|
|
|
<div id="myVoiceEmpty" class="rules-empty">
|
2026-01-18 01:48:30 +08:00
|
|
|
|
<i class="fa-solid fa-inbox" style="font-size: 24px; margin-bottom: 8px; display: block; opacity: 0.5;"></i>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
暂无音色,请从「试用」或「预设库」添加
|
2026-01-18 02:55:49 +08:00
|
|
|
|
</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="voice-add-form">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="form-label" style="margin-bottom: 8px;">手动添加复刻音色 <span class="source-badge auth">鉴权</span></div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-add-row">
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">音色 ID</label>
|
|
|
|
|
|
<input type="text" id="newVoiceId" class="input" placeholder="如 S_xxx">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">名称</label>
|
|
|
|
|
|
<input type="text" id="newVoiceName" class="input" placeholder="显示名称">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
2026-01-26 01:16:35 +08:00
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label class="form-label">复刻版本</label>
|
|
|
|
|
|
<select id="newVoiceResourceId" class="input">
|
|
|
|
|
|
<option value="seed-icl-2.0">复刻 2.0</option>
|
|
|
|
|
|
<option value="seed-icl-1.0">复刻 1.0</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<button class="btn btn-primary" id="addMySpeakerBtn" style="margin-top: 18px;"><i class="fa-solid fa-plus"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 试用音色面板 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-panel" id="panel-trialVoice">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="test-voice-box">
|
|
|
|
|
|
<div class="test-voice-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="testTextTrial" class="input" value="咳~♪祝你生日快乐~" placeholder="输入测试文本">
|
|
|
|
|
|
<button class="btn btn-primary" id="testTrialVoiceBtn"><i class="fa-solid fa-play"></i> 试听</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-voice-status" id="testTrialStatus"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="voice-list" id="trialVoiceList"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="preset-save-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="saveAsNameTrial" class="input" placeholder="保存名称(可选)">
|
|
|
|
|
|
<button class="btn btn-primary" id="saveToMyVoiceTrialBtn"><i class="fa-solid fa-plus"></i> 添加到我的音色</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 预设音色库面板 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-panel" id="panel-authVoice">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div id="authVoiceNotice" class="tip-box warning" style="margin-bottom: 16px;">
|
|
|
|
|
|
<i class="fa-solid fa-exclamation-triangle"></i>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div>使用预设音色库需要先配置鉴权 API,请前往「基础配置」页面设置。</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="test-voice-box">
|
|
|
|
|
|
<div class="test-voice-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="testTextAuth" class="input" value="预备,唱:两只老虎,两只老虎..." placeholder="输入测试文本">
|
|
|
|
|
|
<button class="btn btn-primary" id="testAuthVoiceBtn"><i class="fa-solid fa-play"></i> 试听</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-voice-status" id="testAuthStatus"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="voice-search">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="voiceSearchInput" class="input" placeholder="搜索音色名称...">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-filters">
|
|
|
|
|
|
<select id="voiceGenderFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<option value="all">全部性别</option>
|
|
|
|
|
|
<option value="female">女声</option>
|
|
|
|
|
|
<option value="male">男声</option>
|
|
|
|
|
|
<option value="other">其他</option>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
<select id="voiceModelFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<option value="all">全部模型</option>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<option value="tts2">2.0</option>
|
|
|
|
|
|
<option value="tts1">1.0</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<select id="voiceLangFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<option value="all">全部语种</option>
|
|
|
|
|
|
<option value="zh">中文</option>
|
|
|
|
|
|
<option value="en">英文</option>
|
|
|
|
|
|
<option value="other">日语、西班牙</option>
|
|
|
|
|
|
<option value="multi">多语</option>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
<select id="voiceSceneFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<option value="all">全部场景</option>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="voice-list" id="authVoiceList"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="preset-save-row">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" id="saveAsNameAuth" class="input" placeholder="保存名称(可选)">
|
|
|
|
|
|
<button class="btn btn-primary" id="saveToMyVoiceAuthBtn"><i class="fa-solid fa-plus"></i> 添加到我的音色</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button class="btn btn-primary" id="saveVoiceBtn" style="margin-top: 8px;"><i class="fa-solid fa-floppy-disk"></i> 保存音色设置</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 高级设置 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="view-advanced" class="view">
|
|
|
|
|
|
<div class="view-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h2 class="view-title">高级设置</h2>
|
|
|
|
|
|
<p class="view-desc">计费、缓存与过滤选项(鉴权模式)</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">计费与缓存</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="usageReturn">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="usageReturn">返回计费用量(text_words)</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="serverCacheEnabled">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="serverCacheEnabled">启用火山服务端缓存</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">过滤与识别</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="disableMarkdownFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="disableMarkdownFilter">启用 Markdown 过滤</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="useTts11" checked>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="useTts11">启用 1.1 模型(仅对 seed-tts-1.0 生效)</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="disableEmojiFilter">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="disableEmojiFilter">不过滤 Emoji</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="checkbox-row">
|
|
|
|
|
|
<input type="checkbox" id="enableLanguageDetector">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label for="enableLanguageDetector">启用自动语种识别</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group" style="margin-top: 12px;">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">指定语种</label>
|
|
|
|
|
|
<input type="text" id="explicitLanguage" class="input" placeholder="如:zh-cn / en / crosslingual">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row" style="margin-top: 12px;">
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">括号过滤长度</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<input type="number" id="maxLengthToFilterParenthesis" class="input" min="0" max="500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">音高调整</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<input type="number" id="postProcessPitch" class="input" min="-12" max="12">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button class="btn btn-primary" id="saveAdvancedBtn"><i class="fa-solid fa-floppy-disk"></i> 保存高级设置</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 缓存管理 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="view-cache" class="view">
|
|
|
|
|
|
<div class="view-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h2 class="view-title">缓存管理</h2>
|
|
|
|
|
|
<p class="view-desc">本地音频缓存统计与清理</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="stats-card">
|
|
|
|
|
|
<div class="stats-group">
|
|
|
|
|
|
<div class="stats-item">
|
|
|
|
|
|
<div class="stats-value" id="cacheCount">0</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="stats-label">缓存条数</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-item">
|
|
|
|
|
|
<div class="stats-value" id="cacheSize">0 MB</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="stats-label">占用空间</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-item">
|
|
|
|
|
|
<div class="stats-value" id="cacheHits">0</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="stats-label">命中</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stats-item">
|
|
|
|
|
|
<div class="stats-value" id="cacheMisses">0</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="stats-label">未命中</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="card-title">缓存配置</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">缓存天数</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<input type="number" id="cacheDays" class="input" min="1" max="30">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">最大条数</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<input type="number" id="cacheMaxEntries" class="input" min="10" max="5000">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<label class="form-label">最大容量 (MB)</label>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<input type="number" id="cacheMaxMB" class="input" min="10" max="5000">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="btn-group">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button class="btn btn-primary" id="saveCacheBtn"><i class="fa-solid fa-floppy-disk"></i> 保存</button>
|
|
|
|
|
|
<button class="btn" id="cacheRefreshBtn"><i class="fa-solid fa-arrows-rotate"></i> 刷新</button>
|
|
|
|
|
|
<button class="btn" id="cacheClearExpiredBtn"><i class="fa-solid fa-broom"></i> 清理过期</button>
|
|
|
|
|
|
<button class="btn btn-danger" id="cacheClearAllBtn"><i class="fa-solid fa-trash"></i> 清空全部</button>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<!-- 使用说明 -->
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div id="view-guide" class="view">
|
|
|
|
|
|
<div class="view-header">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h2 class="view-title">使用说明</h2>
|
|
|
|
|
|
<p class="view-desc">配音指令与开通流程</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-terminal"></i> 配音指令</h3>
|
|
|
|
|
|
<p>格式:<code>[tts:speaker=音色名;emotion=情绪;context=语气提示]</code> 放在正文前一行</p>
|
|
|
|
|
|
<p>speaker、emotion、context 三个参数可任意组合、任意顺序,用分号分隔</p>
|
|
|
|
|
|
<p>每遇到一个新 <code>[tts:...]</code> 块会分段朗读,按顺序播放</p>
|
|
|
|
|
|
<p>未写 <code>speaker=</code> 的块使用当前选中的默认音色</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<p style="margin-top: 12px;"><strong>音色(speaker)</strong></p>
|
|
|
|
|
|
<p>只能指定"我的音色"中保存的名称。例如保存了名为"小白"的音色,则可用 <code>speaker=小白</code>。</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<p style="margin-top: 12px;"><strong>情感(emotion)可用值:</strong></p>
|
|
|
|
|
|
<pre>中文:开心、悲伤、生气、惊讶、恐惧、厌恶、激动、冷漠、中性、沮丧、撒娇、害羞、安慰、鼓励、咆哮、焦急、温柔、讲故事、自然讲述、情感电台、磁性、广告营销、气泡音、低语、新闻播报、娱乐八卦、方言、对话、闲聊、温暖、深情、权威
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
英文:happy, sad, angry, surprised, fear, hate, excited, coldness, neutral, depressed, lovey-dovey, shy, comfort, tension, tender, storytelling, radio, magnetic, advertising, vocal-fry, asmr, news, entertainment, dialect, chat, warm, affectionate, authoritative</pre>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<p style="margin-top: 12px;"><strong>语气提示(context)</strong>仅对 seed-tts-2.0 生效:</p>
|
|
|
|
|
|
<p>例如:"用更委屈的语气"、"放慢一点,压低音量"</p>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-user-plus"></i> 复刻音色使用</h3>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<ol>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<li>在火山官网复刻音色</li>
|
|
|
|
|
|
<li>获取音色ID(格式 <code>S_xxxxxxxx</code>)</li>
|
|
|
|
|
|
<li>在"音色管理" → "我的音色"中添加</li>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</ol>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tip-box warning" style="margin-bottom: 16px;">
|
|
|
|
|
|
<i class="fa-solid fa-exclamation-triangle"></i>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div><strong>以下是鉴权模式的开通教程</strong>,试用音色无需配置。</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-server"></i> 开启 CORS 代理</h3>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<ol>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<li>打开酒馆目录的 config.yaml</li>
|
|
|
|
|
|
<li>将 enableCorsProxy 改为 true 并保存</li>
|
|
|
|
|
|
<li>重启酒馆(重启容器/进程,不是 F5 刷新)</li>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</ol>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-check-circle"></i> 开通服务(推荐一次性开通全部)</h3>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="guide-link">
|
|
|
|
|
|
<a href="https://console.volcengine.com/speech/new/setting/activate" target="_blank">https://console.volcengine.com/speech/new/setting/activate</a>
|
|
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<img class="guide-image" src="开通管理.png" alt="开通管理">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-key"></i> 获取 Access Token / AppID</h3>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="guide-link">
|
|
|
|
|
|
<a href="https://console.volcengine.com/speech/service/8" target="_blank">https://console.volcengine.com/speech/service/8</a>
|
|
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<img class="guide-image" src="获取ID和KEY.png" alt="获取ID和KEY">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="guide-box">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<h3><i class="fa-solid fa-microphone-lines"></i> 声音复刻入口(复刻后去音色库拿ID)</h3>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="guide-link">
|
|
|
|
|
|
<a href="https://console.volcengine.com/speech/new/experience/clone" target="_blank">https://console.volcengine.com/speech/new/experience/clone</a>
|
|
|
|
|
|
</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<img class="guide-image" src="声音复刻.png" alt="声音复刻">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<nav class="mobile-nav">
|
|
|
|
|
|
<div class="mobile-nav-inner">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="mobile-nav-item active" data-view="config"><i class="fa-solid fa-sliders"></i><span>配置</span></div>
|
|
|
|
|
|
<div class="mobile-nav-item" data-view="voice"><i class="fa-solid fa-user-astronaut"></i><span>音色</span></div>
|
|
|
|
|
|
<div class="mobile-nav-item" data-view="advanced"><i class="fa-solid fa-gear"></i><span>高级</span></div>
|
|
|
|
|
|
<div class="mobile-nav-item" data-view="cache"><i class="fa-solid fa-database"></i><span>缓存</span></div>
|
|
|
|
|
|
<div class="mobile-nav-item" data-view="guide"><i class="fa-solid fa-circle-question"></i><span>说明</span></div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script src="tts-voices.js"></script>
|
|
|
|
|
|
<script>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 常量与状态
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const PARENT_ORIGIN = (() => {
|
|
|
|
|
|
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
|
|
|
|
|
|
})();
|
|
|
|
|
|
const post = (type, payload) => parent.postMessage({ type, payload }, PARENT_ORIGIN);
|
|
|
|
|
|
|
|
|
|
|
|
let config = {};
|
|
|
|
|
|
let mySpeakers = [];
|
|
|
|
|
|
let selectedVoiceValue = '';
|
|
|
|
|
|
let selectedTrialVoiceValue = '';
|
|
|
|
|
|
let selectedAuthVoiceValue = '';
|
|
|
|
|
|
let editingVoiceValue = null;
|
|
|
|
|
|
let activeSaveBtn = null;
|
|
|
|
|
|
|
|
|
|
|
|
const TRIAL_VOICES = [
|
2026-01-18 03:06:38 +08:00
|
|
|
|
{ key: 'female_1', name: '桃夭', tag: '甜蜜仙子', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_2', name: '霜华', tag: '清冷仙子', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_3', name: '顾姐', tag: '御姐烟嗓', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_4', name: '苏菲', tag: '优雅知性', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_5', name: '嘉欣', tag: '港风甜心', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_6', name: '青梅', tag: '清秀少年音', gender: 'female' },
|
|
|
|
|
|
{ key: 'female_7', name: '可莉', tag: '奶音萝莉', gender: 'female' },
|
|
|
|
|
|
{ key: 'male_1', name: '夜枭', tag: '磁性低音', gender: 'male' },
|
|
|
|
|
|
{ key: 'male_2', name: '君泽', tag: '温润公子', gender: 'male' },
|
|
|
|
|
|
{ key: 'male_3', name: '沐阳', tag: '沉稳暖男', gender: 'male' },
|
|
|
|
|
|
{ key: 'male_4', name: '梓辛', tag: '青春少年', gender: 'male' },
|
2026-01-17 16:34:39 +08:00
|
|
|
|
];
|
|
|
|
|
|
const TRIAL_VOICE_KEYS = new Set(TRIAL_VOICES.map(v => v.key));
|
|
|
|
|
|
|
|
|
|
|
|
const AUTH_VOICE_DATA = Array.isArray(window.XB_TTS_VOICE_DATA) ? window.XB_TTS_VOICE_DATA : [];
|
|
|
|
|
|
const TTS2_VOICES = new Set((window.XB_TTS_TTS2_VOICE_INFO || []).map(item => item.value));
|
|
|
|
|
|
|
|
|
|
|
|
let authVoiceList = [];
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 工具函数
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
|
|
const $$ = sel => document.querySelectorAll(sel);
|
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchView(viewId) {
|
|
|
|
|
|
$$('.view').forEach(v => v.classList.remove('active'));
|
|
|
|
|
|
$$('.nav-item, .mobile-nav-item').forEach(n => n.classList.remove('active'));
|
|
|
|
|
|
$(`view-${viewId}`)?.classList.add('active');
|
|
|
|
|
|
$$(`[data-view="${viewId}"]`).forEach(n => n.classList.add('active'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setSavingState(btn) {
|
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
activeSaveBtn = btn;
|
|
|
|
|
|
const i = btn.querySelector('i');
|
|
|
|
|
|
if (i) { btn._origIcon = i.className; i.className = 'fa-solid fa-spinner fa-spin'; }
|
|
|
|
|
|
btn.classList.add('saving');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleSaveResult(success) {
|
|
|
|
|
|
if (!activeSaveBtn) return;
|
|
|
|
|
|
const btn = activeSaveBtn;
|
|
|
|
|
|
activeSaveBtn = null;
|
|
|
|
|
|
btn.classList.remove('saving');
|
|
|
|
|
|
const i = btn.querySelector('i');
|
|
|
|
|
|
if (success && i) {
|
|
|
|
|
|
i.className = 'fa-solid fa-check';
|
|
|
|
|
|
btn.classList.add('save-success');
|
|
|
|
|
|
setTimeout(() => { btn.classList.remove('save-success'); i.className = btn._origIcon || 'fa-solid fa-floppy-disk'; }, 1500);
|
|
|
|
|
|
} else if (i) {
|
|
|
|
|
|
i.className = btn._origIcon || 'fa-solid fa-floppy-disk';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setTestStatus(elId, status, text) {
|
|
|
|
|
|
const el = $(elId);
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
el.textContent = text;
|
|
|
|
|
|
el.className = 'test-voice-status' + (status ? ' ' + status : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getVoiceSource(value) {
|
|
|
|
|
|
if (!value) return 'free';
|
|
|
|
|
|
if (TRIAL_VOICE_KEYS.has(value)) return 'free';
|
|
|
|
|
|
return 'auth';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isAuthConfigured() {
|
|
|
|
|
|
return !!(config?.volc?.appId && config?.volc?.accessKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isInMyList(value) {
|
|
|
|
|
|
return mySpeakers.some(s => s.value === value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 规则编辑器
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function renderRulesList(listEl, rules, type) {
|
|
|
|
|
|
if (!rules?.length) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
listEl.innerHTML = `<div class="rules-empty">暂无规则</div>`;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
listEl.innerHTML = rules.map((rule, idx) => `
|
|
|
|
|
|
<div class="rule-item" data-idx="${idx}">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" class="rule-input rule-start" value="${escapeHtml(rule.start || '')}" placeholder="起始(可为空)">
|
|
|
|
|
|
<span class="rule-arrow">→</span>
|
|
|
|
|
|
<input type="text" class="rule-input rule-end" value="${escapeHtml(rule.end || '')}" placeholder="结束(可为空)">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<button class="rule-delete" data-type="${type}" data-idx="${idx}"><i class="fa-solid fa-times"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectRules(listEl) {
|
|
|
|
|
|
const rules = [];
|
|
|
|
|
|
listEl.querySelectorAll('.rule-item').forEach(item => {
|
|
|
|
|
|
const start = item.querySelector('.rule-start')?.value?.trim() || '';
|
|
|
|
|
|
const end = item.querySelector('.rule-end')?.value?.trim() || '';
|
|
|
|
|
|
if (start || end) rules.push({ start, end });
|
|
|
|
|
|
});
|
|
|
|
|
|
return rules;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initRulesEditors() {
|
|
|
|
|
|
$('addSkipRule').addEventListener('click', () => {
|
|
|
|
|
|
const rules = collectRules($('skipRulesList'));
|
|
|
|
|
|
rules.push({ start: '', end: '' });
|
|
|
|
|
|
renderRulesList($('skipRulesList'), rules, 'skip');
|
|
|
|
|
|
});
|
|
|
|
|
|
$('addReadRule').addEventListener('click', () => {
|
|
|
|
|
|
const rules = collectRules($('readRulesList'));
|
|
|
|
|
|
rules.push({ start: '', end: '' });
|
|
|
|
|
|
renderRulesList($('readRulesList'), rules, 'read');
|
|
|
|
|
|
});
|
|
|
|
|
|
document.addEventListener('click', e => {
|
|
|
|
|
|
const deleteBtn = e.target.closest('.rule-delete');
|
|
|
|
|
|
if (!deleteBtn) return;
|
|
|
|
|
|
const type = deleteBtn.dataset.type;
|
|
|
|
|
|
const idx = parseInt(deleteBtn.dataset.idx);
|
|
|
|
|
|
const listEl = type === 'skip' ? $('skipRulesList') : $('readRulesList');
|
|
|
|
|
|
const rules = collectRules(listEl);
|
|
|
|
|
|
rules.splice(idx, 1);
|
|
|
|
|
|
renderRulesList(listEl, rules, type);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 音色处理
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function detectGenderByValue(value) {
|
|
|
|
|
|
const v = String(value || '').toLowerCase();
|
|
|
|
|
|
if (v.includes('female')) return 'female';
|
|
|
|
|
|
if (v.includes('male')) return 'male';
|
|
|
|
|
|
return 'other';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function detectModel(value) { return TTS2_VOICES.has(String(value).trim()) ? 'tts2' : 'tts1'; }
|
|
|
|
|
|
|
|
|
|
|
|
function detectTags(value, model) {
|
|
|
|
|
|
const tags = [];
|
|
|
|
|
|
if (model === 'tts2') tags.push('2.0');
|
|
|
|
|
|
if (model === 'tts1') tags.push('1.0');
|
2026-01-18 03:06:38 +08:00
|
|
|
|
if (String(value).includes('emo')) tags.push('多情感');
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return tags;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildAuthVoiceList() {
|
|
|
|
|
|
authVoiceList = AUTH_VOICE_DATA.map(item => {
|
|
|
|
|
|
const value = item.value;
|
|
|
|
|
|
const name = item.name || value;
|
|
|
|
|
|
const gender = detectGenderByValue(value);
|
2026-01-18 03:06:38 +08:00
|
|
|
|
const genderLabel = gender === 'female' ? '女' : gender === 'male' ? '男' : '其他';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const model = detectModel(value);
|
|
|
|
|
|
const scene = String(item.scene || '').trim();
|
|
|
|
|
|
const tags = detectTags(value, model);
|
|
|
|
|
|
if (scene && !tags.includes(scene)) tags.push(scene);
|
2026-01-18 01:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
let language;
|
|
|
|
|
|
if (model === 'tts2') {
|
|
|
|
|
|
language = 'multi';
|
|
|
|
|
|
} else if (value.startsWith('en_')) {
|
|
|
|
|
|
language = 'en';
|
|
|
|
|
|
} else if (value.startsWith('multi_')) {
|
|
|
|
|
|
language = 'other';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
language = 'zh';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return { value, name, gender, genderLabel, model, language, scene, tags };
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTagLabel(tags) { return tags.length ? ` [${tags.join('/')}]` : ''; }
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// UI 更新
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function updateApiStatus() {
|
|
|
|
|
|
const configured = isAuthConfigured();
|
|
|
|
|
|
const box = $('apiStatusBox');
|
|
|
|
|
|
const icon = box.querySelector('.api-status-icon i');
|
|
|
|
|
|
const title = box.querySelector('.api-status-title');
|
|
|
|
|
|
const desc = box.querySelector('.api-status-desc');
|
|
|
|
|
|
const badge = $('badge_auth');
|
|
|
|
|
|
|
|
|
|
|
|
if (configured) {
|
|
|
|
|
|
box.className = 'api-status-box configured';
|
2026-01-18 01:48:30 +08:00
|
|
|
|
icon.className = 'fa-solid fa-link';
|
2026-01-18 03:06:38 +08:00
|
|
|
|
title.textContent = '已配置';
|
|
|
|
|
|
desc.textContent = '可使用预设音色库和复刻音色';
|
2026-01-18 01:48:30 +08:00
|
|
|
|
badge.className = 'header-badge active';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
$('authVoiceNotice').style.display = 'none';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
box.className = 'api-status-box not-configured';
|
2026-01-18 01:48:30 +08:00
|
|
|
|
icon.className = 'fa-solid fa-link-slash';
|
2026-01-18 03:06:38 +08:00
|
|
|
|
title.textContent = '未配置';
|
|
|
|
|
|
desc.textContent = '配置后可使用预设音色库和复刻音色';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
badge.className = 'header-badge';
|
|
|
|
|
|
$('authVoiceNotice').style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateCurrentVoiceDisplay() {
|
|
|
|
|
|
const nameEl = $('currentVoiceName');
|
|
|
|
|
|
const sourceEl = $('currentVoiceSource');
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedVoiceValue) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
nameEl.innerHTML = '未选择';
|
|
|
|
|
|
sourceEl.textContent = '请在下方选择音色';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const myVoice = mySpeakers.find(s => s.value === selectedVoiceValue);
|
|
|
|
|
|
const source = myVoice?.source || getVoiceSource(selectedVoiceValue);
|
|
|
|
|
|
const sourceBadge = source === 'free'
|
2026-01-18 03:06:38 +08:00
|
|
|
|
? '<span class="source-badge trial">试用</span>'
|
|
|
|
|
|
: '<span class="source-badge auth">鉴权</span>';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
if (myVoice) {
|
|
|
|
|
|
nameEl.innerHTML = `${escapeHtml(myVoice.name)} ${sourceBadge}`;
|
2026-01-18 03:06:38 +08:00
|
|
|
|
sourceEl.textContent = '我的音色';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
} else if (source === 'free') {
|
|
|
|
|
|
const tv = TRIAL_VOICES.find(v => v.key === selectedVoiceValue);
|
|
|
|
|
|
nameEl.innerHTML = `${escapeHtml(tv?.name || selectedVoiceValue)} ${sourceBadge}`;
|
2026-01-18 03:06:38 +08:00
|
|
|
|
sourceEl.textContent = tv?.tag || '试用音色';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
const av = authVoiceList.find(v => v.value === selectedVoiceValue);
|
|
|
|
|
|
nameEl.innerHTML = `${escapeHtml(av?.name || selectedVoiceValue)} ${sourceBadge}`;
|
2026-01-18 03:06:38 +08:00
|
|
|
|
sourceEl.textContent = '预设音色(建议先添加到我的音色)';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 渲染音色列表
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function renderMyVoiceList() {
|
|
|
|
|
|
const listEl = $('myVoiceList');
|
|
|
|
|
|
const emptyEl = $('myVoiceEmpty');
|
|
|
|
|
|
$('myVoiceCount').textContent = mySpeakers.length;
|
|
|
|
|
|
|
|
|
|
|
|
if (!mySpeakers.length) {
|
|
|
|
|
|
listEl.innerHTML = '';
|
|
|
|
|
|
listEl.style.display = 'none';
|
|
|
|
|
|
emptyEl.style.display = 'block';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
listEl.style.display = 'flex';
|
|
|
|
|
|
emptyEl.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
const authOk = isAuthConfigured();
|
|
|
|
|
|
|
|
|
|
|
|
listEl.innerHTML = mySpeakers.map(s => {
|
|
|
|
|
|
const isSelected = s.value === selectedVoiceValue;
|
|
|
|
|
|
const isEditing = s.value === editingVoiceValue;
|
|
|
|
|
|
const source = s.source || getVoiceSource(s.value);
|
|
|
|
|
|
const canPlay = source === 'free' || authOk;
|
|
|
|
|
|
const sourceBadge = source === 'free'
|
2026-01-18 03:06:38 +08:00
|
|
|
|
? '<span class="source-badge trial">试用</span>'
|
|
|
|
|
|
: '<span class="source-badge auth">鉴权</span>';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="voice-item${isSelected ? ' selected' : ''}${isEditing ? ' editing' : ''}${!canPlay ? ' disabled' : ''}"
|
|
|
|
|
|
data-value="${escapeHtml(s.value)}" data-source="${source}">
|
|
|
|
|
|
<div class="voice-item-radio"></div>
|
|
|
|
|
|
<div class="voice-item-info">
|
|
|
|
|
|
<div class="voice-item-name">${escapeHtml(s.name || s.value)} ${sourceBadge}</div>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="voice-item-meta">${!canPlay ? '需配置鉴权' : escapeHtml(s.value.slice(0, 25))}</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-item-edit-form">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<input type="text" class="input voice-edit-input" value="${escapeHtml(s.name || '')}" placeholder="输入新名称">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<button class="btn btn-xs btn-primary voice-edit-save"><i class="fa-solid fa-check"></i></button>
|
|
|
|
|
|
<button class="btn btn-xs voice-edit-cancel"><i class="fa-solid fa-times"></i></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-item-actions">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button class="btn btn-xs voice-rename-btn" data-value="${escapeHtml(s.value)}" title="改名">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<i class="fa-solid fa-pen"></i>
|
|
|
|
|
|
</button>
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<button class="btn btn-xs btn-danger voice-delete-btn" data-value="${escapeHtml(s.value)}" title="删除">
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<i class="fa-solid fa-trash"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
bindMyVoiceEvents(listEl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function bindMyVoiceEvents(listEl) {
|
|
|
|
|
|
listEl.querySelectorAll('.voice-item').forEach(item => {
|
|
|
|
|
|
item.addEventListener('click', e => {
|
|
|
|
|
|
if (e.target.closest('button') || e.target.closest('input')) return;
|
|
|
|
|
|
if (item.classList.contains('disabled')) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'error', message: '请先配置鉴权 API' });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (editingVoiceValue) return;
|
|
|
|
|
|
selectedVoiceValue = item.dataset.value;
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-rename-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
editingVoiceValue = btn.dataset.value;
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
listEl.querySelector('.voice-edit-input')?.focus();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-edit-save').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const item = mySpeakers.find(s => s.value === editingVoiceValue);
|
|
|
|
|
|
const input = btn.closest('.voice-item').querySelector('.voice-edit-input');
|
|
|
|
|
|
if (item && input?.value?.trim()) {
|
|
|
|
|
|
item.name = input.value.trim();
|
|
|
|
|
|
post('xb-tts:save-config', collectForm());
|
|
|
|
|
|
}
|
|
|
|
|
|
editingVoiceValue = null;
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-edit-cancel').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
editingVoiceValue = null;
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-delete-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const value = btn.dataset.value;
|
|
|
|
|
|
const item = mySpeakers.find(s => s.value === value);
|
2026-01-18 03:06:38 +08:00
|
|
|
|
if (confirm(`删除「${item?.name || value}」?`)) {
|
2026-01-17 16:34:39 +08:00
|
|
|
|
mySpeakers = mySpeakers.filter(s => s.value !== value);
|
|
|
|
|
|
if (selectedVoiceValue === value) {
|
|
|
|
|
|
selectedVoiceValue = mySpeakers[0]?.value || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
renderTrialVoiceList();
|
|
|
|
|
|
renderAuthVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
post('xb-tts:save-config', collectForm());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderTrialVoiceList() {
|
|
|
|
|
|
const listEl = $('trialVoiceList');
|
|
|
|
|
|
listEl.innerHTML = TRIAL_VOICES.map(v => {
|
|
|
|
|
|
const isSelected = v.key === selectedTrialVoiceValue;
|
|
|
|
|
|
const inMy = isInMyList(v.key);
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="voice-item${isSelected ? ' selected' : ''}${inMy ? ' in-my-list' : ''}" data-value="${v.key}">
|
|
|
|
|
|
<div class="voice-item-radio"></div>
|
|
|
|
|
|
<div class="voice-item-info">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="voice-item-name">${escapeHtml(v.name)}${inMy ? ' <span class="source-badge trial">已添加</span>' : ''}</div>
|
|
|
|
|
|
<div class="voice-item-meta">${escapeHtml(v.tag)} · ${v.gender === 'female' ? '女' : '男'}</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-item').forEach(item => {
|
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
|
selectedTrialVoiceValue = item.dataset.value;
|
|
|
|
|
|
renderTrialVoiceList();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAuthVoiceList() {
|
|
|
|
|
|
const gender = $('voiceGenderFilter')?.value || 'all';
|
|
|
|
|
|
const model = $('voiceModelFilter')?.value || 'all';
|
|
|
|
|
|
const language = $('voiceLangFilter')?.value || 'all';
|
|
|
|
|
|
const scene = $('voiceSceneFilter')?.value || 'all';
|
|
|
|
|
|
const search = ($('voiceSearchInput')?.value || '').toLowerCase().trim();
|
|
|
|
|
|
|
|
|
|
|
|
const list = authVoiceList.filter(item => {
|
|
|
|
|
|
if (gender !== 'all' && item.gender !== gender) return false;
|
|
|
|
|
|
if (model !== 'all' && item.model !== model) return false;
|
|
|
|
|
|
if (language !== 'all' && item.language !== language) return false;
|
|
|
|
|
|
if (scene !== 'all' && item.scene !== scene) return false;
|
|
|
|
|
|
if (search && !item.name.toLowerCase().includes(search) && !item.value.toLowerCase().includes(search)) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}).sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'));
|
|
|
|
|
|
|
|
|
|
|
|
const listEl = $('authVoiceList');
|
|
|
|
|
|
if (!list.length) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
listEl.innerHTML = '<div class="rules-empty">没有匹配的音色</div>';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
listEl.innerHTML = list.map(item => {
|
|
|
|
|
|
const isSelected = item.value === selectedAuthVoiceValue;
|
|
|
|
|
|
const inMy = isInMyList(item.value);
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="voice-item${isSelected ? ' selected' : ''}${inMy ? ' in-my-list' : ''}" data-value="${escapeHtml(item.value)}">
|
|
|
|
|
|
<div class="voice-item-radio"></div>
|
|
|
|
|
|
<div class="voice-item-info">
|
2026-01-18 03:06:38 +08:00
|
|
|
|
<div class="voice-item-name">${escapeHtml(item.name)}${inMy ? ' <span class="source-badge auth">已添加</span>' : ''}</div>
|
2026-01-17 16:34:39 +08:00
|
|
|
|
<div class="voice-item-meta">${escapeHtml(item.genderLabel)}${formatTagLabel(item.tags)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
listEl.querySelectorAll('.voice-item').forEach(item => {
|
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
|
selectedAuthVoiceValue = item.dataset.value;
|
|
|
|
|
|
renderAuthVoiceList();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 数据处理
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function normalizeMySpeakers(list) {
|
|
|
|
|
|
if (!Array.isArray(list)) return [];
|
|
|
|
|
|
return list.map(item => ({
|
|
|
|
|
|
name: String(item?.name || '').trim(),
|
|
|
|
|
|
value: String(item?.value || '').trim(),
|
|
|
|
|
|
source: item?.source || getVoiceSource(item?.value || ''),
|
2026-01-26 01:16:35 +08:00
|
|
|
|
resourceId: item?.resourceId || null,
|
2026-01-17 16:34:39 +08:00
|
|
|
|
})).filter(item => item.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyCacheStats(stats = {}) {
|
|
|
|
|
|
$('cacheCount').textContent = stats.count ?? 0;
|
|
|
|
|
|
$('cacheSize').textContent = `${stats.sizeMB ?? 0} MB`;
|
|
|
|
|
|
$('cacheHits').textContent = stats.hits ?? 0;
|
|
|
|
|
|
$('cacheMisses').textContent = stats.misses ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fillForm(cfg) {
|
|
|
|
|
|
config = cfg || {};
|
|
|
|
|
|
mySpeakers = normalizeMySpeakers(cfg.volc?.mySpeakers);
|
|
|
|
|
|
selectedVoiceValue = cfg.volc?.defaultSpeaker || '';
|
|
|
|
|
|
|
|
|
|
|
|
$('appId').value = cfg.volc?.appId || '';
|
|
|
|
|
|
$('accessKey').value = cfg.volc?.accessKey || '';
|
|
|
|
|
|
$('autoSpeak').checked = cfg.autoSpeak !== false;
|
|
|
|
|
|
|
|
|
|
|
|
const speechRate = Number.isFinite(cfg.volc?.speechRate) ? cfg.volc.speechRate : 1.0;
|
|
|
|
|
|
$('speechRate').value = speechRate;
|
|
|
|
|
|
$('speechRateValue').textContent = `${speechRate.toFixed(1)}x`;
|
|
|
|
|
|
|
|
|
|
|
|
renderRulesList($('skipRulesList'), cfg.skipRanges || [], 'skip');
|
|
|
|
|
|
renderRulesList($('readRulesList'), cfg.readRanges || [], 'read');
|
|
|
|
|
|
$('readRangesEnabled').checked = cfg.readRangesEnabled === true;
|
|
|
|
|
|
|
|
|
|
|
|
$('usageReturn').checked = cfg.volc?.usageReturn === true;
|
|
|
|
|
|
$('serverCacheEnabled').checked = cfg.volc?.serverCacheEnabled === true;
|
|
|
|
|
|
$('disableMarkdownFilter').checked = cfg.volc?.disableMarkdownFilter !== false;
|
|
|
|
|
|
$('useTts11').checked = cfg.volc?.useTts11 !== false;
|
|
|
|
|
|
$('disableEmojiFilter').checked = cfg.volc?.disableEmojiFilter === true;
|
|
|
|
|
|
$('enableLanguageDetector').checked = cfg.volc?.enableLanguageDetector === true;
|
|
|
|
|
|
$('explicitLanguage').value = cfg.volc?.explicitLanguage || '';
|
|
|
|
|
|
$('maxLengthToFilterParenthesis').value = cfg.volc?.maxLengthToFilterParenthesis ?? 100;
|
|
|
|
|
|
$('postProcessPitch').value = cfg.volc?.postProcessPitch ?? 0;
|
|
|
|
|
|
$('cacheDays').value = cfg.volc?.cacheDays ?? 7;
|
|
|
|
|
|
$('cacheMaxEntries').value = cfg.volc?.cacheMaxEntries ?? 200;
|
|
|
|
|
|
$('cacheMaxMB').value = cfg.volc?.cacheMaxMB ?? 200;
|
|
|
|
|
|
applyCacheStats(cfg.cacheStats || {});
|
2026-01-18 23:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
$('showFloorButton').checked = cfg.showFloorButton !== false;
|
|
|
|
|
|
$('showFloatingButton').checked = cfg.showFloatingButton === true;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
updateApiStatus();
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
renderTrialVoiceList();
|
|
|
|
|
|
renderAuthVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferResourceIdBySpeaker(value) {
|
|
|
|
|
|
const v = (value || '').trim().toLowerCase();
|
|
|
|
|
|
if (v.startsWith('icl_') || v.startsWith('s_')) return 'seed-icl-2.0';
|
|
|
|
|
|
if (TTS2_VOICES.has(value)) return 'seed-tts-2.0';
|
|
|
|
|
|
return 'seed-tts-1.0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectForm() {
|
|
|
|
|
|
const speaker = selectedVoiceValue;
|
|
|
|
|
|
const source = getVoiceSource(speaker);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
volc: {
|
|
|
|
|
|
appId: $('appId').value.trim(),
|
|
|
|
|
|
accessKey: $('accessKey').value.trim(),
|
|
|
|
|
|
defaultResourceId: source === 'auth' ? inferResourceIdBySpeaker(speaker) : '',
|
|
|
|
|
|
defaultSpeaker: speaker,
|
|
|
|
|
|
mySpeakers: mySpeakers,
|
|
|
|
|
|
usageReturn: $('usageReturn').checked,
|
|
|
|
|
|
serverCacheEnabled: $('serverCacheEnabled').checked,
|
|
|
|
|
|
disableMarkdownFilter: $('disableMarkdownFilter').checked,
|
|
|
|
|
|
useTts11: $('useTts11').checked,
|
|
|
|
|
|
disableEmojiFilter: $('disableEmojiFilter').checked,
|
|
|
|
|
|
enableLanguageDetector: $('enableLanguageDetector').checked,
|
|
|
|
|
|
explicitLanguage: $('explicitLanguage').value.trim(),
|
|
|
|
|
|
maxLengthToFilterParenthesis: Number($('maxLengthToFilterParenthesis').value) || 0,
|
|
|
|
|
|
postProcessPitch: Number($('postProcessPitch').value) || 0,
|
|
|
|
|
|
speechRate: Number($('speechRate').value) || 1.0,
|
|
|
|
|
|
localCacheEnabled: true,
|
|
|
|
|
|
cacheDays: Math.max(1, Number($('cacheDays').value) || 7),
|
|
|
|
|
|
cacheMaxEntries: Math.max(10, Number($('cacheMaxEntries').value) || 200),
|
|
|
|
|
|
cacheMaxMB: Math.max(10, Number($('cacheMaxMB').value) || 200),
|
|
|
|
|
|
},
|
|
|
|
|
|
autoSpeak: $('autoSpeak').checked,
|
|
|
|
|
|
skipRanges: collectRules($('skipRulesList')),
|
|
|
|
|
|
readRanges: collectRules($('readRulesList')),
|
|
|
|
|
|
readRangesEnabled: $('readRangesEnabled').checked,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 试听功能
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function doTestVoice(speaker, source, textElId, statusElId) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
const text = $(textElId)?.value?.trim() || '你好,这是一段测试语音。';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
if (!speaker) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
setTestStatus(statusElId, 'error', '请先选择一个音色');
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (source === 'auth' && !isAuthConfigured()) {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
setTestStatus(statusElId, 'error', '请先配置鉴权 API');
|
2026-01-17 16:34:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
setTestStatus(statusElId, 'playing', '正在合成...');
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
2026-01-26 01:16:35 +08:00
|
|
|
|
const speakerItem = mySpeakers.find(s => s.value === speaker);
|
|
|
|
|
|
const resolvedResourceId = speakerItem?.resourceId;
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
post('xb-tts:test-speak', {
|
|
|
|
|
|
text,
|
|
|
|
|
|
speaker,
|
|
|
|
|
|
source,
|
2026-01-26 01:16:35 +08:00
|
|
|
|
resourceId: source === 'auth' ? (resolvedResourceId || inferResourceIdBySpeaker(speaker)) : '',
|
2026-01-17 16:34:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 消息处理
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
window.addEventListener('message', ev => {
|
|
|
|
|
|
if (ev.origin !== PARENT_ORIGIN || ev.source !== parent) return;
|
|
|
|
|
|
if (!ev.data?.type?.startsWith('xb-tts:')) return;
|
|
|
|
|
|
const { type, payload } = ev.data;
|
|
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case 'xb-tts:config':
|
|
|
|
|
|
fillForm(payload);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'xb-tts:config-saved':
|
|
|
|
|
|
fillForm(payload);
|
|
|
|
|
|
handleSaveResult(true);
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'success', message: '配置已保存' });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'xb-tts:config-save-error':
|
|
|
|
|
|
handleSaveResult(false);
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'error', message: payload?.message || '保存失败' });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
break;
|
2026-01-18 23:03:36 +08:00
|
|
|
|
case 'xb-tts:button-mode-saved':
|
|
|
|
|
|
break;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
case 'xb-tts:test-done':
|
2026-01-18 03:06:38 +08:00
|
|
|
|
['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, 'playing', '播放中...'));
|
2026-01-17 16:34:39 +08:00
|
|
|
|
setTimeout(() => ['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, '', '')), 3000);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'xb-tts:test-error':
|
2026-01-18 03:06:38 +08:00
|
|
|
|
const errMsg = '失败: ' + (payload || '未知错误');
|
2026-01-17 16:34:39 +08:00
|
|
|
|
['testMyStatus', 'testTrialStatus', 'testAuthStatus'].forEach(id => setTestStatus(id, 'error', errMsg));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'xb-tts:cache-stats':
|
|
|
|
|
|
applyCacheStats(payload || {});
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-18 03:06:38 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 02:55:49 +08:00
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
buildAuthVoiceList();
|
|
|
|
|
|
initRulesEditors();
|
|
|
|
|
|
|
|
|
|
|
|
const scenes = new Set();
|
|
|
|
|
|
authVoiceList.forEach(item => { if (item.scene) scenes.add(item.scene); });
|
2026-01-18 03:06:38 +08:00
|
|
|
|
$('voiceSceneFilter').innerHTML = '<option value="all">全部场景</option>' +
|
2026-01-17 16:34:39 +08:00
|
|
|
|
Array.from(scenes).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN')).map(s => `<option value="${s}">${s}</option>`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
$$('.nav-item, .mobile-nav-item').forEach(item => item.addEventListener('click', () => switchView(item.dataset.view)));
|
|
|
|
|
|
$('tts_close').addEventListener('click', () => post('xb-tts:close'));
|
2026-01-18 23:03:36 +08:00
|
|
|
|
$('showFloorButton').addEventListener('change', () => {
|
|
|
|
|
|
post('xb-tts:save-button-mode', {
|
|
|
|
|
|
showFloorButton: $('showFloorButton').checked,
|
|
|
|
|
|
showFloatingButton: $('showFloatingButton').checked
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
$('showFloatingButton').addEventListener('change', () => {
|
|
|
|
|
|
post('xb-tts:save-button-mode', {
|
|
|
|
|
|
showFloorButton: $('showFloorButton').checked,
|
|
|
|
|
|
showFloatingButton: $('showFloatingButton').checked
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
$('toggleKey').addEventListener('click', () => {
|
|
|
|
|
|
const input = $('accessKey');
|
|
|
|
|
|
const icon = $('toggleKey').querySelector('i');
|
|
|
|
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
|
|
|
|
icon.className = input.type === 'password' ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
['appId', 'accessKey'].forEach(id => {
|
|
|
|
|
|
$(id).addEventListener('input', () => {
|
|
|
|
|
|
config.volc = config.volc || {};
|
|
|
|
|
|
config.volc.appId = $('appId').value.trim();
|
|
|
|
|
|
config.volc.accessKey = $('accessKey').value.trim();
|
|
|
|
|
|
updateApiStatus();
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('speechRate').addEventListener('input', e => {
|
|
|
|
|
|
$('speechRateValue').textContent = `${Number(e.target.value).toFixed(1)}x`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
['voiceGenderFilter', 'voiceModelFilter', 'voiceLangFilter', 'voiceSceneFilter'].forEach(id => {
|
|
|
|
|
|
$(id)?.addEventListener('change', renderAuthVoiceList);
|
|
|
|
|
|
});
|
|
|
|
|
|
$('voiceSearchInput')?.addEventListener('input', renderAuthVoiceList);
|
|
|
|
|
|
|
|
|
|
|
|
$$('.voice-tab').forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', () => {
|
|
|
|
|
|
$$('.voice-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
$$('.voice-panel').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
tab.classList.add('active');
|
|
|
|
|
|
$(`panel-${tab.dataset.panel}`)?.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('testMyVoiceBtn').addEventListener('click', () => {
|
|
|
|
|
|
const my = mySpeakers.find(s => s.value === selectedVoiceValue);
|
|
|
|
|
|
const source = my?.source || getVoiceSource(selectedVoiceValue);
|
|
|
|
|
|
doTestVoice(selectedVoiceValue, source, 'testTextMy', 'testMyStatus');
|
|
|
|
|
|
});
|
|
|
|
|
|
$('testTrialVoiceBtn').addEventListener('click', () => {
|
|
|
|
|
|
doTestVoice(selectedTrialVoiceValue, 'free', 'testTextTrial', 'testTrialStatus');
|
|
|
|
|
|
});
|
|
|
|
|
|
$('testAuthVoiceBtn').addEventListener('click', () => {
|
|
|
|
|
|
doTestVoice(selectedAuthVoiceValue, 'auth', 'testTextAuth', 'testAuthStatus');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('saveToMyVoiceTrialBtn').addEventListener('click', () => {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
if (!selectedTrialVoiceValue) { post('xb-tts:toast', { type: 'error', message: '请先选择一个音色' }); return; }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const tv = TRIAL_VOICES.find(v => v.key === selectedTrialVoiceValue);
|
|
|
|
|
|
const name = $('saveAsNameTrial').value.trim() || tv?.name || selectedTrialVoiceValue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!isInMyList(selectedTrialVoiceValue)) {
|
|
|
|
|
|
mySpeakers.push({ name, value: selectedTrialVoiceValue, source: 'free' });
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedVoiceValue = selectedTrialVoiceValue;
|
|
|
|
|
|
$('saveAsNameTrial').value = '';
|
|
|
|
|
|
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
renderTrialVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
|
|
|
|
|
|
$$('.voice-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
$$('.voice-panel').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
$$('.voice-tab')[0].classList.add('active');
|
|
|
|
|
|
$('panel-myVoice').classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
post('xb-tts:save-config', collectForm());
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('saveToMyVoiceAuthBtn').addEventListener('click', () => {
|
2026-01-18 03:06:38 +08:00
|
|
|
|
if (!selectedAuthVoiceValue) { post('xb-tts:toast', { type: 'error', message: '请先选择一个音色' }); return; }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const av = authVoiceList.find(v => v.value === selectedAuthVoiceValue);
|
|
|
|
|
|
const name = $('saveAsNameAuth').value.trim() || av?.name || selectedAuthVoiceValue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!isInMyList(selectedAuthVoiceValue)) {
|
|
|
|
|
|
mySpeakers.push({ name, value: selectedAuthVoiceValue, source: 'auth' });
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedVoiceValue = selectedAuthVoiceValue;
|
|
|
|
|
|
$('saveAsNameAuth').value = '';
|
|
|
|
|
|
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
renderAuthVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
|
|
|
|
|
|
$$('.voice-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
|
$$('.voice-panel').forEach(p => p.classList.remove('active'));
|
|
|
|
|
|
$$('.voice-tab')[0].classList.add('active');
|
|
|
|
|
|
$('panel-myVoice').classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
post('xb-tts:save-config', collectForm());
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name}` });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('addMySpeakerBtn').addEventListener('click', () => {
|
|
|
|
|
|
const id = $('newVoiceId').value.trim();
|
|
|
|
|
|
const name = $('newVoiceName').value.trim();
|
2026-01-26 01:16:35 +08:00
|
|
|
|
const resourceId = $('newVoiceResourceId').value;
|
2026-01-18 03:06:38 +08:00
|
|
|
|
if (!id) { post('xb-tts:toast', { type: 'error', message: '请输入音色ID' }); return; }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
if (!isInMyList(id)) {
|
2026-01-26 01:16:35 +08:00
|
|
|
|
mySpeakers.push({ name: name || id, value: id, source: 'auth', resourceId });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
selectedVoiceValue = id;
|
|
|
|
|
|
$('newVoiceId').value = '';
|
|
|
|
|
|
$('newVoiceName').value = '';
|
|
|
|
|
|
|
|
|
|
|
|
renderMyVoiceList();
|
|
|
|
|
|
updateCurrentVoiceDisplay();
|
|
|
|
|
|
post('xb-tts:save-config', collectForm());
|
2026-01-18 03:06:38 +08:00
|
|
|
|
post('xb-tts:toast', { type: 'success', message: `已添加:${name || id}` });
|
2026-01-17 16:34:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
['saveConfigBtn', 'saveVoiceBtn', 'saveAdvancedBtn', 'saveCacheBtn'].forEach(id => {
|
|
|
|
|
|
$(id)?.addEventListener('click', () => { setSavingState($(id)); post('xb-tts:save-config', collectForm()); });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$('cacheRefreshBtn').addEventListener('click', () => post('xb-tts:cache-refresh'));
|
|
|
|
|
|
$('cacheClearExpiredBtn').addEventListener('click', () => post('xb-tts:cache-clear-expired'));
|
|
|
|
|
|
$('cacheClearAllBtn').addEventListener('click', () => post('xb-tts:cache-clear-all'));
|
|
|
|
|
|
|
|
|
|
|
|
post('xb-tts:ready');
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
2026-01-18 23:03:36 +08:00
|
|
|
|
</html>
|