917 lines
20 KiB
Vue
917 lines
20 KiB
Vue
|
|
<template>
|
|||
|
|
<!-- #ifdef MP-WEIXIN -->
|
|||
|
|
<!-- 小程序使用原生页面 -->
|
|||
|
|
<view class="upload-container">
|
|||
|
|
<!-- 页面标题 -->
|
|||
|
|
<view class="page-header">
|
|||
|
|
<text class="header-title">创建音色</text>
|
|||
|
|
<text class="header-subtitle">上传音频样本,克隆专属音色</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 音色名称 -->
|
|||
|
|
<view class="form-section">
|
|||
|
|
<view class="form-item">
|
|||
|
|
<text class="form-label">音色名称</text>
|
|||
|
|
<input
|
|||
|
|
class="form-input"
|
|||
|
|
v-model="voiceName"
|
|||
|
|
placeholder="请输入音色名称(如:妈妈的声音)"
|
|||
|
|
maxlength="20"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="form-section">
|
|||
|
|
<view class="form-item">
|
|||
|
|
<text class="form-label">使用 CosyVoice v3 plus</text>
|
|||
|
|
<view class="toggle-row">
|
|||
|
|
<switch :checked="useCosyVoice" @change="onUseCosyVoiceChange" />
|
|||
|
|
<text class="toggle-hint">开启后将使用 cosyvoice-v3-plus 创建音色</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view class="tips-section">
|
|||
|
|
<view class="tips-title-row">
|
|||
|
|
<text class="tips-title">📄 参考文案(可直接朗读)</text>
|
|||
|
|
<button class="switch-btn" @click="switchReferenceText">换一换</button>
|
|||
|
|
</view>
|
|||
|
|
<view class="reference-box">
|
|||
|
|
<text class="reference-text">{{ referenceText }}</text>
|
|||
|
|
</view>
|
|||
|
|
<button class="copy-btn" @click="copyReferenceText">复制文案</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 录制音频 -->
|
|||
|
|
<view class="upload-section">
|
|||
|
|
<text class="section-title">上传音频样本</text>
|
|||
|
|
<text class="section-tip">可录制,建议10-20秒清晰人声</text>
|
|||
|
|
|
|||
|
|
<view v-if="!audioPath" class="upload-mode">
|
|||
|
|
<button class="mode-btn" :disabled="isRecording" @click="startRecord">🎤 录音</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-if="!audioPath" class="upload-box">
|
|||
|
|
<view v-if="isRecording" class="recording-box">
|
|||
|
|
<view class="recording-icon">🔴</view>
|
|||
|
|
<text class="recording-text">录音中... {{ recordDuration }}秒</text>
|
|||
|
|
<button class="stop-btn" @click="stopRecord">停止录音</button>
|
|||
|
|
</view>
|
|||
|
|
<view v-else>
|
|||
|
|
<view class="upload-icon">🎤</view>
|
|||
|
|
<text class="upload-text">点击“录音”开始录制</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<view v-else class="audio-preview">
|
|||
|
|
<view class="audio-info">
|
|||
|
|
<text class="audio-icon">🎵</text>
|
|||
|
|
<text class="audio-name">{{ audioName }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="audio-actions">
|
|||
|
|
<button class="action-btn play-btn" @click="playAudio">{{ isPlaying ? '暂停' : '播放' }}</button>
|
|||
|
|
<button class="action-btn delete-btn" @click="deleteAudio">删除</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 提交按钮 -->
|
|||
|
|
<view class="submit-section">
|
|||
|
|
<button
|
|||
|
|
class="submit-btn"
|
|||
|
|
:disabled="!canSubmit || uploading"
|
|||
|
|
@click="handleSubmit"
|
|||
|
|
>
|
|||
|
|
<text v-if="uploading">上传中...</text>
|
|||
|
|
<text v-else>创建音色</text>
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 说明 -->
|
|||
|
|
<view class="tips-section">
|
|||
|
|
<text class="tips-title">📝 温馨提示</text>
|
|||
|
|
<text class="tips-item">• 音频时长建议10-20秒(最长60秒)</text>
|
|||
|
|
<text class="tips-item">• 必须包含至少3秒连续清晰朗读</text>
|
|||
|
|
<text class="tips-item">• 避免背景音乐、噪音或其他人声</text>
|
|||
|
|
<text class="tips-item">• 音频内容建议为普通话朗读</text>
|
|||
|
|
<text class="tips-item">• 创建成功后可在"声音克隆"中使用</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
</view>
|
|||
|
|
<!-- #endif -->
|
|||
|
|
|
|||
|
|
<!-- #ifndef MP-WEIXIN -->
|
|||
|
|
<!-- App使用H5页面 -->
|
|||
|
|
<view class="upload-page">
|
|||
|
|
<web-view :src="uploadUrl"></web-view>
|
|||
|
|
</view>
|
|||
|
|
<!-- #endif -->
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { API_BASE } from '@/config/api.js';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
voiceName: '',
|
|||
|
|
audioPath: '',
|
|||
|
|
audioName: '',
|
|||
|
|
audioSize: 0,
|
|||
|
|
useCosyVoice: false,
|
|||
|
|
referenceTexts: [
|
|||
|
|
'我想和你说说最近的生活。今天的天气很好,我也在努力让自己过得更开心。希望你也一切都好。',
|
|||
|
|
'你好呀,最近我过得还不错。今天我想和你聊聊我的心情,也想听听你的近况。',
|
|||
|
|
'我在一个安静的房间里,正在认真朗读这段文字。希望声音清晰自然,能够帮助创建更好的音色。',
|
|||
|
|
'今天我精神很好,说话也很清楚。我会保持语速适中、情绪自然,尽量减少停顿与杂音。'
|
|||
|
|
],
|
|||
|
|
referenceTextIndex: 0,
|
|||
|
|
uploading: false,
|
|||
|
|
isPlaying: false,
|
|||
|
|
isRecording: false,
|
|||
|
|
recordDuration: 0,
|
|||
|
|
recordTimer: null,
|
|||
|
|
recorderManager: null,
|
|||
|
|
innerAudioContext: null
|
|||
|
|
// #endif
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
canSubmit() {
|
|||
|
|
return this.voiceName.trim() && this.audioPath;
|
|||
|
|
},
|
|||
|
|
referenceText() {
|
|||
|
|
const list = this.referenceTexts || [];
|
|||
|
|
return list[this.referenceTextIndex] || '';
|
|||
|
|
},
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef MP-WEIXIN
|
|||
|
|
uploadUrl() {
|
|||
|
|
const token = uni.getStorageSync('token') || '';
|
|||
|
|
const userId = uni.getStorageSync('userId') || '';
|
|||
|
|
// 生产默认:使用 API_BASE;如手动指定则读取存储
|
|||
|
|
const apiBase = uni.getStorageSync('apiBase') || (typeof API_BASE !== 'undefined' ? API_BASE : '');
|
|||
|
|
return `/static/upload.html?token=${encodeURIComponent(token)}&userId=${encodeURIComponent(userId)}&apiBase=${encodeURIComponent(apiBase)}`;
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
},
|
|||
|
|
// #ifndef MP-WEIXIN
|
|||
|
|
onShow() {
|
|||
|
|
// APP版本在显示时检查登录
|
|||
|
|
const userId = uni.getStorageSync('userId');
|
|||
|
|
const token = uni.getStorageSync('token');
|
|||
|
|
|
|||
|
|
if (!userId || !token) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '需要登录',
|
|||
|
|
content: '请先登录后再创建音色',
|
|||
|
|
showCancel: false,
|
|||
|
|
success: () => {
|
|||
|
|
uni.reLaunch({
|
|||
|
|
url: '/pages/login/login'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// #endif
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
onLoad() {
|
|||
|
|
// 检查登录状态
|
|||
|
|
this.checkLogin();
|
|||
|
|
// 初始化录音管理器
|
|||
|
|
this.initRecorder();
|
|||
|
|
},
|
|||
|
|
onUnload() {
|
|||
|
|
if (this.innerAudioContext) {
|
|||
|
|
this.innerAudioContext.stop();
|
|||
|
|
this.innerAudioContext.destroy();
|
|||
|
|
}
|
|||
|
|
if (this.recordTimer) {
|
|||
|
|
clearInterval(this.recordTimer);
|
|||
|
|
}
|
|||
|
|
if (this.recorderManager && this.isRecording) {
|
|||
|
|
this.recorderManager.stop();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
onUseCosyVoiceChange(e) {
|
|||
|
|
this.useCosyVoice = !!(e && e.detail && e.detail.value);
|
|||
|
|
},
|
|||
|
|
copyReferenceText() {
|
|||
|
|
uni.setClipboardData({
|
|||
|
|
data: this.referenceText,
|
|||
|
|
success: () => {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已复制',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
switchReferenceText() {
|
|||
|
|
const list = this.referenceTexts || [];
|
|||
|
|
if (list.length <= 1) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
this.referenceTextIndex = (this.referenceTextIndex + 1) % list.length;
|
|||
|
|
},
|
|||
|
|
// 检查登录状态
|
|||
|
|
checkLogin() {
|
|||
|
|
const userId = uni.getStorageSync('userId');
|
|||
|
|
const token = uni.getStorageSync('token');
|
|||
|
|
|
|||
|
|
if (!userId || !token) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '需要登录',
|
|||
|
|
content: '请先登录后再创建音色',
|
|||
|
|
showCancel: false,
|
|||
|
|
success: () => {
|
|||
|
|
uni.reLaunch({
|
|||
|
|
url: '/pages/login/login'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 初始化录音管理器
|
|||
|
|
initRecorder() {
|
|||
|
|
this.recorderManager = uni.getRecorderManager();
|
|||
|
|
|
|||
|
|
// 录音开始
|
|||
|
|
this.recorderManager.onStart(() => {
|
|||
|
|
console.log('录音开始');
|
|||
|
|
this.isRecording = true;
|
|||
|
|
this.recordDuration = 0;
|
|||
|
|
this.recordTimer = setInterval(() => {
|
|||
|
|
this.recordDuration++;
|
|||
|
|
// 超过60秒自动停止
|
|||
|
|
if (this.recordDuration >= 60) {
|
|||
|
|
this.stopRecord();
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音停止
|
|||
|
|
this.recorderManager.onStop((res) => {
|
|||
|
|
console.log('录音停止', res);
|
|||
|
|
this.isRecording = false;
|
|||
|
|
if (this.recordTimer) {
|
|||
|
|
clearInterval(this.recordTimer);
|
|||
|
|
this.recordTimer = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查录音时长
|
|||
|
|
if (this.recordDuration < 3) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '录音时长至少3秒',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.audioPath = res.tempFilePath;
|
|||
|
|
this.audioName = `录音_${this.recordDuration}秒.mp3`;
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '录音完成',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音错误
|
|||
|
|
this.recorderManager.onError((err) => {
|
|||
|
|
console.error('录音错误:', err);
|
|||
|
|
this.isRecording = false;
|
|||
|
|
if (this.recordTimer) {
|
|||
|
|
clearInterval(this.recordTimer);
|
|||
|
|
this.recordTimer = null;
|
|||
|
|
}
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '录音失败,请重试',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 选择音频文件
|
|||
|
|
chooseAudioFile() {
|
|||
|
|
// 先检查登录
|
|||
|
|
if (!this.checkLogin()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uni.chooseMessageFile({
|
|||
|
|
count: 1,
|
|||
|
|
type: 'file',
|
|||
|
|
extension: ['.mp3', '.wav', '.m4a', '.aac'],
|
|||
|
|
success: (res) => {
|
|||
|
|
const file = res.tempFiles && res.tempFiles[0];
|
|||
|
|
if (!file) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '选择文件失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (file.size > 10 * 1024 * 1024) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '文件大小不能超过10MB',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.audioPath = file.path;
|
|||
|
|
this.audioName = file.name || 'audio';
|
|||
|
|
this.audioSize = file.size || 0;
|
|||
|
|
this.recordDuration = 0;
|
|||
|
|
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '已选择音频',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.error('选择文件失败:', err);
|
|||
|
|
uni.showToast({
|
|||
|
|
title: err.errMsg || '选择文件失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 开始录音
|
|||
|
|
startRecord() {
|
|||
|
|
// 先检查登录
|
|||
|
|
if (!this.checkLogin()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保录音管理器已初始化
|
|||
|
|
if (!this.recorderManager) {
|
|||
|
|
this.initRecorder();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先检查权限状态
|
|||
|
|
uni.getSetting({
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.authSetting['scope.record']) {
|
|||
|
|
// 已授权,直接开始录音
|
|||
|
|
this.doStartRecord();
|
|||
|
|
} else if (res.authSetting['scope.record'] === false) {
|
|||
|
|
// 已拒绝,引导用户去设置
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '需要录音权限',
|
|||
|
|
content: '请在设置中开启录音权限',
|
|||
|
|
confirmText: '去设置',
|
|||
|
|
success: (modalRes) => {
|
|||
|
|
if (modalRes.confirm) {
|
|||
|
|
uni.openSetting({
|
|||
|
|
success: (settingRes) => {
|
|||
|
|
if (settingRes.authSetting['scope.record']) {
|
|||
|
|
this.doStartRecord();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 未授权过,请求授权
|
|||
|
|
uni.authorize({
|
|||
|
|
scope: 'scope.record',
|
|||
|
|
success: () => {
|
|||
|
|
this.doStartRecord();
|
|||
|
|
},
|
|||
|
|
fail: () => {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '需要录音权限',
|
|||
|
|
content: '请允许使用麦克风进行录音',
|
|||
|
|
confirmText: '去设置',
|
|||
|
|
success: (modalRes) => {
|
|||
|
|
if (modalRes.confirm) {
|
|||
|
|
uni.openSetting();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: () => {
|
|||
|
|
// 获取设置失败,直接尝试录音
|
|||
|
|
this.doStartRecord();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 执行录音
|
|||
|
|
doStartRecord() {
|
|||
|
|
if (this.recorderManager) {
|
|||
|
|
try {
|
|||
|
|
this.recorderManager.start({
|
|||
|
|
duration: 60000, // 最长60秒
|
|||
|
|
sampleRate: 16000,
|
|||
|
|
numberOfChannels: 1,
|
|||
|
|
encodeBitRate: 96000,
|
|||
|
|
format: 'mp3'
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('启动录音失败:', err);
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '时光意境',
|
|||
|
|
content: '麦克风被其他应用占用\n\n请关闭其他使用麦克风的应用',
|
|||
|
|
confirmText: '确定',
|
|||
|
|
showCancel: false
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '录音初始化失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 停止录音
|
|||
|
|
stopRecord() {
|
|||
|
|
if (this.recorderManager && this.isRecording) {
|
|||
|
|
this.recorderManager.stop();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 播放音频
|
|||
|
|
playAudio() {
|
|||
|
|
if (!this.innerAudioContext) {
|
|||
|
|
this.innerAudioContext = uni.createInnerAudioContext();
|
|||
|
|
this.innerAudioContext.src = this.audioPath;
|
|||
|
|
|
|||
|
|
this.innerAudioContext.onPlay(() => {
|
|||
|
|
this.isPlaying = true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.innerAudioContext.onPause(() => {
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.innerAudioContext.onEnded(() => {
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.innerAudioContext.onError((err) => {
|
|||
|
|
console.error('音频播放失败:', err);
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '播放失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.isPlaying) {
|
|||
|
|
this.innerAudioContext.pause();
|
|||
|
|
} else {
|
|||
|
|
this.innerAudioContext.play();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 删除音频
|
|||
|
|
deleteAudio() {
|
|||
|
|
if (this.innerAudioContext) {
|
|||
|
|
this.innerAudioContext.stop();
|
|||
|
|
this.innerAudioContext.destroy();
|
|||
|
|
this.innerAudioContext = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.audioPath = '';
|
|||
|
|
this.audioName = '';
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 提交创建
|
|||
|
|
handleSubmit() {
|
|||
|
|
if (this.uploading) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!this.canSubmit) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: !this.voiceName.trim() ? '请填写音色名称' : '请先录制音频',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const userId = uni.getStorageSync('userId');
|
|||
|
|
if (!userId) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '请先登录',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
setTimeout(() => {
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: '/pages/login/login'
|
|||
|
|
});
|
|||
|
|
}, 1500);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const doUpload = () => {
|
|||
|
|
this.uploading = true;
|
|||
|
|
|
|||
|
|
const token = uni.getStorageSync('token') || '';
|
|||
|
|
|
|||
|
|
// 为上传的文件生成一个标准的文件名
|
|||
|
|
const timestamp = Date.now();
|
|||
|
|
const fileName = `recording_${timestamp}.mp3`;
|
|||
|
|
|
|||
|
|
const createEndpoint = this.useCosyVoice ? '/api/voice/cosy-create' : '/api/voice/create';
|
|||
|
|
uni.uploadFile({
|
|||
|
|
url: `${API_BASE}${createEndpoint}`,
|
|||
|
|
filePath: this.audioPath,
|
|||
|
|
name: 'audio',
|
|||
|
|
fileName: fileName, // 指定文件名
|
|||
|
|
formData: {
|
|||
|
|
name: this.voiceName,
|
|||
|
|
displayName: this.voiceName
|
|||
|
|
},
|
|||
|
|
header: {
|
|||
|
|
'X-User-Id': userId,
|
|||
|
|
'Authorization': token ? `Bearer ${token}` : ''
|
|||
|
|
},
|
|||
|
|
success: (res) => {
|
|||
|
|
console.log('上传响应:', res);
|
|||
|
|
try {
|
|||
|
|
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
|||
|
|
console.log('解析后的数据:', data);
|
|||
|
|
console.log('状态码:', res.statusCode);
|
|||
|
|
|
|||
|
|
if (res.statusCode === 200 && data.success) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '音色创建成功',
|
|||
|
|
icon: 'success',
|
|||
|
|
duration: 2000
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
uni.navigateBack();
|
|||
|
|
}, 2000);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (res.statusCode === 402) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '需要付费',
|
|||
|
|
content: (data && data.message) ? data.message : '免费次数已用完,请先完成支付',
|
|||
|
|
showCancel: false
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.error('服务器错误:', data && (data.message || data.error) ? (data.message || data.error) : `服务器错误 (${res.statusCode})`, data);
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '创建失败',
|
|||
|
|
content: (data && data.message) ? data.message : '创建失败,请重试',
|
|||
|
|
showCancel: false
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('处理响应失败:', err);
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '创建失败',
|
|||
|
|
content: '创建失败,请重试',
|
|||
|
|
showCancel: false
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: (err) => {
|
|||
|
|
console.error('上传失败:', err);
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '创建失败',
|
|||
|
|
content: '网络请求失败,请检查网络连接',
|
|||
|
|
showCancel: false
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
complete: () => {
|
|||
|
|
this.uploading = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
doUpload();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
/* 小程序样式 */
|
|||
|
|
.upload-container {
|
|||
|
|
min-height: 100vh;
|
|||
|
|
background: linear-gradient(135deg, #FDF8F2 0%, #F5EDE0 100%);
|
|||
|
|
padding: 30upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-header {
|
|||
|
|
text-align: center;
|
|||
|
|
margin-bottom: 40upx;
|
|||
|
|
|
|||
|
|
.header-title {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 44upx;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #333;
|
|||
|
|
margin-bottom: 15upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-subtitle {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-mode {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20upx;
|
|||
|
|
margin: 15upx 0 25upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mode-btn {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 80upx;
|
|||
|
|
border-radius: 15upx;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
background: #F5F5F5;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 20upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-hint {
|
|||
|
|
font-size: 24upx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.reference-box {
|
|||
|
|
background: #F5F5F5;
|
|||
|
|
border-radius: 15upx;
|
|||
|
|
padding: 20upx;
|
|||
|
|
margin-top: 10upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.reference-text {
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #333;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.copy-btn {
|
|||
|
|
margin-top: 20upx;
|
|||
|
|
height: 80upx;
|
|||
|
|
border-radius: 15upx;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
background: #6D8B8B;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tips-title-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.switch-btn {
|
|||
|
|
height: 60upx;
|
|||
|
|
line-height: 60upx;
|
|||
|
|
padding: 0 20upx;
|
|||
|
|
border-radius: 12upx;
|
|||
|
|
font-size: 24upx;
|
|||
|
|
background: #F5F5F5;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-section {
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
padding: 30upx;
|
|||
|
|
margin-bottom: 30upx;
|
|||
|
|
box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-item {
|
|||
|
|
.form-label {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #666;
|
|||
|
|
margin-bottom: 15upx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.form-input {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 80upx;
|
|||
|
|
background: #F5F5F5;
|
|||
|
|
border-radius: 15upx;
|
|||
|
|
padding: 0 20upx;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-section {
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
padding: 30upx;
|
|||
|
|
margin-bottom: 30upx;
|
|||
|
|
box-shadow: 0 4upx 20upx rgba(0, 0, 0, 0.05);
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #666;
|
|||
|
|
margin-bottom: 10upx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-tip {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 24upx;
|
|||
|
|
color: #999;
|
|||
|
|
margin-bottom: 25upx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-box {
|
|||
|
|
border: 2upx dashed #8B7355;
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
padding: 60upx 30upx;
|
|||
|
|
text-align: center;
|
|||
|
|
background: #FAFAFA;
|
|||
|
|
|
|||
|
|
.upload-icon {
|
|||
|
|
font-size: 80upx;
|
|||
|
|
margin-bottom: 20upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-text {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #8B7355;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recording-box {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
|
|||
|
|
.recording-icon {
|
|||
|
|
font-size: 80upx;
|
|||
|
|
margin-bottom: 20upx;
|
|||
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.recording-text {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
color: #FF6B6B;
|
|||
|
|
margin-bottom: 30upx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stop-btn {
|
|||
|
|
width: 200upx;
|
|||
|
|
height: 70upx;
|
|||
|
|
background: #FF6B6B;
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 35upx;
|
|||
|
|
font-size: 28upx;
|
|||
|
|
border: none;
|
|||
|
|
box-shadow: 0 4upx 15upx rgba(255, 107, 107, 0.3);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0%, 100% {
|
|||
|
|
transform: scale(1);
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
transform: scale(1.1);
|
|||
|
|
opacity: 0.8;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audio-preview {
|
|||
|
|
background: #F5F5F5;
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
padding: 25upx;
|
|||
|
|
|
|||
|
|
.audio-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20upx;
|
|||
|
|
|
|||
|
|
.audio-icon {
|
|||
|
|
font-size: 40upx;
|
|||
|
|
margin-right: 15upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audio-name {
|
|||
|
|
flex: 1;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audio-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 15upx;
|
|||
|
|
|
|||
|
|
.action-btn {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 60upx;
|
|||
|
|
line-height: 60upx;
|
|||
|
|
border-radius: 10upx;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.play-btn {
|
|||
|
|
background: #8B7355;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.delete-btn {
|
|||
|
|
background: #FF6B6B;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.submit-section {
|
|||
|
|
margin-bottom: 30upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.submit-btn {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 90upx;
|
|||
|
|
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
|
|||
|
|
border-radius: 45upx;
|
|||
|
|
color: white;
|
|||
|
|
font-size: 32upx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
border: none;
|
|||
|
|
box-shadow: 0 8upx 20upx rgba(139, 115, 85, 0.3);
|
|||
|
|
|
|||
|
|
&[disabled] {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tips-section {
|
|||
|
|
background: rgba(255, 255, 255, 0.8);
|
|||
|
|
border-radius: 20upx;
|
|||
|
|
padding: 25upx;
|
|||
|
|
|
|||
|
|
.tips-title {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 26upx;
|
|||
|
|
color: #8B7355;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 15upx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tips-item {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 24upx;
|
|||
|
|
color: #666;
|
|||
|
|
line-height: 2;
|
|||
|
|
padding-left: 10upx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef MP-WEIXIN
|
|||
|
|
/* App样式 */
|
|||
|
|
.upload-page {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100vh;
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
</style>
|