Ai_GirlFriend/xuniYou/pages/chat/phone.vue

949 lines
28 KiB
Vue
Raw Normal View History

2026-01-31 19:15:41 +08:00
<template>
<view>
<view class="body">
<uni-nav-bar fixed statusBar left-icon="left" background-color="transparent" :border="false" @clickLeft="back"
color="#ffffff"></uni-nav-bar>
<image class="back"
:src="loverBasicList.image_url ? loverBasicList.image_url : 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/39c6f8899c15f60fc59207835f95e07a.png'"
mode="aspectFill"></image>
</view>
<view class="header">
<view class="header_content">
2026-02-02 20:08:28 +08:00
<view class="header_time">{{ isVip ? 'VIP 无限通话' : '通话剩余时间' }}</view>
<view class="header_module fa sb" v-if="!isVip">
<view class="header_title faj">{{ formatTime(remainingTime).hours }}</view>
<view class="header_title faj">{{ formatTime(remainingTime).minutes }}</view>
<view class="header_title faj">{{ formatTime(remainingTime).seconds }}</view>
2026-01-31 19:15:41 +08:00
</view>
2026-02-02 20:08:28 +08:00
<view class="header_recharge faj" @click="goRecharge">充值<image src="/static/images/phone_more.png" mode="widthFix"></image>
2026-01-31 19:15:41 +08:00
</view>
</view>
</view>
<!-- 替换为动态脑电波效果 -->
<view class="dynamic-wave">
<view class="wave-bar" v-for="n in 15" :key="n" :style="{ 'animation-delay': n * 0.1 + 's' }"></view>
</view>
<view class="opt fa sb">
<view @click="hangUp()" class="opt_item">
<image class="opt_image" src="/static/images/phone_a2.png" mode="widthFix"></image>
<view class="opt_name">挂断</view>
</view>
<view class="opt_item" @click="openMicPermission()">
<image class="opt_image" src="/static/images/phone_a1.png" mode="widthFix"></image>
<view class="opt_name">麦克风权限</view>
</view>
</view>
<view class="black"></view>
</view>
</template>
<script>
// #ifdef APP
const recorder = uni.requireNativePlugin('AudioRecode')
// const recorder = uni.requireNativePlugin('LcPrinter')
console.log('recorder123456::', recorder)
// #endif
// let socketTask = null
// socketTask = uni.connectSocket({
// url: 'wss://lovers.shandonghuixing.com/voice/call',//'wss://<host>/voice/call',
// header:{
// Authorization: 'Bearer ' + uni.getStorageSync("token") || "" //'Bearer <token>'
// }
// })
// console.log('socketTask:',socketTask)
import {
} from '@/utils/api.js'
import notHave from '@/components/not-have.vue';
2026-02-02 20:08:28 +08:00
import { baseURLPy } from '@/utils/request.js'
2026-01-31 19:15:41 +08:00
import topSafety from '@/components/top-safety.vue';
const recorderManager = uni.getRecorderManager();
export default {
components: {
notHave,
topSafety,
},
beforeCreate() {
console.log('beforeCreate', )
// const domModule = uni.requireNativePlugin('io.dcolud.audio.recode')
// console.log('beforeCreate',domModule)
},
data() {
return {
loverBasicList: uni.getStorageSync('loverBasicList'),
socketTask: null,
isRecording: false,
status: 'Ready',
audioContext: null,
audioData: [],
isApp: false, // 是否为 App 端
2026-02-02 20:08:28 +08:00
totalDuration: 300000, // 默认 5 分钟
remainingTime: 300000,
timer: null,
isVip: false
2026-01-31 19:15:41 +08:00
}
},
onLoad() {
// 检测平台
const systemInfo = uni.getSystemInfoSync()
console.log('systemInfo', systemInfo)
// console.log('plus', plus)
this.isApp = systemInfo.uniPlatform === 'app'
2026-02-02 20:08:28 +08:00
this.getCallDuration()
2026-01-31 19:15:41 +08:00
this.initAudio()
},
onUnload() {
this.stopCall()
2026-02-02 20:08:28 +08:00
if (this.timer) {
clearInterval(this.timer)
}
2026-01-31 19:15:41 +08:00
},
methods: {
2026-02-02 20:08:28 +08:00
getCallDuration() {
uni.request({
url: baseURLPy + '/voice/call/duration',
method: 'GET',
header: {
'Authorization': 'Bearer ' + uni.getStorageSync("token")
},
success: (res) => {
console.log('通话时长配置:', res.data)
if (res.data.code === 1 && res.data.data) {
const duration = res.data.data.duration
this.isVip = res.data.data.is_vip
if (duration > 0) {
this.totalDuration = duration
this.remainingTime = duration
} else {
// VIP 用户无限制,设置为 24 小时
this.totalDuration = 24 * 60 * 60 * 1000
this.remainingTime = 24 * 60 * 60 * 1000
}
}
this.connectWebSocket()
},
fail: (err) => {
console.error('获取通话时长失败:', err)
// 失败时使用默认值
this.connectWebSocket()
}
})
},
formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return {
hours: String(hours).padStart(2, '0'),
minutes: String(minutes).padStart(2, '0'),
seconds: String(seconds).padStart(2, '0')
}
},
startTimer() {
if (this.timer) {
clearInterval(this.timer)
}
this.timer = setInterval(() => {
this.remainingTime -= 1000
if (this.remainingTime <= 0) {
clearInterval(this.timer)
uni.showToast({
title: '通话时间已用完',
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else if (this.remainingTime === 60000) {
// 剩余 1 分钟提示
uni.showToast({
title: '剩余 1 分钟',
icon: 'none',
duration: 2000
})
}
}, 1000)
},
2026-01-31 19:15:41 +08:00
hangUp() {
uni.showModal({
title: '提示',
content: '挂断改通话',
success: function(res) {
if (res.confirm) {
uni.navigateBack();
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
},
initAudio() {
// 销毁旧的音频上下文(如果存在)
if (this.audioContext) {
this.audioContext.destroy();
}
// 创建新的音频上下文
this.audioContext = uni.createInnerAudioContext()
this.audioContext.volume = 1
// 设置音频源类型
this.audioContext.srcType = 'auto'
// 添加音频事件监听
this.audioContext.onError((err) => {
console.error('音频播放失败:', err)
console.error('错误详情:', JSON.stringify(err))
uni.showToast({
title: '音频播放失败',
icon: 'none'
})
})
this.audioContext.onPlay(() => {
console.log('音频开始播放')
})
this.audioContext.onEnded(() => {
console.log('音频播放结束')
})
this.audioContext.onCanplay(() => {
console.log('音频可以播放了')
})
},
connectWebSocket() {
2026-02-05 09:35:22 +08:00
// 根据 baseURLPy 构建 WebSocket URL
let wsUrl = baseURLPy.replace('http://', 'ws://').replace('https://', 'wss://') + '/voice/call'
console.log('WebSocket URL:', wsUrl)
2026-01-31 19:15:41 +08:00
this.socketTask = uni.connectSocket({
2026-02-05 09:35:22 +08:00
url: wsUrl,
2026-01-31 19:15:41 +08:00
header: {
"content-type": "application/json",
'Authorization': 'Bearer ' + uni.getStorageSync("token") || ""
},
success: () => console.log('WS 连接成功')
});
this.socketTask.onOpen((res) => {
console.log('onOpen:', res)
this.startRecording();
2026-02-02 20:08:28 +08:00
this.startTimer();
2026-01-31 19:15:41 +08:00
});
this.socketTask.onMessage((res) => {
console.log('onMessage:', res.data)
this.handleServerMessage(res.data);
});
this.socketTask.onError((err) => {
console.error('WS 错误', err);
});
this.socketTask.onClose((res) => {
console.log('关闭:', res)
if (this.isApp) {
console.log('关闭1', recorder)
recorder.stop()
}
})
},
// 开始录制
async startRecording() {
console.log('开始录制', )
if (this.isRecording) return;
// App 端不支持 onFrameRecorded
this.isRecording = true;
this.status = 'Call Started';
if (this.isApp) {
console.log('this.isApp:', this.isApp)
this.startRecord()
} else {
// 小程序和 H5 端支持 onFrameRecorded
recorderManager.onFrameRecorded((res) => {
// console.log('onFrameRecorded:', res)
const {
frameBuffer,
isLastFrame
} = res;
if (this.socketTask && this.socketTask.readyState === 1) {
this.socketTask.send({
data: frameBuffer
});
}
});
recorderManager.start({
2026-02-02 20:08:28 +08:00
duration: this.totalDuration,
2026-01-31 19:15:41 +08:00
format: 'pcm', // ⚠️ 必须用 PCMParaformer 实时版只吃 PCM
sampleRate: 16000, // ⚠️ 必须 16000Hz这是 ASR 的标准
numberOfChannels: 1, // 单声道
frameSize: 2, // 单位是 KB。设置小一点(2-4KB)延迟低,设置太大延迟高
audioSource: 'voice_communication'
});
}
},
startRecord() {
const doStart = () => {
recorder.start({
sampleRate: 16000,
frameSize: 640,
source: 'mic'
}, (res) => {
// console.log('socket---res',res)
if (res.type === 'frame') {
const ab = uni.base64ToArrayBuffer(res.data)
// console.log('ab',ab)
this.socketTask.send({
data: ab
})
}
})
}
if (uni.getSystemInfoSync().platform !== 'android') {
doStart()
return
}
if (typeof plus === 'undefined') {
// 还没到 plusready
document.addEventListener('plusready', () => {
plus.android.requestPermissions(['android.permission.RECORD_AUDIO'], (e) => {
if (e.granted && e.granted.length) doStart()
else uni.showModal({
title: '权限不足',
content: '请允许麦克风权限'
})
})
})
return
}
plus.android.requestPermissions(['android.permission.RECORD_AUDIO'], (e) => {
console.log('e.granted', e.granted)
if (e.granted && e.granted.length) doStart()
else uni.showModal({
title: '权限不足',
content: '请允许麦克风权限'
})
})
},
openMicPermission() {
plus.android.requestPermissions(['android.permission.RECORD_AUDIO'], (e) => {
console.log('e.granted', e)
if (e.granted && e.granted.length) {
uni.showToast({
title: '麦克风权限已开启',
icon: 'none'
})
} else {
uni.showModal({
title: '权限不足',
content: '请允许麦克风权限'
})
}
})
},
stopCall() {
this.isRecording = false;
recorderManager.stop();
if (this.socketTask) {
this.socketTask.close();
}
if (this.audioContext) {
this.audioContext.pause();
this.audioContext.destroy();
}
2026-02-02 20:08:28 +08:00
if (this.timer) {
clearInterval(this.timer);
}
2026-01-31 19:15:41 +08:00
},
// 接收消息
async handleServerMessage(data) {
console.log('接受到的信息:', data, typeof(data))
console.log('接受到的信息:', JSON.stringify(data))
console.log('接受到的信息:', Object.prototype.toString.call(data))
if (data && typeof(data) == 'object') {
//处理数据流
uni.hideLoading()
this.audioData.push(data);
console.log('this.audioData:', this.audioData)
} else {
//处理非数据流
let dataInfo = JSON.parse(data)
console.log('dataInfo:', dataInfo.type)
if (dataInfo.type == 'reply_end') {
if(this.isApp){
uni.showLoading({
title: '思考中',
mask: true
});
console.log('reply_endreply_endreply_end',)
// 创建一个完整的 ArrayBuffer 来存储所有音频数据
const totalLength = this.audioData.reduce((acc, buffer) => acc + buffer.byteLength, 0)
console.log('totalLength:',totalLength)
const mergedArrayBuffer = new Uint8Array(totalLength)
// 将音频数据填充到mergedArrayBuffer中修复之前这里没有填充数据
let offset = 0;
for (let i = 0; i < this.audioData.length; i++) {
const buffer = new Uint8Array(this.audioData[i]);
mergedArrayBuffer.set(buffer, offset);
offset += buffer.byteLength;
}
// console.log('mergedArrayBuffer填充完成长度:', mergedArrayBuffer)
const base64Audio = uni.arrayBufferToBase64(mergedArrayBuffer.buffer);
const base64WithPrefix = `data:audio/mp3;base64,${base64Audio}`;
// console.log('base64WithPrefix:',base64WithPrefix)
const filePath = await new Promise((resolve) => {
console.log('this:',this)
const fileName = `_doc/${Date.now()}_numberPerson.mp3`;
this.base64ToFile(base64WithPrefix, fileName, (path) => {
console.log('pathpathpath',path)
resolve(path);
});
});
console.log('pathpathpathfilePath',filePath)
// 使用filePath播放音频
// const audioCtx = uni.createInnerAudioContext();
this.audioContext.src = filePath;
try {
uni.hideLoading()
console.log('尝试延迟播放...');
this.audioContext.play();
// 清空音频数据
this.audioData = [];
} catch (delayError) {
console.error('延迟播放也失败:', delayError);
}
}else{
this.mergeAudioData()
}
}
if (dataInfo.type == 'reply_text') {
// #ifdef APP
uni.showLoading({
title: '思考中',
mask: true
});
// #endif
// #ifdef MP
uni.showLoading({
title: '思考中',
mask: true
});
// #endif
}
if (dataInfo.type == 'interrupt') {
this.audioContext.pause();
// uni.showToast({
// icon: "none",
// duration: 2000,
// title: "被打断"
// })
}
if (dataInfo.type == 'ready') {
// 准备好的
}
}
},
base64ToFile(base64Str, fileName, callback) {
// https://blog.csdn.net/qq_45225600/article/details/149859602
var index = base64Str.indexOf(',');
var base64Str = base64Str.slice(index + 1, base64Str.length);
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {
fs.root.getFile(fileName, { create: true }, function(entry) {
var fullPath = entry.fullPath;
let platform = uni.getSystemInfoSync().platform;
if (platform == 'android') {
// Android实现
var Base64 = plus.android.importClass("android.util.Base64");
var FileOutputStream = plus.android.importClass("java.io.FileOutputStream");
try {
var out = new FileOutputStream(fullPath);
var bytes = Base64.decode(base64Str, Base64.DEFAULT);
out.write(bytes);
out.close();
callback && callback(entry.toLocalURL());
} catch (e) {
console.log(e.message);
}
} else if (platform == 'ios') {
// iOS实现
var NSData = plus.ios.importClass('NSData');
var nsData = new NSData();
nsData = nsData.initWithBase64EncodedStringoptions(base64Str, 0);
if (nsData) {
nsData.plusCallMethod({ writeToFile: fullPath, atomically: true });
plus.ios.deleteObject(nsData);
}
callback && callback(entry.toLocalURL());
}
});
});
},
// 合并音频数据并播放
async mergeAudioData() {
console.log('this.audioData:',this.audioData)
if (this.audioData.length === 0) {
console.log('没有音频数据')
return null
}
try {
// 创建一个完整的 ArrayBuffer 来存储所有音频数据
const totalLength = this.audioData.reduce((acc, buffer) => acc + buffer.byteLength, 0)
console.log('totalLength:',totalLength)
const mergedArrayBuffer = new Uint8Array(totalLength)
console.log('mergedArrayBuffer:',mergedArrayBuffer)
let offset = 0
for (let i = 0; i < this.audioData.length; i++) {
const buffer = new Uint8Array(this.audioData[i])
mergedArrayBuffer.set(buffer, offset)
offset += buffer.byteLength
}
// 将合并后的 ArrayBuffer 保存为文件并播放
const fileName = `recording_${Date.now()}.mp3`
let filePath;
if (this.isApp) {
// App端音频播放处理
try {
// 重新初始化音频上下文,确保状态正确
this.initAudio();
// 先检查存储权限
if (typeof plus !== 'undefined' && plus.android) {
const Context = plus.android.importClass('android.content.Context');
const Environment = plus.android.importClass('android.os.Environment');
const permissions = ['android.permission.READ_EXTERNAL_STORAGE', 'android.permission.WRITE_EXTERNAL_STORAGE'];
plus.android.requestPermissions(permissions, (res) => {
if (res.deniedAlways.length > 0) {
console.error('存储权限被永久拒绝');
}
});
}
// 方案1尝试使用Blob URL直接播放如果支持
if (typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {
console.log('尝试使用Blob URL播放...');
const blob = new Blob([mergedArrayBuffer.buffer], { type: 'audio/mpeg' });
const blobURL = URL.createObjectURL(blob);
console.log('Blob URL:', blobURL);
this.audioContext.src = blobURL;
} else {
// 方案2写入文件后播放
console.log('尝试使用文件方式播放...');
filePath = `${plus.io.PUBLIC_DOWNLOADS}/${fileName}`;
const fileURL = await this.writeFileApp(mergedArrayBuffer.buffer, filePath);
console.log('文件播放路径:', fileURL);
// 尝试多种路径格式
let finalURL = fileURL;
// 如果fileURL是file://格式,尝试去除协议
if (fileURL.startsWith('file://')) {
const pathWithoutProtocol = fileURL.substring(7);
console.log('尝试无协议路径:', pathWithoutProtocol);
}
// 尝试使用plus.io.convertLocalFileSystemURL转换
const convertedURL = plus.io.convertLocalFileSystemURL(filePath);
console.log('转换后的路径:', convertedURL);
this.audioContext.src = finalURL;
}
} catch (error) {
console.error('App端音频播放准备失败:', error);
// 回退到简单的文件写入方式
filePath = `${plus.io.PUBLIC_DOWNLOADS}/${fileName}`;
const fileURL = await this.writeFileApp(mergedArrayBuffer.buffer, filePath);
this.audioContext.src = fileURL;
}
} else {
// 小程序端使用uni API写入文件
filePath = `${uni.env.USER_DATA_PATH}/${fileName}`;
await this.writeFileMiniProgram(mergedArrayBuffer.buffer, filePath);
this.audioContext.src = filePath;
}
console.log('最终音频源:', this.audioContext.src)
// 播放音频
try {
console.log('开始播放...');
// 确保音频上下文已正确创建
if (!this.audioContext) {
console.error('音频上下文未初始化');
this.initAudio();
this.audioContext.src = filePath;
}
// 先设置src再播放
this.audioContext.play();
// 清空音频数据
this.audioData = [];
} catch (playError) {
console.error('音频播放异常:', playError);
console.error('异常详情:', JSON.stringify(playError));
// 尝试延迟播放
setTimeout(() => {
try {
console.log('尝试延迟播放...');
this.audioContext.play();
} catch (delayError) {
console.error('延迟播放也失败:', delayError);
}
}, 500);
this.audioData = [];
}
} catch (error) {
console.error('合并音频数据失败:', error)
return null
}
},
// App端文件写入方法
writeFileApp(buffer, filePath) {
console.log('App端文件写入方法:',buffer, filePath)
return new Promise((resolve, reject) => {
console.log('plus.io.requestFileSystem',plus.io.requestFileSystem)
console.log('plus.io.PUBLIC_DOWNLOADS',plus.io.PUBLIC_DOWNLOADS)
plus.io.requestFileSystem(plus.io.PUBLIC_DOWNLOADS, (fs) => {
console.log('fs.root:',fs.root)
const fileName = filePath.split('/').pop();
fs.root.getFile(fileName, {create: true, exclusive: false}, (fileEntry) => {
console.log('fileEntry',fileEntry)
fileEntry.createWriter((writer) => {
console.log('writer:',writer)
console.log('writer 的方法:', Object.keys(writer))
console.log('buffer type:', typeof buffer, 'buffer instanceof ArrayBuffer:', buffer instanceof ArrayBuffer)
console.log('Blob 是否存在:', typeof Blob !== 'undefined')
console.log('准备进入 try 块...')
try {
console.log('已进入 try 块')
// 检查 Blob 是否可用,如果不可用则尝试直接写入 ArrayBuffer
let writeData;
if (typeof Blob !== 'undefined') {
// 先将 ArrayBuffer 转换为 Blob再写入
console.log('开始创建 Blob...')
writeData = new Blob([buffer], { type: 'audio/mpeg' });
console.log('Blob 创建成功:', writeData, 'size:', writeData.size)
} else {
// Blob 不可用,直接使用 ArrayBuffer
console.log('Blob 不可用,直接使用 ArrayBuffer')
writeData = buffer;
}
// 先设置所有事件监听器
console.log('开始设置事件监听器...')
writer.onerror = (err) => {
console.error('App端文件写入失败:', err)
reject(err)
}
writer.onwriteend = (e) => {
console.log('App端文件写入完成', e)
// 获取文件信息验证
fileEntry.file((file) => {
console.log('文件信息:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
})
// 验证文件大小是否大于0
if (file.size === 0) {
console.error('警告: 文件大小为0可能写入失败')
}
}, (err) => {
console.error('获取文件信息失败:', err)
})
// 获取文件路径的多种方式
const toURL = fileEntry.toURL();
const fullPath = fileEntry.fullPath;
console.log('toURL():', toURL)
console.log('fullPath:', fullPath)
let fileURL;
// 优先使用 toURL()如果返回的是有效路径file://开头)
if (toURL && toURL.startsWith('file://')) {
fileURL = toURL;
console.log('使用 toURL() 路径:', fileURL)
} else if (fullPath && fullPath.startsWith('/')) {
// fullPath 是绝对路径(如 /storage/emulated/0/...
// 直接添加 file:// 前缀
fileURL = 'file://' + fullPath;
console.log('使用 fullPath 并添加 file:// 前缀:', fileURL)
} else if (toURL) {
// toURL() 返回相对路径(如 _downloads/xxx.mp3尝试转换
fileURL = plus.io.convertLocalFileSystemURL(toURL);
console.log('使用 toURL() 相对路径转换:', fileURL)
// 如果转换后仍不是 file:// 开头,手动添加
if (!fileURL || !fileURL.startsWith('file://')) {
// 尝试用 fullPath如果可用
if (fullPath && fullPath.startsWith('/')) {
fileURL = 'file://' + fullPath;
console.log('回退到 fullPath 并添加 file:// 前缀:', fileURL)
}
}
}
console.log('App端最终文件URL:', fileURL)
resolve(fileURL)
}
writer.ontruncate = (e) => {
console.log('App端文件清空完成开始写入', e)
console.log('writer.readyState:', writer.readyState)
// truncate 完成后再写入数据
console.log('准备写入数据:', writeData, '类型:', typeof writeData, '是否是ArrayBuffer:', writeData instanceof ArrayBuffer)
// 添加写入开始监听
writer.onwritestart = (e) => {
console.log('文件写入开始', e)
}
// 如果 Blob 不可用ArrayBuffer 需要转换为 Uint8Array 的 buffer
if (writeData instanceof ArrayBuffer) {
console.log('写入 ArrayBuffer大小:', writeData.byteLength, 'bytes')
// FileWriter.write() 应该支持 ArrayBuffer但需要确保格式正确
writer.write(writeData)
} else {
console.log('写入 Blob大小:', writeData.size)
writer.write(writeData)
}
}
console.log('事件监听器设置完成,开始 seek...')
// 清空文件内容(如果文件已存在)
// 先定位到文件开头
writer.seek(0);
console.log('seek 完成,开始 truncate...')
// 然后清空文件,这会触发 ontruncate 事件
writer.truncate(0);
console.log('truncate 调用完成')
} catch (error) {
console.error('写入过程中发生错误:', error)
reject(error)
}
}, (err) => {
console.error('App端创建写入器失败:', err)
reject(err)
})
}, (err) => {
console.error('App端获取文件失败:', err)
reject(err)
})
}, (err) => {
console.error('App端请求文件系统失败:', err)
reject(err)
})
})
},
// 小程序端文件写入方法
writeFileMiniProgram(buffer, filePath) {
return new Promise((resolve, reject) => {
const fs = uni.getFileSystemManager()
fs.writeFile({
filePath: filePath,
data: buffer,
encoding: 'binary',
success: (res) => {
console.log('小程序端文件写入成功:', res)
resolve(filePath)
},
fail: (err) => {
console.error('小程序端文件写入失败:', err)
reject(err)
}
})
})
},
back() {
uni.navigateBack({
delta: 1,
});
},
generateClick() {
uni.navigateBack({
delta: 2,
});
},
2026-02-02 20:08:28 +08:00
goRecharge() {
uni.showToast({
title: '充值功能开发中',
icon: 'none'
})
}
2026-01-31 19:15:41 +08:00
}
}
</script>
<style>
page {
/* opacity: 0.7; */
}
</style>
<style>
.body {
position: relative;
}
.back {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: block;
}
.body {
position: relative;
padding: 0 60rpx;
}
.header {
position: absolute;
right: 28rpx;
top: 11%;
z-index: 5;
}
.header_content {
position: relative;
padding: 12rpx 24rpx;
background: linear-gradient(135deg, rgba(159, 71, 255, 0.6) 0%, rgba(0, 83, 250, 0.6) 100%);
border-radius: 12rpx;
text-align: center;
}
.header_time {
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
line-height: 50rpx;
}
.header_module {
position: relative;
}
.header_title {
margin: 0 8rpx 0 0;
padding: 0 12rpx;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
line-height: 50rpx;
background: linear-gradient(135deg, rgba(159, 71, 255, 0.6) 0%, rgba(0, 83, 250, 0.6) 100%);
border-radius: 12rpx;
}
.header_title:nth-child(3) {
margin: 0 0 0 0;
}
.header_recharge {
position: relative;
font-weight: 400;
font-size: 24rpx;
color: #FFFFFF;
line-height: 50rpx;
}
.header_recharge image {
margin: 0 0 0 10rpx;
width: 6rpx;
height: 10rpx;
}
/* 删除原来的voice样式添加新的动态波形样式 */
.dynamic-wave {
position: absolute;
left: 0;
right: 0;
bottom: 50%;
width: 300rpx;
/* 增加整体宽度 */
margin: 0 auto;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
height: 150rpx;
/* 增加容器高度 */
}
.wave-bar {
width: 8rpx;
/* 增加单个波形条的宽度 */
height: 40rpx;
/* 增加基础高度 */
margin: 0 4rpx;
/* 增加间距 */
background: linear-gradient(to top, #9f47ff, #0053fa);
border-radius: 4rpx;
/* 调整圆角 */
animation: wave 1.5s infinite ease-in-out;
}
@keyframes wave {
0%,
100% {
height: 40rpx;
/* 调整基础高度 */
opacity: 0.6;
}
50% {
height: 120rpx;
/* 调整最大高度 */
opacity: 1;
}
}
.opt {
position: absolute;
left: 0;
right: 0;
bottom: 15%;
margin: 0 140rpx;
z-index: 5;
}
.opt_item {
position: relative;
}
.opt_image {
width: 132rpx;
height: 132rpx;
border-radius: 100rpx;
display: block;
}
.opt_name {
margin: 26rpx 0 0 0;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
line-height: 50rpx;
text-align: center;
}
.black {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 812rpx;
background: linear-gradient(178deg, rgba(0, 0, 0, 0) 0%, #000000 100%);
z-index: 2;
}
</style>