|
@@ -1,52 +1,9 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="page">
|
|
<view class="page">
|
|
|
- <!-- 顶部导航栏 -->
|
|
|
|
|
- <view class="nav-bar">
|
|
|
|
|
- <view class="nav-content">
|
|
|
|
|
- <text class="page-title">生成音频</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 主内容区 -->
|
|
|
|
|
|
|
+ <!-- 主内容区:只有文本输入 -->
|
|
|
<view class="main-content">
|
|
<view class="main-content">
|
|
|
- <!-- 文档上传区 -->
|
|
|
|
|
- <view class="card doc-upload-card">
|
|
|
|
|
- <view class="card-header">
|
|
|
|
|
- <text class="card-title">📄 上传文档</text>
|
|
|
|
|
- <text v-if="uploading" class="upload-status uploading">解析中...</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <!-- 未上传时:选择文件 -->
|
|
|
|
|
- <view v-if="!uploadedDoc.fileName" class="doc-upload-area" @click="handleUploadDoc">
|
|
|
|
|
- <text class="doc-upload-icon">📁</text>
|
|
|
|
|
- <text class="doc-upload-text">点击上传文档</text>
|
|
|
|
|
- <text class="doc-upload-hint">支持 .txt .docx .pdf 格式,最大 20MB</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <!-- 已上传:显示文档信息 -->
|
|
|
|
|
- <view v-else class="doc-info-bar">
|
|
|
|
|
- <view class="doc-info-left">
|
|
|
|
|
- <text class="doc-format-badge">{{ uploadedDoc.format?.toUpperCase() || 'DOC' }}</text>
|
|
|
|
|
- <view class="doc-info-text">
|
|
|
|
|
- <text class="doc-filename">{{ uploadedDoc.fileName }}</text>
|
|
|
|
|
- <text class="doc-meta">{{ uploadedDoc.wordCount }} 字 · {{ formatFileSize(uploadedDoc.fileSize) }}</text>
|
|
|
|
|
- <text v-if="uploadedDoc.truncated" class="doc-truncated-hint">⚠ 内容过长,已截取前2000字</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="doc-remove-btn" @click="clearDoc">✕</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <!-- 上传中 -->
|
|
|
|
|
- <view v-if="uploading" class="doc-uploading-bar">
|
|
|
|
|
- <text>正在解析文档,请稍候...</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 文本输入区 -->
|
|
|
|
|
- <view class="card input-card">
|
|
|
|
|
- <view class="card-header">
|
|
|
|
|
- <text class="card-title">输入文本</text>
|
|
|
|
|
- <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
|
|
|
|
|
- {{ text.length }}/2000 字
|
|
|
|
|
- </text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <!-- 文本输入 -->
|
|
|
|
|
+ <view class="input-area">
|
|
|
<textarea
|
|
<textarea
|
|
|
v-model="text"
|
|
v-model="text"
|
|
|
class="text-input"
|
|
class="text-input"
|
|
@@ -54,170 +11,171 @@
|
|
|
:maxlength="2000"
|
|
:maxlength="2000"
|
|
|
auto-height
|
|
auto-height
|
|
|
/>
|
|
/>
|
|
|
- <view class="input-actions">
|
|
|
|
|
- <button class="action-btn ai-generate-btn" @click="goToAIGenerate">
|
|
|
|
|
- <text>🤖 AI 生成内容</text>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button class="action-btn" @click="clearText">清空</button>
|
|
|
|
|
- <button class="action-btn" @click="pasteText">粘贴</button>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="input-hint">
|
|
|
|
|
- <text>支持长按粘贴文本内容</text>
|
|
|
|
|
|
|
+ <!-- 字数统计 -->
|
|
|
|
|
+ <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
|
|
|
|
|
+ {{ text.length }}/2000
|
|
|
|
|
+ </text>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已上传文档信息 -->
|
|
|
|
|
+ <view v-if="uploadedDoc.fileName && !uploading" class="doc-info-bar">
|
|
|
|
|
+ <text class="doc-format-badge">{{ uploadedDoc.format?.toUpperCase() || 'DOC' }}</text>
|
|
|
|
|
+ <text class="doc-filename">{{ uploadedDoc.fileName }}</text>
|
|
|
|
|
+ <text class="doc-meta">{{ uploadedDoc.wordCount }} 字</text>
|
|
|
|
|
+ <text v-if="uploadedDoc.truncated" class="doc-truncated-hint">⚠ 已截取</text>
|
|
|
|
|
+ <text class="doc-remove-btn" @click="clearDoc">✕</text>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 音色选择 - 网格布局 -->
|
|
|
|
|
- <view class="card voice-card">
|
|
|
|
|
- <text class="card-title">选择音色</text>
|
|
|
|
|
- <view class="voice-grid">
|
|
|
|
|
- <view
|
|
|
|
|
- v-for="voice in audioStore.voices"
|
|
|
|
|
- :key="voice.id"
|
|
|
|
|
- class="voice-item"
|
|
|
|
|
- :class="{ active: selectedVoice === voice.id }"
|
|
|
|
|
- @click="selectedVoice = voice.id"
|
|
|
|
|
- >
|
|
|
|
|
- <view class="voice-icon">
|
|
|
|
|
- <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <text class="voice-name">{{ voice.name }}</text>
|
|
|
|
|
- <view class="voice-preview-btn" @click.stop="previewVoice(voice.id)">
|
|
|
|
|
- <text v-if="previewingVoice !== voice.id">▶</text>
|
|
|
|
|
- <text v-else>⏹</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <!-- 上传中 -->
|
|
|
|
|
+ <view v-if="uploading" class="doc-uploading-bar">
|
|
|
|
|
+ <text>正在解析文档,请稍候...</text>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
|
|
|
|
|
- <!-- 参数调节 -->
|
|
|
|
|
- <view class="card params-card">
|
|
|
|
|
- <text class="card-title">参数调节</text>
|
|
|
|
|
- <view class="param-item">
|
|
|
|
|
- <view class="param-header">
|
|
|
|
|
- <text class="param-label">语速</text>
|
|
|
|
|
- <view class="param-value">{{ voiceParams.speed.toFixed(1) }}x</view>
|
|
|
|
|
|
|
+ <!-- 工具按钮行 -->
|
|
|
|
|
+ <view class="tool-row">
|
|
|
|
|
+ <view class="tool-btn" @click="handleUploadDoc">
|
|
|
|
|
+ <text>{{ uploadedDoc.fileName ? '📄 重新上传' : '📄 上传文档' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <slider
|
|
|
|
|
- :value="(voiceParams.speed - 0.5) * 100 / 1.5"
|
|
|
|
|
- :min="0"
|
|
|
|
|
- :max="100"
|
|
|
|
|
- @changing="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
|
|
|
|
|
- activeColor="#4F46E5"
|
|
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
|
|
- block-size="20"
|
|
|
|
|
- />
|
|
|
|
|
- <view class="param-range">
|
|
|
|
|
- <text>0.5x</text>
|
|
|
|
|
- <text>2.0x</text>
|
|
|
|
|
|
|
+ <view class="tool-btn" @click="goToAIGenerate">
|
|
|
|
|
+ <text>🤖 AI 生成</text>
|
|
|
</view>
|
|
</view>
|
|
|
- </view>
|
|
|
|
|
- <view class="param-item">
|
|
|
|
|
- <view class="param-header">
|
|
|
|
|
- <text class="param-label">音调</text>
|
|
|
|
|
- <view class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</view>
|
|
|
|
|
|
|
+ <view class="tool-btn" @click="pasteText">
|
|
|
|
|
+ <text>📋 粘贴</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <slider
|
|
|
|
|
- :value="(voiceParams.pitch + 500) / 10"
|
|
|
|
|
- :min="0"
|
|
|
|
|
- :max="100"
|
|
|
|
|
- @changing="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
|
|
|
|
|
- activeColor="#4F46E5"
|
|
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
|
|
- block-size="20"
|
|
|
|
|
- />
|
|
|
|
|
- <view class="param-range">
|
|
|
|
|
- <text>-500</text>
|
|
|
|
|
- <text>+500</text>
|
|
|
|
|
|
|
+ <view class="tool-btn" @click="clearText">
|
|
|
|
|
+ <text>🗑 清空</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="param-item">
|
|
|
|
|
- <view class="param-header">
|
|
|
|
|
- <text class="param-label">音量</text>
|
|
|
|
|
- <view class="param-value">{{ voiceParams.volume }}%</view>
|
|
|
|
|
- </view>
|
|
|
|
|
- <slider
|
|
|
|
|
- :value="voiceParams.volume"
|
|
|
|
|
- :min="0"
|
|
|
|
|
- :max="100"
|
|
|
|
|
- @changing="(e: any) => voiceParams.volume = e.detail.value"
|
|
|
|
|
- activeColor="#4F46E5"
|
|
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
|
|
- block-size="20"
|
|
|
|
|
- />
|
|
|
|
|
- <view class="param-range">
|
|
|
|
|
- <text>0%</text>
|
|
|
|
|
- <text>100%</text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 专辑选择(必填) -->
|
|
|
|
|
+ <view class="album-selector" @click="showAlbumListPanel = true">
|
|
|
|
|
+ <text class="album-selector-label">专辑</text>
|
|
|
|
|
+ <text class="album-selector-value" :class="{ placeholder: !selectedAlbum }">
|
|
|
|
|
+ {{ selectedAlbum ? selectedAlbum.title : '请选择专辑(必填)' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text class="album-selector-arrow">▼</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
|
|
|
- <!-- 专辑选择 -->
|
|
|
|
|
- <view class="card album-card">
|
|
|
|
|
- <view class="card-header-row">
|
|
|
|
|
- <text class="card-title">📚 所属专辑</text>
|
|
|
|
|
- <text class="album-add-btn" @click="showAlbumPanel = true">+ 新建专辑</text>
|
|
|
|
|
|
|
+ <!-- 底部固定区:Tab 工具栏 -->
|
|
|
|
|
+ <view class="bottom-bar">
|
|
|
|
|
+ <!-- 主操作行:生成按钮 + 快捷信息 -->
|
|
|
|
|
+ <view class="bottom-action-row">
|
|
|
|
|
+ <view class="quick-info">
|
|
|
|
|
+ <text class="quick-info-text">🎤 {{ currentVoiceName || '默认音色' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="album-selector" @click="showAlbumListPanel = true">
|
|
|
|
|
- <text v-if="selectedAlbum" class="album-name">{{ selectedAlbum.title }}</text>
|
|
|
|
|
- <text v-else class="album-placeholder">点击选择专辑(可选)</text>
|
|
|
|
|
- <text class="album-arrow">›</text>
|
|
|
|
|
|
|
+ <view class="generate-btn-tab" :class="{ disabled: !canGenerate }" @click="handleGenerate">
|
|
|
|
|
+ <text class="generate-btn-tab-text">{{ generating ? '提交中...' : '开始生成' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 音频生成入口 -->
|
|
|
|
|
- <view class="audio-entry-card" @click="handleGenerate">
|
|
|
|
|
- <view class="audio-entry-content">
|
|
|
|
|
- <text class="audio-entry-icon">🎵</text>
|
|
|
|
|
- <view class="audio-entry-text">
|
|
|
|
|
- <text class="audio-entry-title">音频生成</text>
|
|
|
|
|
- <text class="audio-entry-desc">将文字转换为语音</text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <!-- 图标 Tab 栏(等宽分布,图标+文字居中) -->
|
|
|
|
|
+ <view class="tab-toolbar">
|
|
|
|
|
+ <view class="tab-item" :class="{ active: showVoicePanel }" @click="showVoicePanel = true">
|
|
|
|
|
+ <text class="tab-icon">🎤</text>
|
|
|
|
|
+ <text class="tab-text">音色</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="tab-item" @click="showParamsPanel = true">
|
|
|
|
|
+ <text class="tab-icon">⚙️</text>
|
|
|
|
|
+ <text class="tab-text">参数</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="audio-entry-arrow">→</text>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 音色面板(暗色主题) -->
|
|
|
|
|
+ <view v-if="showVoicePanel" class="bottom-panel-overlay" @click="showVoicePanel = false">
|
|
|
|
|
+ <view class="bottom-panel voice-panel-dark" @click.stop>
|
|
|
|
|
+ <!-- 分类标签栏 -->
|
|
|
|
|
+ <view class="voice-tabs">
|
|
|
|
|
+ <scroll-view scroll-x :show-scrollbar="false" class="voice-tabs-scroll">
|
|
|
|
|
+ <view class="voice-tabs-inner">
|
|
|
|
|
+ <text
|
|
|
|
|
+ v-for="(cat, idx) in voiceCategories"
|
|
|
|
|
+ :key="cat.key"
|
|
|
|
|
+ class="voice-tab"
|
|
|
|
|
+ :class="{ active: voiceCategory === cat.key }"
|
|
|
|
|
+ @click="voiceCategory = cat.key"
|
|
|
|
|
+ >{{ cat.label }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ </view>
|
|
|
|
|
|
|
|
- <!-- 视频生成入口 -->
|
|
|
|
|
- <view v-if="false" class="video-entry-card" @click="goToVideoGenerator">
|
|
|
|
|
- <view class="video-entry-content">
|
|
|
|
|
- <text class="video-entry-icon">🎬</text>
|
|
|
|
|
- <view class="video-entry-text">
|
|
|
|
|
- <text class="video-entry-title">视频生成</text>
|
|
|
|
|
- <text class="video-entry-desc">图片 + 音频 = 精美视频</text>
|
|
|
|
|
|
|
+ <!-- 音色网格 -->
|
|
|
|
|
+ <scroll-view scroll-y class="voice-grid-scroll">
|
|
|
|
|
+ <view class="voice-card-grid">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="voice in filteredVoices"
|
|
|
|
|
+ :key="voice.id"
|
|
|
|
|
+ class="voice-card"
|
|
|
|
|
+ :class="{ active: selectedVoice === voice.id }"
|
|
|
|
|
+ @click="selectVoiceAndClose(voice.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="voice-card-icon">
|
|
|
|
|
+ <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="voice-card-name">{{ voice.name }}</text>
|
|
|
|
|
+ <view v-if="selectedVoice === voice.id" class="voice-card-check">✓</view>
|
|
|
|
|
+ <view class="voice-card-play" @click.stop="previewVoice(voice.id)">
|
|
|
|
|
+ <text>{{ previewingVoice === voice.id ? '⏹' : '▶' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 关闭按钮 -->
|
|
|
|
|
+ <view class="panel-close-bar" @click="showVoicePanel = false">
|
|
|
|
|
+ <view class="panel-close-bar-line"></view>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="video-entry-arrow">→</text>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 新手引导 -->
|
|
|
|
|
- <view v-if="showGuide" class="guide-overlay">
|
|
|
|
|
- <view class="guide-content">
|
|
|
|
|
- <view class="guide-header">
|
|
|
|
|
- <text class="guide-step">步骤 {{ guideStep }} / {{ guideSteps.length }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="guide-body">
|
|
|
|
|
- <text class="guide-title">{{ guideSteps[guideStep - 1]?.title }}</text>
|
|
|
|
|
- <text class="guide-desc">{{ guideSteps[guideStep - 1]?.desc }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="guide-dots">
|
|
|
|
|
- <view
|
|
|
|
|
- v-for="i in guideSteps.length"
|
|
|
|
|
- :key="i"
|
|
|
|
|
- class="guide-dot"
|
|
|
|
|
- :class="{ active: i === guideStep }"
|
|
|
|
|
- />
|
|
|
|
|
- </view>
|
|
|
|
|
- <view class="guide-actions">
|
|
|
|
|
- <button v-if="guideStep > 1" class="guide-btn skip" @click="closeGuide">
|
|
|
|
|
- <text>跳过</text>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button v-if="guideStep < guideSteps.length" class="guide-btn next" @click="nextGuideStep">
|
|
|
|
|
- <text>下一步</text>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button v-else class="guide-btn finish" @click="closeGuide">
|
|
|
|
|
- <text>开始使用</text>
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <!-- 参数面板 -->
|
|
|
|
|
+ <view v-if="showParamsPanel" class="bottom-panel-overlay" @click="showParamsPanel = false">
|
|
|
|
|
+ <view class="bottom-panel" @click.stop>
|
|
|
|
|
+ <view class="panel-header">
|
|
|
|
|
+ <text class="panel-title">参数调节</text>
|
|
|
|
|
+ <text class="panel-close" @click="showParamsPanel = false">✕</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <scroll-view scroll-y class="panel-body">
|
|
|
|
|
+ <view class="param-item">
|
|
|
|
|
+ <view class="param-header">
|
|
|
|
|
+ <text class="param-label">语速</text>
|
|
|
|
|
+ <text class="param-value">{{ voiceParams.speed.toFixed(1) }}x</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <slider
|
|
|
|
|
+ :value="(voiceParams.speed - 0.5) * 100 / 1.5"
|
|
|
|
|
+ :min="0" :max="100"
|
|
|
|
|
+ @changing="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
|
|
|
|
|
+ activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
|
|
|
|
|
+ />
|
|
|
|
|
+ <view class="param-range"><text>0.5x</text><text>2.0x</text></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="param-item">
|
|
|
|
|
+ <view class="param-header">
|
|
|
|
|
+ <text class="param-label">音调</text>
|
|
|
|
|
+ <text class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <slider
|
|
|
|
|
+ :value="(voiceParams.pitch + 500) / 10"
|
|
|
|
|
+ :min="0" :max="100"
|
|
|
|
|
+ @changing="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
|
|
|
|
|
+ activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
|
|
|
|
|
+ />
|
|
|
|
|
+ <view class="param-range"><text>-500</text><text>+500</text></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="param-item">
|
|
|
|
|
+ <view class="param-header">
|
|
|
|
|
+ <text class="param-label">音量</text>
|
|
|
|
|
+ <text class="param-value">{{ voiceParams.volume }}%</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <slider
|
|
|
|
|
+ :value="voiceParams.volume"
|
|
|
|
|
+ :min="0" :max="100"
|
|
|
|
|
+ @changing="(e: any) => voiceParams.volume = e.detail.value"
|
|
|
|
|
+ activeColor="#4F46E5" backgroundColor="#e5e7eb" block-size="20"
|
|
|
|
|
+ />
|
|
|
|
|
+ <view class="param-range"><text>0%</text><text>100%</text></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -228,29 +186,23 @@
|
|
|
<text class="quota-title">今日免费额度已用完</text>
|
|
<text class="quota-title">今日免费额度已用完</text>
|
|
|
<text class="quota-desc">开通会员解锁更多生成次数</text>
|
|
<text class="quota-desc">开通会员解锁更多生成次数</text>
|
|
|
<view class="quota-actions">
|
|
<view class="quota-actions">
|
|
|
- <button class="quota-btn primary" @click="goToMember">
|
|
|
|
|
- <text>开通会员</text>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button class="quota-btn secondary" @click="showQuotaModal = false">
|
|
|
|
|
- <text>稍后再说</text>
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <button class="quota-btn primary" @click="goToMember"><text>开通会员</text></button>
|
|
|
|
|
+ <button class="quota-btn secondary" @click="showQuotaModal = false"><text>稍后再说</text></button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 专辑列表面板 -->
|
|
<!-- 专辑列表面板 -->
|
|
|
- <view v-if="showAlbumListPanel" class="album-panel" @click="showAlbumListPanel = false">
|
|
|
|
|
- <view class="album-content" @click.stop>
|
|
|
|
|
- <view class="album-header">
|
|
|
|
|
- <text class="album-title">选择专辑</text>
|
|
|
|
|
- <text class="album-close" @click="showAlbumListPanel = false">✕</text>
|
|
|
|
|
|
|
+ <view v-if="showAlbumListPanel" class="bottom-panel-overlay" @click="showAlbumListPanel = false">
|
|
|
|
|
+ <view class="bottom-panel" @click.stop>
|
|
|
|
|
+ <view class="panel-header">
|
|
|
|
|
+ <text class="panel-title">选择专辑</text>
|
|
|
|
|
+ <text class="panel-close" @click="showAlbumListPanel = false">✕</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <scroll-view scroll-y class="album-list" :scroll-top="scrollTop">
|
|
|
|
|
|
|
+ <scroll-view scroll-y class="panel-body">
|
|
|
<view
|
|
<view
|
|
|
- v-for="album in albums"
|
|
|
|
|
- :key="album.id"
|
|
|
|
|
- class="album-item"
|
|
|
|
|
- :class="{ active: selectedAlbum?.id === album.id }"
|
|
|
|
|
|
|
+ v-for="album in albums" :key="album.id"
|
|
|
|
|
+ class="album-item" :class="{ active: selectedAlbum?.id === album.id }"
|
|
|
@click="selectAlbum(album)"
|
|
@click="selectAlbum(album)"
|
|
|
>
|
|
>
|
|
|
<view class="album-item-content">
|
|
<view class="album-item-content">
|
|
@@ -259,46 +211,34 @@
|
|
|
</view>
|
|
</view>
|
|
|
<text v-if="selectedAlbum?.id === album.id" class="album-check">✓</text>
|
|
<text v-if="selectedAlbum?.id === album.id" class="album-check">✓</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view v-if="albums.length === 0" class="album-empty">
|
|
|
|
|
- <text>暂无专辑</text>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <view v-if="albums.length === 0" class="album-empty"><text>暂无专辑</text></view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
- <button class="album-create-btn" @click="showAlbumListPanel = false; showAlbumPanel = true">
|
|
|
|
|
|
|
+ <button class="panel-action-btn" @click="showAlbumListPanel = false; showAlbumPanel = true">
|
|
|
<text>+ 新建专辑</text>
|
|
<text>+ 新建专辑</text>
|
|
|
</button>
|
|
</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 新建专辑面板 -->
|
|
<!-- 新建专辑面板 -->
|
|
|
- <view v-if="showAlbumPanel" class="album-panel" @click="showAlbumPanel = false">
|
|
|
|
|
- <view class="album-content" @click.stop>
|
|
|
|
|
- <view class="album-header">
|
|
|
|
|
- <text class="album-title">新建专辑</text>
|
|
|
|
|
- <text class="album-close" @click="showAlbumPanel = false">✕</text>
|
|
|
|
|
|
|
+ <view v-if="showAlbumPanel" class="bottom-panel-overlay" @click="showAlbumPanel = false">
|
|
|
|
|
+ <view class="bottom-panel" @click.stop>
|
|
|
|
|
+ <view class="panel-header">
|
|
|
|
|
+ <text class="panel-title">新建专辑</text>
|
|
|
|
|
+ <text class="panel-close" @click="showAlbumPanel = false">✕</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="album-form">
|
|
|
|
|
|
|
+ <view class="panel-body">
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
<text class="form-label">专辑名称 *</text>
|
|
<text class="form-label">专辑名称 *</text>
|
|
|
- <input
|
|
|
|
|
- v-model="newAlbumTitle"
|
|
|
|
|
- class="form-input"
|
|
|
|
|
- placeholder="例如:我的有声书"
|
|
|
|
|
- maxlength="50"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <input v-model="newAlbumTitle" class="form-input" placeholder="例如:我的有声书" maxlength="50" />
|
|
|
</view>
|
|
</view>
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
<text class="form-label">描述(可选)</text>
|
|
<text class="form-label">描述(可选)</text>
|
|
|
- <textarea
|
|
|
|
|
- v-model="newAlbumDescription"
|
|
|
|
|
- class="form-textarea"
|
|
|
|
|
- placeholder="简单描述这个专辑..."
|
|
|
|
|
- maxlength="200"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <textarea v-model="newAlbumDescription" class="form-textarea" placeholder="简单描述这个专辑..." maxlength="200" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <button class="panel-action-btn" :disabled="!newAlbumTitle.trim()" @click="createAlbum">
|
|
|
|
|
+ <text>创建专辑</text>
|
|
|
|
|
+ </button>
|
|
|
</view>
|
|
</view>
|
|
|
- <button class="album-submit-btn" :disabled="!newAlbumTitle.trim()" @click="createAlbum">
|
|
|
|
|
- <text>创建专辑</text>
|
|
|
|
|
- </button>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -315,40 +255,30 @@ import type { VoiceParams } from '../../types';
|
|
|
const userStore = useUserStore();
|
|
const userStore = useUserStore();
|
|
|
const audioStore = useAudioStore();
|
|
const audioStore = useAudioStore();
|
|
|
|
|
|
|
|
-// 状态
|
|
|
|
|
const text = ref('');
|
|
const text = ref('');
|
|
|
const selectedVoice = ref('voice_01');
|
|
const selectedVoice = ref('voice_01');
|
|
|
-const voiceParams = ref<VoiceParams>({
|
|
|
|
|
- speed: 1.0,
|
|
|
|
|
- pitch: 0,
|
|
|
|
|
- volume: 50,
|
|
|
|
|
-});
|
|
|
|
|
|
|
+const voiceParams = ref<VoiceParams>({ speed: 1.0, pitch: 0, volume: 50 });
|
|
|
const generating = ref(false);
|
|
const generating = ref(false);
|
|
|
-const btnPressed = ref(false);
|
|
|
|
|
const previewingVoice = ref<string | null>(null);
|
|
const previewingVoice = ref<string | null>(null);
|
|
|
let previewAudio: HTMLAudioElement | null = null;
|
|
let previewAudio: HTMLAudioElement | null = null;
|
|
|
|
|
|
|
|
-// 文档上传状态
|
|
|
|
|
-interface UploadedDoc {
|
|
|
|
|
- fileName: string;
|
|
|
|
|
- wordCount: number;
|
|
|
|
|
- fileSize: number;
|
|
|
|
|
- format: string;
|
|
|
|
|
- truncated: boolean;
|
|
|
|
|
-}
|
|
|
|
|
-const uploadedDoc = ref<UploadedDoc>({
|
|
|
|
|
- fileName: '',
|
|
|
|
|
- wordCount: 0,
|
|
|
|
|
- fileSize: 0,
|
|
|
|
|
- format: '',
|
|
|
|
|
- truncated: false,
|
|
|
|
|
-});
|
|
|
|
|
|
|
+// 文档上传
|
|
|
|
|
+interface UploadedDoc { fileName: string; wordCount: number; fileSize: number; format: string; truncated: boolean; }
|
|
|
|
|
+const uploadedDoc = ref<UploadedDoc>({ fileName: '', wordCount: 0, fileSize: 0, format: '', truncated: false });
|
|
|
const uploading = ref(false);
|
|
const uploading = ref(false);
|
|
|
|
|
|
|
|
-// scroll-top 用于避免 scrollTop 错误
|
|
|
|
|
-const scrollTop = ref(0);
|
|
|
|
|
|
|
+// 面板状态
|
|
|
|
|
+const showVoicePanel = ref(false);
|
|
|
|
|
+const showParamsPanel = ref(false);
|
|
|
|
|
+const voiceCategory = ref('all');
|
|
|
|
|
+const voiceCategories = [
|
|
|
|
|
+ { key: 'all', label: '全部' },
|
|
|
|
|
+ { key: 'hot', label: '热门' },
|
|
|
|
|
+ { key: 'female', label: '女声' },
|
|
|
|
|
+ { key: 'male', label: '男声' },
|
|
|
|
|
+];
|
|
|
|
|
|
|
|
-// 专辑相关状态
|
|
|
|
|
|
|
+// 专辑
|
|
|
const selectedAlbum = ref<{ id: string; title: string } | null>(null);
|
|
const selectedAlbum = ref<{ id: string; title: string } | null>(null);
|
|
|
const albums = ref<any[]>([]);
|
|
const albums = ref<any[]>([]);
|
|
|
const showAlbumListPanel = ref(false);
|
|
const showAlbumListPanel = ref(false);
|
|
@@ -356,1288 +286,318 @@ const showAlbumPanel = ref(false);
|
|
|
const newAlbumTitle = ref('');
|
|
const newAlbumTitle = ref('');
|
|
|
const newAlbumDescription = ref('');
|
|
const newAlbumDescription = ref('');
|
|
|
|
|
|
|
|
-// 新手引导状态
|
|
|
|
|
-const showGuide = ref(false); // 已禁用引导功能
|
|
|
|
|
-const guideStep = ref(1);
|
|
|
|
|
-const guideSteps = [
|
|
|
|
|
- { step: 1, title: '输入文本', desc: '在文本框中输入要转换的文本内容', target: '.input-card' },
|
|
|
|
|
- { step: 2, title: '选择音色', desc: '选择你喜欢的声音音色', target: '.voice-card' },
|
|
|
|
|
- { step: 3, title: '调整参数', desc: '可以调整语速、音调、音量', target: '.params-card' },
|
|
|
|
|
- { step: 4, title: '生成音频', desc: '点击按钮开始生成', target: '.generate-btn' },
|
|
|
|
|
-];
|
|
|
|
|
-
|
|
|
|
|
-// 额度用尽弹窗
|
|
|
|
|
const showQuotaModal = ref(false);
|
|
const showQuotaModal = ref(false);
|
|
|
|
|
|
|
|
-// 计算属性
|
|
|
|
|
-const canGenerate = computed(() => {
|
|
|
|
|
- return text.value.trim().length > 0 && !generating.value;
|
|
|
|
|
|
|
+const canGenerate = computed(() => text.value.trim().length > 0 && !!selectedAlbum.value && !generating.value);
|
|
|
|
|
+const currentVoiceName = computed(() => {
|
|
|
|
|
+ const v = audioStore.voices.find(item => item.id === selectedVoice.value);
|
|
|
|
|
+ return v ? v.name : '';
|
|
|
|
|
+});
|
|
|
|
|
+const filteredVoices = computed(() => {
|
|
|
|
|
+ if (voiceCategory.value === 'all') return audioStore.voices;
|
|
|
|
|
+ if (voiceCategory.value === 'female') return audioStore.voices.filter(v => v.gender === 'female');
|
|
|
|
|
+ if (voiceCategory.value === 'male') return audioStore.voices.filter(v => v.gender === 'male');
|
|
|
|
|
+ // hot: return all for now (could be flagged by backend)
|
|
|
|
|
+ return audioStore.voices;
|
|
|
});
|
|
});
|
|
|
|
|
+function selectVoiceAndClose(id: string) { selectedVoice.value = id; showVoicePanel.value = false; }
|
|
|
|
|
|
|
|
-// 专辑相关函数
|
|
|
|
|
async function fetchAlbums() {
|
|
async function fetchAlbums() {
|
|
|
try {
|
|
try {
|
|
|
const result = await get<{ albums: any[] }>('/book-generator/books');
|
|
const result = await get<{ albums: any[] }>('/book-generator/books');
|
|
|
albums.value = result.albums || [];
|
|
albums.value = result.albums || [];
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('获取专辑列表失败:', error);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (error) { console.error('获取专辑列表失败:', error); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function selectAlbum(album: any) {
|
|
|
|
|
- selectedAlbum.value = { id: album.id, title: album.title };
|
|
|
|
|
- showAlbumListPanel.value = false;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function selectAlbum(album: any) { selectedAlbum.value = { id: album.id, title: album.title }; showAlbumListPanel.value = false; }
|
|
|
|
|
|
|
|
async function createAlbum() {
|
|
async function createAlbum() {
|
|
|
if (!newAlbumTitle.value.trim()) return;
|
|
if (!newAlbumTitle.value.trim()) return;
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- const result = await post<{ id: number | string; title: string; description?: string }>('/book-generator/books', {
|
|
|
|
|
- title: newAlbumTitle.value.trim(),
|
|
|
|
|
- description: newAlbumDescription.value.trim(),
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const result = await post<{ id: number | string; title: string }>('/book-generator/books', { title: newAlbumTitle.value.trim(), description: newAlbumDescription.value.trim() });
|
|
|
if (result && result.id) {
|
|
if (result && result.id) {
|
|
|
selectedAlbum.value = { id: String(result.id), title: result.title };
|
|
selectedAlbum.value = { id: String(result.id), title: result.title };
|
|
|
- await fetchAlbums(); // 刷新列表
|
|
|
|
|
|
|
+ await fetchAlbums();
|
|
|
showAlbumPanel.value = false;
|
|
showAlbumPanel.value = false;
|
|
|
newAlbumTitle.value = '';
|
|
newAlbumTitle.value = '';
|
|
|
newAlbumDescription.value = '';
|
|
newAlbumDescription.value = '';
|
|
|
uni.showToast({ title: '专辑创建成功', icon: 'success' });
|
|
uni.showToast({ title: '专辑创建成功', icon: 'success' });
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('创建专辑失败:', error);
|
|
|
|
|
- uni.showToast({ title: '创建失败', icon: 'none' });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (error) { uni.showToast({ title: '创建失败', icon: 'none' }); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 加载用户偏好设置
|
|
|
|
|
async function loadUserPreferences() {
|
|
async function loadUserPreferences() {
|
|
|
try {
|
|
try {
|
|
|
- // 先尝试从本地加载
|
|
|
|
|
const localPrefs = uni.getStorageSync('userPreferences');
|
|
const localPrefs = uni.getStorageSync('userPreferences');
|
|
|
if (localPrefs) {
|
|
if (localPrefs) {
|
|
|
- if (localPrefs.defaultVoiceId) {
|
|
|
|
|
- selectedVoice.value = localPrefs.defaultVoiceId;
|
|
|
|
|
- }
|
|
|
|
|
- if (localPrefs.defaultVolume !== undefined) {
|
|
|
|
|
- voiceParams.value.volume = localPrefs.defaultVolume;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (localPrefs.defaultVoiceId) selectedVoice.value = localPrefs.defaultVoiceId;
|
|
|
|
|
+ if (localPrefs.defaultVolume !== undefined) voiceParams.value.volume = localPrefs.defaultVolume;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 从服务器加载
|
|
|
|
|
const result = await get<any>('/user/preferences');
|
|
const result = await get<any>('/user/preferences');
|
|
|
if (result) {
|
|
if (result) {
|
|
|
- if (result.defaultVoiceId) {
|
|
|
|
|
- selectedVoice.value = result.defaultVoiceId;
|
|
|
|
|
- }
|
|
|
|
|
- if (result.defaultVolume !== undefined) {
|
|
|
|
|
- voiceParams.value.volume = result.defaultVolume;
|
|
|
|
|
- }
|
|
|
|
|
- // 保存到本地
|
|
|
|
|
|
|
+ if (result.defaultVoiceId) selectedVoice.value = result.defaultVoiceId;
|
|
|
|
|
+ if (result.defaultVolume !== undefined) voiceParams.value.volume = result.defaultVolume;
|
|
|
uni.setStorageSync('userPreferences', result);
|
|
uni.setStorageSync('userPreferences', result);
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('加载用户偏好设置失败:', error);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (error) { /* ignore */ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 初始化
|
|
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await audioStore.fetchVoices();
|
|
await audioStore.fetchVoices();
|
|
|
- await fetchAlbums(); // 获取专辑列表
|
|
|
|
|
- await loadUserPreferences(); // 加载用户偏好设置
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否显示新手引导(已禁用)
|
|
|
|
|
- // const hasSeenGuide = uni.getStorageSync('hasSeenCreateGuide');
|
|
|
|
|
- // if (!hasSeenGuide) {
|
|
|
|
|
- // setTimeout(() => {
|
|
|
|
|
- // showGuide.value = true;
|
|
|
|
|
- // }, 500);
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+ await fetchAlbums();
|
|
|
|
|
+ await loadUserPreferences();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 跳转到视频生成页面
|
|
|
|
|
-function goToVideoGenerator() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/video-generator/index' });
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 跳转到 AI 生成内容页面
|
|
|
|
|
-function goToAIGenerate() {
|
|
|
|
|
- uni.navigateTo({ url: '/pages/ai-generate/index' });
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function goToAIGenerate() { uni.navigateTo({ url: '/pages/ai-generate/index' }); }
|
|
|
|
|
|
|
|
-// 页面显示时检查是否有 AI 生成的文本
|
|
|
|
|
onShow(() => {
|
|
onShow(() => {
|
|
|
const aiText = uni.getStorageSync('ai_generated_text');
|
|
const aiText = uni.getStorageSync('ai_generated_text');
|
|
|
- if (aiText) {
|
|
|
|
|
- text.value = aiText;
|
|
|
|
|
- uni.removeStorageSync('ai_generated_text');
|
|
|
|
|
- uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (aiText) { text.value = aiText; uni.removeStorageSync('ai_generated_text'); uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' }); }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-// 清空文本
|
|
|
|
|
-function clearText() {
|
|
|
|
|
- text.value = '';
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 粘贴文本
|
|
|
|
|
-async function pasteText() {
|
|
|
|
|
- const content = await uni.getClipboardData();
|
|
|
|
|
- if (content.data) {
|
|
|
|
|
- text.value = content.data;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function clearText() { text.value = ''; }
|
|
|
|
|
+async function pasteText() { const content = await uni.getClipboardData(); if (content.data) text.value = content.data; }
|
|
|
|
|
|
|
|
-// 上传文档
|
|
|
|
|
|
|
+// 上传文档(保持原逻辑)
|
|
|
async function handleUploadDoc() {
|
|
async function handleUploadDoc() {
|
|
|
if (uploading.value) return;
|
|
if (uploading.value) return;
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 使用 uni.chooseFile 选择文件(H5和App端支持)
|
|
|
|
|
// #ifdef H5
|
|
// #ifdef H5
|
|
|
- const input = document.createElement('input');
|
|
|
|
|
- input.type = 'file';
|
|
|
|
|
- input.accept = '.txt,.docx,.pdf';
|
|
|
|
|
- input.onchange = async (e: any) => {
|
|
|
|
|
- const file = e.target.files?.[0];
|
|
|
|
|
- if (!file) return;
|
|
|
|
|
- await processUploadedFile(file);
|
|
|
|
|
- };
|
|
|
|
|
- input.click();
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ const input = document.createElement('input'); input.type = 'file'; input.accept = '.txt,.docx,.pdf';
|
|
|
|
|
+ input.onchange = async (e: any) => { const file = e.target.files?.[0]; if (!file) return; await processUploadedFile(file); };
|
|
|
|
|
+ input.click(); return;
|
|
|
// #endif
|
|
// #endif
|
|
|
-
|
|
|
|
|
// #ifdef APP-PLUS
|
|
// #ifdef APP-PLUS
|
|
|
- // App 端使用系统文件选择器
|
|
|
|
|
- const result = await (uni as any).chooseFile({
|
|
|
|
|
- extension: ['.txt', '.docx', '.pdf'],
|
|
|
|
|
- type: 'file',
|
|
|
|
|
- });
|
|
|
|
|
- if (result?.tempFiles?.[0]) {
|
|
|
|
|
- await processUploadedFile(result.tempFiles[0]);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const result = await (uni as any).chooseFile({ extension: ['.txt', '.docx', '.pdf'], type: 'file' });
|
|
|
|
|
+ if (result?.tempFiles?.[0]) await processUploadedFile(result.tempFiles[0]);
|
|
|
// #endif
|
|
// #endif
|
|
|
-
|
|
|
|
|
- // 小程序端暂不支持
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
// #ifdef MP-WEIXIN
|
|
|
uni.showToast({ title: '小程序暂不支持文档上传', icon: 'none' });
|
|
uni.showToast({ title: '小程序暂不支持文档上传', icon: 'none' });
|
|
|
// #endif
|
|
// #endif
|
|
|
- } catch (error: any) {
|
|
|
|
|
- console.error('选择文件失败:', error);
|
|
|
|
|
- if (error.message && !error.message.includes('cancel')) {
|
|
|
|
|
- uni.showToast({ title: '选择文件失败', icon: 'none' });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (error: any) { if (error.message && !error.message.includes('cancel')) uni.showToast({ title: '选择文件失败', icon: 'none' }); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 处理上传的文件
|
|
|
|
|
async function processUploadedFile(file: any) {
|
|
async function processUploadedFile(file: any) {
|
|
|
uploading.value = true;
|
|
uploading.value = true;
|
|
|
-
|
|
|
|
|
- // 检查文件大小(20MB)
|
|
|
|
|
- if (file.size && file.size > 20 * 1024 * 1024) {
|
|
|
|
|
- uni.showToast({ title: '文件过大,最大支持20MB', icon: 'none' });
|
|
|
|
|
- uploading.value = false;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 检查格式
|
|
|
|
|
|
|
+ if (file.size && file.size > 20 * 1024 * 1024) { uni.showToast({ title: '文件过大,最大支持20MB', icon: 'none' }); uploading.value = false; return; }
|
|
|
const name = (file.name || '').toLowerCase();
|
|
const name = (file.name || '').toLowerCase();
|
|
|
- if (!name.endsWith('.txt') && !name.endsWith('.docx') && !name.endsWith('.pdf')) {
|
|
|
|
|
- uni.showToast({ title: '不支持的格式,请上传 .txt .docx .pdf', icon: 'none' });
|
|
|
|
|
- uploading.value = false;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (!name.endsWith('.txt') && !name.endsWith('.docx') && !name.endsWith('.pdf')) { uni.showToast({ title: '不支持的格式', icon: 'none' }); uploading.value = false; return; }
|
|
|
uni.showLoading({ title: '解析文档中...', mask: true });
|
|
uni.showLoading({ title: '解析文档中...', mask: true });
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- const filePath = file.path || file.tempFilePath || file;
|
|
|
|
|
let result: any;
|
|
let result: any;
|
|
|
-
|
|
|
|
|
// #ifdef H5
|
|
// #ifdef H5
|
|
|
- // H5 端直接使用 FormData 上传
|
|
|
|
|
- const formData = new FormData();
|
|
|
|
|
- formData.append('file', file);
|
|
|
|
|
|
|
+ const formData = new FormData(); formData.append('file', file);
|
|
|
const baseUrl = (await import('../../utils/config')).getApiBaseUrl();
|
|
const baseUrl = (await import('../../utils/config')).getApiBaseUrl();
|
|
|
const token = uni.getStorageSync('token');
|
|
const token = uni.getStorageSync('token');
|
|
|
- const headers: Record<string, string> = {};
|
|
|
|
|
- if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
-
|
|
|
|
|
- const response = await fetch(baseUrl + '/tts/upload-document', {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- headers,
|
|
|
|
|
- body: formData,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const headers: Record<string, string> = {}; if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
+ const response = await fetch(baseUrl + '/tts/upload-document', { method: 'POST', headers, body: formData });
|
|
|
const json = await response.json();
|
|
const json = await response.json();
|
|
|
- if (json.code === 0) {
|
|
|
|
|
- result = json.data;
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error(json.message || '解析失败');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (json.code === 0) result = json.data; else throw new Error(json.message || '解析失败');
|
|
|
// #endif
|
|
// #endif
|
|
|
-
|
|
|
|
|
// #ifdef APP-PLUS
|
|
// #ifdef APP-PLUS
|
|
|
- result = await uploadFile('/tts/upload-document', filePath);
|
|
|
|
|
|
|
+ result = await uploadFile('/tts/upload-document', file.path || file.tempFilePath || file);
|
|
|
// #endif
|
|
// #endif
|
|
|
-
|
|
|
|
|
if (result && result.text) {
|
|
if (result && result.text) {
|
|
|
- // 更新上传状态
|
|
|
|
|
- uploadedDoc.value = {
|
|
|
|
|
- fileName: result.fileName || file.name,
|
|
|
|
|
- wordCount: result.wordCount || result.text.length,
|
|
|
|
|
- fileSize: result.fileSize || file.size || 0,
|
|
|
|
|
- format: result.format || '',
|
|
|
|
|
- truncated: result.truncated || false,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 截取前2000字填充到文本框
|
|
|
|
|
- const maxLen = 2000;
|
|
|
|
|
|
|
+ uploadedDoc.value = { fileName: result.fileName || file.name, wordCount: result.wordCount || result.text.length, fileSize: result.fileSize || file.size || 0, format: result.format || '', truncated: result.truncated || false };
|
|
|
let displayText = result.text;
|
|
let displayText = result.text;
|
|
|
- if (displayText.length > maxLen) {
|
|
|
|
|
- displayText = displayText.substring(0, maxLen);
|
|
|
|
|
- uploadedDoc.value.truncated = true;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (displayText.length > 2000) { displayText = displayText.substring(0, 2000); uploadedDoc.value.truncated = true; }
|
|
|
text.value = displayText;
|
|
text.value = displayText;
|
|
|
-
|
|
|
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
|
uni.showToast({ title: `解析成功,${uploadedDoc.value.wordCount}字`, icon: 'success' });
|
|
uni.showToast({ title: `解析成功,${uploadedDoc.value.wordCount}字`, icon: 'success' });
|
|
|
- } else {
|
|
|
|
|
- throw new Error('文档内容为空');
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error: any) {
|
|
|
|
|
- uni.hideLoading();
|
|
|
|
|
- console.error('文档上传失败:', error);
|
|
|
|
|
- uni.showToast({
|
|
|
|
|
- title: error.message || '文档解析失败',
|
|
|
|
|
- icon: 'none',
|
|
|
|
|
- duration: 3000,
|
|
|
|
|
- });
|
|
|
|
|
- } finally {
|
|
|
|
|
- uploading.value = false;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 清除已上传的文档
|
|
|
|
|
-function clearDoc() {
|
|
|
|
|
- uploadedDoc.value = {
|
|
|
|
|
- fileName: '',
|
|
|
|
|
- wordCount: 0,
|
|
|
|
|
- fileSize: 0,
|
|
|
|
|
- format: '',
|
|
|
|
|
- truncated: false,
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ } else { throw new Error('文档内容为空'); }
|
|
|
|
|
+ } catch (error: any) { uni.hideLoading(); uni.showToast({ title: error.message || '文档解析失败', icon: 'none', duration: 3000 }); }
|
|
|
|
|
+ finally { uploading.value = false; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 格式化文件大小
|
|
|
|
|
-function formatFileSize(bytes: number): string {
|
|
|
|
|
- if (!bytes || bytes <= 0) return '0 B';
|
|
|
|
|
- if (bytes < 1024) return bytes + ' B';
|
|
|
|
|
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
|
|
|
- return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function clearDoc() { uploadedDoc.value = { fileName: '', wordCount: 0, fileSize: 0, format: '', truncated: false }; }
|
|
|
|
|
|
|
|
-// 试听音色
|
|
|
|
|
async function previewVoice(voiceId: string) {
|
|
async function previewVoice(voiceId: string) {
|
|
|
- // 如果正在试听当前音色,停止试听
|
|
|
|
|
- if (previewingVoice.value === voiceId) {
|
|
|
|
|
- stopPreview();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 停止之前的试听
|
|
|
|
|
|
|
+ if (previewingVoice.value === voiceId) { stopPreview(); return; }
|
|
|
stopPreview();
|
|
stopPreview();
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- previewingVoice.value = voiceId;
|
|
|
|
|
- uni.showLoading({ title: '生成试听音频...' });
|
|
|
|
|
-
|
|
|
|
|
- // 调用API生成预览音频
|
|
|
|
|
- const res = await post<{ previewText: string; voiceId: string; audioUrl: string }>('/tts/preview', {
|
|
|
|
|
- voiceId,
|
|
|
|
|
- voiceParams: voiceParams.value,
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
|
|
+ previewingVoice.value = voiceId; uni.showLoading({ title: '生成试听音频...' });
|
|
|
|
|
+ const res = await post<{ audioUrl: string }>('/tts/preview', { voiceId, voiceParams: voiceParams.value });
|
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
|
-
|
|
|
|
|
if (res.audioUrl) {
|
|
if (res.audioUrl) {
|
|
|
// #ifdef H5
|
|
// #ifdef H5
|
|
|
- previewAudio = new Audio(res.audioUrl);
|
|
|
|
|
- previewAudio.play();
|
|
|
|
|
- previewAudio.onended = () => {
|
|
|
|
|
- previewingVoice.value = null;
|
|
|
|
|
- previewAudio = null;
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ previewAudio = new Audio(res.audioUrl); previewAudio.play();
|
|
|
|
|
+ previewAudio.onended = () => { previewingVoice.value = null; previewAudio = null; };
|
|
|
// #endif
|
|
// #endif
|
|
|
-
|
|
|
|
|
// #ifndef H5
|
|
// #ifndef H5
|
|
|
- // 非H5环境直接停止
|
|
|
|
|
- uni.showToast({ title: '试听已播放', icon: 'success' });
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- previewingVoice.value = null;
|
|
|
|
|
- }, 2000);
|
|
|
|
|
|
|
+ uni.showToast({ title: '试听已播放', icon: 'success' }); setTimeout(() => { previewingVoice.value = null; }, 2000);
|
|
|
// #endif
|
|
// #endif
|
|
|
}
|
|
}
|
|
|
- } catch (error: any) {
|
|
|
|
|
- uni.hideLoading();
|
|
|
|
|
- uni.showToast({ title: '试听生成失败', icon: 'none' });
|
|
|
|
|
- previewingVoice.value = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ } catch (error: any) { uni.hideLoading(); uni.showToast({ title: '试听生成失败', icon: 'none' }); previewingVoice.value = null; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 停止试听
|
|
|
|
|
-function stopPreview() {
|
|
|
|
|
- if (previewAudio) {
|
|
|
|
|
- previewAudio.pause();
|
|
|
|
|
- previewAudio = null;
|
|
|
|
|
- }
|
|
|
|
|
- previewingVoice.value = null;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function stopPreview() { if (previewAudio) { previewAudio.pause(); previewAudio = null; } previewingVoice.value = null; }
|
|
|
|
|
|
|
|
-// 生成音频
|
|
|
|
|
async function handleGenerate() {
|
|
async function handleGenerate() {
|
|
|
if (!canGenerate.value) return;
|
|
if (!canGenerate.value) return;
|
|
|
-
|
|
|
|
|
- // 检查额度(无限额度 -1 跳过检查)
|
|
|
|
|
const quota = userStore.memberStatus?.quota;
|
|
const quota = userStore.memberStatus?.quota;
|
|
|
- const quotaExhausted = quota && quota.dailyRemaining !== -1 && quota.dailyRemaining <= 0;
|
|
|
|
|
- if (quotaExhausted) {
|
|
|
|
|
- showQuotaModal.value = true;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 如果没有选择专辑,使用后端自动创建的默认书籍
|
|
|
|
|
|
|
+ if (quota && quota.dailyRemaining !== -1 && quota.dailyRemaining <= 0) { showQuotaModal.value = true; return; }
|
|
|
const bookId = selectedAlbum.value?.id;
|
|
const bookId = selectedAlbum.value?.id;
|
|
|
-
|
|
|
|
|
generating.value = true;
|
|
generating.value = true;
|
|
|
try {
|
|
try {
|
|
|
- const result = await audioStore.generateAudio(
|
|
|
|
|
- text.value,
|
|
|
|
|
- selectedVoice.value,
|
|
|
|
|
- voiceParams.value,
|
|
|
|
|
- { bookId }
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // 清空文本,方便下次输入
|
|
|
|
|
|
|
+ const result = await audioStore.generateAudio(text.value, selectedVoice.value, voiceParams.value, { bookId });
|
|
|
text.value = '';
|
|
text.value = '';
|
|
|
-
|
|
|
|
|
- // 和书籍创建一样,跳转到详情页等待音频生成
|
|
|
|
|
uni.showToast({ title: '任务已提交', icon: 'success' });
|
|
uni.showToast({ title: '任务已提交', icon: 'success' });
|
|
|
- setTimeout(() => {
|
|
|
|
|
- uni.navigateTo({
|
|
|
|
|
- url: `/pages/book-generator/detail?id=${result.bookId}`
|
|
|
|
|
- });
|
|
|
|
|
- }, 800);
|
|
|
|
|
|
|
+ setTimeout(() => { uni.navigateTo({ url: `/pages/book-generator/detail?id=${result.bookId}` }); }, 800);
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
- console.error('生成失败:', error);
|
|
|
|
|
-
|
|
|
|
|
- if (error.message?.includes('额度') || error.message?.includes('quota')) {
|
|
|
|
|
- showQuotaModal.value = true;
|
|
|
|
|
- } else {
|
|
|
|
|
- uni.showToast({
|
|
|
|
|
- title: error.message || '提交失败,请重试',
|
|
|
|
|
- icon: 'none'
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- } finally {
|
|
|
|
|
- generating.value = false;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 新手引导 - 下一步
|
|
|
|
|
-function nextGuideStep() {
|
|
|
|
|
- if (guideStep.value < guideSteps.length) {
|
|
|
|
|
- guideStep.value++;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (error.message?.includes('额度') || error.message?.includes('quota')) showQuotaModal.value = true;
|
|
|
|
|
+ else uni.showToast({ title: error.message || '提交失败,请重试', icon: 'none' });
|
|
|
|
|
+ } finally { generating.value = false; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 新手引导 - 关闭
|
|
|
|
|
-function closeGuide() {
|
|
|
|
|
- showGuide.value = false;
|
|
|
|
|
- uni.setStorageSync('hasSeenCreateGuide', true);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 跳转到会员页
|
|
|
|
|
-function goToMember() {
|
|
|
|
|
- showQuotaModal.value = false;
|
|
|
|
|
- uni.navigateTo({ url: '/pages/member/index' });
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function goToMember() { showQuotaModal.value = false; uni.navigateTo({ url: '/pages/member/index' }); }
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.page {
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- background: #f5f5f5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.nav-bar {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
|
|
|
- z-index: 100;
|
|
|
|
|
- padding-top: 44rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.nav-content {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- padding: 0 32rpx;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.page-title {
|
|
|
|
|
- font-size: 34rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.main-content {
|
|
|
|
|
- padding: 132rpx 32rpx 220rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.card {
|
|
|
|
|
- background: #ffffff;
|
|
|
|
|
- border-radius: 24rpx;
|
|
|
|
|
- padding: 32rpx;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.card-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.card-title {
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 文档上传卡片 */
|
|
|
|
|
-.doc-upload-card {
|
|
|
|
|
- border-left: 6rpx solid #4F46E5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-upload-area {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- padding: 40rpx 20rpx;
|
|
|
|
|
- border: 2rpx dashed #d1d5db;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- transition: all 0.2s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-upload-area:active {
|
|
|
|
|
- background: #eef2ff;
|
|
|
|
|
- border-color: #4F46E5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-upload-icon {
|
|
|
|
|
- font-size: 64rpx;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-upload-text {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: #4F46E5;
|
|
|
|
|
- margin-bottom: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-upload-hint {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.page { min-height: 100vh; background: #f5f5f5; display: flex; flex-direction: column; }
|
|
|
|
|
|
|
|
-.upload-status {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.upload-status.uploading {
|
|
|
|
|
- color: #f59e0b;
|
|
|
|
|
- animation: pulse 1.5s ease-in-out infinite;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+/* 主内容区 */
|
|
|
|
|
+.main-content { flex: 1; padding: 16rpx 24rpx 0; overflow-y: auto; }
|
|
|
|
|
+.input-area { background: #ffffff; border-radius: 16rpx; padding: 24rpx; }
|
|
|
|
|
|
|
|
-@keyframes pulse {
|
|
|
|
|
- 0%, 100% { opacity: 1; }
|
|
|
|
|
- 50% { opacity: 0.5; }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 文档信息栏 */
|
|
|
|
|
-.doc-info-bar {
|
|
|
|
|
|
|
+/* 专辑选择器(页面内下拉) */
|
|
|
|
|
+.album-selector {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
|
|
- padding: 20rpx;
|
|
|
|
|
- background: #f0fdf4;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- border: 2rpx solid #86efac;
|
|
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ border: 2rpx solid #e5e7eb;
|
|
|
|
|
+ margin-top: 20rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.doc-info-left {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 16rpx;
|
|
|
|
|
|
|
+.album-selector:active { opacity: 0.7; }
|
|
|
|
|
+.album-selector-label { font-size: 26rpx; color: #6b7280; flex-shrink: 0; }
|
|
|
|
|
+.album-selector-value {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
- min-width: 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-format-badge {
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
- background: linear-gradient(135deg, #4F46E5, #7C3AED);
|
|
|
|
|
- padding: 6rpx 14rpx;
|
|
|
|
|
- border-radius: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-info-text {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 4rpx;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-filename {
|
|
|
|
|
font-size: 26rpx;
|
|
font-size: 26rpx;
|
|
|
- font-weight: 600;
|
|
|
|
|
color: #1f2937;
|
|
color: #1f2937;
|
|
|
|
|
+ font-weight: 500;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.doc-meta {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-truncated-hint {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- color: #f59e0b;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-remove-btn {
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- width: 48rpx;
|
|
|
|
|
- height: 48rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- color: #ef4444;
|
|
|
|
|
- background: #fef2f2;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.doc-remove-btn:active {
|
|
|
|
|
- background: #fca5a5;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 上传中提示 */
|
|
|
|
|
-.doc-uploading-bar {
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- background: #fefce8;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: #a16207;
|
|
|
|
|
- margin-top: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.word-count {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.word-count.warning {
|
|
|
|
|
- color: #f59e0b;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.word-count.error {
|
|
|
|
|
- color: #ef4444;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.text-input {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 300rpx;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: flex-end;
|
|
|
|
|
- gap: 16rpx;
|
|
|
|
|
- margin-top: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-hint {
|
|
|
|
|
- margin-top: 12rpx;
|
|
|
|
|
- text-align: right;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.input-hint text {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.action-btn {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- background: #f3f4f6;
|
|
|
|
|
- padding: 12rpx 24rpx;
|
|
|
|
|
- border-radius: 12rpx;
|
|
|
|
|
- border: none;
|
|
|
|
|
- transition: all 0.2s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.action-btn:active {
|
|
|
|
|
- opacity: 0.7;
|
|
|
|
|
- transform: scale(0.98);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.action-btn::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-list {
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- margin-top: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 音色网格布局 - 2列 */
|
|
|
|
|
-.voice-grid {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
- gap: 16rpx;
|
|
|
|
|
- margin-top: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-item {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 24rpx 16rpx;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- border: 2rpx solid transparent;
|
|
|
|
|
- transition: all 0.3s;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-item.active {
|
|
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
|
|
|
|
- border-color: #4f46e5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-item.active .voice-name {
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-item:active {
|
|
|
|
|
- transform: scale(0.95);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-icon {
|
|
|
|
|
- font-size: 48rpx;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-name {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- margin-bottom: 4rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-preview-btn {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 8rpx;
|
|
|
|
|
- right: 8rpx;
|
|
|
|
|
- width: 40rpx;
|
|
|
|
|
- height: 40rpx;
|
|
|
|
|
- background: rgba(79, 70, 229, 0.9);
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-item.active .voice-preview-btn {
|
|
|
|
|
- background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
- color: #4f46e5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.voice-desc {
|
|
|
|
|
- font-size: 20rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-item {
|
|
|
|
|
- margin-top: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-label {
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-value {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #4F46E5;
|
|
|
|
|
- background: #EEF2FF;
|
|
|
|
|
- padding: 4rpx 16rpx;
|
|
|
|
|
- border-radius: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-range {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- margin-top: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.param-range text {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 专辑选择 */
|
|
|
|
|
-.album-card {
|
|
|
|
|
- background: #ffffff;
|
|
|
|
|
- border-radius: 24rpx;
|
|
|
|
|
- padding: 32rpx;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.card-header-row {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-add-btn {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #4f46e5;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-selector {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-name {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-placeholder {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-arrow {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 专辑面板 */
|
|
|
|
|
-.album-panel {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
- z-index: 1000;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: flex-end;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-content {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- max-height: 80vh;
|
|
|
|
|
- background: #ffffff;
|
|
|
|
|
- border-radius: 32rpx 32rpx 0 0;
|
|
|
|
|
- padding: 32rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-title {
|
|
|
|
|
- font-size: 34rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-close {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
- padding: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-list {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- max-height: 50vh;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-item {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- margin-bottom: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-item.active {
|
|
|
|
|
- background: #eef2ff;
|
|
|
|
|
- border: 2px solid #4f46e5;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-item-content {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-item-title {
|
|
|
|
|
- display: block;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- margin-bottom: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-item-desc {
|
|
|
|
|
- display: block;
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-check {
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- color: #4f46e5;
|
|
|
|
|
- margin-left: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-empty {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 60rpx 0;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-create-btn,
|
|
|
|
|
-.album-submit-btn {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100rpx;
|
|
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
|
|
- border-radius: 24rpx;
|
|
|
|
|
- border: none;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
- margin-top: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.album-create-btn::after,
|
|
|
|
|
-.album-submit-btn::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 专辑表单 */
|
|
|
|
|
-.album-form {
|
|
|
|
|
- margin: 24rpx 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.form-item {
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.form-label {
|
|
|
|
|
- display: block;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.form-input {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 0 24rpx;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.form-textarea {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- min-height: 160rpx;
|
|
|
|
|
- background: #f9fafb;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.generate-btn {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- height: 120rpx;
|
|
|
|
|
- padding: 16rpx 32rpx;
|
|
|
|
|
- padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
|
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
|
|
- border-radius: 0;
|
|
|
|
|
- border: none;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- box-shadow: 0 -4rpx 24rpx rgba(79, 70, 229, 0.3);
|
|
|
|
|
- transition: opacity 0.3s ease, transform 0.3s ease;
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- z-index: 1001;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.generate-btn::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.generate-btn.disabled {
|
|
|
|
|
- opacity: 0.5;
|
|
|
|
|
- background: #9ca3af;
|
|
|
|
|
- box-shadow: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.generate-btn.pressed {
|
|
|
|
|
- transform: scale(0.98);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 视频生成入口卡片 */
|
|
|
|
|
-.video-entry-card {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 30rpx;
|
|
|
|
|
- margin-top: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-content {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-icon {
|
|
|
|
|
- font-size: 60rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-text {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 6rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-title {
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-desc {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.video-entry-arrow {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.btn-text {
|
|
|
|
|
- font-size: 34rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
- letter-spacing: 2rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 音频生成入口卡片 */
|
|
|
|
|
-.audio-entry-card {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 30rpx;
|
|
|
|
|
- margin-top: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-content {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-icon {
|
|
|
|
|
- font-size: 60rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-text {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 6rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-title {
|
|
|
|
|
- font-size: 32rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-desc {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.audio-entry-arrow {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.ai-btn {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 新手引导 */
|
|
|
|
|
-.guide-overlay {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.8);
|
|
|
|
|
- z-index: 3000;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-content {
|
|
|
|
|
- width: 600rpx;
|
|
|
|
|
- background: #ffffff;
|
|
|
|
|
- border-radius: 32rpx;
|
|
|
|
|
- padding: 48rpx 40rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-header {
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-step {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #9ca3af;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-body {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-bottom: 32rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-title {
|
|
|
|
|
- font-size: 36rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- display: block;
|
|
|
|
|
- margin-bottom: 16rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-desc {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- display: block;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-dots {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12rpx;
|
|
|
|
|
- margin-bottom: 32rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-dot {
|
|
|
|
|
- width: 12rpx;
|
|
|
|
|
- height: 12rpx;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- background: #e5e7eb;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-dot.active {
|
|
|
|
|
- background: #4f46e5;
|
|
|
|
|
- width: 32rpx;
|
|
|
|
|
- border-radius: 6rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-btn {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
- border-radius: 44rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-btn::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-btn.skip {
|
|
|
|
|
- background: #f3f4f6;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-btn.next,
|
|
|
|
|
-.guide-btn.finish {
|
|
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/* 额度用尽弹窗 */
|
|
|
|
|
-.quota-modal {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- top: 0;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
- z-index: 3000;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-content {
|
|
|
|
|
- width: 600rpx;
|
|
|
|
|
- background: #ffffff;
|
|
|
|
|
- border-radius: 32rpx;
|
|
|
|
|
- padding: 60rpx 40rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-icon {
|
|
|
|
|
- font-size: 100rpx;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-title {
|
|
|
|
|
- font-size: 36rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #1f2937;
|
|
|
|
|
- margin-bottom: 12rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-desc {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- margin-bottom: 40rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-actions {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 20rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-btn {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 88rpx;
|
|
|
|
|
- border-radius: 44rpx;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-btn::after {
|
|
|
|
|
- border: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-btn.primary {
|
|
|
|
|
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.quota-btn.secondary {
|
|
|
|
|
- background: #f3f4f6;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.album-selector-value.placeholder { color: #9ca3af; font-weight: 400; }
|
|
|
|
|
+.album-selector-arrow { font-size: 20rpx; color: #9ca3af; flex-shrink: 0; }
|
|
|
|
|
+
|
|
|
|
|
+.text-input { width: 100%; min-height: 600rpx; font-size: 30rpx; color: #1f2937; line-height: 1.7; }
|
|
|
|
|
+.word-count { display: block; text-align: right; font-size: 22rpx; color: #9ca3af; margin-top: 8rpx; }
|
|
|
|
|
+.word-count.warning { color: #f59e0b; }
|
|
|
|
|
+.word-count.error { color: #ef4444; font-weight: 600; }
|
|
|
|
|
+
|
|
|
|
|
+/* 文档信息 */
|
|
|
|
|
+.doc-info-bar { display: flex; align-items: center; gap: 12rpx; padding: 12rpx 16rpx; background: #f0fdf4; border-radius: 10rpx; border: 1rpx solid #86efac; margin-top: 12rpx; }
|
|
|
|
|
+.doc-format-badge { flex-shrink: 0; font-size: 20rpx; font-weight: 700; color: #fff; background: #4f46e5; padding: 4rpx 10rpx; border-radius: 6rpx; }
|
|
|
|
|
+.doc-filename { flex: 1; font-size: 24rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
+.doc-meta { font-size: 20rpx; color: #6b7280; flex-shrink: 0; }
|
|
|
|
|
+.doc-truncated-hint { font-size: 18rpx; color: #f59e0b; flex-shrink: 0; }
|
|
|
|
|
+.doc-remove-btn { flex-shrink: 0; width: 36rpx; height: 36rpx; border-radius: 50%; background: #fef2f2; color: #ef4444; font-size: 24rpx; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+.doc-uploading-bar { padding: 16rpx; text-align: center; background: #fefce8; border-radius: 10rpx; font-size: 24rpx; color: #a16207; margin-top: 12rpx; }
|
|
|
|
|
+
|
|
|
|
|
+/* 工具按钮行 */
|
|
|
|
|
+.tool-row { display: flex; gap: 12rpx; margin-top: 16rpx; }
|
|
|
|
|
+.tool-btn { flex: 1; font-size: 24rpx; color: #6b7280; background: #f3f4f6; padding: 14rpx 0; border-radius: 10rpx; text-align: center; }
|
|
|
|
|
+.tool-btn:active { opacity: 0.7; }
|
|
|
|
|
+
|
|
|
|
|
+/* 底部固定区:Tab 工具栏风格 */
|
|
|
|
|
+.bottom-bar { background: #ffffff; box-shadow: 0 -2rpx 16rpx rgba(0,0,0,0.08); padding-bottom: calc(env(safe-area-inset-bottom) + 120rpx); }
|
|
|
|
|
+
|
|
|
|
|
+/* 主操作行:左侧快捷信息 + 右侧生成按钮 */
|
|
|
|
|
+.bottom-action-row { display: flex; align-items: center; gap: 16rpx; padding: 16rpx 24rpx; }
|
|
|
|
|
+.quick-info { flex: 1; min-width: 0; }
|
|
|
|
|
+.quick-info-text { font-size: 24rpx; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
|
|
|
|
+
|
|
|
|
|
+/* 生成按钮(紧凑 Tab 风格) */
|
|
|
|
|
+.generate-btn-tab { flex-shrink: 0; padding: 18rpx 36rpx; background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); border-radius: 40rpx; }
|
|
|
|
|
+.generate-btn-tab:active { opacity: 0.8; transform: scale(0.96); }
|
|
|
|
|
+.generate-btn-tab.disabled { opacity: 0.35; pointer-events: none; }
|
|
|
|
|
+.generate-btn-tab-text { font-size: 28rpx; font-weight: 700; color: #ffffff; white-space: nowrap; }
|
|
|
|
|
+
|
|
|
|
|
+/* 图标 Tab 栏(等宽分布,居中) */
|
|
|
|
|
+.tab-toolbar { display: flex; border-top: 1rpx solid #f3f4f6; padding: 10rpx 0 16rpx; }
|
|
|
|
|
+.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4rpx; padding: 6rpx 0; border-radius: 12rpx; transition: all 0.15s; }
|
|
|
|
|
+.tab-item:active { background: #f9fafb; }
|
|
|
|
|
+.tab-item.active .tab-icon { transform: scale(1.15); }
|
|
|
|
|
+.tab-item.active .tab-text { color: #4f46e5; font-weight: 600; }
|
|
|
|
|
+.tab-icon { font-size: 40rpx; line-height: 1; transition: transform 0.15s; }
|
|
|
|
|
+.tab-text { font-size: 22rpx; color: #9ca3af; transition: color 0.15s; }
|
|
|
|
|
+
|
|
|
|
|
+/* 底部面板 */
|
|
|
|
|
+.bottom-panel-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: flex-end; }
|
|
|
|
|
+.bottom-panel { width: 100%; max-height: 70vh; background: #ffffff; border-radius: 24rpx 24rpx 0 0; display: flex; flex-direction: column; }
|
|
|
|
|
+.panel-header { display: flex; justify-content: space-between; align-items: center; padding: 32rpx 32rpx 16rpx; }
|
|
|
|
|
+.panel-title { font-size: 34rpx; font-weight: 600; color: #1f2937; }
|
|
|
|
|
+.panel-close { font-size: 36rpx; color: #9ca3af; padding: 8rpx; }
|
|
|
|
|
+.panel-body { padding: 0 32rpx 32rpx; max-height: 55vh; }
|
|
|
|
|
+
|
|
|
|
|
+/* 音色面板(亮色,与整体风格统一) */
|
|
|
|
|
+.voice-panel-dark { background: #ffffff; border-radius: 24rpx 24rpx 0 0; max-height: 75vh; }
|
|
|
|
|
+
|
|
|
|
|
+/* 分类标签栏 */
|
|
|
|
|
+.voice-tabs { padding: 24rpx 28rpx 8rpx; border-bottom: 1rpx solid #f3f4f6; }
|
|
|
|
|
+.voice-tabs-scroll { white-space: nowrap; }
|
|
|
|
|
+.voice-tabs-inner { display: inline-flex; gap: 16rpx; }
|
|
|
|
|
+.voice-tab { font-size: 26rpx; color: #6b7280; padding: 10rpx 22rpx; border-radius: 20rpx; white-space: nowrap; flex-shrink: 0; background: #f9fafb; }
|
|
|
|
|
+.voice-tab.active { color: #ffffff; background: #4f46e5; font-weight: 600; }
|
|
|
|
|
+
|
|
|
|
|
+/* 音色网格(5列紧凑卡片) */
|
|
|
|
|
+.voice-grid-scroll { max-height: 56vh; }
|
|
|
|
|
+.voice-card-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12rpx; padding: 20rpx 24rpx 32rpx; }
|
|
|
|
|
+.voice-card { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 18rpx 4rpx 14rpx; border-radius: 14rpx; background: #f9fafb; border: 2rpx solid transparent; min-height: 120rpx; }
|
|
|
|
|
+.voice-card.active { background: #eef2ff; border-color: #4f46e5; }
|
|
|
|
|
+.voice-card:active { opacity: 0.7; transform: scale(0.96); }
|
|
|
|
|
+.voice-card-icon { width: 64rpx; height: 64rpx; border-radius: 50%; background: #f3f4f6; display: flex; align-items: center; justify-content: center; margin-bottom: 6rpx; font-size: 30rpx; }
|
|
|
|
|
+.voice-card.active .voice-card-icon { background: rgba(79,70,229,0.12); }
|
|
|
|
|
+.voice-card-name { font-size: 20rpx; color: #374151; text-align: center; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
|
|
|
+.voice-card.active .voice-card-name { color: #4f46e5; font-weight: 600; }
|
|
|
|
|
+.voice-card-check { position: absolute; top: 6rpx; right: 6rpx; font-size: 20rpx; color: #4f46e5; font-weight: 700; }
|
|
|
|
|
+.voice-card-play { position: absolute; top: 4rpx; right: 4rpx; width: 36rpx; height: 36rpx; border-radius: 50%; background: rgba(79,70,229,0.1); display: flex; align-items: center; justify-content: center; font-size: 16rpx; color: #4f46e5; }
|
|
|
|
|
+.voice-card.active .voice-card-play { display: none; }
|
|
|
|
|
+
|
|
|
|
|
+/* 面板底部关闭条 */
|
|
|
|
|
+.panel-close-bar { padding: 16rpx 0 28rpx; display: flex; justify-content: center; }
|
|
|
|
|
+.panel-close-bar-line { width: 80rpx; height: 6rpx; border-radius: 3rpx; background: #e5e7eb; }
|
|
|
|
|
+
|
|
|
|
|
+/* 参数面板 */
|
|
|
|
|
+.param-item { margin-bottom: 32rpx; }
|
|
|
|
|
+.param-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
|
|
|
|
|
+.param-label { font-size: 28rpx; color: #374151; }
|
|
|
|
|
+.param-value { font-size: 28rpx; font-weight: 600; color: #4f46e5; background: #eef2ff; padding: 4rpx 16rpx; border-radius: 8rpx; }
|
|
|
|
|
+.param-range { display: flex; justify-content: space-between; margin-top: 8rpx; }
|
|
|
|
|
+.param-range text { font-size: 22rpx; color: #9ca3af; }
|
|
|
|
|
+
|
|
|
|
|
+/* 专辑 */
|
|
|
|
|
+.album-item { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 24rpx; border-radius: 14rpx; background: #f9fafb; margin-bottom: 12rpx; }
|
|
|
|
|
+.album-item.active { background: #eef2ff; border: 2rpx solid #4f46e5; }
|
|
|
|
|
+.album-item-content { flex: 1; }
|
|
|
|
|
+.album-item-title { display: block; font-size: 28rpx; font-weight: 600; color: #1f2937; margin-bottom: 6rpx; }
|
|
|
|
|
+.album-item-desc { display: block; font-size: 24rpx; color: #6b7280; }
|
|
|
|
|
+.album-check { font-size: 32rpx; color: #4f46e5; }
|
|
|
|
|
+.album-empty { text-align: center; padding: 60rpx 0; color: #9ca3af; font-size: 28rpx; }
|
|
|
|
|
+
|
|
|
|
|
+.panel-action-btn { width: 100%; height: 96rpx; background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%); border-radius: 16rpx; border: none; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; color: #ffffff; margin-top: 16rpx; }
|
|
|
|
|
+.panel-action-btn::after { border: none; }
|
|
|
|
|
+.panel-action-btn[disabled] { opacity: 0.4; }
|
|
|
|
|
+
|
|
|
|
|
+/* 表单 */
|
|
|
|
|
+.form-item { margin-bottom: 24rpx; }
|
|
|
|
|
+.form-label { display: block; font-size: 28rpx; color: #1f2937; font-weight: 500; margin-bottom: 12rpx; }
|
|
|
|
|
+.form-input { width: 100%; height: 88rpx; background: #f9fafb; border-radius: 14rpx; padding: 0 24rpx; font-size: 28rpx; color: #1f2937; }
|
|
|
|
|
+.form-textarea { width: 100%; min-height: 140rpx; background: #f9fafb; border-radius: 14rpx; padding: 20rpx 24rpx; font-size: 28rpx; color: #1f2937; line-height: 1.6; }
|
|
|
|
|
+
|
|
|
|
|
+/* 额度弹窗 */
|
|
|
|
|
+.quota-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 3000; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+.quota-content { width: 600rpx; background: #ffffff; border-radius: 32rpx; padding: 60rpx 40rpx; display: flex; flex-direction: column; align-items: center; }
|
|
|
|
|
+.quota-icon { font-size: 100rpx; margin-bottom: 24rpx; }
|
|
|
|
|
+.quota-title { font-size: 36rpx; font-weight: 600; color: #1f2937; margin-bottom: 12rpx; }
|
|
|
|
|
+.quota-desc { font-size: 28rpx; color: #6b7280; margin-bottom: 40rpx; }
|
|
|
|
|
+.quota-actions { width: 100%; display: flex; flex-direction: column; gap: 16rpx; }
|
|
|
|
|
+.quota-btn { width: 100%; height: 88rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; border: none; }
|
|
|
|
|
+.quota-btn::after { border: none; }
|
|
|
|
|
+.quota-btn.primary { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: #fff; }
|
|
|
|
|
+.quota-btn.secondary { background: #f3f4f6; color: #6b7280; }
|
|
|
</style>
|
|
</style>
|