This commit is contained in:
田岩
2025-12-12 21:33:37 +08:00
parent 20a5b8272c
commit 6d1fdd6b7b
84 changed files with 4506 additions and 2403 deletions
+17 -3
View File
@@ -3,9 +3,11 @@ ENV = 'development'
# 'development'
# VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_API_Url = 'http://192.168.247.200'
@@ -17,6 +19,7 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
#VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786'
# UAT
# VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786'
@@ -30,6 +33,17 @@ VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
# h5专用地址x2
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.32.150:9601'
# dev
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
# sit
#VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url_TRAAPP = 'http://25.64.16.130:9601'
# VITE_APP_BASE_H5_API_Url_TRASTUDY = 'http://25.64.16.130:9602'
# VITE_APP_BASE_H5_API_Url_TRAASK = 'http://25.64.16.144:9605'
#VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.16.139:9604'
+2 -2
View File
@@ -4,8 +4,8 @@ ENV = 'development.yh'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.65:7001'
VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_H5_API_Url = 'http://25.18.122.91:9786'
VITE_APP_BASE_H5_API_Url = 'http://25.18.122.78:9786'
# VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002'
# VITE_APP_BASE_H5_API_Url_TRAEXAM = 'http://25.64.32.154:9604'
# VITE_APP_BASE_H5_API_Url_TRAPRACTICE = 'http://25.64.32.156:9603'
+14
View File
@@ -0,0 +1,14 @@
# 启用严格模式,确保依赖版本一致
strict-peer-dependencies=false
# 配置依赖安装目录(pnpm 特有,节省磁盘空间)
store-dir=~/.pnpm-store
# 兼容 uniapp 对依赖的查找逻辑
node-linker=hoisted
# 忽略废弃依赖警告
ignore-deprecations=all
# 忽略对等依赖警告
strict-peer-dependencies=false
+117 -87
View File
@@ -1,31 +1,34 @@
<script>
export default {
onLaunch: function () {
onLaunch: function() {
uni.onTabBarMidButtonTap(() => {
uni.navigateTo({
url: '/pages/index/dialog',
animationType: 'slide-in-bottom'
});
});
//#ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary')
// #endif
},
onShow: function () {
onShow: function() {
},
onHide: function () {
}
onHide: function() {}
};
</script>
<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
// @import "@/uni_modules/uview-ui/index.scss";
@font-face {
font-family: 'PingFangSC';
src: url('@/static/font/PingFang-SC.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
view {
box-sizing: border-box;
font-family: 'PingFangSC';
@@ -39,85 +42,6 @@
font-style: normal;
}
.max_page {
min-height: 100vh;
}
.mr-10 {
margin-right: 10rpx;
}
.mb-8 {
margin-bottom: 8rpx;
}
.pt-8 {
padding-top: 8rpx;
}
.mb-10 {
margin-bottom: 10rpx;
}
.mb-18 {
margin-bottom: 18rpx;
}
.mb-64 {
margin-bottom: 64rpx;
}
.mb-30 {
margin-bottom: 30rpx;
}
.mb-36 {
margin-bottom: 36rpx;
}
.mb-4 {
margin-bottom: 4rpx;
}
.fz-30 {
font-size: 30rpx;
}
.fz-28 {
font-size: 28rpx;
}
.fz-24 {
font-size: 24rpx;
}
.w_100 {
width: 100%;
}
.h_100 {
height: 100%;
}
.flex {
display: flex;
}
.flex-jcsb {
display: flex;
justify-content: space-between;
}
.flex-c-c {
display: flex;
justify-content: center;
align-items: center;
}
.tar {
text-align: right;
}
.fw-600 {
font-weight: 600;
}
.fw-400 {
font-weight: 400;
}
.img_100 {
width: 100%;
height: 100%;
}
.color-666{
color: #666666;
}
.fl1 {
flex: 1;
}
.font_pf {
font-family: 'PingFangSC';
}
@@ -140,14 +64,120 @@
overflow: hidden;
}
.max_page {
min-height: 100vh;
}
.mr-10 {
margin-right: 10rpx;
}
.mb-8 {
margin-bottom: 8rpx;
}
.pt-8 {
padding-top: 8rpx;
}
.mb-10 {
margin-bottom: 10rpx;
}
.mb-18 {
margin-bottom: 18rpx;
}
.mb-64 {
margin-bottom: 64rpx;
}
.mb-30 {
margin-bottom: 30rpx;
}
.mb-36 {
margin-bottom: 36rpx;
}
.mb-4 {
margin-bottom: 4rpx;
}
.fz-30 {
font-size: 30rpx;
}
.fz-28 {
font-size: 28rpx;
}
.fz-24 {
font-size: 24rpx;
}
.w_100 {
width: 100%;
}
.h_100 {
height: 100%;
}
.flex {
display: flex;
}
.flex-jcsb {
display: flex;
justify-content: space-between;
}
.flex-c-c {
display: flex;
justify-content: center;
align-items: center;
}
.tar {
text-align: right;
}
.fw-600 {
font-weight: 600;
}
.fw-400 {
font-weight: 400;
}
.img_100 {
width: 100%;
height: 100%;
}
.color-666 {
color: #666666;
}
.fl1 {
flex: 1;
}
.no-overflow {
overflow: hidden;
}
.bold {
font-weight: bold;
}
.maoh{
.maoh {
margin-right: 8rpx;
margin-left: 2rpx;
}
</style>
</style>
+2 -2
View File
@@ -86,7 +86,7 @@ export default class WebSocketUtil {
// console.log('WebSocket连接已关闭', res);
clearInterval(this.heartbeatTimer); // 清除心跳计时器
this.triggerEvent('close', res); // 触发连接关闭事件
this.tryReconnect(); // 尝试重连
// this.tryReconnect(); // 尝试重连
});
// 监听WebSocket错误事件
@@ -106,7 +106,7 @@ export default class WebSocketUtil {
*/
on(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event].push(callback);
this.callbacks[event] = [callback];
}
return this;
}
+1
View File
@@ -62,6 +62,7 @@ export const partnerChatReportTrigger = (data) => {
export const partnerChatPrompt = (data) => {
return request({
url: base_url + '/traPartnerChat/partnerChatPrompt',
timeout: 60000,
method: 'post',
data
});
+306
View File
@@ -0,0 +1,306 @@
let popupView = null // 窗口实例
export function showModal(info) {
const title = info && info.title ? info.title : '标题'
const content = info && info.content ? info.content : '请输入内容'
let screenWidth = plus.screen.resolutionWidth
let screenHeight = plus.screen.resolutionHeight
const popupViewWidth = screenWidth * 0.85
const viewContentPadding = 20 // 减小内边距
const viewContentWidth = parseInt(popupViewWidth - (viewContentPadding * 2))
const descriptionList = drawtext(content, viewContentWidth)
let popupViewHeight = 60 + 20 + 20 // 减小整体高度
let popupViewContentList = [{
tag: 'font',
id: 'title',
text: title,
textStyles: {
size: '18px',
color: "#333333",
weight: "bold",
whiteSpace: "normal"
},
position: {
top: '25px', // 调整标题位置
left: viewContentPadding + "px",
width: viewContentWidth + "px",
height: "25px",
}
}]
const textHeight = 24 // 调整行高
let contentTop = 60 // 调整内容起始位置
descriptionList.forEach((item, index) => {
if (index > 0) {
popupViewHeight += textHeight;
contentTop += textHeight;
}
popupViewContentList.push({
tag: 'font',
id: 'content' + index + 1,
text: item.content,
textStyles: {
size: '15px',
color: "#666666",
lineSpacing: "50%", // 调整行间距
align: "left"
},
position: {
top: contentTop + "px",
left: viewContentPadding + "px",
width: viewContentWidth + "px",
height: textHeight + "px",
}
});
if (item.type == "break") {
contentTop += 8; // 减小段落间距
popupViewHeight += 8;
}
})
popupView = new plus.nativeObj.View("popupView", {
tag: "rect",
top: "80px", // 固定在靠近顶部的位置
left: (screenWidth - popupViewWidth) / 2 + "px",
height: popupViewHeight + "px",
width: popupViewWidth + "px"
})
// 绘制主体背景
popupView.drawRect({
color: "#FFFFFF",
radius: "12px",
borderWidth: "1px",
borderColor: "#E5E5E5"
}, {
top: "0px",
height: popupViewHeight + "px",
})
popupView.draw(popupViewContentList)
popupView.show()
}
function closeModal() {
// 在不再需要 popupView 时关闭它
popupView && popupView.close();
popupView = null;
}
// 文字换行
function drawtext(text, maxWidth) {
let textArr = text.split("");
let len = textArr.length;
// 上个节点
let previousNode = 0;
// 记录节点宽度
let nodeWidth = 0;
// 文本换行数组
let rowText = [];
// 如果是字母,侧保存长度
let letterWidth = 0;
// 汉字宽度
let chineseWidth = 14;
// otherFont宽度
let otherWidth = 7;
for (let i = 0; i < len; i++) {
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
if (letterWidth > 0) {
if (nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
});
previousNode = i
nodeWidth = chineseWidth
letterWidth = 0
} else {
nodeWidth += chineseWidth + letterWidth * otherWidth
letterWidth = 0
}
} else {
if (nodeWidth + chineseWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
})
previousNode = i
nodeWidth = chineseWidth
} else {
nodeWidth += chineseWidth
}
}
} else {
if (/\n/g.test(textArr[i])) {
rowText.push({
type: "break",
content: text.substring(previousNode, i)
})
previousNode = i + 1
nodeWidth = 0
letterWidth = 0
} else if (textArr[i] == "\\" && textArr[i + 1] == "n") {
rowText.push({
type: "break",
content: text.substring(previousNode, i)
})
previousNode = i + 2
nodeWidth = 0
letterWidth = 0
} else if (/[a-zA-Z0-9]/g.test(textArr[i])) {
letterWidth += 1;
if (nodeWidth + letterWidth * otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i + 1 - letterWidth)
})
previousNode = i + 1 - letterWidth
nodeWidth = letterWidth * otherWidth
letterWidth = 0
}
} else {
if (nodeWidth + otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
});
previousNode = i
nodeWidth = otherWidth
} else {
nodeWidth += otherWidth
}
}
}
}
if (previousNode < len) {
rowText.push({
type: "text",
content: text.substring(previousNode, len)
})
}
return rowText
}
//权限检测
export function requestPermissions(info) {
const permissionID = info.permissionID
console.log(info,permissionID)
return new Promise((rev, jec) => {
if (!permissionID) {
rev({
isSuc: false,
msg: "缺少permissionID"
})
return false
}
//判断安卓与ios设备
if (plus.os.name == 'Android') {
let _permissionID = 'android.permission.' + permissionID;
plus.android.checkPermission(_permissionID,
granted => {
console.log(granted)
if (granted.checkResult == 0) {
rev({
isSuc: true
})
}
if (granted.checkResult == -1) {
//还未授权当前查询的权限,打开权限申请目的自定义弹框
closeModal() // 先关闭再弹窗
showModal(info)
plus.android.requestPermissions([_permissionID],
(e) => {
console.log(e)
//关闭权限申请目的自定义弹框
if (e.granted.length > 0) {
closeModal()
//当前查询权限已授权,此时可以通知页面执行接下来的操作
rev({
isSuc: true
})
}
if (e.deniedPresent.length > 0) {
closeModal()
//当前查询权限已授权,此时可以通知页面执行接下来的操作
rev({
isSuc: false,
msg: "用户已拒绝"
})
}
if (e.deniedAlways.length > 0) {
//当前查询权限已被永久禁用,此时需要引导用户跳转手机系统设置去开启
uni.showModal({
title: '温馨提示',
content: '还没有该权限,立即去设置开启?',
cancelText: "取消",
confirmText: "去设置",
showCancel: true,
confirmColor: '#000',
cancelColor: '#666',
success: (res) => {
if (res.confirm) {
goSetting();
}
if (res.cancel) {
rev({
isSuc: false,
msg: "取消前往权限设置页面"
})
}
},
fail(e) {
rev({
isSuc: false,
msg: e.message || "弹窗失败"
})
},
complete() {
closeModal()
}
})
}
})
}
},
error => {
rev({
isSuc: false,
mes: error.message || '检查权限失败'
})
}
);
} else {
//IOS不需要添加自定义弹框来描述权限目的,因为在配置文件的隐私信息访问的许可描述里可添加
rev({
isSuc: true
})
}
})
}
//跳转手机系统设置
export function goSetting() {
if (plus.os.name == "iOS") {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("app-settings:");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
+95 -32
View File
@@ -2,7 +2,13 @@
* 本模块封装了Android、iOS的应用权限判断、打开应用权限设置界面、以及位置系统服务是否开启
*/
var isIos
import common from '@/common/common.js'
import {
getPhoneEnvBool
} from '@/common/common.js'
const IsAndEnv = getPhoneEnvBool('AND')
let isIos
// #ifdef APP-PLUS
isIos = (plus.os.name == "iOS")
// #endif
@@ -63,35 +69,90 @@ function judgeIosPermissionLocation() {
// 判断麦克风权限是否开启
function judgeIosPermissionRecord() {
var result = false;
var avaudiosession = plus.ios.import("AVAudioSession");
var avaudio = avaudiosession.sharedInstance();
var permissionStatus = avaudio.recordPermission();
console.log("permissionStatus:" + permissionStatus);
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
console.log("麦克风权限没有开启");
} else {
result = true;
console.log("麦克风权限已经开启");
}
plus.ios.deleteObject(avaudiosession);
return result;
return new Promise(async (resolve, reject) => {
const authSetting = uni.getAppAuthorizeSetting();
const state = authSetting.microphoneAuthorized;
console.log('麦克风state', state);
switch (state) {
case 'authorized':
console.log("麦克风权限是否开启");
resolve()
break;
case 'denied': // ios拒绝,安卓拒绝或者未授权都是这个
if (IsAndEnv) { // 安卓二次判定
const result = await requestAndroidPermission('android.permission.RECORD_AUDIO')
if (result === 1) {
resolve()
break;
}
}
const resultT = await common.show(
'提示信息',
'当前页面需要使用录音权限,是否前往开启?'
)
if (resultT) {
// 前往权限设置页面
uni.openAppAuthorizeSetting();
}
reject()
break;
case 'not determined': // 表示尚未请求授权,仅 iOS 会出现。此种情况下引导用户打开系统设置,不展示开关
resolve()
break;
case 'config error': // 只有在 App 端时返回
resolve()
break;
default:
resolve()
break;
}
})
}
// 判断相机权限是否开启
function judgeIosPermissionCamera() {
var result = false;
var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
console.log("authStatus:" + authStatus);
if (authStatus == 3) {
result = true;
console.log("相机权限已经开启");
} else {
console.log("相机权限没有开启");
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
return new Promise(async (resolve, reject) => {
const authSetting = uni.getAppAuthorizeSetting();
const state = authSetting.cameraAuthorized;
console.log('相机权限', state);
switch (state) {
case 'authorized':
console.log("相机权限已经开启");
resolve()
break;
case 'denied': // ios拒绝,安卓拒绝或者未授权都是这个
console.log('IsAndEnv', IsAndEnv);
if (IsAndEnv) { // 安卓二次判定
const result = await requestAndroidPermission('android.permission.CAMERA')
console.log('安卓二次判定', result);
if (result === 1) {
resolve()
break;
}
}
const resultT = await common.show(
'提示信息',
'当前页面需要使用相机权限,是否前往开启?'
)
if (resultT) {
// 前往权限设置页面
uni.openAppAuthorizeSetting();
}
reject()
break;
case 'not determined': // 表示尚未请求授权,仅 iOS 会出现。此种情况下引导用户打开系统设置,不展示开关
resolve()
break;
case 'config error': // 只有在 App 端时返回
resolve()
break;
default:
resolve()
break;
}
})
}
// 判断相册权限是否开启
@@ -264,9 +325,11 @@ function checkSystemEnableLocation() {
}
}
export default ({
judgeIosPermission: judgeIosPermission,
requestAndroidPermission: requestAndroidPermission,
checkSystemEnableLocation: checkSystemEnableLocation,
gotoAppPermissionSetting: gotoAppPermissionSetting
})
export default {
judgeIosPermission,
requestAndroidPermission,
checkSystemEnableLocation,
gotoAppPermissionSetting
}
@@ -194,19 +194,11 @@
watch(
() => props.modelValue,
(newVal) => {
console.log('modelValue', newVal);
// console.log('modelValue', newVal);
item_answer.value = typeof newVal === 'string' ? newVal.split(',') : newVal;
},
{ deep: true }
);
// watch(
// item_answer,
// (newVal) => {
// console.log('item_answer', newVal);
// emit('update:modelValue', newVal);
// },
// { deep: true }
// );
const feedback_click = () => {
emit('subject_click', 'feedback', props.item);
@@ -24,7 +24,8 @@
watch:{
mdString: {
handler(val){
this.contentHtml = this.parseContent(val)
let _text = val.replace(/</g,'&lt;').replace(/>/g,'&gt;')
this.contentHtml = this.parseContent(_text)
},
deep: true,
immediate: true
+16 -3
View File
@@ -32,7 +32,9 @@
},
/* */
"modules" : {
"Camera" : {},
"Camera" : {
"description" : "用于访问摄像头进行实时预览"
},
"Barcode" : {},
"Record" : {},
"VideoPlayer" : {},
@@ -57,12 +59,23 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
]
},
/* ios */
"ios" : {
"dSYMs" : false
"dSYMs" : false,
"deploymentTarget" : "11.0",
"privacyDescription" : {
"NSCameraUsageDescription" : "此应用需要访问您的摄像头来进行视频录制和拍照功能",
"NSMicrophoneUsageDescription" : "此应用需要访问您的麦克风来录制音频"
},
"capabilities" : {
"entitlements" : {
"com.apple.developer.avfoundation.multitasking-camera-access" : true
}
}
},
/* SDK */
"sdkConfigs" : {},
+1
View File
@@ -590,6 +590,7 @@
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom",
"screenOrientation": "portrait",
"app-plus": {
"titleView": false,
"bounce": "none",
+67 -19
View File
@@ -16,6 +16,8 @@
:current="currentTab" class="font_pf my_tabs"
:inactive-style="{ color: '#333333', fontSize: '28rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '28rpx', fontWeight: '600' }"></uv-tabs>
<view class="tab-btn" v-show="!overLoading" @click="showToast"></view>
<!-- v-if="!overLoading" -->
</view>
<!-- <view class="calendar-box" @click="openCalendar">统计周期</view>
<uv-calendar ref="calendarRef" mode="range" @confirm="calendarConfirm" minDate="2025-01-01" :maxDate="maxDate" monthNum="100" :allowSameDay="true"></uv-calendar> -->
@@ -107,36 +109,36 @@
</view>
</view>
</view>
<view class="evaluation-box dens" v-if="loading">
<view class="evaluation-box dens" v-if="loading1">
<CommonLoading color="#06f" class="loading-box" />
</view>
<view class="evaluation-box dens" v-if="!loading">
<view class="evaluation-box dens" v-if="!loading1">
<view class="dens-item" v-for="(item, index) in dimesionList">
<view :class="['item-title', index%2===0?'study1-tip': 'study2-tip']">{{item.title}}</view>
<view :class="['item-cont ', index%2===0?'study1-bg': 'study2-bg']">{{item.content}}</view>
</view>
</view>
<view class="evaluation-box study" v-if="loading">
<view class="evaluation-box study" v-if="loading2">
<CommonLoading color="#06f" class="loading-box" />
</view>
<view class="evaluation-box study" v-if="!loading">
<view class="evaluation-box study" v-if="!loading2">
<view class="study-item" v-for="(item, index) in studyList">
<view :class="['item-title', index%2===0?'study1-tip': 'study2-tip']">{{item.title}}</view>
<view :class="['item-cont ', index%2===0?'study1-bg': 'study2-bg']">{{item.content}}</view>
</view>
</view>
<view class="evaluation-box study1" v-if="loading">
<view class="evaluation-box study1" v-if="loading3">
<CommonLoading color="#06f" class="loading-box" />
</view>
<view class="evaluation-box study1" v-if="!loading">
<view class="study1-item">
<view class="evaluation-box study1" v-if="!loading3">
<view class="study1-item" v-show="!!suggestObj.content">
<view class="item-cont study3-bg">{{suggestObj.content}}</view>
</view>
</view>
<view class="evaluation-box course" v-if="loading">
<view class="evaluation-box course" v-if="loading4">
<CommonLoading color="#06f" class="loading-box" />
</view>
<view class="evaluation-box course" v-if="!loading">
<view class="evaluation-box course" v-if="!loading4">
<CourseItem v-for="item in courseList" :itemInfo="item" @clickItem="toCourseDetail(item)" />
<view class="empty-box" v-if="courseList.length === 0">暂无数据</view>
</view>
@@ -225,26 +227,57 @@
}
}
const loading = ref(false)
const loading1 = ref(false)
const loading2 = ref(false)
const loading3 = ref(false)
const loading4 = ref(false)
const overLoading = computed(()=>{
return !loading.value && !loading1.value && !loading2.value && !loading3.value && !loading4.value
})
const showToast = () => {
common.msg('学习分析生成中,请稍后再试!')
}
const getInfoNow = async (params) => {
statisticsObj.value = []
dimesionList.value =[]
loading.value = true
try {
let _res1 = await queryStudyStatistics(params)
loading2.value = true
loading1.value = true
loading3.value = true
queryStudyStatistics(params).then(_res1 => {
statisticsObj.value = {
..._res1.body
}
let _res2 = await queryDimesionInfo(params)
loading.value = false
}).catch(() => {
loading.value = false
})
queryDimesionInfo(params).then(_res2 => {
dimesionList.value = [..._res2.body]
let _res3 = await queryHabitInfo(params)
loading1.value = false
}).catch(() => {
loading1.value = false
})
queryHabitInfo(params).then(_res3 => {
studyList.value = [..._res3.body]
let _res4 = await querySuggestInfo(params)
loading2.value = false
}).catch(() => {
loading2.value = false
})
querySuggestInfo(params).then(_res4 => {
if (_res4.body && _res4.body.length > 0) {
suggestObj.value = {
..._res4.body[0]
}
}else{
suggestObj.value = {
content: ''
}
}
} catch {} finally {
loading.value = false
}
loading3.value = false
}).catch(() => {
loading3.value = false
})
}
// 跳转 课程详情
const toCourseDetail = item => {
@@ -263,9 +296,12 @@
}
const getCrsListNow = () => {
loading4.value = true
queryRecmdCrsInfo().then(res => {
console.log(res)
courseList.value = res.body || []
loading4.value = false
}).catch(() => {
loading4.value = false
})
}
onMounted(() => {
@@ -377,8 +413,9 @@
backdrop-filter: blur(10rpx);
z-index: 1;
&.loading-box-cont{
&.loading-box-cont {
background: #ddeafa;
&::before {
content: '';
display: block;
@@ -417,6 +454,17 @@
.tabs-box {
width: 600rpx;
height: 88rpx;
position: relative;
.tab-btn{
position: absolute;
width: 600rpx;
height: 88rpx;
top: 0;
left: 0;
z-index: 23;
// background-color: #ccc;
}
}
.analytics-items {
+3 -3
View File
@@ -147,6 +147,7 @@
const pkData = reactive({
comLimit: 0,
crsId: '',
comId: '',
name: '', // 名称
orgId: '',
papersId: '',
@@ -247,7 +248,7 @@
traQnsInfoVo = res.body.traQnsInfoVo;
}
topic.value.isPreview = true; // 不能在点击了,打开预览模式
console.log('给出来的traQnsInfoVo', traQnsInfoVo);
// console.log('给出来的traQnsInfoVo', traQnsInfoVo);
common.hideLoading();
topic.value.clearResult = false; // 不清除正确答案
topic.value.analyContent = traQnsInfoVo.analyContent; // 答案解析
@@ -257,7 +258,6 @@
topic.value.judgeResult = traQnsInfoVo.judgeResult; // 正确答案字符串分割
if (questionNumber.value >= topicList.length - 1) {
console.log('pk页答完了,带走的数据', pkData);
common.setPageCache('competition_pk_to_resulf', pkData);
nextTick(() => {
common.redirectTo(
@@ -418,7 +418,7 @@
}
nextTick(() => {
common.redirectTo(
`/pages/competition/result?mode=${mode}&execId=${pkData.execId}&orgId=${pkData.orgId}&papersId=${pkData.papersId}&autoSub=01`
`/pages/competition/result?mode=${mode}&execId=${pkData.execId}&orgId=${pkData.orgId}&papersId=${pkData.papersId}&autoSub=01&comId=${pkData.comId}`
);
});
@@ -694,7 +694,7 @@
voiceTime.value = (Math.ceil(talkVoiceObj.value.duration) || 0) + 's';
} else {
if (sum === 100) {
voiceTime.value = 0 + 's';
voiceTime.value = (unref(dataInfo).voiceTime || 0) + 's';
return;
}
setTimeout(() => {
@@ -174,6 +174,7 @@
const initRecord = () => {
recordObjCom.value = uni.getRecorderManager();
unref(recordObjCom)?.onStop((res, duration) => {
isRecording.value = false
if(isGettingPermission.value){
isGettingPermission.value = false
return
@@ -196,13 +197,20 @@
});
} else {
closeModel();
emit('uploadVoice', audioPath.value);
emit('uploadVoice', audioPath.value, recordTime.value);
}
} else {
closeModel();
}
});
unref(recordObjCom).onStart(() => {
isRecording.value = true
recordTime.value = 0
setRecordTime()
if(isGettingPermission.value){
isGettingPermission.value = false
return
}
startFlag.value = 1;
setTimeout(() => {
startFlag.value = 2;
@@ -219,6 +227,16 @@
}
});
};
const setRecordTime = () => {
setTimeout(()=>{
if(isRecording.value){
recordTime.value += 1
setRecordTime()
}
},1000)
}
const isRecording = ref(false)
const recordTime = ref(0)
const startFlag = ref(1);
// const canStart = ref(true)
const startRecord = () => {
+3 -2
View File
@@ -839,7 +839,7 @@ const testAnswer = computed(() => {
let _num = _arr.map(t => [1, 2, 6].indexOf(Number(t.type)) >= 0).filter(t => !!t)
return _num.length < 5
})
const uploadRecordNow = voicePath => {
const uploadRecordNow = (voicePath, voiceTime) => {
if (!testAnswer.value) {
common.msg('每个问题最多连续发送五次答案!')
return
@@ -911,7 +911,8 @@ const uploadRecordNow = voicePath => {
userInfo: {
...choiceTeacherInfo.value
},
id: _id
id: _id,
voiceTime: voiceTime
}
} else {
return t
@@ -1,4 +1,4 @@
<template >
<template>
<!-- 热门课程分模块标题 -->
<view class="module_title_div" v-if="class_list.length !== 0">
<view class="icon icon_1">
@@ -13,23 +13,25 @@
<view class="item_div">
<uv-skeletons :loading="!item.crsNum" avatar avatar-shape="square" :title="false" :skeleton="skeleton">
<view class="item" @click="goto_course_detail(item.crsId)">
<view class="left">
<image-preview
class="img-box img"
:imgId="item.imgId"
:isCache="true"
:width="240"
:height="160"
></image-preview>
<view class="title ellipsis-text">
{{ item.crsName }}
</view>
<view class="right">
<view class="title ellipsis-text">
{{ item.crsName }}
<view class="content">
<view class="left">
<image-preview
class="img-box img"
:imgId="item.imgId"
:isCache="true"
:width="240"
:height="160"
></image-preview>
</view>
<view class="note ellipsis-text">更新{{ (item.mtTime || '').substr(0, 10) }}</view>
<view class="button_div">
<view class="right">
<view class="note ellipsis-text">更新{{ (item.mtTime || '').substr(0, 10) }}</view>
<view class="note ellipsis-text">已学{{ item.accmStdyCnt }}人次</view>
<view class="button">去学习</view>
<view class="button_div">
<view class="button">去学习</view>
</view>
</view>
</view>
</view>
@@ -115,8 +117,7 @@
}
];
const get_data = () => {
class_list.value = [...init_class_data]
class_list.value = [...init_class_data];
fetchFunction().then((res) => {
if (props.name === 'hot_class') {
} else {
@@ -143,74 +144,78 @@
white-space: nowrap;
width: 100%;
}
.scroll-view-item {
display: inline-block;
margin-right: 30rpx;
background-color: #ffffff;
border-radius: 22rpx;
box-shadow: #eaeaea 0 0 6rpx;
padding: 32rpx 20rpx 36rpx 34rpx;
padding: 20rpx 20rpx 36rpx 34rpx;
}
.item_div {
// width: 562rpx;
width: calc(562rpx + 56rpx);
// display: block;
}
.item {
display: flex;
width: calc(562rpx + 56rpx);
.left {
border-radius: 16rpx;
overflow: hidden;
width: 256rpx;
height: 144rpx;
.img {
flex-direction: column;
.title {
font-weight: 500;
font-size: 30rpx;
color: #333333;
margin-bottom: 10rpx;
width: 100%;
}
.content {
display: flex;
.left {
border-radius: 16rpx;
overflow: hidden;
width: 256rpx;
height: 144rpx;
}
}
.right {
overflow: hidden;
flex: 1;
padding-left: 16rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
font-weight: 500;
font-size: 30rpx;
color: #333333;
line-height: 40rpx;
}
.note {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 23rpx;
color: #666666;
line-height: 33rpx;
text-align: left;
font-style: normal;
.img {
width: 256rpx;
height: 144rpx;
}
}
.button_div {
.right {
overflow: hidden;
flex: 1;
padding-left: 16rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.button {
text-align: center;
line-height: 62rpx;
width: 138rpx;
height: 62rpx;
background: #ebf1ff;
border-radius: 30rpx;
font-weight: 500;
font-size: 30rpx;
color: #0066ff;
.note {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 23rpx;
color: #666666;
line-height: 33rpx;
text-align: left;
font-style: normal;
margin-right: 10rpx;
}
.button_div {
display: flex;
justify-content: flex-end;
.button {
text-align: center;
line-height: 62rpx;
width: 138rpx;
height: 62rpx;
background: #ebf1ff;
border-radius: 30rpx;
font-weight: 500;
font-size: 30rpx;
color: #0066ff;
font-style: normal;
margin-right: 10rpx;
}
}
}
}
@@ -221,7 +226,7 @@
display: flex;
margin: 30rpx $bg-margin-left;
align-items: center;
.icon {
margin-right: 6rpx;
}
@@ -249,7 +254,7 @@
height: 31rpx;
}
}
.title {
font-size: 34rpx;
color: #323e57;
@@ -131,7 +131,7 @@ import { onUnload } from '@dcloudio/uni-app';
voiceTime.value = (Math.ceil(talkVoiceObj.value.duration) || 1 ) + 's'
}else{
if(sum === 20) {
voiceTime.value = 0 + 's'
voiceTime.value = (unref(dataInfo).voiceTime || 0) + 's';
return
}
setTimeout(()=>{
+14 -2
View File
@@ -662,7 +662,7 @@
submitAnswerNow(item);
};
//
const uploadRecordNow = (voicePath) => {
const uploadRecordNow = (voicePath, voiceTime) => {
// /traask/traAskchat/voiceTrans
//#ifndef APP-PLUS
// h5
@@ -683,7 +683,8 @@
intrctContent: _res.body.transContent,
bucketKey: _res.body.fileId,
ossFileAddr: _res.body.ossFileAddr,
talkingVoice: voicePath
talkingVoice: voicePath,
voiceTime: voiceTime
};
sendMessage({
commond: 'ASK',
@@ -791,6 +792,9 @@
initsocketTask();
};
onLoad(() => {
//#ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary')
// #endif
changeAppHeightBottom();
if (!common.isLogin()) {
common.navigateTo('/pages/login/login');
@@ -919,6 +923,14 @@
font-weight: 500;
color: #000;
position: relative;
.top-desc{
position: absolute;
bottom: -10rpx;
font-size: 18rpx;
color: #999;
left: 50%;
transform: translateX(-50%);
}
.top-desc{
position: absolute;
+182 -17
View File
@@ -42,7 +42,7 @@
<image class="img" src="@/static/images/pointsAndRank/badge-title-right.png" mode=""></image>
</view>
<view class="item-list-container">
<view class="item" v-for="item in itemList.child">
<view class="item" v-for="item in itemList.child" @click="showDetailed(item)">
<view class="badge-img">
<image-preview
class="img"
@@ -56,8 +56,32 @@
</view>
</view>
</view>
<!-- </view> -->
</z-paging>
<uni-popup ref="popup" class="popup">
<view class="popup-content" @touchmove.stop>
<view class="fl1"></view>
<view class="fl1"></view>
<view class="max-div">
<view class="points-img-div">
<image-preview class="img" :src="detailedInfo.ossAddr" mode="widthFix"></image-preview>
</view>
<view class="points-title">{{ detailedInfo.badgeName }}</view>
<view class="points-div select-institution-div">
<view class="points-remark">{{ detailedInfo.remark }}</view>
<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>
</view>
</view>
<view class="fl1"></view>
</view>
</uni-popup>
</template>
<script setup>
@@ -68,29 +92,63 @@
import useZpaging from '@/composables/useZpaging.js';
import { queryRankWall } from '@/api/pointsAndRank.js';
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',
width: '290rpx',
height: '100rpx',
border: '4rpx solid #EEDA9B',
borderRadius: '180rpx'
}
const popup = ref(null);
const user_info = reactive({
userName: '',
userId: ''
});
const detailedInfo = ref({
ossAddr: '',
message: '',
num: ''
});
const fetchFunction = (params) => {
return new Promise((resolve, reject) => {
queryRankWall().then(({ body }) => {
let originalData = body;
// +
const twoDimensionalArray = [];
//
Object.entries(originalData).forEach(([categoryKey, badgeArray]) => {
// name badgeList
const categoryItem = {
name: categoryKey, // name
child: badgeArray //
};
//
twoDimensionalArray.push(categoryItem);
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: twoDimensionalArray
body: [
{ name: '段位徽章', child: scoreRankBadges },
{ name: '节气徽章', child: otherBadges }
]
});
});
});
@@ -98,7 +156,16 @@
const { pagingRef, queryList, data_list, search } = useZpaging({
fetchFunction
});
//
const showDetailed = (item) => {
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();
@@ -107,6 +174,33 @@
</script>
<style lang="scss" scoped>
.popup {
.popup-content {
width: 100vw;
height: 100vh;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
background-color: rgba(0,0,0,.85);
}
.max-div {
position: relative;
overflow: hidden;
margin-top: var(--status-bar-height);
display: flex;
flex-direction: column;
align-items: center;
.points-img-div {
width: 420rpx;
height: 420rpx;
}
}
}
.main-div {
padding-top: 30rpx;
}
@@ -187,6 +281,7 @@
gap: 40rpx; /* 调大间距(根据需要调整数值) */
padding: 6vw;
box-sizing: border-box;
.item {
/* 计算宽度:总宽度减去两倍间距后三等分(保证三列) */
@@ -195,7 +290,6 @@
flex-direction: column;
align-items: center;
box-sizing: border-box;
.badge-img {
width: calc(27vw - 14rpx); /* 基于父元素宽度计算,更适配 */
height: calc(27vw - 14rpx);
@@ -218,7 +312,6 @@
text-align: center;
font-style: normal;
color: #e2d1cc;
}
}
}
@@ -285,4 +378,76 @@
background-repeat: no-repeat; /* 不重复平铺 */
background-color: #fef1dd;
}
.points-title{
font-weight: 400;
font-size: 54rpx;
text-align: center;
color: #ffffff;
margin: 20rpx 0 60rpx 0;
}
.points-button-div{
margin-bottom: 200rpx;
margin-top: 180rpx;
.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;
display: flex;
flex-direction: column;
justify-content: space-around;
.points-remark{
font-weight: 400;
font-size: 26rpx;
text-align: center;
color: #ffffff;
}
.points-time{
font-weight: 400;
font-size: 26rpx;
text-align: center;
color: #ffffff;
}
}
/* 边框高光效果 */
.select-institution-div2::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4rpx solid transparent;
background: linear-gradient(90deg, transparent, RGBA(251, 231, 178, 1), transparent) border-box;
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
border-radius: 20rpx;
/* 初始位置在左侧外部 */
background-size: 200% 100%;
/* 动画:高光从左到右流动 */
animation: highlightFlow 20s linear infinite;
}
/* 定义高光流动动画 */
@keyframes highlightFlow {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
</style>
@@ -64,6 +64,7 @@
traId: item.traId
}).then(res=>{
if(res.body.inRange){
// todo
common.navigateTo('/pages/sparring/detail?traId=' + item.traId + '&isPreview=1')
}else{
common.show(res.body.message)
+1
View File
@@ -23,6 +23,7 @@
<uv-cell title="性别" @click="sexPicker.open()" :isLink="true" :value="showGenderText"></uv-cell>
<uv-cell title="百禄形象" @click="gotoMascot()" :isLink="true" :value="mascotName"></uv-cell>
<uv-cell title="竞赛是否匿名" @click="isAnonyClick()" :isLink="true" :value="showIsAnonyText"></uv-cell>
<uv-cell
title="用户隐私协议"
@@ -5,13 +5,13 @@
</view>
</view>
<view class="answer-content" v-else>
<view :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<view v-if="showVoice" :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<CommonLoading color="#06f" v-show="voiceLoading" />
</view>
<view class="talking-cont">
<MarkdownPreview :mdString="mdText" />
</view>
<view class="btns-cont" v-if="!dataInfo.isLoading">
<view class="btns-cont" v-if="!dataInfo.isLoading && showVoice">
<!-- <view class="replay-btn" @click="reAnswer" v-if="!props.dataInfo.isHistory">
<image class="icon" src="@/static/images/dialog/answer-restart1.png" style="width: 100%; height: 100%"></image>
</view>
@@ -83,13 +83,18 @@
default: true,
type: Boolean
},
showVoice: {
default: true,
type: Boolean
},
})
const {
isLast,
dataInfo,
roleInfo,
activeVoiceId,
isStudy
isStudy,
showVoice
} = toRefs(props)
const isPlaying = ref(false)
const voiceIsPlaying = ref(false)
@@ -1,9 +1,9 @@
<template>
<view class="talking-info" v-if="dataInfo.intrctWay === '02'">
<view :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<view v-if="showVoice" :class="['talking-gif', !voiceLoading&&(props.activeVoiceId === dataInfo.id) ? 'is-play' : '']">
<CommonLoading color="#f60" v-show="voiceLoading"/>
</view>
<view class="right-time">{{voiceTime}}</view>
<view v-if="showVoice" class="right-time">{{voiceTime}}</view>
<view class="talking-cont">
<rich-text :class="['talking-text translate-text loading']" v-show="!translateLoading" :nodes="`<span>${showContent}</span>`">
</rich-text>
@@ -11,7 +11,7 @@
<CommonLoading color="#f60"/>
</view>
</view>
<view class="btns-cont">
<view class="btns-cont" v-if="showVoice">
<view class="play-btn">
<image class="icon"
v-show="!(!voiceLoading&&(props.activeVoiceId === dataInfo.id))"
@@ -64,8 +64,12 @@ import { onUnload } from '@dcloudio/uni-app';
default:'',
type: String
},
showVoice: {
default: true,
type: Boolean
}
})
const { activeVoiceId, dataInfo } = toRefs(props)
const { activeVoiceId, dataInfo, showVoice } = toRefs(props)
const emit = defineEmits(['changePlay'])
//
const talkVoiceObj = ref(null)
@@ -13,7 +13,7 @@
</view>
</view>
<view class="btns-cont">
<view class="play-btn" v-show="!isTesting">
<view class="play-btn">
<image class="icon" v-show="!(!voiceLoading&&(props.activeVoiceId === dataInfo.id))" @click.stop="playVoice"
src="@/static/images/course/play-blue.png" style="width: 100%;height: 100%;"></image>
<image class="icon" v-show="!voiceLoading&&(props.activeVoiceId === dataInfo.id)" @click.stop="pauseVoice"
@@ -84,6 +84,20 @@
onUnload(() => {
pauseVoice()
})
//
const restartPlay = () => {
if (itemVoice.value) {
emit('changePlay', '');
emit('changePlay', unref(dataInfo)?.id);
setTimeout(() => {
talkUserVoiceObj.value.src = itemVoice.value;
talkUserVoiceObj.value.seek(0);
talkUserVoiceObj.value.play();
}, 100);
} else {
playVoice();
}
};
const pauseVoice = () => {
if (!talkUserVoiceObj.value) return
talkUserVoiceObj.value.pause()
@@ -92,15 +106,22 @@
}
const playVoice = () => {
emit('changePlay', unref(dataInfo)?.id)
if(isTesting.value){
talkUserVoiceObj.value.volume = 1
restartPlay()
return
}
if (talkUserVoiceObj.value && talkUserVoiceObj.value.src) {
talkUserVoiceObj.value.volume = 1
setTimeout(() => {
// talkUserVoiceObj.value.src = itemVoice.value
talkUserVoiceObj.value.src = itemVoice.value
talkUserVoiceObj.value.play()
isPlaying.value = true
}, 300)
return
} else {
talkUserVoiceObj.value.src = ''
talkUserVoiceObj.value.volume = 1
if (unref(dataInfo).talkingVoice) {
setTimeout(() => {
talkUserVoiceObj.value.src = itemVoice.value
@@ -128,10 +149,16 @@
talkUserVoiceObj.value.volume = 1
})
talkUserVoiceObj.value.onError(() => {
isTesting.value = false
talkUserVoiceObj.value.volume = 1
talkUserVoiceObj.value.src = ''
})
talkUserVoiceObj.value.onCanplay(() => {
setVoiceTime(0)
})
itemVoice.value = unref(dataInfo).talkingVoice
talkUserVoiceObj.value.src = itemVoice.value
talkUserVoiceObj.value.sessionCategory = 'soloAmbient'
//#ifdef APP-PLUS
//
if (IsAndEnv) {
@@ -141,22 +168,22 @@
// talkUserVoiceObj.value.pause()
}
// #endif
setTimeout(() => {
setVoiceTime(0)
}, 100)
// setTimeout(() => {
// setVoiceTime(0)
// }, 100)
}
const isTesting = ref(false)
const setVoiceTime = (sum) => {
sum += 1
let _sum = sum + 1
if (Math.ceil(talkUserVoiceObj.value.duration) > 0) {
voiceTime.value = (Math.ceil(talkUserVoiceObj.value.duration) || 1) + 's'
} else {
if (sum === 20) {
voiceTime.value = 0 + 's'
if (_sum === 25) {
voiceTime.value = (props.dataInfo.voiceTime|| 1) + 's'
return
}
setTimeout(() => {
setVoiceTime(sum)
setVoiceTime(_sum)
}, 200)
}
}
@@ -0,0 +1,94 @@
<template>
<view>
<view v-if="isPreviewing" class="live-container">
<cover-view class="pusher-container">
<live-pusher
id="livePusher"
class="livePusher"
:muted="true"
:enable-camera="true"
:auto-focus="true"
:beauty="0"
:whiteness="0"
aspect="9:16"
mode="SD"
></live-pusher>
</cover-view>
</view>
</view>
</template>
<script setup>
import { getPhoneEnvBool } from '@/common/common';
import { onMounted, ref, reactive, nextTick } from 'vue';
const livePusherContext = ref(null);
const isPreviewing = ref(false);
const init = (componentProxy) => {
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy);
livePusherContext.value.switchCamera()
};
// 切换摄像头
const switchCamera = () => {
livePusherContext.value?.switchCamera({
success: (res) => {
console.log('livePusher.switchCamera:', JSON.stringify(res));
},
fail: (err) => {
console.error('切换摄像头失败:', err);
}
});
};
// 开启预览
const startPreview = () => {
console.log('摄像头开启预览');
isPreviewing.value = true;
nextTick(() => {
livePusherContext.value?.startPreview({
success: (res) => {
console.log('摄像头开启成功');
},
fail: (err) => {
console.error('开启预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
if (getPhoneEnvBool('AND')) {
console.log('切换前置');
}
});
};
// 关闭预览
const stopPreview = () => {
console.log('关闭预览');
livePusherContext.value?.stopPreview({
success: (res) => {
console.log('livePusher.stopPreview:', JSON.stringify(res));
},
fail: (err) => {
console.error('关闭预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
isPreviewing.value = false;
};
defineExpose({ init, switchCamera, stopPreview, startPreview });
</script>
<style scoped>
.live-container {
position: absolute;
right: 0;
top: 150rpx;
overflow: hidden;
border-radius: 20rpx;
width: 270rpx;
height: 480rpx;
}
</style>
File diff suppressed because one or more lines are too long
@@ -12,37 +12,76 @@
></view>
</template>
<script setup>
import { ref, onUnmounted, watch, onMounted } from 'vue';
const isStop = ref(false);
const currentPCMChunk = ref(false);
const audioOutput = ref('');
//
const isPlaying = ref(false); //
const volume = ref(); // 0~1
const playPCM = (pcmData) => {
if (!pcmData || pcmData.byteLength === 0) return;
currentPCMChunk.value = pcmData;
};
const close = () => {
isStop.value = false;
};
const changeStreamPlaying = () => {
console.log('播放结束');
};
const switchOutput = (type) => {
if (['speaker', 'earpiece'].includes(type)) {
audioOutput.value = type;
}
};
<script>
export default {
// API
data() {
return {
isStop: false,
currentPCMChunk: false,
playNumber: '',
audioOutput: 'earpiece',
isPlaying: false, //
volume: 1 // 0~1
};
},
defineExpose({ playPCM, close, switchOutput });
//
mounted() {
// onMounted
},
unmounted() {
// onUnmounted
},
// API
methods: {
// PCM
playPCM(pcmData) {
if (!pcmData || pcmData.byteLength === 0) return;
this.currentPCMChunk = pcmData;
},
// id
setPlayNumber(id) {
console.log('组件内设置播放', id);
this.playNumber = id;
},
// /
close() {
this.isStop = false;
},
//
changeStreamPlaying({ status }) {
if (status === false) {
console.log('播放结束组件', this.playNumber);
const aaa = this.playNumber
this.playNumber = ''
this.$emit('playOver', aaa);
}
},
//
switchOutput(type) {
if (['speaker', 'earpiece'].includes(type)) {
this.audioOutput = type;
}
}
},
};
</script>
<script module="renderJS" lang="renderjs">
import { StreamPlayer } from "../js/StreamPlayer";
let player = null;
/**
* 音频路由兼容方案适配 H5/小程序/不支持 setAudioRoute 的设备
* @param {string} type - earpiece听筒/speaker扬声器
*/
export default {
mounted() {
const _this = this;
@@ -54,28 +93,24 @@
pcmType: 'int', // int/float
callback: _this.callback
});
this.switchAudioOutput('speaker');
},
methods: {
// /
switchAudioOutput(type) {
console.log('切换音频输出')
},
appendPCMChunk(numArray) {
// console.log('', numArray.length);
if (!numArray || numArray.length === 0) return;
// ArrayBuffer
const uint8 = new Uint8Array(numArray);
const arrayBuffer = uint8.buffer;
player.appendChunk(arrayBuffer);
},
// /
switchAudioOutput(type) {
// plus.audio.setAudioRoute(type === 'speaker' ? 'speaker' : 'earpiece');
},
//
stop() {
if (player) {
player.destroy();
player = null;
// 便
//
const _this = this;
player = new StreamPlayer({
inputSampleRate: 16000,
@@ -90,7 +125,8 @@
// 线
callback(e) {
if (e === 'ended') {
this.$ownerInstance.callMethod('changeStreamPlaying', { type: false });
console.log('播放结束renderjs', e);
this.$ownerInstance.callMethod('changeStreamPlaying', {status:false});
}
}
}
@@ -1,5 +1,5 @@
<template>
<view class="talking-item">
<view :class="['talking-item', talkingType + '-item']">
<!-- 头像信息 -->
<view class="user-info" v-if="talkingType === 'answer'">
<view class="avatar-box">
@@ -16,7 +16,7 @@
:isAnswering="props.isAnswering" @handleEvents="handleEvents" :activeVoiceId="activeVoiceId">
</answerVue>
<view v-else></view>
<view class="talking-tips" v-if="talkingType === 'answer' && !isExam">
<view class="talking-tips" v-if="talkingType === 'answer' && !isExam && isLast">
<view class="talking-tips-btn" @click="getTips()">回答提示</view>
<view class="talking-tips-loading" v-if="loadingTips">
<CommonLoading color="#06f" />
@@ -131,11 +131,19 @@
</script>
<style scoped lang="scss">
$talkItemPadding: 80rpx;
.talking-item {
overflow: hidden;
margin-top: 0rpx;
position: relative;
margin-bottom: 24rpx;
&.question-item{
padding-left: $talkItemPadding;
}
&.answer-item{
padding-right: $talkItemPadding;
}
&+.talking-item {
margin-top: 30rpx;
}
@@ -199,6 +207,7 @@
font-size: 24rpx;
line-height: 38rpx;
color: #8191AA;
padding: 0 20rpx;
}
.talking-tips-loading{
display: flex;
@@ -1,5 +1,6 @@
<template>
<view class="talking-item">
<!-- <view class="talking-item"> -->
<view :class="['talking-item', talkingType + '-item']">
<!-- 头像信息 -->
<view class="user-info" v-if="talkingType === 'answer'">
<view class="avatar-box">
@@ -11,9 +12,9 @@
</view>
</view>
<questionVue v-if="talkingType === 'question'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:activeVoiceId="activeVoiceId"></questionVue>
:activeVoiceId="activeVoiceId" :showVoice="showVoice"></questionVue>
<answerVue v-else-if="talkingType === 'answer'" @changePlay="changePlay" :dataInfo="talkingInfo" :isLast="isLast" :roleInfo="roleInfo"
:isAnswering="props.isAnswering" @handleEvents="handleEvents" :activeVoiceId="activeVoiceId" :isStudy="false">
:isAnswering="props.isAnswering" @handleEvents="handleEvents" :activeVoiceId="activeVoiceId" :isStudy="false" :showVoice="showVoice">
</answerVue>
<view v-else></view>
</view>
@@ -63,6 +64,10 @@
default: false,
type: Boolean
},
showVoice: {
default: true,
type: Boolean
}
})
watch(() => props.activeVoiceId, () => {}, {
deep: true,
@@ -75,7 +80,8 @@
const {
talkingType,
talkingInfo,
roleInfo
roleInfo,
showVoice
} = toRefs(props)
const loadingTips = ref(false)
@@ -99,11 +105,19 @@
</script>
<style scoped lang="scss">
$talkItemPadding: 80rpx;
.talking-item {
overflow: hidden;
margin-top: 0rpx;
position: relative;
margin-bottom: 24rpx;
&.question-item{
padding-left: $talkItemPadding;
}
&.answer-item{
padding-right: $talkItemPadding;
}
&+.talking-item {
margin-top: 30rpx;
}
@@ -6,7 +6,13 @@
{{'语音对话'}}
</view>
</view>
<uni-popup ref="talkModelRef" type="bottom" @click="hideOverlayTalking"
<!-- <view class="touch-btn" v-else @click="stopRecord()">
<view class="center-text">
<image class="type-icon" src="@/static/images/sparring/talk-icon.png"></image>
{{'语音对话'}}
</view>
</view> -->
<uni-popup ref="talkModelRef" type="bottom" @click="hideOverlayTalking" @touchend="hideOverlayTalking"
mask-background-color="rgba(0,0,0,0.9)">
<view class="overlay-box">
<view class="circle-bg-box">
@@ -54,7 +60,7 @@
onHide,
onShow
} from '@dcloudio/uni-app';
const storageStore = useStorageStore();
const proxy = getCurrentInstance();
const emit = defineEmits(['uploadVoice', 'startRecord']);
@@ -127,12 +133,17 @@
const initRecord = () => {
recordObjCom.value = uni.getRecorderManager();
unref(recordObjCom)?.onStop((res, duration) => {
if(isGettingPermission.value){
isRecording.value = false
console.log(15)
if (isGettingPermission.value) {
isGettingPermission.value = false
console.log(16)
closeModel();
return
}
audioPath.value = res.tempFilePath;
if (!isOut.value) {
console.log(17)
if (startFlag.value !== 2) {
closeModel(() => {
nextTick(() => {
@@ -140,15 +151,21 @@
});
});
} else {
console.log(18)
closeModel();
emit('uploadVoice', audioPath.value);
let _time = Math.ceil(recordTime.value)
emit('uploadVoice', audioPath.value, _time);
recordTime.value = 0
}
} else {
console.log(19)
closeModel();
}
});
unref(recordObjCom).onStart(() => {
if(isGettingPermission.value){
isRecording.value = true
recordTime.value = 0
if (isGettingPermission.value) {
stopRecord();
return
}
@@ -156,20 +173,36 @@
setTimeout(() => {
startFlag.value = 2;
}, 1000);
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'authorized') {
if (isStopNum.value === isStartNum.value) {
stopRecord();
} else {
showTalkingModel.value = true;
}
setRecordTime()
if (isStopNum.value === isStartNum.value) {
stopRecord();
} else {
showTalkingModel.value = true;
}
});
unref(recordObjCom).onError(() => {
isRecording.value = false
isGettingPermission.value = false
if (showTalkingModel.value) {
closeModel();
}
common.msg('语音输入错误,请重试!')
});
};
const setRecordTime = () => {
setTimeout(() => {
recordTime.value += 0.2
if (isRecording.value) {
setRecordTime()
}
}, 200)
}
const isRecording = ref(false)
const recordTime = ref(0)
const startFlag = ref(1);
const startRecord = () => {
isStartNum.value += 1;
console.log(8, '开始录音前')
unref(recordObjCom).start({
format: formatType.value
});
@@ -178,24 +211,34 @@
const stopRecord = () => {
isStopNum.value = isStartNum.value;
try {
if (unref(recordObjCom).stop) {
if (isRecording.value && unref(recordObjCom).stop) {
setTimeout(() => {
isGettingPermission.value = false
}, 1000)
console.log(10, '验证停止方法存在')
closeModel();
unref(recordObjCom).stop();
} else {
console.log(11, '验证停止方法不存在')
isGettingPermission.value = false
closeModel();
}
} catch {
console.log(12, '验证错误')
isGettingPermission.value = false
closeModel();
}
};
const closeModel = (cb = () => {}) => {
console.log(99, '关闭弹窗')
isStopNum.value = isStartNum.value;
setTimeout(() => {
isTalking.value = false;
}, 800);
setTimeout(() => {
showTalkingModel.value = false;
cb && cb();
}, 350);
setTimeout(() => {
isTalking.value = false;
}, 800);
};
// #endif
//#ifndef APP-PLUS
@@ -234,18 +277,20 @@
}, 500);
return;
}
console.log(1, '开始执行获取权限')
// #ifdef APP-PLUS
if (!hasPermission.value) {
let _res = await getPermission()
console.log(2, '取到权限结果:', _res)
if (_res) {
hasPermission.value = true
setTimeout(()=>{
if(!isGettingPermission.value){
setTimeout(() => {
if (!isGettingPermission.value) {
showOverlayTalking(obj);
}else{
} else {
isGettingPermission.value = false
}
},100)
}, 100)
}
} else {
showOverlayTalking(obj);
@@ -288,10 +333,8 @@
return false
} else if (_state === 'not determined') {
// startRecord()
// isGettingPermission.value = false
return true
} else {
// isGettingPermission.value = false
return true
}
}
@@ -303,12 +346,22 @@
const isMoving = ref(false);
const isTalking = ref(false);
const showOverlayTalking = (obj) => {
if (showTalkingModel.value) return;
console.log(3, '权限验证通过,展示录音弹窗')
if (showTalkingModel.value) {
console.log(4, '默认展开状态关闭,return')
showTalkingModel.value = false
return
};
if (isGettingPermission.value) {
stopRecord()
isGettingPermission.value = false
console.log(5, 'onhide状态触发')
if (isRecording.value) {
stopRecord()
} else {
isGettingPermission.value = false
}
return
}
console.log(6, '未触发 hide导致的事件')
if (isLoadingState.value) {
uni.showToast({
title: props.loadingText,
@@ -323,6 +376,7 @@
recNum.value += 1;
let _recnum = recNum.value
if (!isDisabled.value) {
console.log(7, '开始录音')
touchX.value = obj.changedTouches[0].pageX;
touchY.value = obj.changedTouches[0].pageY;
baseY.value = obj.currentTarget.offsetTop;
@@ -331,7 +385,6 @@
startRecord();
// #endif
setTimeout(() => {
console.log(_recnum, recNum.value)
if (_recnum === recNum.value) {
hideOverlayTalking(recNum.value);
}
@@ -347,8 +400,9 @@
};
const hideOverlayTalking = (obj) => {
isMoving.value = false;
if (!showTalkingModel.value) return;
// if (!showTalkingModel.value) return;
//#ifdef APP-PLUS
console.log(9, '关闭录音窗口')
stopRecord();
// #endif
};
@@ -362,14 +416,13 @@
}
}
};
onShow(()=>{
onShow(() => {
// stopRecord()
// if(isGettingPermission.value){
// hideOverlayTalking()
// }
})
onHide(()=>{
console.log('onHide')
onHide(() => {
isGettingPermission.value = true
})
</script>
@@ -390,7 +443,6 @@
background-color: #06f;
font-weight: 400;
position: relative;
;
color: #fff;
font-size: 29rpx;
border-radius: 23rpx;
@@ -420,12 +472,14 @@
// justify-content: flex-end;
width: 100%;
height: 100%;
pointer-events: none;
.circle-bg-box {
height: 234rpx;
background: url(@/static/images/course/talking.png) no-repeat;
background-size: cover;
position: relative;
pointer-events: none;
.icon-box {
position: absolute;
@@ -435,10 +489,12 @@
margin-top: -24rpx;
width: 34rpx;
height: 48rpx;
pointer-events: none;
image {
width: 100%;
height: 100%;
pointer-events: none;
}
}
}
@@ -449,6 +505,7 @@
color: #c0c0c0;
text-align: center;
margin: 12rpx 0 24rpx;
pointer-events: none;
}
.btns-box {
@@ -460,6 +517,7 @@
font-size: 28rpx;
text-align: center;
line-height: 116rpx;
pointer-events: none;
.close-btn {
float: left;
@@ -467,9 +525,11 @@
width: 154rpx;
overflow: hidden;
color: #999;
pointer-events: none;
&.is-out {
color: #fff;
pointer-events: none;
}
}
@@ -479,11 +539,13 @@
width: 154rpx;
overflow: hidden;
color: #999;
pointer-events: none;
}
.btn-box {
width: 100%;
height: 154rpx;
pointer-events: none;
}
}
@@ -494,6 +556,7 @@
background-color: #06f;
border-radius: 32rpx;
position: relative;
pointer-events: none;
&.is-out {
background-color: #ff3e49;
@@ -510,6 +573,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
&::after {
@@ -523,33 +587,7 @@
bottom: -22rpx;
left: 50%;
margin-left: -22rpx;
}
}
.white-bg-box {
height: 365rpx;
background-color: #fff;
overflow: hidden;
border-radius: 32rpx 32rpx 0 0;
padding: 58rpx 36rpx;
.box-title {
font-size: 32rpx;
font-weight: bold;
line-height: 46rpx;
margin-bottom: 46rpx;
}
.box-btns {
height: 46rpx;
line-height: 46rpx;
display: flex;
justify-content: space-between;
color: #999;
.is-active {
color: #06f;
}
pointer-events: none;
}
}
}
@@ -0,0 +1,574 @@
<template>
<view class="talk-btns" ref="talkBtnRef">
<view class="touch-btn" @touchstart="getRecordPermission" @touchend="hideOverlayTalking" @touchmove="handleMove">
<view class="center-text">
<image class="type-icon" src="@/static/images/sparring/talk-icon.png"></image>
{{'语音对话'}}
</view>
</view>
<uni-popup ref="talkModelRef" type="bottom" @click="hideOverlayTalking"
mask-background-color="rgba(0,0,0,0.9)">
<view class="overlay-box">
<view class="circle-bg-box">
<view class="icon-box">
<image src="/src/static/images/course/talk-icon.png" mode=""></image>
</view>
</view>
<view class="tips-box">松开发送</view>
<view class="btns-box">
<view :class="['close-btn', isOut ? 'is-out' : '']">
取消
<image class="btn-box" v-if="!isOut" src="/src/static/images/course/close.png" mode=""></image>
<image class="btn-box" v-else src="/src/static/images/course/close-active.png" mode=""></image>
</view>
</view>
<view :class="['talk-dialog', isOut ? 'is-out' : '']">
<image src="@/static/images/course/voice-white.gif" mode="" class="gif-box"></image>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
toRefs,
ref,
computed,
unref,
onMounted,
onUnmounted,
nextTick,
watch,
getCurrentInstance
} from 'vue';
import common from '../../../common/common';
import permission from '@/common/permission.js';
import {
getPhoneEnvBool
} from '@/common/common.js';
import {
useStorageStore
} from '@/store/storage.js';
import {
onHide,
onShow
} from '@dcloudio/uni-app';
const storageStore = useStorageStore();
const proxy = getCurrentInstance();
const emit = defineEmits(['uploadVoice', 'startRecord']);
const props = defineProps({
isDisabled: {
default: true,
type: Boolean
},
isLoading: {
default: false,
type: Boolean
},
loadingText: {
default: '发送回答中,请稍后再试!',
type: String
},
formatType: {
default: 'mp3', //
type: String
},
});
const {
isDisabled,
onlyVoice,
formatType
} = toRefs(props);
const isLoadingState = computed(() => props.isLoading);
const showTalkingModel = ref(false);
const keyboardIsShow = ref(false);
const talkModelRef = ref();
storageStore.setTouchBtnZIndex(12);
const btnZindex = computed(() => storageStore.getTouchBtnZIndex);
watch(
() => showTalkingModel.value,
(val) => (val ? talkModelRef.value.open() : talkModelRef.value.close())
);
watch(
() => props.isLoading,
() => {}, {
deep: true,
immediate: true
}
);
watch(
() => props.isDisabled,
(val) => {
if (val) {
keyboardIsShow.value = false;
}
}, {
deep: true,
immediate: true
}
);
const answerValue = ref('');
const talkBtnRef = ref();
//
const audioPath = ref('');
const audioObj = uni.createInnerAudioContext();
//#ifdef APP-PLUS
// app
const recordObjCom = ref({});
const IsAndEnv = getPhoneEnvBool('AND');
const isStartNum = ref(0);
const isStopNum = ref(0);
const isGettingPermission = ref(false)
const initRecord = () => {
recordObjCom.value = uni.getRecorderManager();
unref(recordObjCom)?.onStop((res, duration) => {
isRecording.value = false
if(isGettingPermission.value){
isGettingPermission.value = false
return
}
audioPath.value = res.tempFilePath;
if (!isOut.value) {
if (startFlag.value !== 2) {
closeModel(() => {
nextTick(() => {
common.msg('说话时间太短,没有听清!');
});
});
} else {
closeModel();
emit('uploadVoice', audioPath.value, recordTime.value);
}
} else {
closeModel();
}
});
unref(recordObjCom).onStart(() => {
isRecording.value = true
recordTime.value = 0
setRecordTime()
if(isGettingPermission.value){
stopRecord();
return
}
startFlag.value = 1;
setTimeout(() => {
startFlag.value = 2;
}, 1000);
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'authorized') {
if (isStopNum.value === isStartNum.value) {
stopRecord();
} else {
showTalkingModel.value = true;
}
}
});
};
const setRecordTime = () => {
setTimeout(()=>{
if(isRecording.value){
recordTime.value += 1
setRecordTime()
}
},1000)
}
const isRecording = ref(false)
const recordTime = ref(0)
const startFlag = ref(1);
const startRecord = () => {
isStartNum.value += 1;
unref(recordObjCom).start({
format: formatType.value
});
};
const stopRecord = () => {
isStopNum.value = isStartNum.value;
try {
if (unref(recordObjCom).stop) {
unref(recordObjCom).stop();
} else {
closeModel();
}
} catch {
closeModel();
}
};
const closeModel = (cb = () => {}) => {
isStopNum.value = isStartNum.value;
setTimeout(() => {
showTalkingModel.value = false;
cb && cb();
}, 350);
setTimeout(() => {
isTalking.value = false;
}, 800);
};
// #endif
//#ifndef APP-PLUS
const recordObjCom = ref({});
const initRecord = () => {};
// #endif
onMounted(async () => {
initRecord?.();
});
//
//
const touchX = ref(0);
const touchY = ref(0);
const baseY = ref(0);
const windowInfo = uni.getWindowInfo();
//
const isOut = computed(() => {
let _btnHeight = (windowInfo.screenWidth / 750) * 234;
let moveVal = windowInfo.screenHeight - touchY.value > _btnHeight;
return moveVal;
});
const outScreen = computed(() => {
windowInfo.screenHeight - touchY.value <= 0;
});
const recNum = ref(1);
const activeNum = ref(1);
const hasPermission = ref(false)
const getRecordPermission = async (obj) => {
if (isDisabled.value) return;
if (isTalking.value) {
common.msg('操作频繁,请稍后重试!');
setTimeout(() => {
isTalking.value = false;
}, 500);
return;
}
// #ifdef APP-PLUS
if (!hasPermission.value) {
let _res = await getPermission()
if (_res) {
hasPermission.value = true
setTimeout(()=>{
if(!isGettingPermission.value){
showOverlayTalking(obj);
}else{
isGettingPermission.value = false
}
},100)
}
} else {
showOverlayTalking(obj);
}
// #endif
};
const getPermission = async () => {
// #ifdef APP-PLUS
//
if (IsAndEnv) {
let _recordPerm = await permission.requestAndroidPermission('android.permission.RECORD_AUDIO');
if (_recordPerm !== 1) {
showTalkingModel.value = false;
let res = await common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定')
if (res) {
//
permission.gotoAppPermissionSetting();
} else {
//
common.navigateBack();
}
return false
} else {
// isGettingPermission.value = false
return true
}
} else {
let _recordPerm = uni.getAppAuthorizeSetting();
let _state = _recordPerm.microphoneAuthorized;
if (_state === 'denied') {
common.show('提示信息', '当前页面需要使用的录音权限,是否前往开启?', true, '取消', '确定').then((res) => {
if (res) {
//
uni.openAppAuthorizeSetting();
} else {
//
common.navigateBack();
}
});
return false
} else if (_state === 'not determined') {
// startRecord()
// isGettingPermission.value = false
return true
} else {
// isGettingPermission.value = false
return true
}
}
// #endif
// //#ifndef APP-PLUS
// return true
// // #endif
}
const isMoving = ref(false);
const isTalking = ref(false);
const showOverlayTalking = (obj) => {
if (showTalkingModel.value) return;
if (isGettingPermission.value) {
stopRecord()
isGettingPermission.value = false
return
}
if (isLoadingState.value) {
uni.showToast({
title: props.loadingText,
icon: 'none',
duration: 1000
});
return;
}
isMoving.value = true;
isTalking.value = true;
emit('startRecord');
recNum.value += 1;
let _recnum = recNum.value
if (!isDisabled.value) {
touchX.value = obj.changedTouches[0].pageX;
touchY.value = obj.changedTouches[0].pageY;
baseY.value = obj.currentTarget.offsetTop;
//#ifdef APP-PLUS
// canStart.value = false
startRecord();
// #endif
setTimeout(() => {
console.log(_recnum, recNum.value)
if (_recnum === recNum.value) {
hideOverlayTalking(recNum.value);
}
}, 59 * 1010);
} else {
showTalkingModel.value = false;
uni.showToast({
title: '暂无问题需要回答!',
icon: 'none',
duration: 1000
});
}
};
const hideOverlayTalking = (obj) => {
isMoving.value = false;
if (!showTalkingModel.value) return;
//#ifdef APP-PLUS
stopRecord();
// #endif
};
const handleMove = (obj) => {
if (isMoving.value) {
touchX.value = obj.changedTouches[0].pageX;
touchY.value = obj.changedTouches[0].pageY;
if (outScreen.value) {
hideOverlayTalking(obj);
}
}
};
onShow(()=>{
// stopRecord()
// if(isGettingPermission.value){
// hideOverlayTalking()
// }
})
onHide(()=>{
console.log('onHide')
isGettingPermission.value = true
})
</script>
<style lang="scss" scoped>
.talk-btns {
height: 150rpx;
background-color: #fff;
padding: 25rpx 30rpx 0;
position: relative;
z-index: v-bind(btnZindex);
.touch-btn {
height: 92rpx;
// box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.06);
text-align: center;
line-height: 92rpx;
background-color: #06f;
font-weight: 400;
position: relative;
;
color: #fff;
font-size: 29rpx;
border-radius: 23rpx;
.center-text {
height: 35rpx;
line-height: 35rpx;
width: 100%;
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
}
.type-icon {
width: 35rpx;
height: 35rpx;
margin-right: 10rpx;
}
}
}
.overlay-box {
display: flex;
flex-direction: column-reverse;
// justify-content: flex-end;
width: 100%;
height: 100%;
.circle-bg-box {
height: 234rpx;
background: url(@/static/images/course/talking.png) no-repeat;
background-size: cover;
position: relative;
.icon-box {
position: absolute;
left: 50%;
top: 50%;
margin-left: -17rpx;
margin-top: -24rpx;
width: 34rpx;
height: 48rpx;
image {
width: 100%;
height: 100%;
}
}
}
.tips-box {
height: 40rpx;
line-height: 40rpx;
color: #c0c0c0;
text-align: center;
margin: 12rpx 0 24rpx;
}
.btns-box {
height: 270rpx;
// padding: 0 120rpx;
width: 154rpx;
margin: 0 auto 100rpx;
overflow: hidden;
font-size: 28rpx;
text-align: center;
line-height: 116rpx;
.close-btn {
float: left;
height: 100%;
width: 154rpx;
overflow: hidden;
color: #999;
&.is-out {
color: #fff;
}
}
.translate-btn {
float: right;
height: 100%;
width: 154rpx;
overflow: hidden;
color: #999;
}
.btn-box {
width: 100%;
height: 154rpx;
}
}
.talk-dialog {
width: 506rpx;
height: 234rpx;
margin: 0 auto;
background-color: #06f;
border-radius: 32rpx;
position: relative;
&.is-out {
background-color: #ff3e49;
&::after {
background-color: #ff3e49;
}
}
.gif-box {
position: absolute;
width: 200rpx;
height: 80rpx;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&::after {
content: '';
width: 44rpx;
height: 44rpx;
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
background-color: #06f;
overflow: hidden;
position: absolute;
bottom: -22rpx;
left: 50%;
margin-left: -22rpx;
}
}
.white-bg-box {
height: 365rpx;
background-color: #fff;
overflow: hidden;
border-radius: 32rpx 32rpx 0 0;
padding: 58rpx 36rpx;
.box-title {
font-size: 32rpx;
font-weight: bold;
line-height: 46rpx;
margin-bottom: 46rpx;
}
.box-btns {
height: 46rpx;
line-height: 46rpx;
display: flex;
justify-content: space-between;
color: #999;
.is-active {
color: #06f;
}
}
}
}
::v-deep .uv-input__content__field-wrapper__field {
padding-right: 60rpx;
}
</style>
+14 -7
View File
@@ -18,7 +18,8 @@
</view>
<view class="body-top-item"> 已陪练次数{{ detailInfo.practiceTimes }} </view>
<view class="body-top-item"> 类型{{ detailInfo.traInterTypDesc }} </view>
<view class="body-top-item"> 截止时间{{detailInfo.limitTyp === '01' ? '不限制' : detailInfo.endTm}}</view>
<view :class="['body-top-item',detailInfo.limitTyp === '01'?'':'full' ]"> 截止时间{{detailInfo.limitTyp === '01' ? '不限制' : detailInfo.endTm}}</view>
<view class="body-top-item"> 通过分数{{detailInfo.passGrade || 0}} </view>
<scroll-view class="body-top-tags" :scroll-x="true">
<view class="item-tag-view">
<view class="item-tag active-tag" v-for="item in detailInfo.traPartnerTagList || []">
@@ -62,14 +63,13 @@
@click="previewFile(item)"> {{ item.dataName }}.{{ item.dataSuffix }} </view>
</view>
</view>
<view class="body-bottom">
<!-- v-if="isPreview" -->
<!-- <view class="body-bottom-btn" @click="toChooseRole('preview')">预览陪练</view>
</view>
<view class="body-bottom" v-else> -->
<view class="body-bottom" 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-btn no-margin" @click="toChooseRole('preview')">去考试</view>
</view>
</view>
</scroll-view>
</view>
@@ -296,6 +296,9 @@
display: inline-block;
width: 50%;
margin-bottom: 15rpx;
&.full{
width: 100%;
}
}
&-tags {
@@ -358,6 +361,9 @@
margin-left: 36rpx;
background: #06f;
color: #fff;
&.no-margin{
margin-left: 0;
}
}
}
}
@@ -388,7 +394,8 @@
font-size: 28rpx;
line-height: 52rpx;
color: #333;
white-space: pre-line;
&-file {
color: #06f;
}
+1 -1
View File
@@ -18,7 +18,7 @@
:inactive-style="{ color: '#333333', fontSize: '30rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '30rpx', fontWeight: '600' }"></uv-tabs>
<uv-tabs :list="tabSecList" @click="changeTabSec" :current="currentTabSec" :scrollable="true"
v-if="!resetingSec" class="font_pf my_tabs2"
v-if="!resetingSec && (tabSecList.length > 1)" class="font_pf my_tabs2"
:inactive-style="{ color: '#333333', fontSize: '24rpx', fontWeight: '500' }"
:activeStyle="{ color: '#0066FF', fontSize: '24rpx', fontWeight: '500' }" :lineColor="`#06f`"></uv-tabs>
<!-- :lineColor="`url(${LineBg}) 2% 2%`" -->
@@ -60,10 +60,8 @@ export enum CompressionType {
* MessageType.CONTROL_CMD 使
*/
export enum ControlCommand {
HEARTBEAT = 0b0001,
PAUSE = 0b0010,
RESUME = 0b0011,
STOP = 0b0100,
START_PLAY = 0b0001, // 一句话播放开始
FINISH_PLAY = 0b0010, // 一句话播放结束
}
/**
@@ -171,7 +169,6 @@ export class ProtocolCodec {
// STRING 序列化:必须传入字符串(非 PING 消息已校验非空)
serializedBody = textEncoder.encode(body as string);
break;
case SerializationType.JSON:
// 断言:body 是 string 或 object
if (typeof body === 'string') {
@@ -0,0 +1,300 @@
! function() {
"use strict";
function t(t, e) {
for (var r = 0; r < e.length; r++) {
var n = e[r];
n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object
.defineProperty(t, (i = n.key, o = void 0, "symbol" == typeof(o = function(t, e) {
if ("object" != typeof t || null === t) return t;
var r = t[Symbol.toPrimitive];
if (void 0 !== r) {
var n = r.call(t, e || "default");
if ("object" != typeof n) return n;
throw new TypeError("@@toPrimitive must return a primitive value.")
}
return ("string" === e ? String : Number)(t)
}(i, "string")) ? o : String(o)), n)
}
var i, o
}
function e(t) {
return e = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function(t) {
return t.__proto__ || Object.getPrototypeOf(t)
}, e(t)
}
function r(t, e) {
return r = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function(t, e) {
return t.__proto__ = e, t
}, r(t, e)
}
function n() {
if ("undefined" == typeof Reflect || !Reflect.construct) return !1;
if (Reflect.construct.sham) return !1;
if ("function" == typeof Proxy) return !0;
try {
return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function() {}))), !0
} catch (t) {
return !1
}
}
function i(t, e, o) {
return i = n() ? Reflect.construct.bind() : function(t, e, n) {
var i = [null];
i.push.apply(i, e);
var o = new(Function.bind.apply(t, i));
return n && r(o, n.prototype), o
}, i.apply(null, arguments)
}
function o(t) {
var n = "function" == typeof Map ? new Map : void 0;
return o = function(t) {
if (null === t || (o = t, -1 === Function.toString.call(o).indexOf("[native code]"))) return t;
var o;
if ("function" != typeof t) throw new TypeError("Super expression must either be null or a function");
if (void 0 !== n) {
if (n.has(t)) return n.get(t);
n.set(t, a)
}
function a() {
return i(t, arguments, e(this).constructor)
}
return a.prototype = Object.create(t.prototype, {
constructor: {
value: a,
enumerable: !1,
writable: !0,
configurable: !0
}
}), r(a, t)
}, o(t)
}
function a(t) {
if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return t
}
function s(t) {
var r = n();
return function() {
var n, i = e(t);
if (r) {
var o = e(this).constructor;
n = Reflect.construct(i, arguments, o)
} else n = i.apply(this, arguments);
return function(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError(
"Derived constructors may only return object or undefined");
return a(t)
}(this, n)
}
}
function f(t) {
return function(t) {
if (Array.isArray(t)) return u(t)
}(t) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array
.from(t)
}(t) || function(t, e) {
if (!t) return;
if ("string" == typeof t) return u(t, e);
var r = Object.prototype.toString.call(t).slice(8, -1);
"Object" === r && t.constructor && (r = t.constructor.name);
if ("Map" === r || "Set" === r) return Array.from(t);
if ("Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)) return u(t, e)
}(t) || function() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
)
}()
}
function u(t, e) {
(null == e || e > t.length) && (e = t.length);
for (var r = 0, n = new Array(e); r < e; r++) n[r] = t[r];
return n
}
function l(t, e, r, n) {
this.fromSampleRate = t, this.toSampleRate = e, this.channels = 0 | r, this.noReturn = !!n, this.initialize()
}
l.prototype.initialize = function() {
if (!(this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0)) throw new Error(
"Invalid settings specified for the resampler.");
this.fromSampleRate == this.toSampleRate ? (this.resampler = this.bypassResampler, this.ratioWeight = 1) : (
this.fromSampleRate < this.toSampleRate ? (this.lastWeight = 1, this.resampler = this
.compileLinearInterpolation) : (this.tailExists = !1, this.lastWeight = 0, this.resampler = this
.compileMultiTap), this.ratioWeight = this.fromSampleRate / this.toSampleRate)
}, l.prototype.compileLinearInterpolation = function(t) {
var e = t.length;
this.initializeBuffers(e);
var r, n, i = this.outputBufferSize,
o = this.ratioWeight,
a = this.lastWeight,
s = 0,
f = 0,
u = 0,
l = this.outputBuffer;
if (e % this.channels == 0) {
if (e > 0) {
for (; a < 1; a += o)
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = this.lastOutput[r] * s + t[
r] * f;
for (a--, e -= this.channels, n = Math.floor(a) * this.channels; u < i && n < e;) {
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = t[n + r] * s + t[n + this
.channels + r] * f;
a += o, n = Math.floor(a) * this.channels
}
for (r = 0; r < this.channels; ++r) this.lastOutput[r] = t[n++];
return this.lastWeight = a % 1, this.bufferSlice(u)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.compileMultiTap = function(t) {
var e = [],
r = t.length;
this.initializeBuffers(r);
var n = this.outputBufferSize;
if (r % this.channels == 0) {
if (r > 0) {
for (var i = this.ratioWeight, o = 0, a = 0; a < this.channels; ++a) e[a] = 0;
var s = 0,
f = 0,
u = !this.tailExists;
this.tailExists = !1;
var l = this.outputBuffer,
h = 0,
c = 0;
do {
if (u)
for (o = i, a = 0; a < this.channels; ++a) e[a] = 0;
else {
for (o = this.lastWeight, a = 0; a < this.channels; ++a) e[a] += this.lastOutput[a];
u = !0
}
for (; o > 0 && s < r;) {
if (!(o >= (f = 1 + s - c))) {
for (a = 0; a < this.channels; ++a) e[a] += t[s + a] * o;
c += o, o = 0;
break
}
for (a = 0; a < this.channels; ++a) e[a] += t[s++] * f;
c = s, o -= f
}
if (0 != o) {
for (this.lastWeight = o, a = 0; a < this.channels; ++a) this.lastOutput[a] = e[a];
this.tailExists = !0;
break
}
for (a = 0; a < this.channels; ++a) l[h++] = e[a] / i
} while (s < r && h < n);
return this.bufferSlice(h)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.bypassResampler = function(t) {
return this.noReturn ? (this.outputBuffer = t, t.length) : t
}, l.prototype.bufferSlice = function(t) {
if (this.noReturn) return t;
try {
return this.outputBuffer.subarray(0, t)
} catch (e) {
try {
return this.outputBuffer.length = t, this.outputBuffer
} catch (e) {
return this.outputBuffer.slice(0, t)
}
}
}, l.prototype.initializeBuffers = function(t) {
this.outputBufferSize = Math.ceil(t * this.toSampleRate / this.fromSampleRate);
try {
this.outputBuffer = new Float32Array(this.outputBufferSize), this.lastOutput = new Float32Array(this
.channels)
} catch (t) {
this.outputBuffer = [], this.lastOutput = []
}
};
var h = function(e) {
! function(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError(
"Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: !0,
configurable: !0
}
}), Object.defineProperty(t, "prototype", {
writable: !1
}), e && r(t, e)
}(h, e);
var n, i, o, u = s(h);
function h() {
var t;
! function(t, e) {
if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function")
}(this, h);
var e = a(t = u.call(this));
return t.port.onmessage = function(t) {
var r = t.data,
n = r.type,
i = r.data;
if (console.log("type", n), "init" === n) {
var o = i.frameSize,
a = i.toSampleRate,
s = i.arrayBufferType,
f = i.fromSampleRate;
return e.frameSize = o * Math.floor(f / a), e.resampler = new l(f, a, 1), e
.frameBuffer = [], void(e.arrayBufferType = s),e.gain = i.gain || 1.0;
}
"stop" === n && (e.port.postMessage({
frameBuffer: e.transData(e.frameBuffer),
isLastFrame: !0
}), e.frameBuffer = [])
}, t
}
return n = h, (i = [{
key: "process",
value: function(t) {
var e, r = t[0][0];
return this.frameSize ? ((e = this.frameBuffer).push.apply(e, f(r)), this
.frameBuffer.length >= this.frameSize && (this.port.postMessage({
frameBuffer: this.transData(this.frameBuffer),
isLastFrame: !1
}), this.frameBuffer = []), !0) : (r && this.port.postMessage({
frameBuffer: this.transData(r),
isLastFrame: !1
}), !0)
}
}, {
key: "transData",
value: function(t) {
const gain = this.gain; // 增益系数,可根据实际需求修改
t = t.map(sample => sample * gain); // 对每个样本应用增益
return "short16" === this.arrayBufferType && (t = function(t) {
for (var e = new ArrayBuffer(2 * t.length), r = new DataView(e), n = 0,
i = 0; i < t.length; i += 1, n += 2) {
var o = Math.max(-1, Math.min(1, t[i]));
r.setInt16(n, o < 0 ? 32768 * o : 32767 * o, !0)
}
return r.buffer
}(t = this.resampler.resampler(t))), t
}
}]) && t(n.prototype, i), o && t(n, o), Object.defineProperty(n, "prototype", {
writable: !1
}), h
}(o(AudioWorkletProcessor));
registerProcessor("processor-worklet", h)
}();
@@ -1,149 +0,0 @@
import {
ref,
onUnmounted,
watch
} from 'vue';
import {
ProtocolCodec,
MessageType,
SerializationType,
CompressionType,
ControlCommand,
ProtocolConst
} from './ProtocolCodec';
/**
* 麦克风操作Hook封装开始/停止录音状态管理
* @param {Object} options - 录音配置项
* @param {number} options.duration - 最大录音时长默认60
* @param {string} options.format - 录音格式mp3/wav/pcm默认mp3
* @param {number} options.sampleRate - 采样率默认16000
* @param {number} options.bitRate - 码率默认16000
* @param {Function} options.onData - 实时音频数据回调仅H5/PCM格式生效
* @param {Function} options.onStop - 结束回调
* @param {Function} options.onError - 错误回调
* @returns {Object} 录音控制方法+状态
*/
export default function useMicrophone(options = {}) {
console.log('options', options);
const send = options.send
const recordFrame = ref(null)
const renderJSModule = ref(null)
// 默认配置
const defaultOptions = {
duration: 60,
format: 'mp3',
sampleRate: 16000,
bitRate: 16000,
onData: () => {},
onStop: () => {},
onError: (err) => console.error('录音错误:', err)
};
const finalOptions = {
...defaultOptions,
...options
};
// 响应式状态
const isRecording = ref(false); // 是否正在录音
const recordTime = ref(0); // 录音时长(秒)
const recordResult = ref(null); // 录音结果(临时文件路径/Blob)
const recorderManager = ref(null); // 录音管理器实例(uniapp/小程序)
const audioContext = ref(null); // AudioContextH5
const mediaRecorder = ref(null); // MediaRecorderH5
const timer = ref(null); // 录音时长定时器
// 平台判断
const platform = uni.getSystemInfoSync().platform;
// ==================== 核心方法:开始录音 ====================
const startRecording = async () => {
// 防止重复点击
if (isRecording.value) return;
// 重置状态
recordTime.value = 0;
recordResult.value = null;
console.log('开始', recordFrame.value);
recordFrame.value.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
// console.log('录音返回', frameBuffer);
const data = ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
// console.log('录音返回', data);
send(data)
// try {
// ws.value.send({
// data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
// });
// } catch (e) {}
},
onDecibels: (decibels) => {
// this.currentDecibels = decibels;
}
});
// this.isRecording = true;
// this.status = '录音中...';
// isRecording.value = true;
};
// ==================== 核心方法:停止录音 ====================
const stopRecord = () => {
// if (!isRecording.value) return;
// try {
// if (isH5) {
// mediaRecorder.value?.stop();
// } else if (isMiniProgram || platform === 'app-plus') {
// recorderManager.value?.stop();
// }
// isRecording.value = false;
// clearInterval(timer.value);
// } catch (err) {
// finalOptions.onError(`停止录音失败:${err.message}`);
// }
};
// ==================== 资源清理 ====================
const cleanup = () => {
// stopRecord();
// // H5端额外清理
// if (isH5) {
// if (mediaRecorder.value) mediaRecorder.value = null;
// if (audioContext.value) audioContext.value.close();
// // 释放Blob URL
// if (recordResult.value?.url) {
// URL.revokeObjectURL(recordResult.value.url);
// }
// }
// // 小程序/APP端清理
// if (recorderManager.value) recorderManager.value = null;
};
// 组件卸载时自动清理
// onUnmounted(() => {
// cleanup();
// });
// 监听最大时长,自动停止
// watch(
// () => recordTime.value,
// (val) => {
// if (val >= finalOptions.duration) {
// stopRecord();
// }
// }
// );
// 返回控制方法+状态
return {
isRecording,
renderJSModule,
startRecording,
recordFrame
};
};
-129
View File
@@ -1,129 +0,0 @@
import {
ref,
onUnmounted,
watch
} from 'vue';
/**
* 扬声器管理Hook音频输出
* @param {Object} options - 播放配置
* @param {number} options.sampleRate - 播放采样率默认16000需与音频源一致
* @param {number} options.volume - 初始音量0~1默认1
* @param {boolean} options.autoPlay - 是否自动播放默认false需用户交互触发
* @param {Function} options.onPlay - 播放开始回调
* @param {Function} options.onError - 播放错误回调
* @returns {Object} 播放控制方法+状态
*/
export const useSpeaker = (options = {}) => {
// 默认配置
const defaultOptions = {
sampleRate: 16000,
volume: 1.0,
autoPlay: false,
onPlay: () => {},
onError: (err) => console.error('扬声器播放错误:', err)
};
const finalOptions = {
...defaultOptions,
...options
};
// 响应式状态
const isPlaying = ref(false); // 是否正在播放
const volume = ref(finalOptions.volume); // 播放音量(0~1
const audioContext = ref(null); // 音频播放上下文(H5
const audioBufferSource = ref(null); // 音频缓冲源(播放PCM流)
const audioElement = ref(null); // 音频元素(播放文件/URL
const playQueue = ref([]); // 音频数据播放队列(适配实时流)
// 平台判断
const isH5 = uni.getSystemInfoSync().platform === 'h5';
// ==================== 初始化播放上下文 ====================
const initAudioContext = () => {
};
// ==================== 核心方法:播放PCM音频流(实时) ====================
/**
* 播放PCM音频数据适配WebSocket传输的实时流
* @param {ArrayBuffer} pcmData - PCM音频数据16位单声道
*/
const playPCM = (pcmData) => {
};
// ==================== 控制方法:暂停/停止/调节音量 ====================
const pausePlay = () => {
if (!isPlaying.value) return;
if (isH5) {
if (audioBufferSource.value) {
audioBufferSource.value.stop();
audioBufferSource.value = null;
}
audioElement.value?.pause();
} else {
uni.createInnerAudioContext().pause();
}
isPlaying.value = false;
};
const stopPlay = () => {
pausePlay();
playQueue.value = []; // 清空播放队列
if (audioContext.value) {
audioContext.value.close().then(() => {
audioContext.value = null;
});
}
audioElement.value = null;
};
const setVolume = (val) => {
if (val < 0) val = 0;
if (val > 1) val = 1;
volume.value = val;
if (isH5) {
audioContext.value?.destination.setVolume(val);
audioElement.value.volume = val;
} else {
uni.createInnerAudioContext().volume = val;
}
};
// ==================== 资源清理 ====================
const cleanup = () => {
stopPlay();
isPlaying.value = false;
volume.value = finalOptions.volume;
playQueue.value = [];
};
// 组件卸载时自动清理
onUnmounted(() => {
cleanup();
});
// 监听音量变化,同步更新
watch(volume, (newVal) => {
setVolume(newVal);
});
// 返回控制方法+状态
return {
// 响应式状态
isPlaying, // 是否正在播放
volume, // 当前音量(0~1
// 核心方法
playPCM, // 播放实时PCM音频流(适配WebSocket
playAudio, // 播放音频文件/URL
pausePlay, // 暂停播放
stopPlay, // 停止播放
setVolume, // 调节音量(0~1
cleanup // 手动清理资源
};
};
+52 -14
View File
@@ -22,8 +22,8 @@ const DEFAULT_AUTH_PARAMS = {
user_id: '1001', // 默认用户ID(可从缓存/全局状态取)
token: '', // 优先从缓存获取,避免硬编码
name: '测试用户',
device_id: uni.getSystemInfoSync().deviceId || '', // 设备ID(补充)
timestamp: Date.now() // 时间戳(防篡改)
// device_id: uni.getSystemInfoSync().deviceId || '', // 设备ID(补充)
// timestamp: Date.now() // 时间戳(防篡改)
};
// const wsUrl = 'ws://127.0.0.1:8000/ws/audio'
const url = '/trapractice/voiceCallSocket/voiceCall'
@@ -31,23 +31,27 @@ const _baseUrl = get_base_url(url).split('http').join('ws')
export default function useWebSocket(aaas = null, options = {}) {
const isRecording = ref(false) //是否接通
const ws = ref(null)
const initConnection = (authParams = {}, onStartRecord, sssssss) => {
const initConnection = ({
authParams = {},
onStartRecord,
playPcmCallback,
setPlayNumber = () => {}, //设置当前播放的音频的id,用于播放完成回调
onError = () => {},
onClose = () => {},
}) => {
const wsUrl = _baseUrl + url + '?summary=' + getToken()
console.log('wsUrl', wsUrl);
const finalAuthParams = {
...DEFAULT_AUTH_PARAMS,
...authParams,
};
ws.value = uni.connectSocket({
url: wsUrl,
fail: () => {
console.log('fail');
// console.log('fail');
},
success: () => {
console.log('web');
},
fail: () => {
console.log('fail');
// console.log('web');
}
});
ws.value.onOpen((res) => {
@@ -55,12 +59,20 @@ export default function useWebSocket(aaas = null, options = {}) {
data: ProtocolCodec.pack(MessageType.IDENTITY, finalAuthParams)
});
});
ws.value.onError((err) => onError(err))
ws.value.onClose((err) => {
console.log('onClose', err);
onClose(err)
});
ws.value.onMessage((res) => {
const {
msgType,
body
} = ProtocolCodec.unpack(res.data);
switch (msgType) {
case MessageType.IDENTITY:
console.log('身份校验', body);
onStartRecord(body)
@@ -70,16 +82,41 @@ export default function useWebSocket(aaas = null, options = {}) {
if (buffer.byteLength < 2) break;
const uint8 = new Uint8Array(buffer);
const numArray = Array.from(uint8);
// currentPCMChunk.value = numArray;
sssssss(numArray)
// console.log('音频消息', body);
playPcmCallback(numArray)
break;
case MessageType.ERROR:
switch (body.errCode) {
case 1:
// ASR或TTS初始化出错
console.log('后端返回的错误', body.errMsg);
// this.handleErrorMessage(body);
case 4:
console.log('通话接通失败', body.errMsg);
// 通话接通失败,请稍后再试!
ws.value.close({
code: 4001,
reason: body.errMsg
})
break;
default:
// 其他错误码的默认处理(如果需要)
// ws.value.close({
// code: 1001,
// reason: body.errMsg
// })
break;
}
break;
case MessageType.CONTROL_CMD:
// 控制消息
console.log('控制消息', msgType, body);
if (body.type === 1) { // 开始播放,记录当前播放的语音id
console.log('开始播放,记录当前播放的语音id', body.lock);
setPlayNumber(body.lock)
}
break;
default:
console.log('其他消息', body);
console.log('其他消息', msgType, body);
break;
}
});
@@ -89,6 +126,7 @@ export default function useWebSocket(aaas = null, options = {}) {
data
});
}
const close = () => {
ws.value.close({
code: 1000,
+10 -5
View File
@@ -41,12 +41,12 @@
<view class="loading-box" v-if="isLoading"></view>
<TalkingPreviewItem class="talk-item" v-for="item in showTalkList" :talkingType="item.type"
:talkingInfo="item" :roleInfo="roleInfo" :traId="traId" :execId="execId" :isAnswering="false"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingPreviewItem>
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents" :showVoice="isTalking"></TalkingPreviewItem>
</view>
<view :class="['body-top-collepse-cont', showAllTalk?'show-all':'']">
<TalkingPreviewItem class="talk-item" v-for="item in hideTalkList" :talkingType="item.type"
:talkingInfo="item" :roleInfo="roleInfo" :traId="traId" :execId="execId" :isAnswering="false"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingPreviewItem>
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents" :showVoice="isTalking"></TalkingPreviewItem>
</view>
</view>
<view class="body-bottom">
@@ -210,6 +210,7 @@
const negativeList = ref([]) //
const positiveList = ref([]) //
const isLoading = ref(false)
const isTalking = ref(true)
const getData = () => {
isLoading.value = true
partnerChatReport({
@@ -228,6 +229,7 @@
positiveList.value = res.body.positiveList || []
negativeList.value = res.body.negativeList || []
hitBanWords.value = res.body.traPartnerExecuteHisVo?.hitBanWords || ''
isTalking.value = res.body.traPartnerExecuteHisVo?.traInterTyp === '01'
let _evalDimens = res.body.traPartnerSettlement || []
evalDimens.value = _evalDimens.map((t, index) => {
return {
@@ -294,8 +296,7 @@
const isOver = ref(false)
const goBack = () => {
if (isOver.value) {
console.log(3)
common.navigateBack(3)
common.navigateBack(2)
} else {
common.navigateBack()
}
@@ -642,6 +643,7 @@
.item-cont {
font-size: 28rpx;
line-height: 50rpx;
white-space: pre-line;
}
}
@@ -671,7 +673,7 @@
font-size: 66rpx;
color: #06f;
font-family: Arial, Helvetica, sans-serif;
marign-bottom: 20rpx;
// marign-bottom: 20rpx;
}
.static-desc {
@@ -698,5 +700,8 @@
:deep(.uv-tabs__wrapper__nav__item) {
padding: 0 20rpx !important;
}
:deep(.uv-tabs__wrapper__nav__item__text){
white-space: nowrap;
}
}
</style>
+279 -120
View File
@@ -1,16 +1,18 @@
<template>
<view>
<cached-avatar class="show-box-avatar"></cached-avatar>
<view class="show-box">
<image-preview :src="dataInfo.ossAddr" class="show-box-avatar"></image-preview>
<view class="show-box" :class="{ isVideo: isVideoStyle }">
<nav-bar :is_seat="true"></nav-bar>
<view class="switch-div">
<view class="switch-text">场景提示</view>
<switch class="switch" :checked="!sceneDescShow" @click="switchChange" />
<view class="switch-div" v-if="isVideoStyle">
<!-- <view class="switch-text">场景提示</view> -->
<!-- <switch class="switch" :checked="!sceneDescShow" @click="switchChange" /> -->
<view class="title-time" v-if="isRecording">{{ formattedTime }}</view>
<view class="title-tip" v-if="!isRecording">正在接通中</view>
</view>
<view class="fl1-div-1"></view>
<view class="role-info">
<cached-avatar class="role-avatar"></cached-avatar>
<view class="role-name">{{ dataInfo.userName }}</view>
<image-preview :src="dataInfo.ossAddr" class="role-avatar" @click="test()"></image-preview>
<view class="role-name">{{ dataInfo.chaName }}</view>
<view class="title-time" v-if="isRecording">{{ formattedTime }}</view>
<view class="title-tip" v-if="!isRecording">正在接通中</view>
</view>
@@ -21,132 +23,253 @@
<view class="close-btn" @click="closeTalking"></view>
</view>
<talk-microphone ref="talkMicrophoneRef"></talk-microphone>
<talk-speaker ref="talkSpeakerRef"></talk-speaker>
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
<talk-camera ref="talkCameraRef" v-if="isVideo"></talk-camera>
</view>
</template>
<script setup>
import { getUserInfo } from '@/common/common';
import { queryTraPartnerCharacterInfoById, partnerChatReportTrigger } from '@/api/sparring.js';
import common from '@/common/common';
import talkMicrophone from './components/talk-microphone.vue';
import talkSpeaker from './components/talk-speaker.vue';
import useWebSocket from './js/useWebSocket';
import useMicrophone from './js/useMicrophone';
const talkMicrophoneRef = ref(null);
const talkSpeakerRef = ref(null);
const { initConnection, close, send, isRecording } = useWebSocket();
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { onShow, onLoad, onUnload, onBackPress } from '@dcloudio/uni-app';
import {
ref,
reactive,
onMounted,
onUnmounted,
computed,
getCurrentInstance,
nextTick
} from 'vue';
import {
onShow,
onLoad,
onUnload,
onBackPress,
onReady
} from '@dcloudio/uni-app';
import {
ProtocolCodec,
MessageType,
SerializationType,
CompressionType,
ControlCommand,
ProtocolConst
ControlCommand
} from './js/ProtocolCodec';
import { getToken } from '@/common/common';
import common from '@/common/common';
import talkMicrophone from './components/talk-microphone.vue';
import talkSpeaker from './components/talk-speaker.vue';
import talkCamera from './components/talk-camera';
import useWebSocket from './js/useWebSocket';
const talkMicrophoneRef = ref(null);
const talkSpeakerRef = ref(null);
const talkCameraRef = ref(null);
const {
initConnection,
close,
send,
isRecording
} = useWebSocket();
const isVideo = ref(false);
const isVideoStyle = computed(() => dataInfo.traInterTyp === '03');
const dataInfo = reactive({
traId: '',
chaId: '',
type: '',
traType: '',
traInterTyp: '',
execId: '',
sceneDesc: '',
userName: '',
preview: false //
preview: false, //
traInterTypDesc: '', //
ossAddr: '', //
chaName: '' //
});
const sceneDescShow = ref(false);
const switchChange = () => {sceneDescShow.value = !sceneDescShow.value};
const switchChange = () => {
sceneDescShow.value = !sceneDescShow.value;
};
//
const closeTalking = async () => {
// this.overTalking();
talkMicrophoneRef.value.stop();
talkSpeakerRef.value.close();
close();
common.navigateBack(1, () => {
partnerChatReportTrigger({
execId: dataInfo.execId,
traId: dataInfo.traId,
talkingType: dataInfo.isPreview ? 'preview' : 'practice'
}).then((res) => {
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.type +
'&isPreview=' +
(dataInfo.isPreview ? '1' : '')
);
});
// todo
common.show('', '挂断' + (isVideoStyle.value ? '视频' : '电话') + '将结束训练,是否确认?').then((res) => {
if (res) {
destroy(); //
}
});
};
function frameRecorded(res) {
// console.log('res', res);
send(res);
}
const test = () => {
isVideo.value && talkCameraRef.value.switchCamera();
};
//
const destroy = (type = 0) => {
if (type <= 0) {
isVideo.value && talkCameraRef.value.stopPreview();
talkMicrophoneRef.value.stop();
talkSpeakerRef.value.close();
}
if (type <= 1) {
close();
}
if (type <= 2) {
nextTick(() => {
setTimeout(() => {
if (dataInfo.execId) {
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.traType +
'&isPreview=' +
(dataInfo.preview ? '1' : '')
);
} else {
common.navigateBack()
}
}, 300);
});
}
};
//
const noConnectDestroy = () => {
console.log('未接通');
setTimeout(() => {
common.show('', '当前服务器繁忙,请稍后在试!', false).then((res) => {
if (res) common.navigateBack();
});
}, 500);
};
const onCurrentDecibels = (res) => {
console.log('res', res);
console.log('当前分贝', res);
};
// 1. -
const targetTimestamp = ref(0); // 15
//
//
let timer = null;
// 2. mm:ss
const formatTime = (totalSeconds) => {
if (totalSeconds <= 0) return '0:00'; // 0:00
const minutes = Math.floor(totalSeconds / 60); //
if (totalSeconds <= 0) return '00:00'; // 0:00
const minutes = Math.floor(totalSeconds / 60)
.toString()
.padStart(2, '0'); //
const seconds = totalSeconds % 60; //
return `${minutes}:${seconds.toString().padStart(2, '0')}`; //
};
const targetTimestamp = ref(0);
//
const formattedTime = ref(0);
const formattedTime = ref('00:00');
// 3.
const updateRemaining = () => {
const now = Date.now();
const diff = now - targetTimestamp.value;
formattedTime.value = formatTime(Math.max(0, Math.floor(diff / 1000)));
const seconds = Math.max(0, Math.floor(diff / 1000));
formattedTime.value = formatTime(seconds);
};
const onStartRecord = (body) => {
dataInfo.execId = body.execId;
targetTimestamp.value = Date.now();
// ws
console.log('连接成功开始录音');
console.log('连接成功开始' + dataInfo.traInterTypDesc);
isRecording.value = true;
timer = setInterval(updateRemaining, 1000);
isVideo.value && talkCameraRef.value.startPreview();
if (timer) clearInterval(timer); //
timer = setInterval(updateRemaining, 400);
//
updateRemaining();
talkMicrophoneRef.value.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
// websocket
onFrameRecorded: ({ isLastFrame, frameBuffer }) => send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
onFrameRecorded: ({
isLastFrame,
frameBuffer
}) => send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
});
};
const playOver = (id) => {
console.log('播放完毕talk通知后端', id);
if (id) {
setTimeout(() => {
send(
ProtocolCodec.pack(MessageType.CONTROL_CMD, {
type: 2,
lock: id
})
);
}, 300);
}
};
const onError = (error) => {
// websocket
console.log('websocket错误处理', error.code);
common.show('', '系统或网络异常,通话结束!', false).then((res) => {
if (res) {
destroy();
}
});
};
const onClose = (error) => {
// websocket
console.log('websocketonClose关闭', error);
if (error?.code === 1002) {
//
if (dataInfo.execId !== '') {
// execId
destroy();
} else {
//
noConnectDestroy();
}
} else if (error?.code === 1001) {
// 线
// onError({code:1001}); //
} else if (error?.code === 1011) {
//
onError({
code: 1011
}); //
} else if (error?.code === 1000) {
//
} else if (error?.code === 4001) {
//
noConnectDestroy();
}
};
onLoad((e) => {
dataInfo.traId = e.traId ?? '';
dataInfo.chaId = e.chaId ?? '';
dataInfo.type = e.type ?? '';
dataInfo.preview = e.isPreview === '1';
const userInfo = getUserInfo();
dataInfo.userName = userInfo.userName;
const pageData = common.getPageCache()
dataInfo.sceneDesc = pageData.sceneDesc
dataInfo.traType = '02'; // 01- 02-
dataInfo.traInterTyp = e.traInterTyp ?? '02'; // 01 02 03
// dataInfo.traInterType = '02'; // 01 02 03
//#ifdef APP-PLUS
// app
isVideo.value = dataInfo.traInterTyp === '03';
//#endif
dataInfo.preview = e.isPreview === '1'; //
const pageData = common.getPageCache('tipMsgsceneDesc');
dataInfo.ossAddr = pageData.ossAddr;
dataInfo.chaName = pageData.chaName;
dataInfo.traInterTypDesc = pageData.traInterTypDesc;
});
onMounted(() => {
initConnection(dataInfo, onStartRecord, (res) => talkSpeakerRef.value.playPCM(res));
initConnection({
authParams: Object.assign({}, dataInfo),
onStartRecord: onStartRecord,
playPcmCallback: (res) => talkSpeakerRef.value.playPCM(res),
setPlayNumber: (id) => talkSpeakerRef.value.setPlayNumber(id),
onError,
onClose
});
});
onReady(() => {
if (isVideo.value) {
const instance = getCurrentInstance();
const componentProxy = instance.ctx;
nextTick(() => {
talkCameraRef.value.init(componentProxy);
});
}
});
onUnmounted(() => {
console.log('组件卸载');
@@ -156,6 +279,74 @@
</script>
<style scoped lang="scss">
.isVideo {
// background-color: red;
.role-info {
display: none;
}
&.show-box {
background: linear-gradient(120deg,
rgba(51, 51, 51, 0.2) 0%,
rgba(102, 102, 102, 0.2) 50%,
rgba(51, 51, 51, 0.2) 100%);
backdrop-filter: blur(0px);
}
}
.title-time,
.title-tip {
text-align: center;
color: #fff;
line-height: 58rpx;
font-size: 38rpx;
margin: 0 auto;
}
.role-name {
margin-bottom: 26rpx;
// position: relative;
// width: 100vw;
text-align: center;
color: #fff;
line-height: 58rpx;
font-size: 38rpx;
}
.title-tip {
padding-right: 20rpx;
position: relative;
&::after {
content: '';
position: absolute;
animation: loading 2.5s infinite;
}
@keyframes loading {
0% {
content: '';
}
25% {
content: '.';
}
50% {
content: '..';
}
75% {
content: '...';
}
100% {
content: '';
}
}
}
.switch-div {
display: flex;
justify-content: flex-end;
@@ -163,13 +354,16 @@
margin-right: 20rpx;
margin-top: 10rpx;
width: 100%;
.switch-text {
font-weight: 400;
font-size: 28rpx;
color: #ffffff;
}
.switch {
transform: scale(0.7) rotate(180deg);
:deep(.uni-switch-input) {
&.uni-switch-input-checked {
border-color: #9eacbf !important;
@@ -182,10 +376,12 @@
width: 20px;
height: 20px;
}
&:before {
background-color: #62a1ff !important;
border-color: #62a1ff !important;
}
&:not(.uni-switch-input-checked) {
background-color: #62a1ff !important;
border-color: #62a1ff !important;
@@ -193,15 +389,17 @@
}
}
}
.fl1-div-1 {
min-height: 120rpx;
max-height: 240rpx;
}
.show-box {
width: 100vw;
height: 100vh;
background: linear-gradient(120deg, #333333cc 0%, #666666cc 50%, #333333cc 100%);
backdrop-filter: blur(10px);
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
@@ -224,10 +422,6 @@
position: relative;
width: 100vw;
padding-top: 210rpx;
text-align: center;
color: #fff;
line-height: 58rpx;
font-size: 38rpx;
.role-avatar {
width: 185rpx;
@@ -239,42 +433,6 @@
transform: translateX(-50%);
top: 0;
}
.role-name {
margin-bottom: 26rpx;
}
.title-tip {
padding-right: 20rpx;
position: relative;
&::after {
content: '';
position: absolute;
animation: loading 2.5s infinite;
}
@keyframes loading {
0% {
content: '';
}
25% {
content: '.';
}
50% {
content: '..';
}
75% {
content: '...';
}
100% {
content: '';
}
}
}
.title-tip {
&:not(.connecting) {
// display: none;
}
}
}
.body-scene {
@@ -287,6 +445,7 @@
border-radius: 16rpx;
opacity: 0;
transition: opacity 0.5s ease;
&.show {
opacity: 1;
}
@@ -304,4 +463,4 @@
margin-bottom: 112rpx;
flex-shrink: 0;
}
</style>
</style>
-147
View File
@@ -1,147 +0,0 @@
<template>
<view>
<video :is-live="true" id="myVideo" :src="videoSrc" :autoplay="true"></video>
<live-pusher id="livePusher" ref="livePusher" class="livePusher" url="rtmp://25.18.122.148:9605/voice_in/aa" mode="SD" :muted="true" :enable-camera="false" :auto-focus="true" :beauty="1" whiteness="2" aspect="9:16" @statechange="statechange" @netstatus="netstatus" @error="error"></live-pusher>
<view class="btn" @click="prepare"><text>预热</text></view>
<view class="btn" @click="start"><text>开始推流</text></view>
<view class="btn" @click="pause"><text>暂停推流</text></view>
<view class="btn" @click="resume"><text>恢复推流</text></view>
<view class="btn" @click="stop"><text>停止推流</text></view>
<view class="btn" @click="snapshot"><text>快照</text></view>
<view class="btn" @click="startPreview"><text>开启摄像头预览</text></view>
<view class="btn" @click="stopPreview"><text>关闭摄像头预览</text></view>
<view class="btn" @click="switchCamera"><text>切换摄像头</text></view>
</view>
</template>
<script>
import { prepareApi } from '@/api/talk.js'
var pusher
export default {
data() {
return {
context: null,
baseName: 'zhangsan'
}
},
computed: {
videoSrc() {
return 'rtmp://25.18.122.148:9605/voice_out/' + this.baseName
}
},
onReady() {
const videoContext = uni.createVideoContext('myVideo')
videoContext.setVolume(1.0)
// onReady onLoad
// pusher = uni.createLivePusherContext('livePusher', this)
// console.log(pusher)
},
methods: {
statechange(e) {
console.log('statechange:', e, JSON.stringify(e))
},
netstatus(e) {
console.log('netstatus:' + JSON.stringify(e))
},
error(e) {
console.log('error:' + JSON.stringify(e))
},
start: function () {
console.log('statechange:', pusher.start)
pusher.start({
success: a => {
console.log('livePusher.start:' + JSON.stringify(a))
}
})
},
prepare: function () {
prepareApi(this.baseName)
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
if (err === 'prepare') {
this.initPusher()
}
})
},
close: function () {
pusher.close({
success: a => {
console.log('livePusher.close:' + JSON.stringify(a))
}
})
},
snapshot: function () {
pusher.snapshot({
success: e => {
console.log(JSON.stringify(e))
}
})
},
resume: function () {
pusher.resume({
success: a => {
console.log('livePusher.resume:' + JSON.stringify(a))
}
})
},
pause: function () {
pusher.pause({
success: a => {
console.log('livePusher.pause:' + JSON.stringify(a))
}
})
},
stop: function () {
pusher.stop({
success: a => {
console.log(JSON.stringify(a))
}
})
},
switchCamera: function () {
pusher.switchCamera({
success: a => {
console.log('livePusher.switchCamera:' + JSON.stringify(a))
}
})
},
startPreview: function () {
pusher.startPreview({
success: a => {
console.log('livePusher.startPreview:' + JSON.stringify(a))
}
})
},
stopPreview: function () {
pusher.stopPreview({
success: a => {
console.log('livePusher.stopPreview:' + JSON.stringify(a))
}
})
},
initPusher() {
console.log(1)
pusher = new plus.video.LivePusher('pusher-box', {
url: 'rtmp://25.18.122.148:9605/voice_in/' + this.baseName,
'enable-camera': false
})
pusher.addEventListener('statechange', this.statechange)
pusher.addEventListener('netstatus', this.netstatus)
pusher.addEventListener('error', this.error)
console.log(pusher)
}
}
}
</script>
<style>
.btn {
width: 100%;
height: 80rpx;
background-color: #ccc;
margin-bottom: 5rpx;
}
</style>
-182
View File
@@ -1,182 +0,0 @@
<template>
<view class="container">
<button @click="startPush" v-if="!isPushing">开始语音推流</button>
<button @click="stopPush" v-else>停止语音推流</button>
</view>
</template>
<script>
export default {
data() {
return {
isPushing: false, //
pusher: null, //
currentWebview: null // Webview
}
},
onReady() {
// onReady +
if (uni.getSystemInfoSync().platform === 'android') {
// 1000ms 3 Webview
let retryCount = 0
const getWebview = () => {
this.currentWebview = this.$mp.page.$getAppWebview()
if (this.currentWebview) {
console.log('Webview 获取成功:', this.currentWebview)
return
}
retryCount++
if (retryCount < 3) {
setTimeout(getWebview, 500) // 500ms
} else {
uni.showToast({ title: 'Webview 初始化失败', icon: 'none' })
}
}
setTimeout(getWebview, 1000) // 1
}
},
onLoad() {
// // App Webview
// if (uni.getSystemInfoSync().platform !== 'app-plus') {
// uni.showToast({ title: ' App ', icon: 'none' });
// return;
// }
// this.currentWebview = this.$mp.page.$getAppWebview();
},
onUnload() {
//
this.stopPush()
},
methods: {
//
async requestMicPermission() {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
['android.permission.RECORD_AUDIO'], // Android
res => {
const granted = res.filter(item => item.granted).length === 1
granted ? resolve() : reject('麦克风权限申请失败')
},
err => reject(err)
)
// iOS
})
},
async startPush() {
if (uni.getSystemInfoSync().platform !== 'app-plus') return
try {
await this.requestMicPermission()
// API Webview
this.currentWebview = plus.webview.currentWebview() || this.$mp.page.$getAppWebview()
if (!this.currentWebview) {
throw new Error('无法获取页面 Webview 实例')
}
// pusher + append
this.pusher = plus.video.createLivePusher('', {
/* 配置 */
})
this.currentWebview.append(this.pusher) // null
// ...
} catch (err) {
console.error('推流启动失败:', err)
}
},
//
async startPush2() {
console.log('11', uni.getSystemInfoSync().platform)
// 1.
if (uni.getSystemInfoSync().platform !== 'android') return
console.log('22')
try {
// 2.
// await this.requestMicPermission();
console.log('33')
// 3. LivePusher
this.pusher = plus.video.createLivePusher('', {
url: 'rtmp://25.18.122.148:9605/voice_in/aa', // RTMP
top: '0px',
left: '0px',
width: '1px', //
height: '1px',
position: 'static',
//
video: {
enable: false //
},
audio: {
enable: true, //
sampleRate: 16000, // AI 16k
bitrate: 64000, //
codec: 'aac' // RTMP
},
//
mode: 'rtmp',
autopush: false //
})
console.log('44')
// 4. Webview
this.currentWebview.append(this.pusher)
// 5.
this.pusher.addEventListener('statechange', e => {
console.log('推流状态变化:', e)
switch (e.state) {
case 'connecting':
uni.showToast({ title: '正在连接推流服务器', icon: 'none' })
break
case 'connected':
this.isPushing = true
uni.showToast({ title: '推流成功', icon: 'success' })
break
case 'disconnected':
this.isPushing = false
uni.showToast({ title: '推流断开', icon: 'none' })
break
case 'error':
this.isPushing = false
uni.showToast({ title: `推流错误:${e.error}`, icon: 'none' })
break
}
})
// 6. +
this.pusher.startPreview() //
this.pusher.start() //
} catch (err) {
uni.showToast({ title: err, icon: 'none' })
console.error('推流启动失败:', err)
}
},
//
stopPush() {
if (!this.pusher) return
// +
this.pusher.stop()
this.pusher.stopPreview()
//
this.pusher.close()
this.pusher = null
this.isPushing = false
uni.showToast({ title: '推流已停止', icon: 'none' })
}
}
}
</script>
<style scoped>
.container {
padding: 20px;
}
button {
height: 80rpx;
line-height: 80rpx;
background: #007aff;
color: #fff;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
</style>
-492
View File
@@ -1,492 +0,0 @@
<template>
<view>
<cached-avatar class="show-box-avatar"></cached-avatar>
<view class="show-box">
<nav-bar :is_seat="true"></nav-bar>
<view class="switch-div">
<view class="switch-text">场景提示</view>
<switch class="switch" :checked="!sceneDescShow" @click="switchChange" />
</view>
<view class="fl1-div-1"></view>
<view class="role-info">
<cached-avatar class="role-avatar"></cached-avatar>
<view class="role-name">{{ userInfo.userName }}</view>
<view class="title-time" v-if="isConnecting">{{ timeShow }}</view>
<view class="title-tip" v-if="!isConnecting">正在接通中</view>
</view>
<view class="fl1"></view>
<view class="body-scene" :class="{ show: sceneDescShow }">
{{ sceneDesc }}
</view>
<view class="close-btn" @click="closeTalking"></view>
</view>
<yao-RecordFrame
ref="recordFrame"
@onFrameRecorded="frameRecorded"
@currentDecibels="onCurrentDecibels"
@onStop="stopIt"
></yao-RecordFrame>
<view
ref="renderJSModule"
:pcmChunk="currentPCMChunk"
:change:pcmChunk="renderJS.appendPCMChunk"
:isStop="isStop"
:change:isStop="renderJS.stop"
type="renderjs"
module="renderJS"
></view>
</view>
</template>
<script>
import { getUserInfo } from '@/common/common';
import talkCom from './talkCom.nvue';
import { queryTraPartnerCharacterInfoById, partnerChatReportTrigger } from '@/api/sparring.js';
import common from '@/common/common';
import useWebSocket from './js/useWebSocket';
const { initConnection, ws } = useWebSocket();
import {
ProtocolCodec,
MessageType,
SerializationType,
CompressionType,
ControlCommand,
ProtocolConst
} from './js/ProtocolCodec';
import { getToken } from '@/common/common';
export default {
components: {
talkCom //
},
data() {
return {
traId: '',
chaId: '',
talkingType: '',
execId: '',
isPreview: '',
chaName: '',
sceneDesc: '场景提示文案文儿提示文',
sceneDescShow: false,
timeShow: '11:19',
isConnecting: false, //
userInfo: {
userName: ''
},
ws: null,
wsUrl: 'wss://aitstest.jlbank.com.cn:7001/trapractice/voiceCallSocket/voiceCall?summary=' + getToken(), //
//
currentPCMChunk: null,
isStop: false //
};
},
methods: {
callPhone() {
//
this.ws = uni.connectSocket({
url: this.wsUrl,
fail: () => {
console.log('fail');
},
success: () => {
console.log('web');
},
fail: () => {
console.log('fail');
}
});
this.ws.onOpen((res) => {
console.log('WebSocket连接已打开type', this.type);
this.ws.send({
data: ProtocolCodec.pack(MessageType.IDENTITY, {
traId: this.traId,
chaId: this.chaId,
type: this.type,
user_id: '1001',
token: 'your_auth_token',
name: '测试用户'
})
});
});
this.ws.onMessage((res) => {
const { msgType, body } = ProtocolCodec.unpack(res.data);
switch (msgType) {
case MessageType.IDENTITY:
console.log('身份校验成功', body);
this.onStartRecord();
this.isConnecting = true;
break;
case MessageType.AUDIO_DATA:
const buffer = body;
if (buffer.byteLength < 2) break;
const uint8 = new Uint8Array(buffer);
const numArray = Array.from(uint8);
this.currentPCMChunk = numArray;
break;
case MessageType.ERROR:
this.handleErrorMessage(body);
break;
default:
console.log('其他消息', body);
break;
}
});
},
handleErrorMessage(body) {
//
console.log(body);
},
disconnect() {
//
//
// this.onStopRecord();
//
// this.currentPCMChunk = null;
// this.isConnecting = false;
},
manualDisconnect() {
//
//
this.isManualDisconnect = true;
this.disconnect();
this.handleFinalDisconnect();
this.isManualDisconnect = false;
},
startHeartbeat() {
//
clearInterval(this.heartbeatTimer);
//
this.heartbeatTimer = setInterval(() => {
if (!this.isConnected) return;
// MessageType.HEARTBEAT
this.sendMsg({
type: MessageType.HEARTBEAT,
body: { timestamp: Date.now() }
});
}, this.heartbeatInterval);
},
onStartRecord() {
console.log('开始录音');
try {
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
// console.log('', frameBuffer);
try {
ws.value.send({
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
});
} catch (e) {}
},
onDecibels: (decibels) => {
this.currentDecibels = decibels;
}
});
this.isRecording = true;
this.status = '录音中...';
} catch (e) {
console.error('启动录音失败:', e);
this.status = '启动录音失败';
this.isRecording = false;
}
},
//
frameRecorded({ isLastFrame, frameBuffer }) {
if (this.ws) {
try {
this.ws.send({
data: frameBuffer
});
} catch (e) {
console.error('发送音频帧失败:', e);
}
}
},
//
onCurrentDecibels(decibels) {
// this.currentDecibels = decibels.toFixed(2);
// console.log("", this.currentDecibels);
},
//
stopIt(base64) {
this.stopRecordAndClean();
console.log('录音停止,最终音频Base64', base64?.substring(0, 50) + '...');
},
//
closeTalking() {
// this.overTalking();
common.navigateBack();
},
//
overTalking() {
partnerChatReportTrigger({
execId: this.execId,
traId: this.traId,
talkingType: this.isPreview ? 'preview' : 'practice'
}).then((res) => {
common.navigateTo(
'/pages/sparring/result?isOver=1&traId=' +
this.traId +
'&execId=' +
this.execId +
'&type=' +
this.type +
'&isPreview=' +
(this.isPreview ? '1' : '')
);
});
},
switchChange(e) {
this.sceneDescShow = !this.sceneDescShow;
this.isConnecting = !this.isConnecting;
},
//
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;
}
},
onLoad(e) {
//
this.traId = e.traId ?? '';
this.chaId = e.chaId ?? '';
this.type = e.type ?? '';
this.isPreview = e.isPreview === '1';
this.userInfo = getUserInfo();
// this.callPhone();
initConnection({ traId: this.traId, chaId: this.chaId, type: this.type }, this.onStartRecord);
},
onBackPress(res) {
return res.from === 'backbutton';
}
};
</script>
<script module="renderJS" lang="renderjs">
import { StreamPlayer } from "./js/StreamPlayer";
let player = null;
export default {
mounted() {
const _this = this;
player = new StreamPlayer({
inputSampleRate: 44100, // PCM8000/24000
numChannels: 1, // /
bitDepth: 16, // 16/32bit
littleEndian: true, //
pcmType: 'int', // int/float
callback: _this.callback
});
},
methods: {
appendPCMChunk(numArray) {
// console.log('', numArray.length);
if (!numArray || numArray.length === 0) return;
// ArrayBuffer
const uint8 = new Uint8Array(numArray);
const arrayBuffer = uint8.buffer;
player.appendChunk(arrayBuffer);
},
//
stop() {
if (player) {
player.destroy();
player = null;
// 便
const _this = this;
player = new StreamPlayer({
inputSampleRate: 16000,
numChannels: 1,
bitDepth: 16,
littleEndian: true,
pcmType: 'int',
callback: _this.callback
});
}
},
// 线
callback(e) {
if (e === 'ended') {
this.$ownerInstance.callMethod('changeStreamPlaying', { type: false });
}
}
}
};
</script>
<style scoped lang="scss">
.switch-div {
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: 20rpx;
margin-top: 10rpx;
width: 100%;
.switch-text {
font-weight: 400;
font-size: 28rpx;
color: #ffffff;
}
.switch {
transform: scale(0.7) rotate(180deg);
:deep(.uni-switch-input) {
&.uni-switch-input-checked {
border-color: #9eacbf !important;
background-color: #9eacbf !important;
}
&:after {
top: 5px;
left: 5px;
width: 20px;
height: 20px;
}
&:before {
background-color: #62a1ff !important;
border-color: #62a1ff !important;
}
&:not(.uni-switch-input-checked) {
background-color: #62a1ff !important;
border-color: #62a1ff !important;
}
}
}
}
.fl1-div-1 {
min-height: 120rpx;
max-height: 240rpx;
}
.show-box {
width: 100vw;
height: 100vh;
background: linear-gradient(120deg, #333333cc 0%, #666666cc 50%, #333333cc 100%);
backdrop-filter: blur(10px);
position: fixed;
top: 0;
left: 0;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
}
.show-box-avatar {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 900;
}
.role-info {
position: relative;
width: 100vw;
padding-top: 210rpx;
text-align: center;
color: #fff;
line-height: 58rpx;
font-size: 38rpx;
.role-avatar {
width: 185rpx;
height: 185rpx;
border-radius: 50%;
overflow: hidden;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 0;
}
.role-name {
margin-bottom: 26rpx;
}
.title-tip {
padding-right: 20rpx;
position: relative;
&::after {
content: '';
position: absolute;
animation: loading 2.5s infinite;
}
@keyframes loading {
0% {
content: '';
}
25% {
content: '.';
}
50% {
content: '..';
}
75% {
content: '...';
}
100% {
content: '';
}
}
}
.title-tip {
&:not(.connecting) {
// display: none;
}
}
}
.body-scene {
overflow: auto;
width: calc(100% - 92rpx);
// margin: 46rpx;
padding: 26rpx;
color: rgba(255, 255, 255, 0.8);
border: 4rpx dashed rgba(255, 255, 255, 0.3);
border-radius: 16rpx;
opacity: 0;
transition: opacity 0.5s ease;
&.show {
opacity: 1;
}
}
.close-btn {
width: 118rpx;
height: 118rpx;
background-image: url(@/static/images/sparring/close-talk.png);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
border-radius: 50%;
margin-top: 46rpx;
margin-bottom: 112rpx;
flex-shrink: 0;
}
</style>
-158
View File
@@ -1,158 +0,0 @@
<template>
<video :is-live="true" :src="videoSrc" class="video-box" :autoplay="true" @error="error"></video>
<view class="hidden">
<live-pusher id="pusher-box" :enable-camera="false"></live-pusher>
<!-- <button class="btn" @click="prepare">预热</button>
<button class="btn" @click="start">开始推流1</button>
<button class="btn" @click="pause">暂停推流</button>
<button class="btn" @click="resume">恢复</button>
<button class="btn" @click="stop">停止推流</button> -->
<!-- <button class="btn" @click="snapshot">快照</button>
<button class="btn" @click="startPreview">开启摄像头预览1</button>
<button class="btn" @click="stopPreview">关闭摄像头预览</button>
<button class="btn" @click="switchCamera">切换摄像头</button> -->
<!-- <cover-view class="full-view"></cover-view> -->
</view>
</template>
<script>
import {
prepareApi
} from "@/api/talk.js";
import { onReady } from '@dcloudio/uni-app'
var pusher = null;
export default {
data() {
return {
baseName: 'wangwu',
context: null,
isGetting: true
}
},
computed: {
videoSrc() {
return 'rtmp://25.18.122.148:9605/voice_out/' + this.baseName;
},
},
mounted() {
// this.prepare()
},
beforeUnmount() {
this.isGetting = false
this.stop()
this.close()
},
methods: {
statechange(e) {
console.log('statechange:', e);
let {
detail
} = e
console.log(detail);
switch (detail.code) {
case '3004':
console.log(3004);
this.close()
break;
default:
break;
}
},
netstatus(e) {
console.log('netstatus');
},
error(e) {
console.log('error:', e);
},
start() {
console.log('start');
pusher.start(a => {
console.log('livePusher.start:');
}, b => {
console.log('livePusher.start.err:');
});
},
close() {
console.log('close');
pusher.close();
},
snapshot() {},
resume() {
pusher.resume();
},
pause() {
pusher.pause()
},
stop() {
pusher.stop()
},
switchCamera() {},
startPreview() {},
stopPreview() {},
prepare({traId, chaId, traTyp}) {
if(!this.isGetting){
this.isGetting = true
return
}
prepareApi({
traId: traId,
chaId: chaId,
traTyp: traTyp,
}).then(res => {
console.log(res)
if(res.body.prepare){
this.baseName = res.body.sid
this.initPusher()
}else{
setTimeout(()=> {
this.prepare({traId, chaId, traTyp})
},2000)
}
}).catch(err => {
console.log(err)
})
},
initPusher() {
console.log('rtmp://25.18.122.148:9605/voice_in/' + this.baseName)
pusher = new plus.video.LivePusher('pusher-box', {
url: 'rtmp://25.18.122.148:9605/voice_in/' + this.baseName,
"enable-camera": true,
"min-bitrate": 16,
"max-bitrate": 16,
top: '100px',
left: '0px',
width: '1px',
height: '1px',
position: 'static'
})
pusher.addEventListener('statechange', this.statechange)
pusher.addEventListener('netstatus', this.netstatus)
pusher.addEventListener('error', this.error)
console.log(pusher)
this.start()
}
}
}
</script>
<style>
.hidden{
width: 0px;
height: 0px;
overflow: hidden;
display: flex;
}
.full-view{
width: 200px;
height: 200px;
position: sticky;
flex: 1;
/* background-color: #fff; */
}
.pusher-box{
width: 1px;
height: 1px;
}
.video-box{
width: 1px;
height: 1px;
}
</style>
+298 -164
View File
@@ -1,8 +1,7 @@
<template>
<view class="sparring-detail max_page">
<view class="sparring-detail-loading" v-show="showLoading">
<view class="loading-img">
</view>
<view class="loading-img"></view>
</view>
<view class="sparring-detail-body">
<view class="top-box">
@@ -12,28 +11,38 @@
</view>
<view class="top-desc">内容由AI生成</view>
</view>
<scroll-view :class="[' body-content index-dialog-body', detailInfo.traInterTyp === '01'?'talking-type1':'']"
:scroll-y="true" refresher-default-style="none" :scroll-top="scrollTop" :show-scrollbar="false"
:scroll-with-animation="false">
<view :class="['body-content-role', showRoleInfo?'':'hide-info']">
<scroll-view
:class="[' body-content index-dialog-body', detailInfo.traInterTyp === '01' ? 'talking-type1' : '']"
:scroll-y="true"
refresher-default-style="none"
:scroll-top="scrollTop"
:show-scrollbar="false"
:scroll-with-animation="false"
>
<view :class="['body-content-role', showRoleInfo ? '' : 'hide-info']">
<view :class="['role-title']">
<ImagePreview class="role-title-avatar" :src="detailInfo.ossAddr" :isCache="true">
</ImagePreview>
<ImagePreview class="role-title-avatar" :src="detailInfo.ossAddr" :isCache="true"></ImagePreview>
<view class="role-title-info" @click="showRoleInfo = !showRoleInfo">
<view class="arrow-r-icon"></view>
<view class="role-name card-title">
<text class="card-title-name">{{ detailInfo.chaName }}</text>
<text class="card-title-age">{{ detailInfo.chaAge }}</text>
<text class="card-title-img">
<image class="card-title-icon" v-if="detailInfo.gender === 'm'||detailInfo.gender === '01'"
src="@/static/images/me/man.png">
</image>
<image class="card-title-icon" v-if="detailInfo.gender === 'f'||detailInfo.gender === '02'"
src="@/static/images/me/wuman.png">
</image>
<image
class="card-title-icon"
v-if="detailInfo.gender === 'm' || detailInfo.gender === '01'"
src="@/static/images/me/man.png"
></image>
<image
class="card-title-icon"
v-if="detailInfo.gender === 'f' || detailInfo.gender === '02'"
src="@/static/images/me/wuman.png"
></image>
</text>
</view>
<view class="role-desc">预计时间 {{detailInfo.traStartLimit}} ~ {{detailInfo.traEndLimit}} 分钟</view>
<view class="role-desc">
预计时长 {{ detailInfo.traStartLimit }} ~ {{ detailInfo.traEndLimit }} 分钟
</view>
</view>
</view>
<view class="role-content">
@@ -60,29 +69,52 @@
<!-- sceneDesc -->
{{ detailInfo.sceneDesc }}
</view>
<view class="talking-box">
<TalkingItem v-for="(item, index) in talkingList" :talkingType="item.type" :talkingInfo="item"
:roleInfo="detailInfo" :traId="traId" :execId="execId" :isExam="talkingType === '02'"
:isLast="index + 1 === talkingList.length" :isAnswering="!answerIsOver"
:activeVoiceId="activeVoiceTalkingItemId" @handleEvents="talkItemEvents"></TalkingItem>
<view class="talking-desc">陪练角色不具备主动终止场景陪练的功能当用户完成预设陪练目标时需由用户手动发起结束陪练操作</view>
<view class="talking-box" v-if="detailInfo.traInterTyp !== '01'">
<view class="tip-text">-请点击下方按钮开始{{ detailInfo.traInterTypDesc }}</view>
</view>
<view class="talking-box" v-else>
<TalkingItem
v-for="(item, index) in talkingList"
:talkingType="item.type"
:talkingInfo="item"
:roleInfo="detailInfo"
:traId="traId"
:execId="execId"
:isExam="talkingType === '02'"
:isLast="index + 1 === talkingList.length"
:isAnswering="!answerIsOver"
:activeVoiceId="activeVoiceTalkingItemId"
@handleEvents="talkItemEvents"
></TalkingItem>
</view>
</scroll-view>
<view class="over-btn-box" v-if="detailInfo.traInterTyp === '01'">
<view class="over-btn" @click="overTalking">
结束陪练
</view>
<view class="over-btn" @click="overTalking">结束陪练</view>
</view>
<TouchBtn class="body-bottom" @uploadVoice="uploadRecordNow" @startRecord="changePlayItemId('')"
:isLoading="!answerIsOver" :isDisabled="false" v-if="detailInfo.traInterTyp === '01'" />
<TouchBtn
class="body-bottom"
@uploadVoice="uploadRecordNow"
@startRecord="changePlayItemId('')"
:isLoading="!answerIsOver"
:isDisabled="false"
v-if="detailInfo.traInterTyp === '01'"
/>
<view class="body-bottom" v-else>
<view class="body-bottom-btn" @click="toTalking">
<view class="center-text">
<image class="type-icon" src="@/static/images/sparring/video-icon.png"
v-if="detailInfo.traInterTyp === '03'"></image>
<image class="type-icon" src="@/static/images/sparring/phone-icon.png"
v-else-if="detailInfo.traInterTyp === '02'"></image>
<image
class="type-icon"
src="@/static/images/sparring/video-icon.png"
v-if="detailInfo.traInterTyp === '03'"
></image>
<image
class="type-icon"
src="@/static/images/sparring/phone-icon.png"
v-else-if="detailInfo.traInterTyp === '02'"
></image>
<image v-else class="type-icon" src="@/static/images/sparring/talk-icon.png"></image>
{{detailInfo.traInterTypDesc}}
{{ detailInfo.traInterTypDesc }}
</view>
</view>
</view>
@@ -93,92 +125,66 @@
</template>
<script setup>
import {
ref,
unref,
onMounted,
computed,
nextTick
} from 'vue'
import {
onShow,
onLoad,
onUnload,
onBackPress
} from '@dcloudio/uni-app';
import { ref, unref, onMounted, computed, nextTick } from 'vue';
import { onShow, onHide, onLoad, onUnload, onLaunch, onBackPress } from '@dcloudio/uni-app';
import common from '@/common/common';
import { getPhoneEnvBool } from '@/common/common';
import TouchBtn from './components/touchBtn.vue';
import ImagePreview from '@/components/image-preview/image-preview.vue';
import {
commonUploadVoiceFile
} from '@/api/common.js';
import {
queryTraPartnerCharacterInfoById,
partnerChatReportTrigger
} from "@/api/sparring.js"
import {
useSocketStore
} from '@/store/socket.js';
import {
useStorageStore
} from '@/store/storage.js';
import {
storeToRefs
} from 'pinia';
import TalkingItem from './components/talking-item.vue'
import { commonUploadVoiceFile } from '@/api/common.js';
import { queryTraPartnerCharacterInfoById, partnerChatReportTrigger } from '@/api/sparring.js';
import { useSocketStore } from '@/store/socket.js';
import { useStorageStore } from '@/store/storage.js';
import { storeToRefs } from 'pinia';
import TalkingItem from './components/talking-item.vue';
import badge from '@/components/points-and-badge/badge';
import points from '@/components/points-and-badge/points';
import usePointsAndBadge from '@/components/points-and-badge/usePointsAndBadge';
const {
pointAndBadgesRun,
badgeRef,
pointsRef
} = usePointsAndBadge();
import { initRecord } from '@/composables/useExamSubject.js';
import permissionApi from '@/common/permission'
const { pointAndBadgesRun, badgeRef, pointsRef } = usePointsAndBadge();
const socketStore = useSocketStore();
const {
SocketObj
} = storeToRefs(socketStore);
const { SocketObj } = storeToRefs(socketStore);
const traId = ref('')
const chaId = ref('')
const talkingType = ref('')
const traId = ref('');
const chaId = ref('');
const talkingType = ref('');
// const badgeRef = ref(null);
// const pointsRef = ref(null);
const isPreview = ref(false)
const detailInfo = ref({})
const currentIndex = ref(0)
const showRoleInfo = ref(false)
const showLoading = ref(true)
const talkingList = ref([])
const isPreview = ref(false);
const detailInfo = ref({});
const currentIndex = ref(0);
const showRoleInfo = ref(false);
const showLoading = ref(true);
const talkingList = ref([]);
// {
// type: 'answer',
// talkingText: 'asad',
// isLoading: false,
// id: new Date().getTime(),
// },
const askData = ref({})
const execId = ref('')
const askData = ref({});
const execId = ref('');
const answerIsOver = ref(true);
const getDetailInfo = () => {
queryTraPartnerCharacterInfoById({
traId: traId.value,
chaId: chaId.value
}).then(res => {
console.log(res)
}).then((res) => {
console.log(res);
detailInfo.value = {
...res.body
}
};
if (detailInfo.value.traInterTyp === '01') {
initsocketTask()
initsocketTask();
}
setTimeout(() => {
showLoading.value = false
}, Math.random() * 1000 + 500)
})
}
showLoading.value = false;
}, Math.random() * 1000 + 500);
});
};
const scrollTop = ref(99999);
const scrollToBottomNow = (time = 100) => {
nextTick(() => {
@@ -188,7 +194,7 @@
}, time);
});
};
const uploadRecordNow = (voicePath) => {
const uploadRecordNow = (voicePath, voiceTime) => {
//#ifndef APP-PLUS
// h5
// #endif
@@ -199,6 +205,8 @@
isLoading: true
});
scrollToBottomNow();
//
answerIsOver.value = false;
commonUploadVoiceFile(voicePath, '/trapractice/traPartnerChat/voiceTrans', {})
.then((_res) => {
if (!!(_res.body.transContent || '').trim()) {
@@ -208,13 +216,15 @@
intrctContent: _res.body.transContent,
fileId: _res.body.fileId,
ossFileAddr: _res.body.ossFileAddr,
talkingVoice: voicePath
talkingVoice: voicePath,
voiceTime: voiceTime
};
sendMessage({
commond: 'ASK',
body: askData.value
});
} else {
answerIsOver.value = true;
talkingList.value.pop();
uni.showToast({
title: '未识别到文字',
@@ -224,6 +234,7 @@
}
})
.catch((err) => {
answerIsOver.value = true;
talkingList.value.pop();
uni.showToast({
title: '系统异常,请联系管理员',
@@ -232,26 +243,51 @@
});
});
// #endif
}
const toTalking = () => {
common.setPageCache({
sceneDesc:detailInfo.value.sceneDesc
})
common.navigateTo('/pages/sparring/talk?traId=' + traId.value + '&chaId=' + chaId.value + '&type=' + talkingType
.value + '&isPreview=' + (isPreview.value ? '1' : '0'))
}
const activeVoiceTalkingItemId = ref('')
};
const toTalking = async () => {
try {
//#ifdef APP-PLUS
console.log('申请权限');
try {
// await initRecord();
await permissionApi.judgeIosPermission('record')
await permissionApi.judgeIosPermission('camera')
} catch(e) {
console.error('摄像头或者麦克风权限申请失败:', e);
return;
}
//#endif
common.setPageCache('tipMsgsceneDesc', {
ossAddr: detailInfo.value.ossAddr,
chaName: detailInfo.value.chaName,
traInterTyp: detailInfo.value.traInterTyp,
traInterTypDesc: detailInfo.value.traInterTypDesc,
});
common.redirectTo(
'/pages/sparring/talk?traId=' +
traId.value +
'&chaId=' +
chaId.value +
'&type=' +
talkingType.value +
'&traInterTyp=' +
detailInfo.value.traInterTyp +
'&isPreview=' +
(isPreview.value ? '1' : '0')
);
} catch (err) {
console.error('申请权限失败');
}
};
const activeVoiceTalkingItemId = ref('');
//
const changePlayItemId = (id = '') => {
activeVoiceTalkingItemId.value = id
}
activeVoiceTalkingItemId.value = id;
};
const talkItemEvents = ({
type,
data,
id,
info = {}
}) => {
const talkItemEvents = ({ type, data, id, info = {} }) => {
switch (type) {
case 'changePlay':
activeVoiceTalkingItemId.value = id;
@@ -265,12 +301,13 @@
};
const watchFlag = ref(1);
const showModelComfirm = ref(false)
const reconcatNum = ref(0)
const answerText = ref('')
const touchBtnCallBack = ref(null)
const showModelComfirm = ref(false);
const reconcatNum = ref(0);
const answerText = ref('');
const touchBtnCallBack = ref(null);
let isLeave = false;
const initsocketTask = () => {
let _src = isPreview.value ? '/trapractice/previewsocket/open' : '/trapractice/partnersocket/open'
let _src = isPreview.value ? '/trapractice/previewsocket/open' : '/trapractice/partnersocket/open';
socketStore.creatSocket(_src + '?summary=');
// unref(SocketObj).on('open', (res)=>{
// if(!answerIsOver.value){
@@ -288,28 +325,40 @@
if (watchFlag.value === 1 || showModelComfirm.value) return;
if (reconcatNum.value === 5) {
common.msg('系统网络异常,请稍后再试!');
common.redirectTo(`/pages/index/dialog`);
return;
}
showModelComfirm.value = true;
reconcatNum.value += 1;
common
.show('提示信息', '网络环境不稳定,请重试!', false, '', '确认')
.then((res) => {
reconcatNum.value += 1;
common.redirectTo(`/pages/index/dialog`);
overNow(() => {
reconcatNum.value += 1;
// common.redirectTo(`/pages/index/dialog`);
socketStore?.closeSocket();
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
traId.value +
'&execId=' +
execId.value +
'&type=' +
talkingType.value +
'&isPreview=' +
(isPreview.value ? '1' : '')
);
});
})
.finally(() => {
showModelComfirm.value = false;
});
});
unref(SocketObj).on('message', (res) => {
const {
rtnCode,
body,
commond
} = JSON.parse(res.data);
const { rtnCode, body, commond } = JSON.parse(res.data);
let _id = new Date().getTime() + 'id';
if (rtnCode === '0000') {
if (commond === 'ASK-OPEN') {
console.log('ASK-OPEN', body);
//
sendMessage({
commond: 'ASK-START',
@@ -320,10 +369,16 @@
execId: execId.value || ''
}
});
sendMessage({
commond: 'ASK-PING',
body: {
execId: execId.value || ''
}
});
return;
}
if (commond === 'ASK-START') {
// console.log('')
console.log('开启对话!', body);
execId.value = body.execId;
return;
}
@@ -332,7 +387,9 @@
console.log(body);
console.log(talkingList.value[talkingList.value.length - 1].isLoading);
if (talkingList.value.length > 0 && talkingList.value[talkingList.value.length - 1].isLoading) {
talkingList.value.pop();
talkingList.value = talkingList.value.filter((t) => {
return !(t.type === 'question' && t.isLoading);
});
}
// seqNo.value = body.seqNo;
// touchBtnCallBack.value && touchBtnCallBack.value();
@@ -367,7 +424,10 @@
if (body.event === 'start') {
answerIsOver.value = false;
answerText.value = '';
talkingList.value.pop();
// talkingList.value.pop();
talkingList.value = talkingList.value.filter((t) => {
return !(t.type === 'answer' && t.isLoading);
});
talkingList.value.push({
type: 'answer',
talkingText: answerText.value,
@@ -377,7 +437,7 @@
...body,
askData: {
...askData.value
},
}
});
} else if (body.event === 'token') {
answerIsOver.value = false;
@@ -386,7 +446,7 @@
} else if (body.event === 'end') {
answerIsOver.value = true;
} else if (body.event === 'recordAi') {
talkingList.value[talkingList.value.length - 1].execLogId = body.execLogId
talkingList.value[talkingList.value.length - 1].execLogId = body.execLogId;
}
scrollToBottomNow();
return;
@@ -410,14 +470,33 @@
return;
}
if (commond === 'ASK-OVER') {
console.log(body );
console.log(body);
console.log('陪练已结束,去结果页面!');
common.navigateTo('/pages/sparring/result?isOver=1&traId=' + traId.value
+ '&execId=' + execId.value
+ '&type=' + talkingType.value
+ '&isPreview=' + (isPreview.value ? '1' : ''))
socketStore?.closeSocket();
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
traId.value +
'&execId=' +
execId.value +
'&type=' +
talkingType.value +
'&isPreview=' +
(isPreview.value ? '1' : '')
);
return;
}
if (commond === 'ASK-PONG') {
console.log('PONG');
if (isLeave) return;
setTimeout(() => {
sendMessage({
commond: 'ASK-PING',
body: {
execId: execId.value || ''
}
});
}, 30000);
}
} else {
if (['0005', 'QQ0005'].includes(res.rtnCode)) {
goto_login_fun();
@@ -428,63 +507,108 @@
};
const goBack = () => {
if (detailInfo.value.traInterTyp === '01') {
overTalking()
overTalking();
} else {
common.navigateBack()
common.navigateBack();
}
}
};
const overTalking = () => {
common.show('提示', '确定结束陪练吗?').then(res => {
common.show('提示', '确定结束陪练吗?').then((res) => {
if (res) {
partnerChatReportTrigger({
execId: execId.value,
traId: traId.value,
talkingType: isPreview.value ? 'preview' : 'practice'
}).then(res => {
watchFlag.value = 1;
sendMessage({
commond: 'ASK-STOP',
body: {
execId: execId.value || ''
}
})
socketStore?.closeSocket();
changePlayItemId('')
common.navigateTo('/pages/sparring/result?isOver=1&traId=' + traId.value + '&execId=' + execId
.value + '&type=' + talkingType.value + '&isPreview=' + (isPreview.value ? '1' : ''))
// pointAndBadgesRun(talkingType.value === '01'? '07': '08', '', ()=>{
// common.navigateTo('/pages/sparring/result?isOver=1&traId=' + traId.value + '&execId=' + execId.value + '&type=' + talkingType.value + '&isPreview=' + (isPreview.value ?'1':''))
// })
})
overNow();
}
});
};
const overNow = (cb = () => {}) => {
partnerChatReportTrigger({
execId: execId.value,
traId: traId.value,
talkingType: isPreview.value ? 'preview' : 'practice'
})
}
.then((res) => {
watchFlag.value = 1;
sendMessage({
commond: 'ASK-STOP',
body: {
execId: execId.value || ''
}
});
socketStore?.closeSocket();
changePlayItemId('');
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
traId.value +
'&execId=' +
execId.value +
'&type=' +
talkingType.value +
'&isPreview=' +
(isPreview.value ? '1' : '')
);
// pointAndBadgesRun(talkingType.value === '01'? '07': '08', '', ()=>{
// common.redirectTo('/pages/sparring/result?isOver=1&traId=' + traId.value + '&execId=' + execId.value + '&type=' + talkingType.value + '&isPreview=' + (isPreview.value ?'1':''))
// })
})
.catch(() => {
cb();
});
};
const sendMessage = (data) => {
unref(SocketObj).send(data);
if (data.commond == 'ASK-PING') {
console.log('PING');
}
};
// let timer = null
//#ifdef APP-PLUS
onShow(() => {});
onShow(() => {
isLeave = false;
// timer = setInterval(() => {
// sendMessage({
// commond: 'ASK-PING',
// body: {
// execId: execId.value || ''
// }
// }, 1000)
// })
});
onHide(() => {
isLeave = true;
// clearInterval(timer)
// timer = null
});
// #endif
onLoad((params) => {
traId.value = params.traId;
chaId.value = params.chaId;
talkingType.value = params.type;
isPreview.value = params.isPreview === '1'
getDetailInfo()
isPreview.value = params.isPreview === '1';
getDetailInfo();
console.log('onLoad');
//#ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary');
// #endif
});
onUnload(() => {
console.log('onUnload');
watchFlag.value = 1;
sendMessage({
commond: 'ASK-STOP',
body: {
execId: execId.value || ''
}
})
socketStore?.closeSocket();
});
if (detailInfo.value.traInterTyp === '01') {
socketStore?.closeSocket();
}
execId.value = '';
});
onLaunch(() => {
console.log('onLaunch');
});
onMounted(() => {
// getDetailInfo()
})
});
onBackPress((res) => res.from === 'backbutton');
</script>
@@ -689,7 +813,6 @@
width: 30rpx;
}
}
}
.role-desc {
@@ -755,7 +878,7 @@
.body-scene {
background: linear-gradient(209deg, #fefeff 0%, #d7dfff 100%);
border: 2rpx solid rgba(255, 255, 255, .65);
border: 2rpx solid rgba(255, 255, 255, 0.65);
position: relative;
line-height: 42rpx;
font-size: 29rpx;
@@ -776,9 +899,20 @@
text-align: center;
}
}
.talking-desc{
// padding-top: 24rpx;
padding: 24rpx 20rpx 0;
font-size: 24rpx;
// text-align: center;
color: #999;
}
.talking-box {
padding-top: 24rpx;
.tip-text {
color: #7295cb;
text-align: center;
font-size: 29rpx;
}
}
}
@@ -842,4 +976,4 @@
}
}
}
</style>
</style>
+103
View File
@@ -0,0 +1,103 @@
<template>
<view>
<view v-if="isPreviewing" class="live-container">
<cover-view class="pusher-container">
<live-pusher
id="livePusher"
class="livePusher"
:muted="true"
:enable-camera="true"
:auto-focus="true"
:beauty="0"
:whiteness="0"
aspect="9:16"
mode="SD"
orientation
></live-pusher>
</cover-view>
<!-- <cover-view class="corner-mask"></cover-view> -->
</view>
</view>
</template>
<script setup>
import { onMounted, ref, reactive, nextTick } from 'vue';
const livePusherContext = reactive({
value: null
});
const isPreviewing = ref(false);
const init = (componentProxy) => {
console.log('init');
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy);
};
// 切换摄像头
const switchCamera = () => {
livePusherContext.value?.switchCamera({
success: (res) => {
console.log('livePusher.switchCamera:', JSON.stringify(res));
},
fail: (err) => {
console.error('切换摄像头失败:', err);
}
});
};
const rebuildContext = () => {
clearTimeout(contextTimer);
contextTimer = setTimeout(() => {
try {
// 先销毁旧上下文,再重建
cameraContext.value = null;
cameraContext.value = uni.createLivePusherContext('customCamera', instance);
} catch (err) {
console.error('重建上下文失败:', err);
}
}, 100);
};
// 开启预览
const startPreview = () => {
isPreviewing.value = true;
nextTick(() => {
livePusherContext.value?.startPreview({
success: (res) => {
console.log('摄像头开启成功');
},
fail: (err) => {
console.error('开启预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
});
};
// 关闭预览
const stopPreview = () => {
console.log('关闭预览');
livePusherContext.value?.stopPreview({
success: (res) => {
console.log('livePusher.stopPreview:', JSON.stringify(res));
},
fail: (err) => {
console.error('关闭预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
isPreviewing.value = false;
};
defineExpose({ init, switchCamera, stopPreview, startPreview });
</script>
<style scoped>
.live-container {
position: absolute;
right: 0;
top: 150rpx;
overflow: hidden;
border-radius: 20rpx;
width: 270rpx;
height: 480rpx;
}
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<view >
<view v-if="isPreviewing">
<live-pusher
id="livePusher"
class="livePusher"
:muted="true"
:enable-camera="true"
:auto-focus="true"
:beauty="1"
whiteness="2"
aspect="9:16"
mode="SD"
></live-pusher>
</view>
<button class="btn" @click="startPreview">开启摄像头预览</button>
<button class="btn" @click="stopPreview">关闭摄像头预览</button>
<button class="btn" @click="switchCamera">切换摄像头</button>
</view>
</template>
<script setup>
import { onMounted, ref, reactive, nextTick } from 'vue';
const livePusherContext = reactive({
value: null
});
const isPreviewing = ref(false);
const init = (componentProxy) => {
console.log('init');
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy);
};
//
const switchCamera = () => {
livePusherContext.value?.switchCamera({
success: (res) => {
console.log('livePusher.switchCamera:', JSON.stringify(res));
},
fail: (err) => {
console.error('切换摄像头失败:', err);
}
});
};
const rebuildContext = () => {
clearTimeout(contextTimer);
contextTimer = setTimeout(() => {
try {
//
cameraContext.value = null;
cameraContext.value = uni.createLivePusherContext('customCamera', instance);
} catch (err) {
console.error('重建上下文失败:', err);
}
}, 100);
};
//
const startPreview = () => {
isPreviewing.value = true;
nextTick(() => {
livePusherContext.value?.startPreview({
success: (res) => {
console.log('摄像头开启成功');
},
fail: (err) => {
console.error('开启预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
});
};
//
const stopPreview = () => {
console.log('关闭预览');
livePusherContext.value?.stopPreview({
success: (res) => {
console.log('livePusher.stopPreview:', JSON.stringify(res));
},
fail: (err) => {
console.error('关闭预览失败:', err);
},
complete: (err) => {
console.error('complete预览:', err);
}
});
isPreviewing.value = false;
};
defineExpose({ init, switchCamera, stopPreview, startPreview });
</script>
<style scoped>
/* 可根据需要添加样式 */
.livePusher {
width: 100%;
height: 400px;
}
.btn {
margin: 10px;
padding: 10px 20px;
}
</style>
+3 -2
View File
@@ -23,6 +23,7 @@
<script setup>
import common from '@/common/common';
import permision from '@/common/permission'
import {
get_base_url
} from '../../api/request';
@@ -34,8 +35,8 @@
const bbbb = () => {
}
const aaaa = () => {
common.navigateTo('/pages/test/testChat')
const aaaa = async() => {
}
</script>
+30 -228
View File
@@ -1,236 +1,38 @@
<template>
<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 ref="aaa">xx</sdx-StreamPlayer>
<button @click="test">接通电话</button>
<button @click="applyRecordPermission">申请录音权限</button>
<view class="">
<view class="cssss">
<CameraPreview ref="CameraPreviewRef"></CameraPreview>
</view>
<view style="margin-top: 400rpx">
<button class="btn" @click="startPreview">开启摄像头预览</button>
<button class="btn" @click="stopPreview">关闭摄像头预览</button>
<button class="btn" @click="switchCamera">切换摄像头</button>
</view>
</view>
</template>
<script>
import {
ProtocolCodec,
MessageType,
SerializationType,
CompressionType,
ControlCommand,
ProtocolConst,
} from './ProtocolCodec';
export default {
data() {
return {
status: "未录音",
currentDecibels: 0,
isRecording: false,
ws: null, // WebSocket
audioContext: null, //
scriptProcessor: null, //
audioBufferSource: null, //
frameBufferList: [], //
// wsUrl: "ws://127.0.0.1:8000/ws/audio",
wsUrl: "ws://25.64.32.157:9603/trapractice/voiceCallSocket/voiceCall?summary=3D072C9D242C96EA3F0FD647CB14A6F4B4BEA4493BB2FAAB065935FB405071B8A903128E488B6D06BEB23FD4E3D15EE6", //
};
},
onUnload() {
//
this.stopRecordAndClean();
},
methods: {
test() {
this.ws = uni.connectSocket({
url: this.wsUrl,
fail: () => {
console.log('fail');
},
success: () => {
console.log('web');
},
fail: () => {
console.log('fail');
},
});
this.ws.onOpen((res) => {
console.log('WebSocket连接已打开', res);
this.ws.send({
data: ProtocolCodec.pack(MessageType.IDENTITY, {
user_id: '1001',
token: 'your_auth_token',
name: '测试用户'
})
});
});
this.ws.onMessage((res) => {
const {
msgType,
body
} = ProtocolCodec.unpack(res.data)
console.log('msgType', msgType);
if (msgType === MessageType.IDENTITY) {
console.log('身份校验成功', body);
this.onStartRecord()
}
})
<script setup>
import CameraPreview from './CameraPreview';
import { onReady } from '@dcloudio/uni-app';
import { ref, getCurrentInstance } from 'vue';
},
//
async applyRecordPermission() {
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;
const CameraPreviewRef = ref(null);
// 2: 使
// return String.fromCharCode.apply(null, new Uint8Array(buffer));
},
//
onStartRecord() {
console.log('开始录音');
try {
this.$refs.recordFrame.start({
sampleRate: 16000,
frameSize: 1024,
gain: 1.0,
onFrameRecorded: ({
isLastFrame,
frameBuffer
}) => {
try{
this.ws.send({
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
});
} catch(e){}
},
onDecibels: (decibels) => {
this.currentDecibels = decibels;
}
});
this.isRecording = true;
this.status = "录音中...";
} catch (e) {
console.error("启动录音失败:", e);
this.status = "启动录音失败";
this.isRecording = false;
}
},
//
onStopRecord() {
this.stopRecordAndClean();
},
//
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 {
this.ws.send({
data: frameBuffer
});
} catch (e) {
console.error("发送音频帧失败:", e);
}
}
},
//
onCurrentDecibels(decibels) {
// this.currentDecibels = decibels.toFixed(2);
// console.log("", this.currentDecibels);
},
//
stopIt(base64) {
this.stopRecordAndClean();
console.log("录音停止,最终音频Base64", base64?.substring(0, 50) + "...");
},
},
const instance = getCurrentInstance();
const componentProxy = instance.ctx;
const startPreview = () => {
CameraPreviewRef.value.startPreview();
};
const stopPreview = () => {
CameraPreviewRef.value.stopPreview();
};
const switchCamera = () => {
CameraPreviewRef.value.switchCamera();
};
onReady(() => {
CameraPreviewRef.value.init(componentProxy);
});
</script>
<style scoped>
.container {
padding: 20rpx;
}
button {
margin: 10rpx 0;
padding: 15rpx 30rpx;
background: #007aff;
color: white;
border: none;
border-radius: 8rpx;
}
button:disabled {
background: #ccc;
}
.tip {
margin: 15rpx 0;
font-size: 28rpx;
color: #333;
}
</style>
<style>
</style>
-147
View File
@@ -1,147 +0,0 @@
<template>
<view>
<video :is-live="true" id="myVideo" :src="videoSrc" :autoplay="true"></video>
<live-pusher id="livePusher" ref="livePusher" class="livePusher" url="rtmp://25.18.122.148:9605/voice_in/aa" mode="SD" :muted="true" :enable-camera="false" :auto-focus="true" :beauty="1" whiteness="2" aspect="9:16" @statechange="statechange" @netstatus="netstatus" @error="error"></live-pusher>
<view class="btn" @click="prepare"><text>预热</text></view>
<view class="btn" @click="start"><text>开始推流</text></view>
<view class="btn" @click="pause"><text>暂停推流</text></view>
<view class="btn" @click="resume"><text>恢复推流</text></view>
<view class="btn" @click="stop"><text>停止推流</text></view>
<view class="btn" @click="snapshot"><text>快照</text></view>
<view class="btn" @click="startPreview"><text>开启摄像头预览</text></view>
<view class="btn" @click="stopPreview"><text>关闭摄像头预览</text></view>
<view class="btn" @click="switchCamera"><text>切换摄像头</text></view>
</view>
</template>
<script>
import { prepareApi } from '@/api/talk.js'
var pusher
export default {
data() {
return {
context: null,
baseName: 'aaa'
}
},
computed: {
videoSrc() {
return 'rtmp://rtmp.home.tymas.cn:1935/voice_in/' + this.baseName
}
},
onReady() {
const videoContext = uni.createVideoContext('myVideo')
videoContext.setVolume(1.0)
// onReady onLoad
// pusher = uni.createLivePusherContext('livePusher', this)
// console.log(pusher)
},
methods: {
statechange(e) {
console.log('statechange:', e, JSON.stringify(e))
},
netstatus(e) {
console.log('netstatus:' + JSON.stringify(e))
},
error(e) {
console.log('error:' + JSON.stringify(e))
},
start: function () {
console.log('statechange:', pusher.start)
pusher.start({
success: a => {
console.log('livePusher.start:' + JSON.stringify(a))
}
})
},
prepare: function () {
this.initPusher()
// prepareApi(this.baseName)
// .then(res => {
// console.log(res)
// })
// .catch(err => {
// console.log(err)
// if (err === 'prepare') {
// this.initPusher()
// }
// })
},
close: function () {
pusher.close({
success: a => {
console.log('livePusher.close:' + JSON.stringify(a))
}
})
},
snapshot: function () {
pusher.snapshot({
success: e => {
console.log(JSON.stringify(e))
}
})
},
resume: function () {
pusher.resume({
success: a => {
console.log('livePusher.resume:' + JSON.stringify(a))
}
})
},
pause: function () {
pusher.pause({
success: a => {
console.log('livePusher.pause:' + JSON.stringify(a))
}
})
},
stop: function () {
pusher.stop({
success: a => {
console.log(JSON.stringify(a))
}
})
},
switchCamera: function () {
pusher.switchCamera({
success: a => {
console.log('livePusher.switchCamera:' + JSON.stringify(a))
}
})
},
startPreview: function () {
pusher.startPreview({
success: a => {
console.log('livePusher.startPreview:' + JSON.stringify(a))
}
})
},
stopPreview: function () {
pusher.stopPreview({
success: a => {
console.log('livePusher.stopPreview:' + JSON.stringify(a))
}
})
},
initPusher() {
pusher = new plus.video.LivePusher('pusher-box', {
url: 'rtmp://rtmp.home.tymas.cn:1935/voice_in/' + this.baseName,
'enable-camera': true
})
pusher.addEventListener('statechange', this.statechange)
pusher.addEventListener('netstatus', this.netstatus)
pusher.addEventListener('error', this.error)
console.log(pusher)
}
}
}
</script>
<style>
.btn {
width: 100%;
height: 80rpx;
background-color: #ccc;
margin-bottom: 5rpx;
}
</style>
+762
View File
@@ -0,0 +1,762 @@
<template>
<view class="page-container">
<!-- 头部标题栏 -->
<view class="header">
<text class="title">摄像头预览</text>
</view>
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 预览窗口容器 -->
<view class="preview-section" v-if="isPreviewing">
<view class="section-title">
<text>摄像头预览</text>
<text class="preview-status">● 预览中</text>
</view>
<!-- 预览窗口 -->
<view class="preview-box">
<live-pusher
id="livePusher"
ref="livePusher"
class="live-preview"
:url="''"
mode="SD"
:autopush="false"
:muted="true"
:enable-camera="true"
:auto-focus="true"
:orientation="orientation"
:beauty="beautyLevel"
:whiteness="whiteLevel"
:aspect="aspectRatio"
:min-bitrate="300"
:max-bitrate="1000"
@statechange="onStateChange"
@error="onError"
@netstatus="onNetStatus"
/>
<!-- 预览窗口边框装饰 -->
<view class="preview-frame"></view>
</view>
<!-- 预览控制按钮 -->
<view class="preview-controls">
<button
class="control-btn switch-btn"
@click="switchCamera"
:disabled="!isPreviewing"
>
🔄 切换摄像头
</button>
<button
class="control-btn close-btn"
@click="closePreview"
>
❌ 关闭预览
</button>
</view>
</view>
<!-- 未预览时的占位区域 -->
<view class="placeholder-section" v-else>
<view class="section-title">
<text>预览功能</text>
</view>
<view class="placeholder-box">
<text class="placeholder-icon">📷</text>
<text class="placeholder-text">点击开始预览体验摄像头功能</text>
</view>
<button class="start-btn" @click="startPreview">
🎥 开始预览
</button>
</view>
<!-- 设置面板 -->
<view class="settings-section" v-if="showSettings">
<view class="section-title">
<text>预览设置</text>
<text class="close-settings" @click="closeSettings">✕</text>
</view>
<view class="settings-content">
<view class="setting-group">
<text class="group-title">显示设置</text>
<view class="setting-item">
<text class="setting-label">方向模式</text>
<picker
@change="onOrientationChange"
:value="orientationIndex"
:range="orientationOptions"
class="setting-picker"
>
<view class="picker-display">
{{ orientationOptions[orientationIndex] }}
<text class="arrow">▼</text>
</view>
</picker>
</view>
<view class="setting-item">
<text class="setting-label">画面比例</text>
<picker
@change="onAspectChange"
:value="aspectIndex"
:range="aspectOptions"
class="setting-picker"
>
<view class="picker-display">
{{ aspectOptions[aspectIndex] }}
<text class="arrow">▼</text>
</view>
</picker>
</view>
</view>
<view class="setting-group">
<text class="group-title">美化设置</text>
<view class="setting-item">
<text class="setting-label">美颜等级: {{ beautyLevel }}</text>
<slider
:value="beautyLevel"
min="0"
max="9"
step="1"
@change="onBeautyChange"
activeColor="#007AFF"
class="setting-slider"
/>
</view>
<view class="setting-item">
<text class="setting-label">美白等级: {{ whiteLevel }}</text>
<slider
:value="whiteLevel"
min="0"
max="9"
step="1"
@change="onWhiteChange"
activeColor="#007AFF"
class="setting-slider"
/>
</view>
</view>
<button class="apply-btn" @click="applySettings">应用设置</button>
</view>
</view>
</view>
<!-- 底部功能区 -->
<view class="bottom-section">
<!-- 快捷设置按钮 -->
<view class="quick-actions" v-if="isPreviewing">
<button class="action-btn settings-action" @click="openSettings">
⚙️ 设置
</button>
<button class="action-btn capture-action" @click="captureFrame">
📸 截图
</button>
</view>
<!-- 功能说明 -->
<view class="info-section">
<text class="info-text">
💡 提示:预览功能仅用于查看摄像头画面,不会产生任何推流或录制
</text>
</view>
</view>
<!-- 遮罩层 -->
<view
class="overlay"
v-if="showSettings"
@click="closeSettings"
></view>
</view>
</template>
<script>
export default {
data() {
return {
// 预览状态
isPreviewing: false,
showSettings: false,
sssssssss: true,
// 预览配置
orientation: 'vertical',
orientationOptions: ['竖屏', '横屏'],
orientationIndex: 0,
aspectRatio: '9:16',
aspectOptions: ['9:16', '3:4', '1:1'],
aspectIndex: 0,
beautyLevel: 0,
whiteLevel: 0,
// LivePusher 上下文
livePusherCtx: null
}
},
methods: {
// 开始预览
async startPreview() {
try {
console.log('开始启动预览...');
// 创建 live-pusher 上下文
this.livePusherCtx = uni.createLivePusherContext('livePusher', this);
// 显示预览界面
this.isPreviewing = true;
// 启动预览(不推流)
this.$nextTick(() => {
if (this.livePusherCtx) {
this.livePusherCtx.startPreview({
success: () => {
console.log('预览启动成功');
uni.showToast({
title: '预览已开始',
icon: 'success'
});
},
fail: (err) => {
console.error('预览启动失败:', err);
this.handlePreviewError(err);
}
});
}
});
} catch (error) {
console.error('启动预览异常:', error);
uni.showToast({
title: '预览启动失败',
icon: 'error'
});
}
},
// 关闭预览
closePreview() {
uni.showModal({
title: '确认关闭',
content: '确定要关闭摄像头预览吗?',
success: (res) => {
if (res.confirm) {
this.doClosePreview();
}
}
});
},
// 执行关闭预览
doClosePreview() {
try {
console.log('正在关闭预览...');
// 停止预览
if (this.livePusherCtx) {
this.livePusherCtx.stopPreview({
success: () => {
console.log('预览已停止');
},
fail: (err) => {
console.error('停止预览失败:', err);
}
});
}
// 重置状态
this.isPreviewing = false;
this.showSettings = false;
uni.showToast({
title: '预览已关闭1',
icon: 'success'
});
// this.sssssssss = false;
} catch (error) {
console.error('关闭预览异常:', error);
// 强制更新状态
this.isPreviewing = false;
this.showSettings = false;
}
},
// 切换摄像头
switchCamera() {
if (!this.livePusherCtx || !this.isPreviewing) return;
uni.showLoading({
title: '切换中...'
});
this.livePusherCtx.switchCamera({
success: () => {
console.log('摄像头切换成功');
uni.hideLoading();
uni.showToast({
title: '摄像头已切换',
icon: 'success'
});
},
fail: (err) => {
console.error('摄像头切换失败:', err);
uni.hideLoading();
uni.showToast({
title: '切换失败',
icon: 'error'
});
}
});
},
// 打开设置面板
openSettings() {
this.showSettings = true;
},
// 关闭设置面板
closeSettings() {
this.showSettings = false;
},
// 应用设置
applySettings() {
uni.showToast({
title: '设置已应用',
icon: 'success'
});
this.closeSettings();
},
// 方向改变
onOrientationChange(e) {
this.orientationIndex = e.detail.value;
this.orientation = this.orientationIndex === 0 ? 'vertical' : 'horizontal';
},
// 宽高比改变
onAspectChange(e) {
this.aspectIndex = e.detail.value;
this.aspectRatio = this.aspectOptions[this.aspectIndex];
},
// 美颜等级改变
onBeautyChange(e) {
this.beautyLevel = e.detail.value;
},
// 美白等级改变
onWhiteChange(e) {
this.whiteLevel = e.detail.value;
},
// 截图功能(可选)
captureFrame() {
uni.showToast({
title: '截图功能开发中',
icon: 'none'
});
},
// 预览状态变化
onStateChange(e) {
console.log('预览状态变化:', e.detail);
},
// 错误处理
onError(e) {
console.error('预览错误:', e.detail);
const error = e.detail;
let errorMsg = '预览出现错误';
switch(error.errCode) {
case 1001:
errorMsg = '摄像头权限被拒绝';
break;
case 1002:
errorMsg = '麦克风权限被拒绝';
break;
case 1003:
errorMsg = '摄像头被其他应用占用';
break;
case 1004:
errorMsg = '设备不支持';
break;
}
uni.showModal({
title: '预览错误',
content: errorMsg,
showCancel: false,
success: () => {
this.closePreview();
}
});
},
// 网络状态
onNetStatus(e) {
console.log('预览网络状态:', e.detail);
},
// 处理预览错误
handlePreviewError(err) {
uni.showModal({
title: '预览启动失败',
content: '请检查摄像头权限或稍后重试',
showCancel: false,
success: () => {
this.isPreviewing = false;
}
});
}
},
beforeDestroy() {
if (this.isPreviewing) {
this.doClosePreview();
}
}
}
</script>
<style scoped>
.page-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.title {
color: white;
font-size: 20px;
font-weight: bold;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* 区域标题 */
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 0 10px;
}
.section-title text:first-child {
font-size: 18px;
font-weight: bold;
color: #333;
}
.preview-status {
font-size: 14px;
color: #4CAF50;
}
.close-settings {
font-size: 20px;
color: #999;
padding: 5px 10px;
cursor: pointer;
}
/* 预览区域 */
.preview-section, .placeholder-section {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
/* 预览窗口盒子 */
.preview-box {
position: relative;
width: 280px;
height: 373px; /* 9:16 比例 */
margin: 0 auto 20px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.live-preview {
width: 100%;
height: 100%;
background-color: #000;
}
/* 预览窗口边框装饰 */
.preview-frame {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 3px solid rgba(0, 122, 255, 0.3);
border-radius: 12px;
pointer-events: none;
}
/* 占位符样式 */
.placeholder-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 12px;
margin-bottom: 20px;
}
.placeholder-icon {
font-size: 48px;
margin-bottom: 15px;
}
.placeholder-text {
color: white;
font-size: 16px;
text-align: center;
}
/* 按钮样式 */
.control-btn, .start-btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.control-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin: 0 10px;
}
.control-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.close-btn {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
color: white;
}
.start-btn {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
width: 200px;
}
.preview-controls {
display: flex;
justify-content: center;
align-items: center;
}
/* 设置区域 */
.settings-section {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 20px;
padding: 25px;
width: 90%;
max-width: 450px;
max-height: 80vh;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.settings-content {
margin-top: 15px;
}
.setting-group {
margin-bottom: 25px;
}
.group-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.setting-item {
margin-bottom: 20px;
}
.setting-label {
display: block;
font-size: 14px;
color: #555;
margin-bottom: 8px;
font-weight: 500;
}
.setting-picker {
width: 100%;
}
.picker-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
}
.arrow {
color: #999;
font-size: 12px;
}
.setting-slider {
width: 100%;
margin: 10px 0;
}
.apply-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
margin-top: 10px;
}
/* 底部区域 */
.bottom-section {
background: white;
padding: 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.quick-actions {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
}
.action-btn {
padding: 10px 20px;
border: none;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.settings-action {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.capture-action {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.info-section {
text-align: center;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.info-text {
font-size: 13px;
color: #666;
line-height: 1.5;
}
/* 遮罩层 */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-content {
padding: 15px;
}
.preview-box {
width: 240px;
height: 320px;
}
.preview-controls {
flex-direction: column;
gap: 10px;
}
.control-btn {
margin: 5px 0;
}
.quick-actions {
flex-direction: column;
align-items: center;
}
.action-btn {
width: 200px;
}
}
</style>
+20
View File
@@ -0,0 +1,20 @@
<template>
<view class="">
<CameraPreview ref="CameraPreviewRef"></CameraPreview>
</view>
</template>
<script setup>
import CameraPreview from './CameraPreview'
import { onReady } from '@dcloudio/uni-app';
import { ref , getCurrentInstance} from 'vue';
const CameraPreviewRef = ref(null);
const instance = getCurrentInstance();
const componentProxy = instance.ctx
onReady(() => {
CameraPreviewRef.value.init(componentProxy)
})
</script>
+1
View File
@@ -103,6 +103,7 @@
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom",
"screenOrientation": "portrait",
"app-plus": {
"titleView": false,
"bounce": "none",
+300
View File
@@ -0,0 +1,300 @@
! function() {
"use strict";
function t(t, e) {
for (var r = 0; r < e.length; r++) {
var n = e[r];
n.enumerable = n.enumerable || !1, n.configurable = !0, "value" in n && (n.writable = !0), Object
.defineProperty(t, (i = n.key, o = void 0, "symbol" == typeof(o = function(t, e) {
if ("object" != typeof t || null === t) return t;
var r = t[Symbol.toPrimitive];
if (void 0 !== r) {
var n = r.call(t, e || "default");
if ("object" != typeof n) return n;
throw new TypeError("@@toPrimitive must return a primitive value.")
}
return ("string" === e ? String : Number)(t)
}(i, "string")) ? o : String(o)), n)
}
var i, o
}
function e(t) {
return e = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function(t) {
return t.__proto__ || Object.getPrototypeOf(t)
}, e(t)
}
function r(t, e) {
return r = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function(t, e) {
return t.__proto__ = e, t
}, r(t, e)
}
function n() {
if ("undefined" == typeof Reflect || !Reflect.construct) return !1;
if (Reflect.construct.sham) return !1;
if ("function" == typeof Proxy) return !0;
try {
return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], (function() {}))), !0
} catch (t) {
return !1
}
}
function i(t, e, o) {
return i = n() ? Reflect.construct.bind() : function(t, e, n) {
var i = [null];
i.push.apply(i, e);
var o = new(Function.bind.apply(t, i));
return n && r(o, n.prototype), o
}, i.apply(null, arguments)
}
function o(t) {
var n = "function" == typeof Map ? new Map : void 0;
return o = function(t) {
if (null === t || (o = t, -1 === Function.toString.call(o).indexOf("[native code]"))) return t;
var o;
if ("function" != typeof t) throw new TypeError("Super expression must either be null or a function");
if (void 0 !== n) {
if (n.has(t)) return n.get(t);
n.set(t, a)
}
function a() {
return i(t, arguments, e(this).constructor)
}
return a.prototype = Object.create(t.prototype, {
constructor: {
value: a,
enumerable: !1,
writable: !0,
configurable: !0
}
}), r(a, t)
}, o(t)
}
function a(t) {
if (void 0 === t) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
return t
}
function s(t) {
var r = n();
return function() {
var n, i = e(t);
if (r) {
var o = e(this).constructor;
n = Reflect.construct(i, arguments, o)
} else n = i.apply(this, arguments);
return function(t, e) {
if (e && ("object" == typeof e || "function" == typeof e)) return e;
if (void 0 !== e) throw new TypeError(
"Derived constructors may only return object or undefined");
return a(t)
}(this, n)
}
}
function f(t) {
return function(t) {
if (Array.isArray(t)) return u(t)
}(t) || function(t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"]) return Array
.from(t)
}(t) || function(t, e) {
if (!t) return;
if ("string" == typeof t) return u(t, e);
var r = Object.prototype.toString.call(t).slice(8, -1);
"Object" === r && t.constructor && (r = t.constructor.name);
if ("Map" === r || "Set" === r) return Array.from(t);
if ("Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)) return u(t, e)
}(t) || function() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
)
}()
}
function u(t, e) {
(null == e || e > t.length) && (e = t.length);
for (var r = 0, n = new Array(e); r < e; r++) n[r] = t[r];
return n
}
function l(t, e, r, n) {
this.fromSampleRate = t, this.toSampleRate = e, this.channels = 0 | r, this.noReturn = !!n, this.initialize()
}
l.prototype.initialize = function() {
if (!(this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0)) throw new Error(
"Invalid settings specified for the resampler.");
this.fromSampleRate == this.toSampleRate ? (this.resampler = this.bypassResampler, this.ratioWeight = 1) : (
this.fromSampleRate < this.toSampleRate ? (this.lastWeight = 1, this.resampler = this
.compileLinearInterpolation) : (this.tailExists = !1, this.lastWeight = 0, this.resampler = this
.compileMultiTap), this.ratioWeight = this.fromSampleRate / this.toSampleRate)
}, l.prototype.compileLinearInterpolation = function(t) {
var e = t.length;
this.initializeBuffers(e);
var r, n, i = this.outputBufferSize,
o = this.ratioWeight,
a = this.lastWeight,
s = 0,
f = 0,
u = 0,
l = this.outputBuffer;
if (e % this.channels == 0) {
if (e > 0) {
for (; a < 1; a += o)
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = this.lastOutput[r] * s + t[
r] * f;
for (a--, e -= this.channels, n = Math.floor(a) * this.channels; u < i && n < e;) {
for (s = 1 - (f = a % 1), r = 0; r < this.channels; ++r) l[u++] = t[n + r] * s + t[n + this
.channels + r] * f;
a += o, n = Math.floor(a) * this.channels
}
for (r = 0; r < this.channels; ++r) this.lastOutput[r] = t[n++];
return this.lastWeight = a % 1, this.bufferSlice(u)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.compileMultiTap = function(t) {
var e = [],
r = t.length;
this.initializeBuffers(r);
var n = this.outputBufferSize;
if (r % this.channels == 0) {
if (r > 0) {
for (var i = this.ratioWeight, o = 0, a = 0; a < this.channels; ++a) e[a] = 0;
var s = 0,
f = 0,
u = !this.tailExists;
this.tailExists = !1;
var l = this.outputBuffer,
h = 0,
c = 0;
do {
if (u)
for (o = i, a = 0; a < this.channels; ++a) e[a] = 0;
else {
for (o = this.lastWeight, a = 0; a < this.channels; ++a) e[a] += this.lastOutput[a];
u = !0
}
for (; o > 0 && s < r;) {
if (!(o >= (f = 1 + s - c))) {
for (a = 0; a < this.channels; ++a) e[a] += t[s + a] * o;
c += o, o = 0;
break
}
for (a = 0; a < this.channels; ++a) e[a] += t[s++] * f;
c = s, o -= f
}
if (0 != o) {
for (this.lastWeight = o, a = 0; a < this.channels; ++a) this.lastOutput[a] = e[a];
this.tailExists = !0;
break
}
for (a = 0; a < this.channels; ++a) l[h++] = e[a] / i
} while (s < r && h < n);
return this.bufferSlice(h)
}
return this.noReturn ? 0 : []
}
throw new Error("Buffer was of incorrect sample length.")
}, l.prototype.bypassResampler = function(t) {
return this.noReturn ? (this.outputBuffer = t, t.length) : t
}, l.prototype.bufferSlice = function(t) {
if (this.noReturn) return t;
try {
return this.outputBuffer.subarray(0, t)
} catch (e) {
try {
return this.outputBuffer.length = t, this.outputBuffer
} catch (e) {
return this.outputBuffer.slice(0, t)
}
}
}, l.prototype.initializeBuffers = function(t) {
this.outputBufferSize = Math.ceil(t * this.toSampleRate / this.fromSampleRate);
try {
this.outputBuffer = new Float32Array(this.outputBufferSize), this.lastOutput = new Float32Array(this
.channels)
} catch (t) {
this.outputBuffer = [], this.lastOutput = []
}
};
var h = function(e) {
! function(t, e) {
if ("function" != typeof e && null !== e) throw new TypeError(
"Super expression must either be null or a function");
t.prototype = Object.create(e && e.prototype, {
constructor: {
value: t,
writable: !0,
configurable: !0
}
}), Object.defineProperty(t, "prototype", {
writable: !1
}), e && r(t, e)
}(h, e);
var n, i, o, u = s(h);
function h() {
var t;
! function(t, e) {
if (!(t instanceof e)) throw new TypeError("Cannot call a class as a function")
}(this, h);
var e = a(t = u.call(this));
return t.port.onmessage = function(t) {
var r = t.data,
n = r.type,
i = r.data;
if (console.log("type", n), "init" === n) {
var o = i.frameSize,
a = i.toSampleRate,
s = i.arrayBufferType,
f = i.fromSampleRate;
return e.frameSize = o * Math.floor(f / a), e.resampler = new l(f, a, 1), e
.frameBuffer = [], void(e.arrayBufferType = s),e.gain = i.gain || 1.0;
}
"stop" === n && (e.port.postMessage({
frameBuffer: e.transData(e.frameBuffer),
isLastFrame: !0
}), e.frameBuffer = [])
}, t
}
return n = h, (i = [{
key: "process",
value: function(t) {
var e, r = t[0][0];
return this.frameSize ? ((e = this.frameBuffer).push.apply(e, f(r)), this
.frameBuffer.length >= this.frameSize && (this.port.postMessage({
frameBuffer: this.transData(this.frameBuffer),
isLastFrame: !1
}), this.frameBuffer = []), !0) : (r && this.port.postMessage({
frameBuffer: this.transData(r),
isLastFrame: !1
}), !0)
}
}, {
key: "transData",
value: function(t) {
const gain = this.gain; // 增益系数,可根据实际需求修改
t = t.map(sample => sample * gain); // 对每个样本应用增益
return "short16" === this.arrayBufferType && (t = function(t) {
for (var e = new ArrayBuffer(2 * t.length), r = new DataView(e), n = 0,
i = 0; i < t.length; i += 1, n += 2) {
var o = Math.max(-1, Math.min(1, t[i]));
r.setInt16(n, o < 0 ? 32768 * o : 32767 * o, !0)
}
return r.buffer
}(t = this.resampler.resampler(t))), t
}
}]) && t(n.prototype, i), o && t(n, o), Object.defineProperty(n, "prototype", {
writable: !1
}), h
}(o(AudioWorkletProcessor));
registerProcessor("processor-worklet", h)
}();
-19
View File
@@ -1,19 +0,0 @@
import {
defineConfig
} from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import path from 'path'
export default defineConfig({
plugins: [uni()],
// 新增服务器配置,指定启动端口
server: {
port: 5174,
strictPort: true, // 可选:端口被占用时直接报错(避免自动切换端口)
open: false // 可选:启动后自动打开浏览器(按需开启)
},
base: './', // 关键:设置基础路径为相对路径
build: {
outDir: 'dist',
assetsDir: 'assets' // 和打包后的目录一致
}
})
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

@@ -1,5 +0,0 @@
测试账号
账号:015169
密码:Aa111111
隐私政策(URL):https://aits.jlbank.com.cn:7001/traapp/dfAgtInfo/queryValidDfAgtHtml
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
cn.com.jlbank.ai
@@ -1,36 +0,0 @@
APP名称:吉AI学
APP包名:cn.com.jlbank.ai
versionName0.0.5
versionCode5
签名MD5值/证书MD5指纹(32位)
d5dc64cddda525fe26b8ef248a2bb389
公钥(十六进制 512位)
9d972f990ca99f917a1f29b8e75ecab936f914a2577f867e401aab03661a00bdd50eb1e7f6cd48a6446eec5c26aaa121607b3bd81137178d696cb933835efc77d82b485309f0ddceb4673d0f8fe3b78b1e4b26767cf5a448ddfcba940f7ad1448d8406250dcc782a9b85d5c65c343e5d18bfc0bb15a53ec3a75d6ed07dabeee144a43f39924267a28c85749029b666dda9bfce8b9c026c9651c0ed41495de160ec394fdff8c9ec4b46d79238388c9aaef18c06a8b5fc5235eecea4e1a504105ef389ec9a99720d1d2695daa1f5eb020dc4a5072c1620edb2f5acd6c7c17813247fdcf4c9c22d3799e405a520768a85d0c198105d7955f19d15716c74251a862f
公钥(十进制)
19893966439922127693384889719938315036246244972917959385248316118565725143577774036887780362653488778541217930399876645058313599133123463497736546146477791544117358463680573389561564122662732863094890558191705481620786157835099824817072995429209906478607521681804646130325624863706603729124166653309099138828812936635381758248002921848461146778345641566513142267099505733743260834038131644946170268034014656863696128367747110259139002386246751925404899322985476977818658456537060869883795126190386613636784820986934886571172792850421551099690646290355407553479248915543674214152326021332306334646652057751589152785967
SHA1值:
de:f9:24:0c:b3:52:c1:8c:7b:af:12:00:2c:e3:89:f1:73:09:2c:8c
以上信息来自,APP备案助手,下载链接 https://sj.qq.com/appdetail/cn.houapp.beian.zhushou,或者各大安卓应用市场搜索 APP备案助手 即可。
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqOBG7pYHgQ3wUf9pj6qA
RvKSgNEnREQfZX58O5Zc6FhGO+CLX+GXXxgtLQ4b637YLHCS8KWQhlK3WIWjBOEP
Kq3lVxjPrKqbBqFLMnvwR5DfWg3oEDn7pge+hDDuf+OjnqTzm+vCsivmKTrpyb/H
9lbBfBU5+AqkNdqauAmJ/5Xx9f0Fj+xxSfgbHVxjcpiyjE6Bxa7ZGbzDzS2Gs0ND
uhLKhCcM44Z9ht+1Pd9cd19vbfzfQOOcAkSXJd9k2SaoMW7Pq9/ftn+rdzkTjBty
OO1F6wvtUQNgJferVWogsi4gVJY2wuqm36yO6ypw/TSWUMGOQnOVQsFzgelUUmdS
sQIDAQAB
keytool -exportcert -alias ai_jlyh_app -keystore ai_jlyh_app.keystore -file cert.cer
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

@@ -0,0 +1,306 @@
let popupView = null // 窗口实例
export function showModal(info) {
const title = info && info.title ? info.title : '标题'
const content = info && info.content ? info.content : '请输入内容'
let screenWidth = plus.screen.resolutionWidth
let screenHeight = plus.screen.resolutionHeight
const popupViewWidth = screenWidth * 0.85
const viewContentPadding = 20 // 减小内边距
const viewContentWidth = parseInt(popupViewWidth - (viewContentPadding * 2))
const descriptionList = drawtext(content, viewContentWidth)
let popupViewHeight = 60 + 20 + 20 // 减小整体高度
let popupViewContentList = [{
tag: 'font',
id: 'title',
text: title,
textStyles: {
size: '18px',
color: "#333333",
weight: "bold",
whiteSpace: "normal"
},
position: {
top: '25px', // 调整标题位置
left: viewContentPadding + "px",
width: viewContentWidth + "px",
height: "25px",
}
}]
const textHeight = 24 // 调整行高
let contentTop = 60 // 调整内容起始位置
descriptionList.forEach((item, index) => {
if (index > 0) {
popupViewHeight += textHeight;
contentTop += textHeight;
}
popupViewContentList.push({
tag: 'font',
id: 'content' + index + 1,
text: item.content,
textStyles: {
size: '15px',
color: "#666666",
lineSpacing: "50%", // 调整行间距
align: "left"
},
position: {
top: contentTop + "px",
left: viewContentPadding + "px",
width: viewContentWidth + "px",
height: textHeight + "px",
}
});
if (item.type == "break") {
contentTop += 8; // 减小段落间距
popupViewHeight += 8;
}
})
popupView = new plus.nativeObj.View("popupView", {
tag: "rect",
top: "80px", // 固定在靠近顶部的位置
left: (screenWidth - popupViewWidth) / 2 + "px",
height: popupViewHeight + "px",
width: popupViewWidth + "px"
})
// 绘制主体背景
popupView.drawRect({
color: "#FFFFFF",
radius: "12px",
borderWidth: "1px",
borderColor: "#E5E5E5"
}, {
top: "0px",
height: popupViewHeight + "px",
})
popupView.draw(popupViewContentList)
popupView.show()
}
function closeModal() {
// 在不再需要 popupView 时关闭它
popupView && popupView.close();
popupView = null;
}
// 文字换行
function drawtext(text, maxWidth) {
let textArr = text.split("");
let len = textArr.length;
// 上个节点
let previousNode = 0;
// 记录节点宽度
let nodeWidth = 0;
// 文本换行数组
let rowText = [];
// 如果是字母,侧保存长度
let letterWidth = 0;
// 汉字宽度
let chineseWidth = 14;
// otherFont宽度
let otherWidth = 7;
for (let i = 0; i < len; i++) {
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
if (letterWidth > 0) {
if (nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
});
previousNode = i
nodeWidth = chineseWidth
letterWidth = 0
} else {
nodeWidth += chineseWidth + letterWidth * otherWidth
letterWidth = 0
}
} else {
if (nodeWidth + chineseWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
})
previousNode = i
nodeWidth = chineseWidth
} else {
nodeWidth += chineseWidth
}
}
} else {
if (/\n/g.test(textArr[i])) {
rowText.push({
type: "break",
content: text.substring(previousNode, i)
})
previousNode = i + 1
nodeWidth = 0
letterWidth = 0
} else if (textArr[i] == "\\" && textArr[i + 1] == "n") {
rowText.push({
type: "break",
content: text.substring(previousNode, i)
})
previousNode = i + 2
nodeWidth = 0
letterWidth = 0
} else if (/[a-zA-Z0-9]/g.test(textArr[i])) {
letterWidth += 1;
if (nodeWidth + letterWidth * otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i + 1 - letterWidth)
})
previousNode = i + 1 - letterWidth
nodeWidth = letterWidth * otherWidth
letterWidth = 0
}
} else {
if (nodeWidth + otherWidth > maxWidth) {
rowText.push({
type: "text",
content: text.substring(previousNode, i)
});
previousNode = i
nodeWidth = otherWidth
} else {
nodeWidth += otherWidth
}
}
}
}
if (previousNode < len) {
rowText.push({
type: "text",
content: text.substring(previousNode, len)
})
}
return rowText
}
//权限检测
export function requestPermissions(info) {
const permissionID = info.permissionID
console.log(info,permissionID)
return new Promise((rev, jec) => {
if (!permissionID) {
rev({
isSuc: false,
msg: "缺少permissionID"
})
return false
}
//判断安卓与ios设备
if (plus.os.name == 'Android') {
let _permissionID = 'android.permission.' + permissionID;
plus.android.checkPermission(_permissionID,
granted => {
console.log(granted)
if (granted.checkResult == 0) {
rev({
isSuc: true
})
}
if (granted.checkResult == -1) {
//还未授权当前查询的权限,打开权限申请目的自定义弹框
closeModal() // 先关闭再弹窗
showModal(info)
plus.android.requestPermissions([_permissionID],
(e) => {
console.log(e)
//关闭权限申请目的自定义弹框
if (e.granted.length > 0) {
closeModal()
//当前查询权限已授权,此时可以通知页面执行接下来的操作
rev({
isSuc: true
})
}
if (e.deniedPresent.length > 0) {
closeModal()
//当前查询权限已授权,此时可以通知页面执行接下来的操作
rev({
isSuc: false,
msg: "用户已拒绝"
})
}
if (e.deniedAlways.length > 0) {
//当前查询权限已被永久禁用,此时需要引导用户跳转手机系统设置去开启
uni.showModal({
title: '温馨提示',
content: '还没有该权限,立即去设置开启?',
cancelText: "取消",
confirmText: "去设置",
showCancel: true,
confirmColor: '#000',
cancelColor: '#666',
success: (res) => {
if (res.confirm) {
goSetting();
}
if (res.cancel) {
rev({
isSuc: false,
msg: "取消前往权限设置页面"
})
}
},
fail(e) {
rev({
isSuc: false,
msg: e.message || "弹窗失败"
})
},
complete() {
closeModal()
}
})
}
})
}
},
error => {
rev({
isSuc: false,
mes: error.message || '检查权限失败'
})
}
);
} else {
//IOS不需要添加自定义弹框来描述权限目的,因为在配置文件的隐私信息访问的许可描述里可添加
rev({
isSuc: true
})
}
})
}
//跳转手机系统设置
export function goSetting() {
if (plus.os.name == "iOS") {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("app-settings:");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
@@ -0,0 +1,21 @@
{
"id": "gary-check",
"name": "解决APP需要同步用户告知权限申请目的,导致安卓,华为,小米等上架被拒问题",
"displayName": "解决APP需要同步用户告知权限申请目的,导致安卓,华为,小米等上架被拒问题",
"version": "1.0.1",
"description": "超简单即可调用成功,JS单文件,解决APP需要同步用户告知权限申请目的,导致安卓,华为,小米等上架被拒问题",
"keywords": [
"华为,小米,上架,上架拒绝,拒绝,权限"
],
"dcloudext": {
"type": "js_sdk",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
}
}
}