JLBA202505130001_关于吉林银行新建AI智能培训系统的需求_sit前检查功能
This commit is contained in:
@@ -7,7 +7,11 @@
|
||||
"type" : "uni-app:app-android"
|
||||
},
|
||||
{
|
||||
"bundleId" : "cn.com.jlbank.aits",
|
||||
"certificateFile" : "/Users/jlbank/Desktop/tra-app/certificate/ios/开发证书.p12",
|
||||
"certificateProfileFile" : "/Users/jlbank/Desktop/tra-app/certificate/ios/jiaixuedev.mobileprovision",
|
||||
"playground" : "standard",
|
||||
"runSignStatus" : 1,
|
||||
"type" : "uni-app:app-ios"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
import request from '@/api/request'
|
||||
// 积分商城-活动列表
|
||||
export const queryPointsActivityPaging = (data) => {
|
||||
return request({
|
||||
url: '/traapp/traPointsActivity/queryPointsActivityPaging',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
};
|
||||
@@ -75,8 +75,8 @@ function judgeIosPermissionRecord() {
|
||||
console.log('麦克风state', state);
|
||||
switch (state) {
|
||||
case 'authorized':
|
||||
console.log("麦克风权限是否开启");
|
||||
resolve()
|
||||
console.log("麦克风权限已经开启");
|
||||
resolve('authorized')
|
||||
break;
|
||||
case 'denied': // ios拒绝,安卓拒绝或者未授权都是这个
|
||||
if (IsAndEnv) { // 安卓二次判定
|
||||
@@ -94,10 +94,10 @@ function judgeIosPermissionRecord() {
|
||||
// 前往权限设置页面
|
||||
uni.openAppAuthorizeSetting();
|
||||
}
|
||||
reject()
|
||||
reject(new Error('前往权限设置页面用户点击了否'))
|
||||
break;
|
||||
case 'not determined': // 表示尚未请求授权,仅 iOS 会出现。此种情况下引导用户打开系统设置,不展示开关
|
||||
resolve()
|
||||
resolve('not determined')
|
||||
break;
|
||||
case 'config error': // 只有在 App 端时返回
|
||||
resolve()
|
||||
|
||||
@@ -96,12 +96,13 @@
|
||||
import { wsUrl, bottomAreaTopBoundary, horizontalSplitX } from './touch-btn';
|
||||
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
|
||||
import common from '@/common/common';
|
||||
import { onHide } from '@dcloudio/uni-app';
|
||||
import { onHide, onLaunch } from '@dcloudio/uni-app';
|
||||
import { getPhoneEnvBool, getToken } from '@/common/common.js';
|
||||
import { startAudioRecord, stopAudioRecord } from '@/uni_modules/ty-recording';
|
||||
import { startAudioRecord, stopAudioRecord,preRequestRecordPermission } from '@/uni_modules/ty-recording';
|
||||
import { ProtocolCodec, MessageType, ControlCommand, ControlCode } from '@/pages/sparring/js/ProtocolCodec';
|
||||
import permissionApi from '@/common/permission';
|
||||
|
||||
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
const ws = ref(null);
|
||||
const status = ref<StatusType>('voice');
|
||||
@@ -164,9 +165,13 @@
|
||||
const inputText = ref('');
|
||||
let startTime = 0;
|
||||
// 开启语音
|
||||
const startRecord = async (obj) => {
|
||||
const startRecord = async (obj: { touches: any[]; }) => {
|
||||
try {
|
||||
await permissionApi.judgeIosPermission('record');
|
||||
const state = await permissionApi.judgeIosPermission('record');
|
||||
if(state === 'not determined') { // 苹果专用
|
||||
preRequestRecordPermission(() => {})
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('麦克风权限申请失败:', e);
|
||||
return;
|
||||
@@ -174,54 +179,61 @@
|
||||
const changedTouche = obj.touches[0];
|
||||
touchPos.x = changedTouche.clientX;
|
||||
touchPos.y = changedTouche.clientY;
|
||||
ws.value = uni.connectSocket({
|
||||
url: wsUrl + '?summary=' + getToken(),
|
||||
fail: () => {},
|
||||
success: () => {}
|
||||
});
|
||||
ws.value.onOpen((res) => {
|
||||
console.log('ws已连接');
|
||||
popupRef.value.open();
|
||||
startAudioRecord({
|
||||
onFrame: (pcmData) => {
|
||||
console.log(pcmData.length);
|
||||
ws.value.send({
|
||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
||||
});
|
||||
},
|
||||
onStart: ({ startTime }) => {
|
||||
console.log('开始', startTime);
|
||||
},
|
||||
onStop: () => {
|
||||
console.log('关闭');
|
||||
},
|
||||
onError: ({ errCode, errMsg }) => {
|
||||
if (errCode === 9010001) {
|
||||
console.log(errMsg);
|
||||
}
|
||||
popupRef.value.open();
|
||||
startAudioRecord({
|
||||
onFrame: (pcmData: any) => {
|
||||
console.log('pcmData', pcmData);
|
||||
// console.log('pcmData', pcmData.length);
|
||||
// ws.value.send({
|
||||
// data: ProtocolCodec.pack(MessageType.AUDIO_DATA, pcmData)
|
||||
// });
|
||||
},
|
||||
onStart: (res: any) => {
|
||||
console.log('启动', res);
|
||||
|
||||
},
|
||||
onStop: () => {
|
||||
console.log('关闭');
|
||||
},
|
||||
onError: ({ errCode, errMsg }) => {
|
||||
console.log('onError',errMsg);
|
||||
if (errCode === 9010001) {
|
||||
console.log(errMsg);
|
||||
}
|
||||
});
|
||||
});
|
||||
ws.value.onError((err) => {
|
||||
console.log('err', err);
|
||||
});
|
||||
ws.value.onClose((err) => {
|
||||
console.log('onClose', err);
|
||||
});
|
||||
ws.value.onMessage((res) => {
|
||||
try {
|
||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
console.log('来消息了', msgType, body);
|
||||
if (msgType === MessageType.TEXT_MESSAGE) {
|
||||
tempText.value = '';
|
||||
allText.value = allText.value + body;
|
||||
} else if (msgType === MessageType.TIP_MESSAGE) {
|
||||
tempText.value = body;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('后端发来的信息有问题');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ws.value = uni.connectSocket({
|
||||
// url: wsUrl + '?summary=' + getToken(),
|
||||
// fail: () => {},
|
||||
// success: () => {}
|
||||
// });
|
||||
// ws.value.onOpen((res) => {
|
||||
// console.log('ws已连接');
|
||||
// popupRef.value.open();
|
||||
|
||||
// });
|
||||
// ws.value.onError((err) => {
|
||||
// console.log('err', err);
|
||||
// });
|
||||
// ws.value.onClose((err) => {
|
||||
// console.log('onClose', err);
|
||||
// });
|
||||
// ws.value.onMessage((res) => {
|
||||
// try {
|
||||
// const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
// console.log('来消息了', msgType, body);
|
||||
// if (msgType === MessageType.TEXT_MESSAGE) {
|
||||
// tempText.value = '';
|
||||
// allText.value = allText.value + body;
|
||||
// } else if (msgType === MessageType.TIP_MESSAGE) {
|
||||
// // tempText.value = body;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.log('后端发来的信息有问题');
|
||||
// }
|
||||
// });
|
||||
};
|
||||
// 关掉遮罩层
|
||||
const closePopup = () => {
|
||||
|
||||
+10
-3
@@ -420,6 +420,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/pointsMall/pointsMall",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分商城",
|
||||
"app-plus": {
|
||||
"titleNView": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/course/index",
|
||||
"style": {
|
||||
@@ -605,9 +614,7 @@
|
||||
"titleView": false,
|
||||
"bounce": "none",
|
||||
"softinputNavBar": "none"
|
||||
},
|
||||
"rpxCalcMaxDeviceWidth": 600,
|
||||
"rpxCalcBaseDeviceWidth": 375
|
||||
}
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<!-- 热门课程分模块标题 -->
|
||||
<view class="module_title_div" v-if="class_list.length !== 0">
|
||||
<view class="module_title_div" >
|
||||
<view class="icon icon_1">
|
||||
<image class="img" :src="icon"></image>
|
||||
</view>
|
||||
<view class="text font_pf">{{ title }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门课程 -->
|
||||
<view class="hot_class_div">
|
||||
<view class="no-data-style" v-if="class_list.length === 0">
|
||||
暂无数据...
|
||||
</view>
|
||||
<scroll-view class="scroll-view" :scroll-x="true" :show-scrollbar="false">
|
||||
<view class="scroll-view-item" v-for="(item, index) in class_list" :key="index">
|
||||
<!-- 角标 -->
|
||||
@@ -18,7 +22,7 @@
|
||||
<uv-skeletons :loading="!item.id" avatar avatar-shape="square" :title="false" :skeleton="skeleton">
|
||||
<view class="item" @click="goto_course_detail(item)">
|
||||
<view class="title ellipsis-text">
|
||||
{{ item.nama }}
|
||||
{{ item.name}}
|
||||
</view>
|
||||
<view class="content">
|
||||
<view class="left">
|
||||
@@ -34,7 +38,7 @@
|
||||
<view class="note ellipsis-text">更新:{{ (item.mtTime || '').substr(0, 10) }}</view>
|
||||
<view class="note ellipsis-text">已学:{{ item.accmCnt }}</view>
|
||||
<view class="button_div">
|
||||
<view class="button">去学习</view>
|
||||
<view class="button">{{item.type === '00'?'去学习':'去陪练'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -145,8 +149,6 @@
|
||||
common.navigateTo('/pages/sparring/detail?traId=' + id);
|
||||
}
|
||||
|
||||
// todo
|
||||
//
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -156,6 +158,13 @@
|
||||
// 热门课程等三个横向滑动模块的样式
|
||||
.hot_class_div {
|
||||
margin-left: $bg-margin-left;
|
||||
.no-data-style{
|
||||
text-align: center;
|
||||
line-height: 80rpx;
|
||||
min-height: 80rpx;
|
||||
color: #666666;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.scroll-view {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
@@ -163,8 +163,10 @@
|
||||
onShow,
|
||||
onUnload,
|
||||
onHide,
|
||||
onBackPress
|
||||
onBackPress,
|
||||
onLaunch
|
||||
} from '@dcloudio/uni-app';
|
||||
|
||||
import {
|
||||
get_base_url,
|
||||
goto_login_fun
|
||||
|
||||
@@ -50,22 +50,12 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- 滚动通知 -->
|
||||
<view class="notice_div">
|
||||
<!-- <uv-notice-bar
|
||||
v-show="notice_text.length"
|
||||
:text="notice_text"
|
||||
:bgColor="null"
|
||||
color="rgba(255,255,255,0.7)"
|
||||
:speed="40"
|
||||
direction="column"
|
||||
></uv-notice-bar> -->
|
||||
</view>
|
||||
<view class="notice_div"></view>
|
||||
<!-- 学习时长和飞天兔子,为了挡住部分兔子只能多一个盒子 -->
|
||||
<view class="study_duration_and_profile_div">
|
||||
<!-- 飞天兔子 -->
|
||||
<view class="profile_div">
|
||||
<image :src="mascotInfo?.imgAddr || ''" alt="" class="profile" mode="widthFix"></image>
|
||||
<!-- <image src="/static/images/index/fttz.png" class="profile"></image> -->
|
||||
</view>
|
||||
<!-- 学习时长 -->
|
||||
<view class="study_duration_div">
|
||||
@@ -79,7 +69,7 @@
|
||||
:decimals="1"
|
||||
ref="stdtTmLenRef"
|
||||
></uv-count-to>
|
||||
h
|
||||
<text>h</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item">
|
||||
@@ -103,7 +93,6 @@
|
||||
:useEasing="true"
|
||||
ref="accmExamCntRef"
|
||||
></uv-count-to>
|
||||
|
||||
<span class="number">次</span>
|
||||
</view>
|
||||
</view>
|
||||
@@ -123,7 +112,6 @@
|
||||
<image class="icon" src="@/static/images/index/navigation_question_answering.png"></image>
|
||||
<view class="title">AI问答</view>
|
||||
</view>
|
||||
|
||||
<view class="item" @click="common.switchTab('/pages/sparring/index')">
|
||||
<image class="icon" src="@/static/images/index/navigation_ai.png"></image>
|
||||
<view class="title">AI陪练</view>
|
||||
@@ -153,7 +141,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <classScroll :ref="(el) => (classScrollRef[0] = el)" name="hot_class"></classScroll> -->
|
||||
<classScroll :ref="(el) => (classScrollRef[1] = el)" name="recommend_class"></classScroll>
|
||||
<classScroll :ref="(el) => (classScrollRef[2] = el)" name="new_class"></classScroll>
|
||||
<toDoCom></toDoCom>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
border-radius: 16rpx !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed_search_div {
|
||||
padding: calc(20rpx + var(--status-bar-height)) 15rpx 34rpx 30rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
const socketStore = useSocketStore();
|
||||
const { taskSocket } = storeToRefs(socketStore);
|
||||
const show_password = ref<Boolean>(false);
|
||||
const account = ref<string>('');
|
||||
const password = ref<string>('');
|
||||
const account = ref<string>('admin');
|
||||
const password = ref<string>('123456');
|
||||
const read_text_state = ref<Array<unknown>>([]);
|
||||
const imeHeight = ref('0px');
|
||||
const back_state = ref(true);
|
||||
|
||||
@@ -136,9 +136,15 @@
|
||||
title="我的预览"
|
||||
:isLink="true"
|
||||
:clickable="false"
|
||||
:border="false"
|
||||
@click="common.navigateTo('/pages/preview/index')"
|
||||
></uv-cell>
|
||||
<uv-cell
|
||||
title="积分兑换"
|
||||
:isLink="true"
|
||||
:clickable="false"
|
||||
:border="false"
|
||||
@click="common.navigateTo('/pages/pointsMall/pointsMall?points='+statistics_data['currentPoints'])"
|
||||
></uv-cell>
|
||||
</uv-cell-group>
|
||||
</view>
|
||||
<view class="cell_div">
|
||||
|
||||
@@ -71,12 +71,7 @@
|
||||
<view class="points-time">获得时间:{{ detailedInfo.obtainTm }}</view>
|
||||
</view>
|
||||
<view class="points-button-div">
|
||||
<uv-button
|
||||
class="button"
|
||||
text="关闭"
|
||||
:custom-style="customStyle"
|
||||
@click="closeDetailed()"
|
||||
></uv-button>
|
||||
<uv-button class="button" text="关闭" :custom-style="customStyle" @click="closeDetailed()"></uv-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
@@ -94,13 +89,13 @@
|
||||
import { getUserInfo } from '@/common/common';
|
||||
const buttonColor = '';
|
||||
const customStyle = {
|
||||
background:"linear-gradient( 0deg, RGBA(8, 9, 4, 1) 0%, RGBA(100, 100, 100, .5) 100%)",
|
||||
color:'#fff',
|
||||
background: 'linear-gradient( 0deg, RGBA(8, 9, 4, 1) 0%, RGBA(100, 100, 100, .5) 100%)',
|
||||
color: '#fff',
|
||||
width: '290rpx',
|
||||
height: '100rpx',
|
||||
border: '4rpx solid #EEDA9B',
|
||||
borderRadius: '180rpx'
|
||||
}
|
||||
};
|
||||
const popup = ref(null);
|
||||
const user_info = reactive({
|
||||
userName: '',
|
||||
@@ -114,40 +109,12 @@
|
||||
const fetchFunction = (params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
queryRankWall().then(({ body }) => {
|
||||
let originalData = body;
|
||||
const scoreRankBadges = originalData['积分段位徽章'];
|
||||
|
||||
// 2. 动态合并除“积分段位徽章”外的所有徽章数组
|
||||
const otherBadges = [];
|
||||
// 遍历原始数据的所有键
|
||||
Object.keys(originalData).forEach((key) => {
|
||||
// 排除固定键,其余全部合并
|
||||
if (key !== '积分段位徽章') {
|
||||
// 确保对应值是数组,避免非数组类型导致错误
|
||||
if (Array.isArray(originalData[key])) {
|
||||
otherBadges.push(...originalData[key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// // 处理逻辑:构建二维数组(外层分类列表 + 内层徽章列表)
|
||||
// const twoDimensionalArray = [];
|
||||
// // 遍历每个分类,生成二维数组的每一项
|
||||
// Object.entries(originalData).forEach(([categoryKey, badgeArray]) => {
|
||||
// // 构建外层列表项:包含 name 和子列表 badgeList
|
||||
// const categoryItem = {
|
||||
// name: categoryKey, // 第一维度的 name 是原始键
|
||||
// child: badgeArray // 子列表:直接放入当前分类的所有徽章数据
|
||||
// };
|
||||
// // 将分类项加入二维数组
|
||||
// twoDimensionalArray.push(categoryItem);
|
||||
// });
|
||||
|
||||
uni.$emit('update_me_data', true);
|
||||
resolve({
|
||||
body: [
|
||||
{ name: '段位徽章', child: scoreRankBadges },
|
||||
{ name: '节气徽章', child: otherBadges }
|
||||
{ name: '段位徽章', child: body['积分段位徽章'] },
|
||||
{ name: '节气徽章', child: body['节气徽章'] },
|
||||
{ name: '其他徽章', child: body['其他徽章'] }
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -158,14 +125,14 @@
|
||||
});
|
||||
// 显示详细信息
|
||||
const showDetailed = (item) => {
|
||||
if(item.isOpen !== 'Y') return;
|
||||
if (item.isOpen !== 'Y') return;
|
||||
detailedInfo.value = item;
|
||||
popup.value.open();
|
||||
};
|
||||
const closeDetailed = () => {
|
||||
popup.value.close();
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
Object.assign(user_info, getUserInfo());
|
||||
pagingRef.value.reload();
|
||||
@@ -174,7 +141,6 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.popup {
|
||||
.popup-content {
|
||||
width: 100vw;
|
||||
@@ -184,7 +150,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
background-color: rgba(0,0,0,.85);
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
.max-div {
|
||||
position: relative;
|
||||
@@ -198,7 +164,6 @@
|
||||
width: 420rpx;
|
||||
height: 420rpx;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.main-div {
|
||||
@@ -281,7 +246,6 @@
|
||||
gap: 40rpx; /* 调大间距(根据需要调整数值) */
|
||||
padding: 6vw;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
.item {
|
||||
/* 计算宽度:总宽度减去两倍间距后三等分(保证三列) */
|
||||
@@ -377,40 +341,39 @@
|
||||
background-repeat: no-repeat; /* 不重复平铺 */
|
||||
background-color: #fef1dd;
|
||||
}
|
||||
.points-title{
|
||||
.points-title {
|
||||
font-weight: 400;
|
||||
font-size: 54rpx;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
margin: 20rpx 0 60rpx 0;
|
||||
}
|
||||
.points-button-div{
|
||||
.points-button-div {
|
||||
margin-bottom: 200rpx;
|
||||
margin-top: 180rpx;
|
||||
.button{
|
||||
|
||||
.button {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.select-institution-div {
|
||||
position: relative;
|
||||
border-radius: 20rpx;
|
||||
width: 560rpx;
|
||||
height: 130rpx;
|
||||
padding: 12rpx 0;
|
||||
border: 2rpx solid #EEDA9B;
|
||||
background: linear-gradient(150deg, RGBA(86, 82, 73, 1), RGBA(17, 17, 17, .4)) border-box;
|
||||
border: 2rpx solid #eeda9b;
|
||||
background: linear-gradient(150deg, RGBA(86, 82, 73, 1), RGBA(17, 17, 17, 0.4)) border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.points-remark{
|
||||
|
||||
.points-remark {
|
||||
font-weight: 400;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
.points-time{
|
||||
.points-time {
|
||||
font-weight: 400;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<z-paging
|
||||
ref="pagingRef"
|
||||
v-model="data_list"
|
||||
@query="queryList"
|
||||
:fixed="false"
|
||||
class="scroll_div"
|
||||
:auto="true"
|
||||
empty-view-text="暂无学习任务"
|
||||
>
|
||||
<template #top>
|
||||
<nav-bar :is_seat="false"></nav-bar>
|
||||
<view class="fixed_search_div">
|
||||
<view class="back_div" @click="common.navigateBack()">
|
||||
<view class="back-icon"></view>
|
||||
</view>
|
||||
<view class="title font_pf">积分商城</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="eye-area-div">
|
||||
<view class="mascot-div">
|
||||
<image src="@/static/images/pointsMall/mascot.png" class="img" mode="widthFix"></image>
|
||||
</view>
|
||||
<view class="text-tip-div">
|
||||
<view class="fl1"></view>
|
||||
<view class="now-points-title">当前积分</view>
|
||||
<view class="now-points">{{points}}</view>
|
||||
<view class="now-points-target" @click="common.navigateTo('/pages/pointsAndRank/points');">积分明细></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="main-list-div">
|
||||
<view class="item" v-for="item in data_list" @click="common.navigateTo('/pages/pointsRedemption/index?id=' + item.actId);">
|
||||
<view class="product-image">
|
||||
<image-preview :src="item.ossAddr" class="img_100"></image-preview>
|
||||
</view>
|
||||
<view class="product-title-div">
|
||||
<view class="fl1 title ellipsis-text">
|
||||
{{ item.actName }}
|
||||
</view>
|
||||
<view class="img-div">
|
||||
<image :src="numImg" mode="widthFix" class="img"></image>
|
||||
</view>
|
||||
<view class="number">
|
||||
{{ item.excPoints }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-time">
|
||||
{{ item.endTm.substr(0,10) }}截止
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</z-paging>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import numImg from '@/static/images/pointsMall/num.png';
|
||||
import common from '@/common/common';
|
||||
import { queryPointsActivityPaging } from '@/api/pointsMall';
|
||||
import useZpaging from '@/composables/useZpaging.js';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { ref } from 'vue';
|
||||
const { pagingRef, queryList, data_list, reload, search, total, clear } = useZpaging({
|
||||
fetchFunction:queryPointsActivityPaging
|
||||
});
|
||||
const points = ref(0);
|
||||
onLoad((e) => {
|
||||
points.value = e.points??0;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.scroll_div{
|
||||
background-color: #efefef;
|
||||
}
|
||||
// 主要列表区域
|
||||
.main-list-div {
|
||||
padding: 18rpx 8rpx;
|
||||
border-radius: 20rpx;
|
||||
height: calc(100% - 240rpx);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.item{
|
||||
width: calc(50% - 24rpx);
|
||||
margin: 12rpx 12rpx;
|
||||
border-radius: 18rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4rpx 4rpx #c0c0c0;
|
||||
.product-image{
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
box-shadow: 0 4rpx 8rpx #f3f3f3;
|
||||
}
|
||||
.product-title-div{
|
||||
display: flex;
|
||||
padding: 10rpx 20rpx 4rpx 20rpx;
|
||||
.title{
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.number{
|
||||
font-family: DINAlternate, DINAlternate;
|
||||
font-size: 32rpx;
|
||||
color: rgb(230, 38, 44);
|
||||
}
|
||||
.img-div{
|
||||
.img{
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
}
|
||||
.product-time{
|
||||
// background-color: brown;
|
||||
padding: 4rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgb(94, 101, 118);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 上方眼睛区域样式
|
||||
.eye-area-div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding-top: 30rpx;
|
||||
height: 240rpx;
|
||||
background-color: #fff;
|
||||
|
||||
// 左侧百禄形象图
|
||||
.mascot-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.img {
|
||||
width: 180rpx;
|
||||
}
|
||||
}
|
||||
.text-tip-div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 0 20rpx 60rpx;
|
||||
.now-points-title,
|
||||
.now-points-target {
|
||||
font-size: 24rpx;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
.now-points {
|
||||
font-family: DINAlternate, DINAlternate;
|
||||
font-weight: 500;
|
||||
font-size: 66rpx;
|
||||
color: #000000;
|
||||
line-height: 100rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixed_search_div {
|
||||
padding: calc(20rpx + var(--status-bar-height)) 15rpx 34rpx 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
.title {
|
||||
color: #000000;
|
||||
font-size: 34rpx;
|
||||
height: 34rpx;
|
||||
font-weight: 600;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
.back_div {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: var(--status-bar-height);
|
||||
padding: 10rpx;
|
||||
|
||||
.back-icon {
|
||||
position: relative;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-icon::before,
|
||||
.back-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-icon::after {
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
border-top: 4rpx solid #000000;
|
||||
border-left: 4rpx solid #000000;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,18 @@
|
||||
</view>
|
||||
</view>
|
||||
<!-- 按钮 -->
|
||||
<view class="answerTipButton" v-show="tipButtonStatus" @click="clickTipButton">回答提示</view>
|
||||
<view class="answerTipButtonDiv" v-show="tipButtonStatus">
|
||||
<uv-button
|
||||
type="primary"
|
||||
color="#2688f9"
|
||||
shape="circle"
|
||||
text="回答提示"
|
||||
:loading="sendLoading"
|
||||
:disabled="!!talkTipList"
|
||||
:custom-style="customStyle"
|
||||
@click="clickTipButton"
|
||||
></uv-button>
|
||||
</view>
|
||||
<!-- ai提示 -->
|
||||
<view class="talk-tip-list-div">
|
||||
<view class="talk-tip-list">
|
||||
@@ -23,22 +34,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const textText =
|
||||
'你核心的诉求现在非常明确了:文字必须放在单个不拆分的盒子里(这样才能滑动查看上方内容),同时要实现两个效果 —— 一是文字整体越往上颜色越淡,二是底部新增文字时,内容能缓慢向上挤(有平滑动画),且不能用 JS 拆分文字行。我现在提供的方案完全满足你的要求:无 JS 拆分、单个文字盒子、支持滑动查看上方内容、文字越往上越淡、新增文字缓慢上挤动画,所有效果都通过 CSS 核心实现,JS 仅做 “追加文字” 的极简操作(无任何拆分逻辑)。';
|
||||
|
||||
import loading from '@/components/loading';
|
||||
import { ref, nextTick, onMounted } from 'vue';
|
||||
const talkTextList = ref('');
|
||||
const talkTipList = ref('');
|
||||
const sendLoading = ref(false);
|
||||
const hidden = ref(false);
|
||||
const tipButtonStatus = ref(false);
|
||||
const aaaaaaa = ref(false);
|
||||
let clearTimer // 关闭清理的定时器
|
||||
const emits = defineEmits(['clickTipButton'])
|
||||
|
||||
|
||||
const emits = defineEmits(['clickTipButton']);
|
||||
const customStyle = {
|
||||
height: '60rpx',
|
||||
width: '200rpx'
|
||||
};
|
||||
// 点击回答提示按钮
|
||||
const clickTipButton = () => {
|
||||
emits('clickTipButton')
|
||||
if (talkTipList.value || sendLoading.value) return;
|
||||
sendLoading.value = true;
|
||||
console.log('1111111');
|
||||
emits('clickTipButton');
|
||||
};
|
||||
// 添加文字
|
||||
const addText = (text) => {
|
||||
@@ -46,39 +60,38 @@
|
||||
};
|
||||
// 添加提示
|
||||
const addTip = (text) => {
|
||||
if (talkTipList.value === '') {
|
||||
sendLoading.value = false;
|
||||
}
|
||||
talkTipList.value = talkTipList.value + text;
|
||||
};
|
||||
// 展示提示框
|
||||
const showTipButton = () => {
|
||||
tipButtonStatus.value = true;
|
||||
};
|
||||
|
||||
|
||||
// 清除全部数据
|
||||
const clear = () => {
|
||||
hidden.value = true;
|
||||
// clearTimer = setTimeout(() => {
|
||||
nextTick(() => {
|
||||
tipButtonStatus.value = false;
|
||||
talkTextList.value = '';
|
||||
talkTipList.value = '';
|
||||
hidden.value = false;
|
||||
clearTimer = null
|
||||
});
|
||||
// }, 300);
|
||||
nextTick(() => {
|
||||
tipButtonStatus.value = false;
|
||||
talkTextList.value = '';
|
||||
talkTipList.value = '';
|
||||
sendLoading.value = false;
|
||||
hidden.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 控制状态变化
|
||||
const ctrlTalk = (state) => {
|
||||
console.log('控制状态变化', state);
|
||||
if(state === 'textStart') {
|
||||
clear() // 清除全部数据
|
||||
} else if(state === 'textEnd') {
|
||||
showTipButton() // 展示提示框
|
||||
if (state === 'textStart') {
|
||||
clear(); // 清除全部数据
|
||||
} else if (state === 'textEnd') {
|
||||
showTipButton(); // 展示提示框
|
||||
}
|
||||
|
||||
};
|
||||
onMounted(() => {
|
||||
});
|
||||
onMounted(() => {});
|
||||
defineExpose({ addText, addTip, clear, ctrlTalk });
|
||||
</script>
|
||||
|
||||
@@ -125,16 +138,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.answerTipButtonDiv {
|
||||
margin: 20rpx 0 20rpx 0;
|
||||
}
|
||||
.answerTipButton {
|
||||
margin: 20rpx 0 20rpx 0;
|
||||
width: 190rpx;
|
||||
width: 200rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 60rpx;
|
||||
line-height: 60rpx;
|
||||
background-color: #2688f9;
|
||||
text-align: center;
|
||||
padding-right: 10rpx;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
position: relative;
|
||||
.loading-div {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
top: calc(50% - 4rpx);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.talk-tip-list-div {
|
||||
|
||||
@@ -64,13 +64,15 @@
|
||||
@click="previewFile(item)"> {{ item.dataName }}.{{ item.dataSuffix }} </view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="body-bottom" v-if="detailInfo.traInterTyp === '01'">
|
||||
<view class="body-bottom">
|
||||
<!-- 2026年2月28日下午3点,杨航同意这么改的 -->
|
||||
<!-- v-if="detailInfo.traInterTyp === '01'" -->
|
||||
<view class="body-bottom-btn" @click="toChooseRole('01')">去练习</view>
|
||||
<view class="body-bottom-btn" @click="toChooseRole('02')">去考试</view>
|
||||
</view>
|
||||
<view class="body-bottom" v-else>
|
||||
<!-- <view class="body-bottom" v-else>
|
||||
<view class="body-bottom-btn no-margin" @click="toChooseRole('preview')">去考试</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -110,9 +110,6 @@ export default function useWebSocket(aaas = null, options = {}) {
|
||||
break;
|
||||
case MessageType.CONTROL_CMD:
|
||||
// 控制消息
|
||||
console.log('控制消息', body, body.type === ControlCode.AI_ANSWER_BEGIN);
|
||||
console.log('控制消息2', ControlCode.AI_ANSWER_BEGIN);
|
||||
|
||||
if (body.type === ControlCode.FINISH_PUSH) { // 开始播放,记录当前播放的语音id
|
||||
console.log('开始播放,记录当前播放的语音id', body.lock);
|
||||
setPlayNumber(body.lock)
|
||||
@@ -125,7 +122,6 @@ export default function useWebSocket(aaas = null, options = {}) {
|
||||
}
|
||||
break;
|
||||
case MessageType.TEXT_MESSAGE:
|
||||
console.log('TEXT_MESSAGE', body);
|
||||
addTalkText(body)
|
||||
break;
|
||||
case MessageType.TIP_MESSAGE:
|
||||
|
||||
+12
-15
@@ -19,24 +19,13 @@
|
||||
{{ dataInfo.sceneDesc }}
|
||||
</view>
|
||||
<view class="close-btn" @click="closeTalking"></view>
|
||||
<view
|
||||
v-if="isVideo && !isIOS"
|
||||
style="
|
||||
width: 240rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 150rpx;
|
||||
"
|
||||
>
|
||||
<view if="isVideo && !isIOS" class="canvas-div">
|
||||
<canvas
|
||||
canvas-id="firstCanvas"
|
||||
style="width: 240rpx; height: 360rpx; position: absolute; left: 0; top: 0"
|
||||
></canvas>
|
||||
</view>
|
||||
<talkList ref="talkListRef" class="talk-list" @clickTipButton="clickTipButton"></talkList>
|
||||
<talkList v-if="dataInfo.traType === '01'" ref="talkListRef" class="talk-list" @clickTipButton="clickTipButton"></talkList>
|
||||
</view>
|
||||
<talk-microphone ref="talkMicrophoneRef" @noPermission="microphoneNoPermission"></talk-microphone>
|
||||
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
|
||||
@@ -85,7 +74,6 @@
|
||||
|
||||
// 点击回答提示
|
||||
const clickTipButton = () => {
|
||||
console.log('点击回答提示', {type:ControlCode.AI_CLUE});
|
||||
send(ProtocolCodec.pack(MessageType.CONTROL_CMD, {type:ControlCode.AI_CLUE}))
|
||||
}
|
||||
|
||||
@@ -271,7 +259,7 @@
|
||||
onLoad((e) => {
|
||||
dataInfo.traId = e.traId ?? '';
|
||||
dataInfo.chaId = e.chaId ?? '';
|
||||
dataInfo.traType = '02'; // 陪练类型 01-练习 02-考试
|
||||
dataInfo.traType = e.type; // 陪练类型 01-练习 02-考试
|
||||
dataInfo.traInterTyp = e.traInterTyp ?? '02'; // 陪练交互类型 01 对话 02 语音 03 视频
|
||||
// dataInfo.traInterType = '02'; // 陪练交互类型 01 对话 02 语音 03 视频
|
||||
//#ifdef APP-PLUS
|
||||
@@ -337,6 +325,15 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.canvas-div{
|
||||
width: 240rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 150rpx;
|
||||
}
|
||||
.talk-list {
|
||||
left: 30rpx;
|
||||
top: calc(150rpx + var(--status-bar-height) + 200rpx + 180rpx);
|
||||
|
||||
@@ -354,6 +354,14 @@
|
||||
"titleNView": false
|
||||
}
|
||||
}
|
||||
},{
|
||||
"path": "pages/pointsMall/pointsMall",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分商城",
|
||||
"app-plus": {
|
||||
"titleNView": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 引用 iOS 系统库,示例如下:
|
||||
* import { UIDevice } from "UIKit";
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
|
||||
|
||||
/* 引入 unierror.uts 文件中定义的变量 */
|
||||
import { MyApiFailImpl } from '../unierror';
|
||||
|
||||
/**
|
||||
* 引入三方库
|
||||
* [可选实现,按需引入]
|
||||
*
|
||||
* 在 iOS 平台引入三方库有以下两种方式:
|
||||
* 1、通过引入三方库framework 或者.a 等方式,需要将 .framework 放到 ./Frameworks 目录下,将.a 放到 ./Libs 目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-平台原生配置)
|
||||
* 2、通过 cocoaPods 方式引入,将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
|
||||
*
|
||||
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import:
|
||||
* 示例:import { LottieLoopMode } from 'Lottie'
|
||||
*/
|
||||
|
||||
/**
|
||||
* UTSiOS 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 异步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApi } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApi({
|
||||
* paramA: false,
|
||||
* complete: (res) => {
|
||||
* console.log(res)
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*/
|
||||
export const myApi : MyApi = function (options : MyApiOptions) {
|
||||
|
||||
if (options.paramA == true) {
|
||||
// 返回数据
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: true,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
options.success?.(res);
|
||||
options.complete?.(res);
|
||||
|
||||
} else {
|
||||
// 返回错误
|
||||
let failResult = new MyApiFailImpl(9010001);
|
||||
options.fail?.(failResult)
|
||||
options.complete?.(failResult)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步方法
|
||||
*
|
||||
* uni-app项目中(vue/nvue)调用示例:
|
||||
* 1、引入方法声明 import { myApiSync } from "@/uni_modules/uts-api"
|
||||
* 2、方法调用
|
||||
* myApiSync(true);
|
||||
*
|
||||
*/
|
||||
export const myApiSync : MyApiSync = function (paramA : boolean) : MyApiResult {
|
||||
// 返回数据,根据插件功能获取实际的返回值
|
||||
const res : MyApiResult = {
|
||||
fieldA: 85,
|
||||
fieldB: paramA,
|
||||
fieldC: 'some message'
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更多插件开发的信息详见:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html
|
||||
*/
|
||||
|
||||
@@ -96,4 +96,5 @@ export function stopAudioRecord() : void {
|
||||
}
|
||||
}
|
||||
}
|
||||
export function preRequestRecordPermission(callback?: (granted: boolean) => void) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { StartAudioRecordOptions } from '../interface.uts';
|
||||
import { StartAudioRecordFailImpl, AudioErrorCode } from '../unierror.uts';
|
||||
|
||||
|
||||
import { UTSiOS } from "DCloudUTSFoundation"
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer, AVAudioCommonFormat } from "AVFoundation";
|
||||
import { NSData, NSMutableData } from "Foundation";
|
||||
import { Int, Int16 } from 'Swift';
|
||||
|
||||
// @argumentLabel("forBus")
|
||||
/**
|
||||
* 音频采集状态(全局变量,保证生命周期)
|
||||
*/
|
||||
let isRecording = false;
|
||||
/**
|
||||
* 音频引擎实例(全局变量,避免异步回调中被释放)
|
||||
*/
|
||||
let audioEngine: AVAudioEngine | null = null;
|
||||
/**
|
||||
* 音频输入节点(全局变量,避免被释放)
|
||||
*/
|
||||
let inputNode: AVAudioInputNode | null = null;
|
||||
// 全局复用缓冲区(避免每次创建新数组)
|
||||
let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
// 新增:缓存录制的所有Int16 PCM数据
|
||||
let recordedPCMData: UInt8[] = [];
|
||||
// 3. 保存完整PCM数据到文件
|
||||
const sampleRate = 16000.0; // 和采集时的目标格式一致
|
||||
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
||||
|
||||
/**
|
||||
* 启动音频采集(iOS端核心实现)
|
||||
* @param options 采集配置及回调
|
||||
*/
|
||||
@UTSJS.keepAlive
|
||||
export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
// 避免重复启动
|
||||
console.log('避免重复启动', isRecording);
|
||||
// if (isRecording) return;
|
||||
|
||||
try {
|
||||
recordedPCMData = [];
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.playAndRecord,
|
||||
mode=AVAudioSession.Mode.spokenAudio,
|
||||
options= AVAudioSession.CategoryOptions.duckOthers // 可选:其他音频静音
|
||||
));
|
||||
// UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.playAndRecord));
|
||||
// UTSiOS.try(audioSession.setMode(AVAudioSession.Mode.spokenAudio));
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
// 闭包内处理权限结果(异步执行)
|
||||
if (granted) {
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
console.log("硬件原生采样率:", inputFormat.sampleRate);
|
||||
console.log("硬件原生声道数:", inputFormat.channelCount);
|
||||
console.log("硬件原生格式:", inputFormat.commonFormat);
|
||||
|
||||
const targetFormat = AVAudioFormat.init(
|
||||
commonFormat= AVAudioCommonFormat.pcmFormatInt16, // 16位整型(文档枚举值)
|
||||
sampleRate= inputFormat.sampleRate, // 采样率(Double类型,符合文档定义)
|
||||
channels= inputFormat.channelCount, // 声道数(AVAudioChannelCount)
|
||||
interleaved= false // 是否交错(Bool,单声道无影响)
|
||||
);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 2048, // 缓冲区大小,常用 1024/2048
|
||||
format= targetFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const frameLen = buffer.frameLength as number;
|
||||
const bufferListRawPtr = buffer.audioBufferList;
|
||||
const audioBufferList = bufferListRawPtr.pointee; // 拿到结构体本身
|
||||
const audioBufferPtr = audioBufferList.mBuffers; // AudioBuffer指针
|
||||
// // console.log(audioBufferPtr.mData);
|
||||
const mData = audioBufferPtr.mData;
|
||||
|
||||
// const mDataByteSize:Int = 9600; // 你之前看到的字节数
|
||||
// const uint8Ptr = mData!.bindMemory(to= UInt8.self, capacity= mDataByteSize);
|
||||
// const pcmNormalArray: number[] = [];
|
||||
// for (let i:Int = 0; i < mDataByteSize; i++) {
|
||||
// // 直接读取指针的第i个字节,转为0-255的普通数字
|
||||
// pcmNormalArray.push(uint8Ptr[i] as number);
|
||||
// }
|
||||
// const halfLength:Int = 4800;
|
||||
// const firstHalf = pcmNormalArray.slice(0, halfLength);
|
||||
// const secondHalf = pcmNormalArray.slice(halfLength, mDataByteSize);
|
||||
// options.onFrame?.(firstHalf);
|
||||
// options.onFrame?.(secondHalf);
|
||||
|
||||
|
||||
// options.onFrame?.(pcmNormalArray);
|
||||
// let uint8 = new Uint8Array([4, 5, 8, 12]);
|
||||
// let array = Array.from(uint8);
|
||||
// console.log(array);
|
||||
// options.onFrame?.(array);
|
||||
// const audioBuffer = audioBufferPtr.pointee; // 拿到AudioBuffer结构体
|
||||
// console.log("音频数据字节数:", audioBuffer.mDataByteSize); // 比如4096(2048帧×2字节)
|
||||
// console.log("声道数:", audioBuffer.mNumberChannels); // 1
|
||||
// console.log("二进制数据指针:", audioBuffer.mData); // 指向16位PCM数据的内存地址
|
||||
|
||||
// const int16Ptr = buffer.int16ChannelData;
|
||||
// if (int16Ptr != null && frameLen > 0) {
|
||||
// const dataPtr = int16Ptr![0];
|
||||
// const CHUNK_FRAME_SIZE = frameLen / 2; // 16位=2字节/帧 → 512帧/分片
|
||||
// // 关键2:循环拆分 9600 字节为多个小分片
|
||||
// for (let startFrame:number = 0; startFrame < frameLen; startFrame += CHUNK_FRAME_SIZE) {
|
||||
// // 计算当前分片的结束帧(避免越界)
|
||||
// const endFrame = Math.min(startFrame + CHUNK_FRAME_SIZE, frameLen);
|
||||
// let _recordedPCMData: UInt8[] = [];
|
||||
// // 只处理当前分片的帧
|
||||
// for (let i: Int = startFrame as Int; i < endFrame; i++) {
|
||||
// const int16Value = dataPtr[i];
|
||||
// let byte1 = UInt8(int16Value & 0xFF);
|
||||
// let byte2 = UInt8((int16Value >> 8) & 0xFF);
|
||||
// _recordedPCMData.push(byte1);
|
||||
// _recordedPCMData.push(byte2);
|
||||
// }
|
||||
// options.onFrame?.(_recordedPCMData);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
} else {
|
||||
// 权限被拒绝:回调错误
|
||||
isRecording = false;
|
||||
}
|
||||
});
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
// 初始化失败回调错误
|
||||
const err = new StartAudioRecordFailImpl(9010002);
|
||||
options.onError?.(err);
|
||||
// 重置状态
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止音频采集(配套停止逻辑,保证功能完整)
|
||||
* @param options 回调配置
|
||||
*/
|
||||
|
||||
export function stopAudioRecord(): void {
|
||||
console.log('停止音频采集');
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
}
|
||||
const _audioEngine = audioEngine!
|
||||
if (_audioEngine.isRunning) {
|
||||
_audioEngine.stop();
|
||||
}
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
}
|
||||
const _inputNode = inputNode!
|
||||
_inputNode.removeTap(onBus= 0); // 移除音频回调
|
||||
console.log('移除音频回调');
|
||||
|
||||
// 停用音频会话
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(false));
|
||||
}catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
console.log('数组大小',recordedPCMData.length);
|
||||
const filePath = savePCMToFile(recordedPCMData, sampleRate, channels);
|
||||
console.log('filePath', filePath);
|
||||
inputNode = null;
|
||||
audioEngine = null; // 释放全局引擎
|
||||
isRecording = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Int16 PCM数组保存为文件(iOS沙盒路径)
|
||||
* @param pcmData Int16格式的PCM数据
|
||||
* @param sampleRate 采样率
|
||||
* @param channels 声道数
|
||||
* @returns 文件路径 | null
|
||||
*/
|
||||
function savePCMToFile(pcmData: UInt8[], sampleRate: number, channels: number): string | null {
|
||||
try {
|
||||
const dataPath = UTSiOS.getDataPath();
|
||||
console.log('dataPath', dataPath);
|
||||
if (dataPath === null) {
|
||||
throw new Error("获取应用数据路径失败");
|
||||
}
|
||||
// 2. 生成唯一文件名(避免覆盖)
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const filePath = dataPath + "/" + "recording_" + timestamp + ".pcm";
|
||||
const fileURL = NSURL.fileURL(withPath= filePath);
|
||||
console.log(fileURL);
|
||||
|
||||
// 3. 将Int16数组转为NSData(字节数据)
|
||||
const byteCount = pcmData.length * 2; // 每个Int16占2字节
|
||||
const byteArray = Uint8Array.from(recordedPCMData);
|
||||
const dataOptional = NSMutableData(length= byteArray.length);
|
||||
// const dataOptional = NSMutableData(length= byteCount);
|
||||
if (dataOptional === null) throw new Error("创建NSMutableData失败");
|
||||
const data = dataOptional!;
|
||||
|
||||
for (let j:Int = 0; j < recordedPCMData.length; j++) {
|
||||
// 获取当前字节值(UInt8)
|
||||
const byteValue = recordedPCMData[j];
|
||||
// 将单个字节写入 NSMutableData 的指定位置
|
||||
data.replaceBytes(in = NSMakeRange(j, 1), withBytes= UTSiOS.getPointer(byteValue), length= 1);
|
||||
}
|
||||
|
||||
|
||||
// 4. 写入文件(UTS兼容的Swift风格)
|
||||
const writeSuccess = data.write(toFile=filePath, atomically= true);
|
||||
// console.log(data);
|
||||
if (writeSuccess) {
|
||||
console.log("PCM文件保存成功:", filePath);
|
||||
return filePath;
|
||||
} else {
|
||||
throw new Error("文件写入失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存PCM文件出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import { StartAudioRecordFailImpl, AudioErrorCode } from '../unierror.uts';
|
||||
|
||||
|
||||
import { UTSiOS } from "DCloudUTSFoundation"
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer } from "AVFoundation";
|
||||
import { AVAudioEngine, AVAudioSession, AVAudioFormat, AVAudioInputNode, AVAudioPCMBuffer, AVAudioCommonFormat } from "AVFoundation";
|
||||
import { NSData, NSMutableData } from "Foundation";
|
||||
import { Int, Int16 } from 'Swift';
|
||||
|
||||
// @argumentLabel("forBus")
|
||||
/**
|
||||
* 音频采集状态(全局变量,保证生命周期)
|
||||
*/
|
||||
@@ -21,6 +21,57 @@ let audioEngine: AVAudioEngine | null = null;
|
||||
let inputNode: AVAudioInputNode | null = null;
|
||||
// 全局复用缓冲区(避免每次创建新数组)
|
||||
let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
// 新增:缓存录制的所有Int16 PCM数据
|
||||
let recordedPCMData: UInt8[] = [];
|
||||
// 3. 保存完整PCM数据到文件
|
||||
const sampleRate = 16000.0; // 和采集时的目标格式一致
|
||||
const channels = 1; // 实际以采集时的声道数为准(可从inputFormat获取)
|
||||
|
||||
// 新增:录音启动时间戳(毫秒)
|
||||
let recordStartTime: number | null = null;
|
||||
// 新增:实时时长定时器(用于更新UI)
|
||||
let durationTimer: any = null;
|
||||
|
||||
// 预申请权限函数(可在App启动时调用)
|
||||
/**
|
||||
* 预申请麦克风权限(规范版,含资源管理)
|
||||
* @param callback 权限申请结果回调(可选)
|
||||
*/
|
||||
export function preRequestRecordPermission(callback?: (granted: boolean) => void) {
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
|
||||
try {
|
||||
// 1. 配置基础音频会话(仅为权限申请,用record类别)
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.record,
|
||||
mode=AVAudioSession.Mode.default
|
||||
));
|
||||
|
||||
// 2. 激活会话(权限申请建议激活,避免部分设备弹窗不触发)
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
|
||||
// 3. 发起权限申请
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
console.log("预申请麦克风权限结果:", granted);
|
||||
|
||||
// 4. 权限申请完成后,立即停用会话(核心:释放资源)
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(
|
||||
false,
|
||||
options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation
|
||||
));
|
||||
} catch (e) {
|
||||
console.warn("预申请后停用音频会话失败:", e);
|
||||
}
|
||||
|
||||
// 5. 回调权限结果(供上层处理)
|
||||
callback?.(granted);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("预申请麦克风权限失败:", error);
|
||||
callback?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动音频采集(iOS端核心实现)
|
||||
@@ -30,79 +81,136 @@ let reuseInt32Array: Int32Array | null = new Int32Array(4800);
|
||||
export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
// 避免重复启动
|
||||
console.log('避免重复启动', isRecording);
|
||||
// if (isRecording) return;
|
||||
|
||||
if (isRecording) return;
|
||||
try {
|
||||
recordedPCMData = new Array<UInt8>();
|
||||
// 获取 iOS 系统的音频会话单例对象。
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
// 设置音频会话的类别、模式和附加选项,定义 App 的音频行为。
|
||||
UTSiOS.try(audioSession.setCategory(
|
||||
AVAudioSession.Category.playAndRecord,
|
||||
mode=AVAudioSession.Mode.spokenAudio,
|
||||
options= AVAudioSession.CategoryOptions.duckOthers // 可选:其他音频静音
|
||||
));
|
||||
// UTSiOS.try(audioSession.setCategory(AVAudioSession.Category.playAndRecord));
|
||||
// UTSiOS.try(audioSession.setMode(AVAudioSession.Mode.spokenAudio));
|
||||
UTSiOS.try(audioSession.setActive(true));
|
||||
isRecording = true;
|
||||
audioSession.requestRecordPermission((granted: boolean) => {
|
||||
// 闭包内处理权限结果(异步执行)
|
||||
if (granted) {
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
if(!granted){
|
||||
try {
|
||||
UTSiOS.try(audioSession.setActive(false, options= AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation));
|
||||
} catch (e) {
|
||||
console.warn("停用音频会话失败:", e);
|
||||
}
|
||||
// 2. 重置状态
|
||||
isRecording = false;
|
||||
const err = new StartAudioRecordFailImpl(9010002); // 麦克风权限被永久拒绝,请前往设置开启
|
||||
options.onError?.(err)
|
||||
} else {
|
||||
|
||||
// 权限通过:继续初始化采集逻辑
|
||||
try {
|
||||
// 创建音频引擎(移到闭包内,保证权限通过后才执行)
|
||||
audioEngine = AVAudioEngine.init();
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
console.log("硬件原生采样率:", inputFormat.sampleRate);
|
||||
console.log("硬件原生声道数:", inputFormat.channelCount);
|
||||
console.log("硬件原生格式:", inputFormat.commonFormat);
|
||||
recordStartTime = new Date().getTime();
|
||||
// 1. 生成录音唯一标识(UUID,方便多录音实例管理)
|
||||
const recordId = `${new Date().getTime()}-${Math.floor(Math.random() * 10000)}`;
|
||||
options.onStart?.({
|
||||
recordId: recordId, // 唯一标识
|
||||
sampleRate: inputFormat.sampleRate, // 采样率
|
||||
startTime: recordStartTime, // 启动时间戳
|
||||
});
|
||||
const targetFormat = AVAudioFormat.init(
|
||||
commonFormat= AVAudioCommonFormat.pcmFormatInt16, // 16位整型(文档枚举值)
|
||||
sampleRate= inputFormat.sampleRate, // 采样率(Double类型,符合文档定义)
|
||||
channels= inputFormat.channelCount, // 声道数(AVAudioChannelCount)
|
||||
interleaved= false // 是否交错(Bool,单声道无影响)
|
||||
);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 2048, // 缓冲区大小,常用 1024/2048
|
||||
format= targetFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const frameLen = buffer.frameLength as number;
|
||||
const bufferListRawPtr = buffer.audioBufferList;
|
||||
const audioBufferList = bufferListRawPtr.pointee; // 拿到结构体本身
|
||||
const audioBufferPtr = audioBufferList.mBuffers; // AudioBuffer指针
|
||||
// // console.log(audioBufferPtr.mData);
|
||||
const mData = audioBufferPtr.mData;
|
||||
|
||||
// const mDataByteSize:Int = 9600; // 你之前看到的字节数
|
||||
// const uint8Ptr = mData!.bindMemory(to= UInt8.self, capacity= mDataByteSize);
|
||||
// const pcmNormalArray: number[] = [];
|
||||
// for (let i:Int = 0; i < mDataByteSize; i++) {
|
||||
// // 直接读取指针的第i个字节,转为0-255的普通数字
|
||||
// pcmNormalArray.push(uint8Ptr[i] as number);
|
||||
// }
|
||||
// const halfLength:Int = 4800;
|
||||
// const firstHalf = pcmNormalArray.slice(0, halfLength);
|
||||
// const secondHalf = pcmNormalArray.slice(halfLength, mDataByteSize);
|
||||
// options.onFrame?.(firstHalf);
|
||||
// options.onFrame?.(secondHalf);
|
||||
|
||||
|
||||
// options.onFrame?.(pcmNormalArray);
|
||||
// let uint8 = new Uint8Array([4, 5, 8, 12]);
|
||||
// let array = Array.from(uint8);
|
||||
// console.log(array);
|
||||
// options.onFrame?.(array);
|
||||
// const audioBuffer = audioBufferPtr.pointee; // 拿到AudioBuffer结构体
|
||||
// console.log("音频数据字节数:", audioBuffer.mDataByteSize); // 比如4096(2048帧×2字节)
|
||||
// console.log("声道数:", audioBuffer.mNumberChannels); // 1
|
||||
// console.log("二进制数据指针:", audioBuffer.mData); // 指向16位PCM数据的内存地址
|
||||
|
||||
// const int16Ptr = buffer.int16ChannelData;
|
||||
// if (int16Ptr != null && frameLen > 0) {
|
||||
// const dataPtr = int16Ptr![0];
|
||||
// const CHUNK_FRAME_SIZE = frameLen / 2; // 16位=2字节/帧 → 512帧/分片
|
||||
// // 关键2:循环拆分 9600 字节为多个小分片
|
||||
// for (let startFrame:number = 0; startFrame < frameLen; startFrame += CHUNK_FRAME_SIZE) {
|
||||
// // 计算当前分片的结束帧(避免越界)
|
||||
// const endFrame = Math.min(startFrame + CHUNK_FRAME_SIZE, frameLen);
|
||||
// let _recordedPCMData: UInt8[] = [];
|
||||
// // 只处理当前分片的帧
|
||||
// for (let i: Int = startFrame as Int; i < endFrame; i++) {
|
||||
// const int16Value = dataPtr[i];
|
||||
// let byte1 = UInt8(int16Value & 0xFF);
|
||||
// let byte2 = UInt8((int16Value >> 8) & 0xFF);
|
||||
// _recordedPCMData.push(byte1);
|
||||
// _recordedPCMData.push(byte2);
|
||||
// }
|
||||
// options.onFrame?.(_recordedPCMData);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
inputNode = audioEngine!.inputNode;
|
||||
if (inputNode === null) { // 解包:先判断是否为nil
|
||||
throw new Error("音频输入节点为空,无法获取音频格式");
|
||||
}
|
||||
const unwrappedInputNode = inputNode!;
|
||||
const inputFormat = unwrappedInputNode.inputFormat(forBus= 0);
|
||||
unwrappedInputNode.installTap(
|
||||
onBus= 0, // 总线编号,通常传 0
|
||||
bufferSize= 1024, // 缓冲区大小,常用 1024/2048
|
||||
format= inputFormat, // 音频格式
|
||||
block=(buffer: AVAudioPCMBuffer, time: any) => {
|
||||
const floatData = buffer.floatChannelData;
|
||||
const channelData = floatData![0];
|
||||
const frameLength = buffer.frameLength as number;
|
||||
const bytesPerSample = 2; // 16位整型=2字节/样本
|
||||
const totalBytes = frameLength * bytesPerSample;
|
||||
|
||||
console.log(channelData);
|
||||
console.log(totalBytes);
|
||||
const pcmData = NSMutableData.dataWithCapacity(20); // 可变Data,支持写入
|
||||
|
||||
// const data = new Float32Array(int32Array.byteLength);
|
||||
// data.set(int32Array.buffer, 1); // 内存块拷贝,替代逐字节写入
|
||||
|
||||
options.onFrame?.(pcmData);
|
||||
// if (floatData !== null) {
|
||||
// // 单声道取第0个通道(立体声可取[0]左声道、[1]右声道)
|
||||
// const channelData = floatData[0];
|
||||
|
||||
// options.onFrame?.(channelData)
|
||||
// }
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
} else {
|
||||
// 权限被拒绝:回调错误
|
||||
isRecording = false;
|
||||
}
|
||||
);
|
||||
UTSiOS.try(audioEngine!.start());
|
||||
if (audioEngine!.isRunning) {
|
||||
console.log("引擎确实处于运行状态");
|
||||
isRecording = true;
|
||||
} else {
|
||||
console.error("调用 start() 但引擎未运行");
|
||||
}
|
||||
isRecording = true;
|
||||
} catch (error) {
|
||||
isRecording = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
isRecording = true;
|
||||
|
||||
} catch (error) {
|
||||
console.log('报错了', error);
|
||||
// 初始化失败回调错误
|
||||
const err = new StartAudioRecordFailImpl(9010002);
|
||||
options.onError?.(err);
|
||||
@@ -117,6 +225,7 @@ export function startAudioRecord(options: StartAudioRecordOptions): void {
|
||||
*/
|
||||
|
||||
export function stopAudioRecord(): void {
|
||||
console.log('停止音频采集');
|
||||
if (audioEngine === null) { // 解包:先判断是否为nil
|
||||
isRecording = false;
|
||||
return; // 直接返回,不执行后续逻辑
|
||||
@@ -131,7 +240,7 @@ export function stopAudioRecord(): void {
|
||||
}
|
||||
const _inputNode = inputNode!
|
||||
_inputNode.removeTap(onBus= 0); // 移除音频回调
|
||||
|
||||
console.log('移除音频回调');
|
||||
|
||||
// 停用音频会话
|
||||
const audioSession = AVAudioSession.sharedInstance();
|
||||
@@ -141,8 +250,62 @@ export function stopAudioRecord(): void {
|
||||
}catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
// console.log('数组大小',recordedPCMData.length);
|
||||
// const filePath = savePCMToFile(recordedPCMData, sampleRate, channels);
|
||||
// console.log('filePath', filePath);
|
||||
inputNode = null;
|
||||
audioEngine = null; // 释放全局引擎
|
||||
isRecording = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Int16 PCM数组保存为文件(iOS沙盒路径)
|
||||
* @param pcmData Int16格式的PCM数据
|
||||
* @param sampleRate 采样率
|
||||
* @param channels 声道数
|
||||
* @returns 文件路径 | null
|
||||
*/
|
||||
function savePCMToFile(pcmData: UInt8[], sampleRate: number, channels: number): string | null {
|
||||
try {
|
||||
const dataPath = UTSiOS.getDataPath();
|
||||
console.log('dataPath', dataPath);
|
||||
if (dataPath === null) {
|
||||
throw new Error("获取应用数据路径失败");
|
||||
}
|
||||
// 2. 生成唯一文件名(避免覆盖)
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const filePath = dataPath + "/" + "recording_" + timestamp + ".pcm";
|
||||
const fileURL = NSURL.fileURL(withPath= filePath);
|
||||
console.log(fileURL);
|
||||
|
||||
// 3. 将Int16数组转为NSData(字节数据)
|
||||
const byteCount = pcmData.length * 2; // 每个Int16占2字节
|
||||
const byteArray = Uint8Array.from(recordedPCMData);
|
||||
const dataOptional = NSMutableData(length= byteArray.length);
|
||||
// const dataOptional = NSMutableData(length= byteCount);
|
||||
if (dataOptional === null) throw new Error("创建NSMutableData失败");
|
||||
const data = dataOptional!;
|
||||
|
||||
for (let j:Int = 0; j < recordedPCMData.length; j++) {
|
||||
// 获取当前字节值(UInt8)
|
||||
const byteValue = recordedPCMData[j];
|
||||
// 将单个字节写入 NSMutableData 的指定位置
|
||||
data.replaceBytes(in = NSMakeRange(j, 1), withBytes= UTSiOS.getPointer(byteValue), length= 1);
|
||||
}
|
||||
|
||||
|
||||
// 4. 写入文件(UTS兼容的Swift风格)
|
||||
const writeSuccess = data.write(toFile=filePath, atomically= true);
|
||||
// console.log(data);
|
||||
if (writeSuccess) {
|
||||
console.log("PCM文件保存成功:", filePath);
|
||||
return filePath;
|
||||
} else {
|
||||
throw new Error("文件写入失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存PCM文件出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
export type AudioStartRes = {
|
||||
recordId ?: string; // 录音唯一标识(方便多录音实例管理)
|
||||
sampleRate ?: number; // 采样率(如16000)
|
||||
format ?: string; // 录音格式(如pcm/mp3)
|
||||
startTime : number; // 开启时间戳(毫秒)
|
||||
};
|
||||
|
||||
@@ -51,7 +50,7 @@ export type StartAudioRecordOptions = {
|
||||
onVolume ?: (volume : number) => void; // 实时音量回调(如每秒5次,按设备采集频率)
|
||||
|
||||
// 【一次性状态回调】- 特定节点仅触发一次
|
||||
onStart ?: (res : AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
|
||||
onStart ?: (res: AudioStartRes) => void; // 成功开启录音的回调(仅触发1次)
|
||||
onError ?: (res : AudioErrorRes) => void; // 录音错误回调(任意阶段出错都触发,仅1次)
|
||||
onStop ?: () => void; // 录音结束的纯状态回调(仅通知结束,无返回,1次)
|
||||
onFile ?: (res : AudioFileRes) => void; // 录音结束后,返回录音文件的回调(1次,核心)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export function startAudioRecord(callback) {}
|
||||
export function stopAudioRecord() {}
|
||||
export function stopAudioRecord() {}
|
||||
export function preRequestRecordPermission() {}
|
||||
Reference in New Issue
Block a user