This commit is contained in:
Home
2025-12-01 03:42:34 +08:00
parent 492a164bff
commit fde86ef902
1917 changed files with 21835 additions and 214147 deletions
+198 -393
View File
@@ -1,432 +1,237 @@
<template>
<view class="ai-chat-container">
<!-- 聊天消息列表 -->
<scroll-view class="chat-list" scroll-y scroll-bottom="{{scrollBottom}}"
:style="{height: `calc(100vh - ${inputAreaHeight}px)`}">
<!-- 系统消息 -->
<view class="chat-item system">
<view class="chat-avatar system-avatar">
<text class="iconfont icon-robot"></text>
</view>
<view class="chat-content">
<view class="content-text">你好我是智能助手有什么可以帮你的</view>
</view>
</view>
<!-- 用户消息 -->
<view class="chat-item user">
<view class="chat-content">
<view class="content-text">请问uniapp怎么实现语音流式传输</view>
</view>
<view class="chat-avatar user-avatar">
<text class="iconfont icon-user"></text>
</view>
</view>
<!-- AI回复消息 -->
<view class="chat-item ai">
<view class="chat-avatar ai-avatar">
<text class="iconfont icon-ai"></text>
</view>
<view class="chat-content">
<view class="content-text">
UniApp实现语音流式传输可通过5+ API采集音频帧结合WebSocket分块发送核心步骤包括
1. 初始化录音实例16k采样率单声道PCM
2. 建立WebSocket连接
3. 分块发送音频数据
4. 接收实时ASR结果
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 + 按住发送按钮 -->
<view class="input-area" :style="{height: inputAreaHeight + 'px'}">
<!-- 按住发送按钮底部固定 -->
<view class="send-btn-area">
<button class="hold-send-btn" @touchstart="startRecord">
<text class="btn-text">{{status ? '松开发送' : '按住 说话/发送'}}</text>
</button>
</view>
</view>
<view class="container">
<button @click="onStartRecord" :disabled="isRecording">开始录音</button>
<button @click="onStopRecord" :disabled="!isRecording">停止录音</button>
<view class="tip">{{ status }}</view>
<view class="tip">当前分贝{{ currentDecibels }}</view>
<yao-RecordFrame ref="recordFrame" @onFrameRecorded="frameRecorded" @currentDecibels="onCurrentDecibels"
@onStop="stopIt">
</yao-RecordFrame>
<sdx-StreamPlayer>xx</sdx-StreamPlayer>
</view>
</template>
<script>
export default {
data() {
return {
inputAreaHeight: 80,
status: "未录音",
recorder: null, // 录音实例
recordTimer: null, // 定时读取定时器
currentDecibels: 0,
isRecording: false,
ws: null, // WebSocket 实例
audioContext: null, // 音频上下文
scriptProcessor: null, // 音频处理节点
audioBufferSource: null, // 音频源节点
frameBufferList: [], // 缓存音频帧
wsUrl: "ws://10.10.10.201:8000/ws/audio", // 替换为实际后端地址
};
},
onUnload() {
// 页面卸载清理资源
this.stopRecord();
// 页面卸载清理资源
this.stopRecordAndClean();
},
methods: {
// 申请录音权限
async applyRecordPermission() {
// const res = await uni.requestPermissions({ scope: "scope.record" });
return true; // 1 表示授权成功
try {
const res = await uni.requestPermissions({
scope: "scope.record"
});
const isGranted = res[0].grantStatus === 1;
if (!isGranted) {
uni.showToast({
title: "请授予录音权限",
icon: "none"
});
}
return isGranted;
} catch (e) {
console.error("申请权限失败:", e);
return false;
}
},
// ArrayBuffer 转字符串
// 兼容的 ArrayBuffer 转字符串方法
arrayBufferToStringCompat(buffer) {
// 方法1: 使用 String.fromCharCode 和 Uint8Array
const uint8Array = new Uint8Array(buffer);
let str = '';
for (let i = 0; i < uint8Array.length; i++) {
str += String.fromCharCode(uint8Array[i]);
}
return str;
// 方法2: 或者使用更简洁的方式
// return String.fromCharCode.apply(null, new Uint8Array(buffer));
},
// 开始录音
async startRecord() {
// 1. 导入Android原生类
const AudioRecord = plus.android.importClass("android.media.AudioRecord");
const AudioFormat = plus.android.importClass("android.media.AudioFormat");
const MediaRecorder = plus.android.importClass("android.media.MediaRecorder");
const Context = plus.android.importClass("android.content.Context");
const Activity = plus.android.runtimeMainActivity();
async onStartRecord() {
// 1. 申请权限
// const hasPermission = await this.applyRecordPermission();
// if (!hasPermission) return;
this.ws = uni.connectSocket({
url: this.wsUrl, //仅为示例,并非真实接口地址。
complete: () => {
console.log('complete');
},
success: () => {
console.log('web');
this.ws.onOpen((res) => {
console.log('WebSocket连接已打开', res);
this.ws.onMessage((res) => {
let messageData = res.data;
// 处理不同类型的数据
if (typeof messageData === 'string') {
// 文本数据
try {
const parsedData = JSON.parse(messageData);
this.handleMessage(parsedData);
} catch (e) {
this.handleMessage(messageData);
}
} else if (messageData instanceof ArrayBuffer) {
// 二进制数据
try {
// const str = this.arrayBufferToStringCompat(messageData);
// console.log('转换后的字符串:', str);
} catch (e) {
console.log('二进制数据解析失败:', e);
}
}
})
});
},
fail: () => {
console.log('fail');
},
});
// 2. 录音参数配置(固定:16bit单声道,44.1kHz采样率)
const SAMPLE_RATE = 44100; // 采样率
const CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; // 单声道
const AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // 16bit PCM
const BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
// await this.initWebSocket();
// 3. 创建AudioRecord实例(仅启动一次)
const audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC, // 麦克风源
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
BUFFER_SIZE * 2 // 缓冲区放大2倍,避免数据溢出
);
// 3. 启动录音
try {
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
onFrameRecorded: ({
isLastFrame,
frameBuffer
}) => {
// 处理帧数据(如实时上传/渲染)
// console.log('帧数据:', frameData);
this.ws.send({
data: frameBuffer
});
},
onDecibels: (decibels) => {
// 处理分贝数据(如实时更新UI
// console.log('当前分贝:', decibels);
this.currentDecibels = decibels;
}
});
this.isRecording = true;
this.status = "录音中...";
} catch (e) {
console.error("启动录音失败:", e);
this.status = "启动录音失败";
this.isRecording = false;
}
},
// 4. 初始化PCM数据缓冲区和切片定时器
const pcmDataList = []; // 临时存储实时PCM数据
let sliceTimer = null;
const SLICE_DURATION = 200; // 200ms切片
const SAMPLES_PER_SLICE = SAMPLE_RATE * (SLICE_DURATION / 1000); // 每切片的采样点数:44100*0.2=8820
// 停止录音
onStopRecord() {
this.stopRecordAndClean();
},
// 5. 修复核心:创建Android short数组(替代废弃的plus.android.newArray
/**
* 兼容版创建Java short数组
* @param {number} length 数组长度
* @returns {JavaObject} Android short[] 对象
*/
function createShortArray(length) {
// 停止录音并清理资源
stopRecordAndClean() {
if (this.isRecording) {
// 停止录音组件
this.$refs.recordFrame.stop();
this.isRecording = false;
this.status = "已停止录音";
}
// 关闭 WebSocket
if (this.ws) {
this.ws.close();
this.ws = null;
}
// 清理音频上下文
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
this.scriptProcessor = null;
this.frameBufferList = [];
}
// 重置状态
this.currentDecibels = 0;
},
// 接收音频帧并处理
frameRecorded({
isLastFrame,
frameBuffer
}) {
// console.log("收到音频帧:", isLastFrame, frameBuffer.length);
// 2. 通过 WebSocket 发送给后端
if (this.ws) {
try {
// 方案1:新版API(推荐)
const ArrayClass = plus.android.importClass("java.lang.reflect.Array");
const ShortClass = plus.android.importClass("java.lang.Short").TYPE;
return ArrayClass.newInstance(ShortClass, length);
this.ws.send({
data: frameBuffer
});
} catch (e) {
// 方案2:兼容旧版(备用)
const ShortArray = plus.android.createInstance("short[]", [length]);
return ShortArray;
console.error("发送音频帧失败:", e);
}
}
// 6. 开始录音(仅一次启动)
audioRecord.startRecording();
console.log("Android原生录音启动,开始实时PCM捕获");
// 7. 持续读取PCM数据到内存(修复newArray报错)
const readPCMData = () => {
console.log('持续读取PCM数据到内存(修复newArray报错)', audioRecord.getRecordingState(), AudioRecord.RECORDSTATE_RECORDING);
if (!audioRecord || audioRecord.getRecordingState() !== AudioRecord.RECORDSTATE_RECORDING)
return;
// 修复:使用兼容版创建short数组
const shortArray = createShortArray(BUFFER_SIZE);
// 读取麦克风的PCM数据
const readSize = audioRecord.read(shortArray, 0, BUFFER_SIZE);
console.log('读取麦克风的PCM数据', readSize);
if (readSize > 0) {
// 将原生short数组转为JS数组(兼容写法)
const pcmChunk = [];
for (let i = 0; i < readSize; i++) {
try {
// 新版:通过get获取数组元素
pcmChunk.push(plus.android.getArrayElement(shortArray, i));
} catch (e) {
// 旧版:兼容getAttribute
pcmChunk.push(plus.android.getAttribute(shortArray, i));
}
}
pcmDataList.push(...pcmChunk); // 追加到总数据池
}
// 递归读取(保持实时性,避免阻塞)
setTimeout(readPCMData, 0); // 改用setTimeout,避免requestAnimationFrame的帧率限制
};
setTimeout(() => {
readPCMData();
}, 1000)
// 8. 每200ms截取一次PCM切片
sliceTimer = setInterval(() => {
// console.log(' 每200ms截取一次PCM切片',pcmDataList.length, SAMPLES_PER_SLICE);
if (pcmDataList.length >= SAMPLES_PER_SLICE) {
// 截取200ms对应的PCM数据(8820个采样点)
const slicePCM = pcmDataList.splice(0, SAMPLES_PER_SLICE);
// 处理切片(如封装为WAV、上传、分析等)
handlePCMSlice(slicePCM, SAMPLE_RATE);
console.log(`截取200ms PCM切片,采样点数:${slicePCM.length}`);
}
}, SLICE_DURATION);
// var r = plus.audio.getRecorder();
// // 初始化录音实例(WAV格式,含PCM原始数据)
// r.record( {filename:"_doc/audio/"}, function () {
// console.log( "Audio record success!" );
// }, function ( e ) {
// console.log( "Audio record failed: " + e.message );
// } );
// setTimeout(() => {
// r.stop();
// }, 2000)
},
// 停止录音
stopRecord() {
if (this.recorder) {
this.recorder.stop();
this.recorder = null;
}
if (this.recordTimer) {
clearInterval(this.recordTimer);
this.recordTimer = null;
}
this.status = "录音停止";
}
}
// 监听分贝值
onCurrentDecibels(decibels) {
// this.currentDecibels = decibels.toFixed(2);
// console.log("当前分贝:", this.currentDecibels);
},
// 录音停止回调
stopIt(base64) {
this.stopRecordAndClean();
console.log("录音停止,最终音频Base64", base64?.substring(0, 50) + "...");
},
},
};
</script>
<style scoped lang="scss">
// 全局容器
.ai-chat-container {
width: 100%;
height: 100vh;
background-color: #f5f7fa;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom); // 适配iOS底部安全区
<style scoped>
.container {
padding: 20rpx;
}
// 聊天列表
.chat-list {
width: 100%;
padding: 20rpx 16rpx;
box-sizing: border-box;
// 消息项通用样式
.chat-item {
display: flex;
margin-bottom: 30rpx;
max-width: 85%;
// 头像通用样式
.chat-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.iconfont {
font-size: 40rpx;
}
}
// 消息内容
.chat-content {
.content-text {
padding: 24rpx 28rpx;
border-radius: 20rpx;
line-height: 1.6;
font-size: 32rpx;
color: #333;
word-wrap: break-word;
word-break: break-all;
}
}
}
// 系统消息
.system {
margin: 0 auto 30rpx;
.chat-avatar {
background-color: #e8f4f8;
.iconfont {
color: #4299e1;
}
}
.chat-content {
.content-text {
background-color: #e8f4f8;
color: #2d3748;
border-radius: 16rpx;
}
}
}
// 用户消息(右对齐)
.user {
margin-left: auto;
flex-direction: row-reverse;
.chat-avatar {
margin-right: 0;
margin-left: 20rpx;
background-color: #4299e1;
.iconfont {
color: #fff;
}
}
.chat-content {
.content-text {
background-color: #4299e1;
color: #fff;
border-radius: 20rpx 20rpx 8rpx 20rpx;
}
}
}
// AI消息(左对齐)
.ai {
.chat-avatar {
background-color: #9f7aea;
.iconfont {
color: #fff;
}
}
.chat-content {
.content-text {
background-color: #fff;
border-radius: 20rpx 20rpx 20rpx 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
}
}
button {
margin: 10rpx 0;
padding: 15rpx 30rpx;
background: #007aff;
color: white;
border: none;
border-radius: 8rpx;
}
// 输入区域
.input-area {
width: 100%;
background-color: #fff;
padding: 16rpx 20rpx;
box-sizing: border-box;
border-top: 1rpx solid #eee;
// 输入框容器
.input-box {
position: relative;
width: 100%;
margin-bottom: 20rpx;
.input-text {
width: 100%;
min-height: 80rpx;
max-height: 200rpx;
padding: 20rpx 24rpx;
padding-right: 80rpx;
box-sizing: border-box;
border: 1rpx solid #e5e7eb;
border-radius: 40rpx;
font-size: 32rpx;
line-height: 1.5;
background-color: #f9fafb;
resize: none;
outline: none;
}
.clear-btn {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
.iconfont {
font-size: 32rpx;
color: #6b7280;
}
}
}
// 按住发送按钮区域
.send-btn-area {
width: 100%;
display: flex;
justify-content: center;
.hold-send-btn {
width: 80%;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
background-color: #4299e1;
color: #fff;
font-size: 34rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(66, 153, 225, 0.3);
transition: all 0.2s ease;
padding: 0;
margin: 0;
&:active {
background-color: #3182ce;
transform: scale(0.98);
}
.btn-text {
font-size: 34rpx;
}
}
}
button:disabled {
background: #ccc;
}
// 图标占位样式(可替换为自定义图标/图片)
.iconfont {
font-family: "iconfont" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-robot::before {
content: "\e600";
}
.icon-user::before {
content: "\e601";
}
.icon-ai::before {
content: "\e602";
}
.icon-clear::before {
content: "\e603";
.tip {
margin: 15rpx 0;
font-size: 28rpx;
color: #333;
}
</style>
@@ -0,0 +1,70 @@
<template>
<view class="container">
<button @click="onStartRecord">开始录音</button>
<button @click="onStopRecord">停止录音</button>
<view class="tip">{{ status }}</view>
<yao-RecordFrame ref="recordFrame" @onFrameRecorded="frameRecorded" @currentDecibels="onCurrentDecibels"
@onStop="stopIt"></yao-RecordFrame>
</view>
</template>
<script>
export default {
data() {
return {
status: "未录音",
recorder: null, // 录音实例
recordTimer: null, // 定时读取定时器
};
},
onUnload() {
},
methods: {
// 申请录音权限
async applyRecordPermission() {
const res = await uni.requestPermissions({
scope: "scope.record"
});
return res[0].grantStatus === 1; // 1 表示授权成功
},
onStartRecord() {
//开启录音(仅支持两种参数)
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0 //增益值,数字越高音频声音越大 1.0~20.0
})
},
onStopRecord() {
//停止录音
this.$refs.recordFrame.stop();
},
//停止录音
stopIt(base64) {
//base64音频只能在浏览器播放
//也可以base64音频转成文件音频可app播放
// console.log(base64);
},
frameRecorded({
isLastFrame,
frameBuffer
}) {
console.log(isLastFrame, frameBuffer);
if (!isLastFrame) {
this.frameBuffer = frameBuffer;
}
this.isLastFrame = isLastFrame ? 'true' : 'false';
},
onCurrentDecibels(decibels) {
//当前分贝值 最小可测分贝(-80) 最大可测分贝(-30)
console.log("当前分贝:" + decibels)
}
}
};
</script>
<style scoped>
</style>
@@ -0,0 +1,353 @@
<template>
<view class="ai-chat-container">
<!-- 聊天消息列表 -->
<scroll-view class="chat-list" scroll-y scroll-bottom="{{scrollBottom}}"
:style="{height: `calc(100vh - ${inputAreaHeight}px)`}">
<!-- 系统消息 -->
<view class="chat-item system">
<view class="chat-avatar system-avatar">
<text class="iconfont icon-robot"></text>
</view>
<view class="chat-content">
<view class="content-text">你好我是智能助手有什么可以帮你的</view>
</view>
</view>
<!-- 用户消息 -->
<view class="chat-item user">
<view class="chat-content">
<view class="content-text">请问uniapp怎么实现语音流式传输</view>
</view>
<view class="chat-avatar user-avatar">
<text class="iconfont icon-user"></text>
</view>
</view>
<!-- AI回复消息 -->
<view class="chat-item ai">
<view class="chat-avatar ai-avatar">
<text class="iconfont icon-ai"></text>
</view>
<view class="chat-content">
<view class="content-text">
UniApp实现语音流式传输可通过5+ API采集音频帧结合WebSocket分块发送核心步骤包括
1. 初始化录音实例16k采样率单声道PCM
2. 建立WebSocket连接
3. 分块发送音频数据
4. 接收实时ASR结果
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 + 按住发送按钮 -->
<view class="input-area" :style="{height: inputAreaHeight + 'px'}">
<!-- 按住发送按钮底部固定 -->
<view class="send-btn-area">
<button class="hold-send-btn" @touchstart="startRecord">
<text class="btn-text">{{status ? '松开发送' : '按住 说话/发送'}}</text>
</button>
</view>
</view>
<yao-RecordFrame ref="recordFrame" @onFrameRecorded="frameRecorded" @currentDecibels="onCurrentDecibels"
@onStop="stopIt"></yao-RecordFrame>
</view>
</template>
<script>
import {
myApi
} from "@/uni_modules/ty-uts-test";
export default {
data() {
return {
inputAreaHeight: 80,
status: "未录音",
recorder: null, // 录音实例
recordTimer: null, // 定时读取定时器
};
},
onUnload() {
// 页面卸载清理资源
this.stopRecord();
},
methods: {
// 开始录音
async startRecord() {
// 获取电量信息
// uni.getBatteryInfo({
// success(res) {
// console.log(res);
// uni.showToast({
// title: "当前电量:" + res.level + '%',
// icon: 'none'
// });
// }
// })
// * 1、引入方法声明 import { myApi } from "@/uni_modules/uts-api"
// * 2、方法调用
// myApi({
// paramA: true,
// complete: (res) => {
// console.log(res)
// }
// });
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0 //增益值,数字越高音频声音越大 1.0~20.0
})
},
// 停止录音
stopRecord() {
},
frameRecorded({
isLastFrame,
frameBuffer
}) {
// console.log(isLastFrame,frameBuffer);
if (!isLastFrame) {
this.frameBuffer = frameBuffer;
}
this.isLastFrame = isLastFrame ? 'true' : 'false';
},
onCurrentDecibels(decibels) {
//当前分贝值 最小可测分贝(-80) 最大可测分贝(-30)
console.log("当前分贝:" + decibels)
}
}
};
</script>
<style scoped lang="scss">
// 全局容器
.ai-chat-container {
width: 100%;
height: 100vh;
background-color: #f5f7fa;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom); // 适配iOS底部安全区
}
// 聊天列表
.chat-list {
width: 100%;
padding: 20rpx 16rpx;
box-sizing: border-box;
// 消息项通用样式
.chat-item {
display: flex;
margin-bottom: 30rpx;
max-width: 85%;
// 头像通用样式
.chat-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.iconfont {
font-size: 40rpx;
}
}
// 消息内容
.chat-content {
.content-text {
padding: 24rpx 28rpx;
border-radius: 20rpx;
line-height: 1.6;
font-size: 32rpx;
color: #333;
word-wrap: break-word;
word-break: break-all;
}
}
}
// 系统消息
.system {
margin: 0 auto 30rpx;
.chat-avatar {
background-color: #e8f4f8;
.iconfont {
color: #4299e1;
}
}
.chat-content {
.content-text {
background-color: #e8f4f8;
color: #2d3748;
border-radius: 16rpx;
}
}
}
// 用户消息(右对齐)
.user {
margin-left: auto;
flex-direction: row-reverse;
.chat-avatar {
margin-right: 0;
margin-left: 20rpx;
background-color: #4299e1;
.iconfont {
color: #fff;
}
}
.chat-content {
.content-text {
background-color: #4299e1;
color: #fff;
border-radius: 20rpx 20rpx 8rpx 20rpx;
}
}
}
// AI消息(左对齐)
.ai {
.chat-avatar {
background-color: #9f7aea;
.iconfont {
color: #fff;
}
}
.chat-content {
.content-text {
background-color: #fff;
border-radius: 20rpx 20rpx 20rpx 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
}
}
}
// 输入区域
.input-area {
width: 100%;
background-color: #fff;
padding: 16rpx 20rpx;
box-sizing: border-box;
border-top: 1rpx solid #eee;
// 输入框容器
.input-box {
position: relative;
width: 100%;
margin-bottom: 20rpx;
.input-text {
width: 100%;
min-height: 80rpx;
max-height: 200rpx;
padding: 20rpx 24rpx;
padding-right: 80rpx;
box-sizing: border-box;
border: 1rpx solid #e5e7eb;
border-radius: 40rpx;
font-size: 32rpx;
line-height: 1.5;
background-color: #f9fafb;
resize: none;
outline: none;
}
.clear-btn {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
.iconfont {
font-size: 32rpx;
color: #6b7280;
}
}
}
// 按住发送按钮区域
.send-btn-area {
width: 100%;
display: flex;
justify-content: center;
.hold-send-btn {
width: 80%;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
background-color: #4299e1;
color: #fff;
font-size: 34rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(66, 153, 225, 0.3);
transition: all 0.2s ease;
padding: 0;
margin: 0;
&:active {
background-color: #3182ce;
transform: scale(0.98);
}
.btn-text {
font-size: 34rpx;
}
}
}
}
// 图标占位样式(可替换为自定义图标/图片)
.iconfont {
font-family: "iconfont" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-robot::before {
content: "\e600";
}
.icon-user::before {
content: "\e601";
}
.icon-ai::before {
content: "\e602";
}
.icon-clear::before {
content: "\e603";
}
</style>