Merge branch 'cyl/master-0804'

cyl/master-0819
wangxia 10 months ago
commit 5c53ec1f9f

@ -13,7 +13,7 @@ export default {
themeColor: "#00b666",
themeBackgroundColor: "#00b66621",
},
onLaunch() {
onLaunch() {
let that = this;
uni.removeStorageSync("selectedCity");
//

@ -0,0 +1,17 @@
let bindInfo = {
chat_create:"/yishoudan/common/user/job/match/getTalk",// 创建会话
chat_send:"/yishoudan/common/user/job/match/doTalk",// 处理会话
chat_ai_job:"/yishoudan/store/job/getShareJobs",// gpt返回的职位列表
chat_daotian_job:"/yishoudan/store/job/platform/list",// 稻田职位列表
job_detail_daotian:"/yishoudan/store/job/platform/detail",// 稻田职位详情
chat_config:'/yishoudan/common/user/job/match/getBotInfo',// 获取机器人配置
ai_config:'/robot/config/getRobotList',// 获取机器人列表
chat_getConversationld:'/chat/message/getConversationId',// 获取会话id
chat_getHistory:'/chat/message/getPageList',// 获取历史记录
chat_addHistory:'/chat/message/addChatMessage',// 新增聊天记录
chat_delMsg:'/chat/message/deleteMessage',// 删除聊天记录
}
export default bindInfo;

@ -6,6 +6,7 @@ import orderInfo from './order.js';
import personInfo from './person.js';
import wyyxInfo from './wyyx.js';
import commonInfo from './common.js';
import chatInfo from './chat.js';
let testInfo = {// 测试接口
testUrl:"/overall/store/job/list",// 来源:稻田 小程序首页列表
@ -19,4 +20,4 @@ let baseInfo = {// 公共接口
getConfig:"/yishoudan/weixin/config/getConfig",
}
export default Object.assign(testInfo,baseInfo,loginInfo,userInfo,bindInfo,jobInfo,orderInfo,personInfo,commonInfo,wyyxInfo);
export default Object.assign(testInfo,baseInfo,loginInfo,userInfo,bindInfo,jobInfo,orderInfo,personInfo,commonInfo,wyyxInfo,chatInfo);

@ -22,7 +22,8 @@ let jobInfo = {
hasSee: "/yishoudan/job/view/record/addRecord",
getCityNameByLatLng: '/location/getCityNameByLatLng' ,// 根据经纬度获取城市
get_city_list: '/city/getAllCityLevel2', // 获取城市列表
new_job_list: '/yishoudan/custom/job/listV2', // 新职位列表
dao_job_list: '/yishoudan/store/job/platform/list', // 稻田职位列表
}
export default jobInfo;

@ -17,7 +17,7 @@
<g-button btnText="去登录" size="small" class="g_mt_32" @clickBtn="goLogin" />
</view>
</view>
<scroll-view v-if="speed > 0" :style="{ height: `calc(100vh - ${navInfo.statusBarHeight + 52}px)` }" :scroll-y="true" @scrolltolower="reachBottom" :lower-threshold="100">
<scroll-view v-if="speed > 0" :style="{ height: `calc(100vh - ${99 + tabbarHeight}px)` }" :scroll-y="true" @scrolltolower="reachBottom" :lower-threshold="100">
<view class="" style="min-height: calc(100% - 90px)">
<view class="item g_pt_18 g_pl_10 g_pr_10 g_bg_f" hover-class="g_bg_f_5" v-for="(item, index) in query.list" :key="index" @click="goDetail(item, index)">
<view class="g_border_e_b g_flex_row_start g_pb_18">
@ -114,13 +114,17 @@ export default {
data() {
return {
cdnBaseImg: this.G.store().cdnBaseImg,
tabbarHeight: 0,
};
},
//
computed: {},
//
watch: {},
created() {},
created() {
this.tabbarHeight = uni.getStorageSync("TABBAR_HEIGHT");
console.log('this.tabbarHeight',this.tabbarHeight);
},
mounted() {},
//
methods: {

@ -8,6 +8,7 @@ import wyyx from './utils/wyyx.js';
import apiInfo from './api/index.js';
import gEmpty from './components/empty.vue';
import gTabbar from './components/customTabbar.vue';
import gLoading from './components/loading.vue';
import gButton from './components/button.vue';
import gListJob from './components/list/job.vue';
@ -33,6 +34,7 @@ export function createApp () {
app.config.productionTip = false;
app.component('g-empty', gEmpty);
app.component('g-tabbar', gTabbar);
app.component('g-loading', gLoading);
app.component('g-button', gButton);
app.component('g-list-job', gListJob);

268
package-lock.json generated

@ -15,11 +15,13 @@
"fetch-event-source": "^1.0.0-alpha.2",
"mobx": "^6.6.1",
"nim-web-sdk-ng": "^10.4.0",
"pinyin": "^3.1.0"
"pinyin": "^3.1.0",
"recorder-core": "^1.3.25011100"
},
"devDependencies": {
"file-saver": "^2.0.5",
"pako": "^2.1.0",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.86.0",
"sass-loader": "^16.0.5"
}
@ -756,11 +758,27 @@
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"optional": true,
"devOptional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.0.0.tgz",
@ -895,6 +913,41 @@
"node": ">=10"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz",
@ -976,6 +1029,16 @@
}
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1016,7 +1079,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"optional": true
"devOptional": true
},
"node_modules/error-ex": {
"version": "1.3.2",
@ -1068,6 +1131,16 @@
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
@ -1205,6 +1278,16 @@
"node": ">=10"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -1395,6 +1478,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
@ -1409,7 +1508,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"optional": true,
"devOptional": true,
"engines": {
"node": ">=8"
}
@ -1437,6 +1536,19 @@
"node": ">=0.12.0"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1694,6 +1806,24 @@
"wrappy": "1"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
@ -1828,6 +1958,21 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/recorder-core": {
"version": "1.3.25011100",
"resolved": "https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz",
"integrity": "sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
@ -1882,6 +2027,50 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "5.14.0",
"resolved": "https://registry.npmmirror.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
"dev": true,
"license": "MIT",
"dependencies": {
"open": "^8.4.0",
"picomatch": "^4.0.2",
"source-map": "^0.7.4",
"yargs": "^17.5.1"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"rolldown": "1.x",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rolldown": {
"optional": true
},
"rollup": {
"optional": true
}
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1995,6 +2184,16 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"optional": true
},
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2017,7 +2216,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"optional": true,
"devOptional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@ -2031,7 +2230,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"optional": true,
"devOptional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@ -2129,12 +2328,40 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"optional": true
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
@ -2149,6 +2376,35 @@
"engines": {
"node": ">= 6"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
}
}
}

@ -16,9 +16,11 @@
"fetch-event-source": "^1.0.0-alpha.2",
"mobx": "^6.6.1",
"nim-web-sdk-ng": "^10.4.0",
"pinyin": "^3.1.0"
"pinyin": "^3.1.0",
"recorder-core": "^1.3.25011100"
},
"devDependencies": {
"rollup-plugin-visualizer": "^5.14.0",
"file-saver": "^2.0.5",
"pako": "^2.1.0",
"sass": "^1.86.0",

File diff suppressed because it is too large Load Diff

@ -8,12 +8,12 @@
<!-- 列表区 -->
<!-- <div @click="showPop = true"> 1123 </div> -->
<!-- `calc(${navInfo.windowHeight}px - ${navInfo.navigationBarHeight + navInfo.statusBarHeight}px)` -->
<scroll-view class="m-list" id="listBox" :scroll-into-view="scrollTo" :style="{ height: `100vh ` }" :scroll-y="true" @scrolltolower="reachBottom" @scroll="checkScroll">
<scroll-view class="m-list" id="listBox" :scroll-into-view="scrollTo" :style="{ height: `calc(100vh - ${tabbarHeight}px)` }" :scroll-y="true" @scrolltolower="reachBottom" @scroll="checkScroll">
<div class="g_h_all" hover-class="none" hover-stop-propagation="false">
<div class="g_position_rela g_flex_row_between flex_center g_ml_10 g_mr_10">
<div class="the_city g_flex_column_center" style hover-class="thover" @click="goCity">
<div class="g_flex_row_center">
<div class="g_fs_16 g_fw_600 " style="min-width: 36px; display: inline-block">{{ selectedCity }}</div>
<div class="g_fs_16 g_fw_600" style="min-width: 36px; display: inline-block">{{ selectedCity }}</div>
<div class="g_flex_column_center">
<div class="iconfont icon-zhankai g_fs_12" style="display: inline-block"></div>
</div>
@ -26,10 +26,10 @@
</div>
<div hover-class="none" class="g_bg_main g_flex_column_center g_radius_50 g_w_40 g_h_40 g_text_c g_c_f g_fs_11 g_fw_600" style="" hover-stop-propagation="false" @click="toShare" v-if="isLogin && userInfo.agencyId">
<div class="g_mb_3" style="line-height: 1">分享</div>
<div class="" style="line-height: 1">职位</div>
<div class="" style="line-height: 1">职位</div>
</div>
</div>
<div style="min-height:100vh">
<div :style="{ minHeight: `100%` }">
<div class="sticky" v-if="isLogin">
<div class="g_flex_row_between flex_center" :class="toTop ? 'g_bg_f' : 'g_bg_f_5'" :style="{ 'box-shadow': toTop ? '0px 2px 3px 0px rgba(214, 214, 214, 0.3)' : '' }" id="tttop">
<div class="g_flex_1">
@ -102,7 +102,7 @@
</div>
</div>
</scroll-view>
<div hover-class="none" style="position: fixed; right: 20px; bottom: 20px; z-index: 0" hover-stop-propagation="false" @click="toRecord" v-if="isLogin && userInfo.agencyId">
<div hover-class="none" style="position: fixed; right: 20px; bottom: 120px; z-index: 0" hover-stop-propagation="false" @click="toRecord" v-if="isLogin && userInfo.agencyId">
<g-panel-image :url="cdnBaseImg + 'quick_application0610.svg'" size="96" />
</div>
@ -132,6 +132,7 @@
</div>
</u-popup>
</div>
<g-tabbar class="tabbar"></g-tabbar>
</div>
</template>
@ -147,6 +148,7 @@ export default {
};
// return this.G.shareFun();
},
onLoad() {
let that = this;
that.navInfo = that.G.getNavInfo();
@ -219,6 +221,20 @@ export default {
// this.searchAnimate();
// #endif
},
mounted() {
console.log("mountedmountedmountedmountedmountedmounted");
const query = uni.createSelectorQuery().in(this);
console.log("query", query);
// DOM
query
.select(".tabbar")
.boundingClientRect((res) => {
console.log("resresresrserserserser", res);
this.tabbarHeight = res.height;
uni.setStorageSync("TABBAR_HEIGHT", res.height);
})
.exec();
},
data() {
return {
scrollTo: "",
@ -227,6 +243,7 @@ export default {
userInfo: {},
whichOneShow: "",
toTop: false, //
tabbarHeight: 0,
getFilterData: {
sex: "-1",
ageRangeStr: "",

@ -0,0 +1,511 @@
<template>
<view class="p-bind-inedx g_w_all g_h_all g_bg_f_5 g_kuaishou">
<!-- 搜索栏 -->
<view class="m-top">
<view class="m-search g_p_10 g_pb_0 g_position_rela g_p_12 g_bg_f_5">
<u-search height="80" v-model="keyword" :placeholder="tabInfo.active == 0 ? '搜索已关注的发单号' : '搜索我的粉丝'" @btnSearch="getSearch" bg-color="#fff" :show-action="false" @change="getSearch" @clear="getSearch" @custom="getSearch" @search="getSearch" search-icon-color="#999999" :maxlength="20"></u-search>
</view>
</view>
<view class="m-panel">
<view class="center">
<!-- 列表区 -->
<view class="m-list">
<view class="link" :class="isShow ? '' : ''">
<view class="item g_flex_row_between g_pt_12 g_pb_12 g_pl_16 g_pr_16 g_bg_f" @click="goApply" style="border-bottom: 1rpx solid #eee" v-if="tabInfo.active == 1">
<view class="g_flex_1 g_flex_row_start">
<view class="g_flex_column_center g_pr_16 g_flex_none">
<view class="g_w_48 g_h_48 g_radius_8 g_flex_c" style="background-color: #ffeceb">
<i class="iconfont icon-xindeshenqing g_fs_22" style="color: #ff7b7b; font-size: 22px"></i>
</view>
</view>
<view class="g_flex_column_center g_flex_1">
<view class="g_ell_1 g_fs_18 g_c_3">{{ tabInfo.active == 0 ? "新的关注" : "新的粉丝" }}</view>
<view
class="g_ell_1 g_fs_14 g_c_9 g_mt_4"
v-if="tabInfo.active == 0"
:style="{
color: Number(waitNum_downs) == 0 ? '#fff' : '#999',
}"
>您有{{ waitNum_downs }}条信息等待对方通过</view
>
<view
class="g_ell_1 g_fs_14 g_c_9 g_mt_4"
v-else
:style="{
color: Number(waitNum_downs) == 0 ? '#fff' : '#999',
}"
>您有{{ waitNum_downs }}条新的粉丝申请</view
>
</view>
</view>
<view class="g_flex_none g_h_all g_flex_column_center g_pl_20 g_h_48" v-if="Number(waitNum_ups) > 0">
<view class="tip g_w_16 g_h_16 g_bg_f0a g_c_f g_flex_c g_fs_14 g_radius_50">{{ waitNum_ups }}</view>
</view>
</view>
<g-loading :loading="loading" v-if="loading && speed == -1" />
<view v-if="!loading && speed == 0 && inviteNum <= 0" class="g_pt_130" style="padding-top: 130px">
<g-empty :text="isLogin ? '暂无数据' : '请登录'" />
</view>
<view v-if="speed > 0 || inviteNum > 0">
<view v-if="inviteNum > 0" class="item g_flex_row_between flex_center g_pl_16 g_pr_16 g_pb_12 g_pt_12 g_bg_f g_border_e_b" @click="goInviteList">
<view class="g_flex_row_start flex_center">
<g-panel-image size="96" radius="6" />
<view class="g_ml_16">
<view class="g_ell_1 g_fs_18 g_c_0 g_flex_none" style="max-width: 460rpx">关注邀请</view>
<view class="g_mt_4 g_text_c">{{ inviteNum }}个发单号向你发出邀请</view>
</view>
</view>
<view class="g_dot_18 g_bg_f40 g_c_f g_text_c" v-if="showDot">{{ inviteNum }}</view>
</view>
<div class="g_h_10"></div>
<div class="r_box g_ml_10 g_mr_10" style="overflow: hidden">
<view class="item g_flex_row_between g_pl_16 g_pr_16 g_pb_6 g_pt_6 g_bg_f" hover-class="g_bg_e" v-for="(item, index) in query.list" :key="index" @click="goMain(item)" :class="index == 0 ? 'g_pt_12' : ''" :style="{ 'border-bottom': index == query.list.length - 1 ? '1rpx solid #fff' : '1rpx solid #eee' }">
<view class="g_flex_none g_flex_row_start">
<view class="g_flex_column_center g_pr_16 g_flex_none" style="position: relative">
<g-panel-image :url="item.logo || 'https://matripe-cms.oss-cn-beijing.aliyuncs.com/dailibaoming/qiyelogo0610.svg'" size="96" radius="50" />
<!-- <view v-if="item.supplierAccount == 1" style="position: absolute; left: 0; width: 48px; height: 48px" class="g_flex_row_end">
<view class="g_flex_c" style="position: absolute; overflow: hidden; width: 20px; height: 10px; right: -2px; top: 0px">
<image :src="cdnBaseImg + 'fadanhao.svg'" mode="widthFix" style="width: 16px; height: 13px"></image>
</view>
</view>-->
</view>
</view>
<view class="g_flex_1 g_h_all g_flex_column_center g_h_60 _right">
<view class="g_flex_row_between">
<view class="g_flex_column_center g_flex_1 g_mr_12">
<view class="g_flex_row_start flex_center">
<view class="g_ell_1 g_fs_18 g_c_0 g_flex_none" style="max-width: 100%">{{ item.fullName || item.agencyName || "-" }}</view>
<!-- <view class="g_ml_6 g_text_c g_flex_1">
<view class="g_w_36 g_h_18 g_fs_12 g_redius_4" style="background-color: #f2f7ff; color: #1677ff" v-if="item.teamType == 2"></view>
<view class="g_w_36 g_h_18 g_fs_12 g_redius_4" style="background-color: #fff5f5; color: #ff4d4f" v-if="item.teamType == 1"></view>
</view>-->
</view>
<view class="g_fs_14 g_c_9 g_mt_4">
<view class="g_flex_row_start">
<view class="g_flex_row_start">
<view>总职位</view>
<view class="g_mr_8">{{ item.jobNum ? item.jobNum : "0" }}</view>
</view>
<view class="g_flex_row_start">
<view>在招</view>
<view class="g_mr_8">{{ item.recruitmentJobNum ? item.recruitmentJobNum : "0" }}</view>
</view>
<view class="g_flex_row_start">
<view>粉丝</view>
<view class="g_mr_8">{{ item.downNum ? item.downNum : "0" }}</view>
</view>
</view>
</view>
</view>
<view class="g_flex_column_center g_mb_12" v-if="tabInfo.active == 1">
<view v-if="item.recordStatus == 1 && item.supplierAccount == 1 && item.recordStatus == 5">
<view class="g_radius_14 g_h_28 g_border_e g_fs_14 g_c_main g_pr_8 g_pl_8 g_flex_row_center flex_center" @click.stop="goApplyForm(item, index)" hover-class="g_bg_f_5" hover-stop-propagation>
<i class="iconfont icon-tianjia g_fsi_12 g_mr_6"></i>
等待通过
</view>
</view>
<view v-if="item.recordStatus == 1 && item.supplierAccount == 1 && item.recordStatus == 6">
<view class="g_radius_14 g_h_28 g_border_e g_fs_14 g_c_main g_pr_8 g_pl_8 g_flex_row_center flex_center" @click.stop="goApplyForm(item, index)" hover-class="g_bg_f_5" hover-stop-propagation>
<i class="iconfont icon-tianjia g_fsi_12 g_mr_6"></i>
已过期
</view>
</view>
<view v-if="item.recordStatus == 1 && item.supplierAccount == 1">
<view class="g_radius_14 g_h_28 g_border_e g_fs_14 g_c_main g_pr_8 g_pl_8 g_flex_row_center flex_center" @click.stop="goApplyForm(item, index)" hover-class="g_bg_f_5" hover-stop-propagation>
<i class="iconfont icon-tianjia g_fsi_12 g_mr_6"></i>
关注
</view>
</view>
<view v-if="item.recordStatus == 2">
<view class="g_radius_14 g_h_28 g_border_e g_fs_14 g_c_6 g_pr_8 g_pl_8 g_flex_row_center flex_center">
<i class="iconfont icon-a-bianzu11beifen2 g_fsi_12 g_mr_6"></i>
互相关注
</view>
</view>
</view>
</view>
</view>
</view>
</div>
</view>
</view>
</view>
<view v-if="isShow && speed > 0" class="g_bg_f_5">
<g-panel-hr :str="query.isFinish >= 0 && query.isFinish < query.size ? speed + '个发单号' : '加载中'" />
</view>
<!-- #ifdef H5 -->
<view style="height: 50px"></view>
<!-- #endif -->
</view>
</view>
<g-tabbar class="tabbar"></g-tabbar>
</view>
</template>
<script>
export default {
onReady() {
this.G.setNavStyle();
},
onShareAppMessage() {
return this.G.shareFun();
},
data() {
return {
cdnBaseImg: this.G.store().cdnBaseImg,
isLogin: null,
isShow: false,
loading: true,
showDot: true,
speed: -1,
query: {
page: 1,
size: 50,
list: [],
isFinish: -1,
},
keyword: "",
scrollTop: 0,
domHeight: 0,
waitNum_downs: 0,
waitNum_ups: 0,
cardTab: [
{
icon: "t-icon-xunzhaoshangyou",
title: "搜索发单号",
remark: "关注更多单子的更新",
},
{
icon: "t-icon-fazhanxiayou",
title: "发展代理",
remark: "让更多代理关注我",
},
],
tabInfo: {
list: [
{
name: "关注(0)",
num: 0,
tip: 0,
cate_count: 0,
},
{
name: "粉丝(0)",
num: 0,
tip: 1,
cate_count: 0,
},
],
active: 0,
},
tabFansInfo: {
list: [
{
name: "新的代理",
},
{
name: "全部粉丝",
},
],
active: 0,
},
inviteNum: 0, //
};
},
onLoad(options) {
let that = this;
// that.tabInfo.active = options.active
// if (that.tabInfo.active == 0) {
// uni.setNavigationBarTitle({
// title: "",
// });
// } else {
// if (that.tabInfo.active == 1) {
// uni.setNavigationBarTitle({
// title: "",
// });
// }
// }
const query = uni.createSelectorQuery().in(this);
query
.select(".m-top")
.boundingClientRect((data) => {
that.domHeight = data.height + 34;
})
.exec();
},
onShow() {
let that = this;
that.isLogin = uni.getStorageSync("apply-token");
if (uni.getStorageSync("watch_invite")) {
if (uni.getStorageSync("watch_invite") == 1) {
that.showDot = false; //
}
}
if (!that.isLogin) {
that.waitNum_downs = 0;
that.waitNum_ups = 0;
that.tabInfo.list[0].name = "关注(0)";
that.tabInfo.list[1].name = "粉丝(0)";
that.isShow = true;
that.speed = 0;
that.loading = false;
that.query.list = [];
} else {
that.query.page = 1;
that.getWaitNum();
that.getNum();
that.getList();
that.getInviteNum();
that.getPoint();
}
},
onReachBottom() {
let that = this;
if (that.query.isFinish == -1 || that.query.isFinish == that.query.size) {
that.query.page++;
that.getList("concat");
}
},
onPageScroll(e) {
this.scrollTop = e.scrollTop;
},
methods: {
handleUpdateFensTab(e) {},
getPoint() {
let that = this;
that.G.Get(
that.api.bind_getWaitNum + "/2",
{
type: 2,
},
(res) => {
that.tabInfo.list[1].cate_count = res.unread;
}
);
},
getWaitNum() {
let that = this;
that.G.Get(
that.api.bind_getWaitNum + "/" + (that.tabInfo.active * 1 + 1),
{
type: that.tabInfo.active * 1 + 1,
},
(res) => {
that.waitNum_downs = res.total;
that.waitNum_ups = res.unread;
console.log("获取指定数量:", res);
that.$forceUpdate();
}
);
},
getNum() {
let that = this;
that.G.Post(
that.api.bind_getListNum,
{
keys: that.keyword,
},
(res) => {
that.tabInfo.list[0].name = "关注(" + res.ups + ")";
that.tabInfo.list[1].name = "粉丝(" + res.downs + ")";
}
);
},
getInviteNum() {
let that = this;
that.G.Get(that.api.user_getInviteNum, {}, (res) => {
console.log(res);
that.inviteNum = res;
});
},
getList($type = "init") {
let that = this;
that.isShow = false;
that.G.Post(
that.api.bind_list,
{
pageNum: that.query.page,
pageSize: that.query.size,
keys: that.keyword,
platform: "pc",
type: that.tabInfo.active * 1 + 1,
formdata: true,
},
(res) => {
that.isShow = true;
that.speed = res.recordCount;
if (that.speed == 0) {
that.loading = false;
} else {
that.loading = true;
}
that.query.isFinish = res.recordList.length;
if ($type == "init") {
that.query.list = [];
that.query.list = res.recordList;
} else {
that.query.list = that.query.list.concat(res.recordList);
}
}
);
},
getSearch(e) {
if (e == "string") {
this.keyword = e;
}
this.G.isLogin();
if (this.G.isLogin()) {
this.speed = -1;
this.query.page = 1;
this.getList();
}
},
handleUpdateTab(e) {
this.G.isLogin();
if (this.G.isLogin()) {
this.tabInfo.active = e;
this.speed = -1;
this.query.page = 1;
this.getList();
this.getWaitNum();
this.getNum();
this.getPoint();
}
},
goMain($item) {
let that = this;
this.G.isLogin();
if (this.G.isLogin()) {
if ($item.supplierAccount == 1) {
let params = {
id: $item.agencyId,
type: that.tabInfo.active * 1 + 1,
bindid: $item.id,
isShowMore: false,
isShowJob: false,
shareForm: "apply",
};
if (that.tabInfo.active == 0) {
//
params.isShowMore = true;
params.isShowJob = true;
} else {
//
if ($item.recordStatus == 1) {
//
params.isShowMore = true;
params.isShowJob = false;
} else {
//
params.isShowMore = true;
params.isShowJob = true;
}
}
uni.navigateTo({
url: "/root/detail/userShare?" + that.G.objToStr(params),
});
} else {
//
uni.navigateTo({
url: "/root/bind/more?delta=1&id=" + $item.agencyId + "&himSee=" + $item.himSee + "&lookHim=" + $item.lookHim + "&type=" + 2 + "&hid=" + $item.id,
});
}
}
},
goApply() {
let that = this,
str = "";
this.G.isLogin();
if (this.G.isLogin()) {
uni.navigateTo({
url: "/root/bind/applyList?active=2",
});
}
},
goSearch($item, $index) {
let that = this;
that.G.isLogin();
if (that.G.isLogin()) {
uni.navigateTo({
url: "/root/bind/search?active=" + $index,
});
}
},
goApplyForm($item, $index) {
let that = this;
if ($item.recordStatus == 5 || $item.recordStatus == 6) {
} else {
uni.navigateTo({
url: "/root/bind/applyForm?code=" + $item.agencyId + "&form=1",
});
}
},
goInviteList() {
uni.navigateTo({
url: "/root/bind/inviteList?active=0",
});
},
},
};
</script>
<style lang="scss">
.p-bind-inedx {
.m-card {
padding-left: 10px;
padding-right: 10px;
.item {
width: calc(50% - 5px);
}
}
.showdoc {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04), 0 0 6px rgba(0, 0, 0, 0.02);
}
.suffix {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
z-index: 1;
border-top-right-radius: 110rpx;
border-bottom-right-radius: 110rpx;
}
.tab-fixed {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 44px;
background-color: #fff;
z-index: 1;
}
.m-search {
// .u-icon-wrap{
// position: absolute;
// left: 190rpx;
// }
}
.m-search-active {
.u-icon-wrap {
position: inherit;
left: 0rpx;
}
}
}
</style>

@ -4,12 +4,7 @@
<view class="m-tabs" v-if="isLogin" style="position: fixed; width: 100%; z-index: 999">
<u-tabs :list="typeGroup" :is-scroll="false" v-model="current" @change="updateType" item-width="126" :active-color="globalData.themeColor" bar-width="60" bar-height="6" font-size="32" :gutter="22" duration="0.1" itemWidth="auto" height="84"></u-tabs>
</view>
<scroll-view @scroll="getScrollInfo"
:scroll-top="resetScroll"
:style="{ height: `calc(100vh - ${isLogin ? tabbarHeight + 43 : 0}px)`, 'padding-top': isLogin ? '43px' : '' }"
:scroll-y="true"
v-if="isLogin"
>
<scroll-view @scroll="getScrollInfo" :scroll-top="resetScroll" :style="{ height: `calc(100vh - ${isLogin ? tabbarHeight + 43 : 0}px)`, 'padding-top': isLogin ? '43px' : '' }" :scroll-y="true" v-if="isLogin">
<view class="" v-show="current == 0">
<view class="g_bg_f">
<ConversationList ref="contactList" />
@ -20,14 +15,12 @@
</view>
</scroll-view>
<view class="g_h_all" v-if="!isLogin">
<view class="" hover-class="none" hover-stop-propagation="false" style="height: 108px;">
</view>
<view class="" hover-class="none" hover-stop-propagation="false" style="height: 108px"> </view>
<view class="">
<g-empty text="您还有没有登录,请登录后查看消息" />
<view class="g_h_32"></view>
<g-button btnText="去登录" size="small" class="g_mt_32" @clickBtn="goLogin" />
</view>
<g-empty text="您还有没有登录,请登录后查看消息" />
<view class="g_h_32"></view>
<g-button btnText="去登录" size="small" class="g_mt_32" @clickBtn="goLogin" />
</view>
</view>
</view>
<servicePopup
@ -101,8 +94,7 @@ const typeGroup = ref([
const current = ref(0);
const count = ref(0); //
const userInfo = ref({});
onShow(() => {
onLoad(() => {
isLogin.value = uni.getStorageSync("apply-token") ? true : false;
console.log("count.value", count.value);
corpUserFlag.value = uni.getStorageSync("apply-userinfo").corpUserFlag;
@ -150,6 +142,7 @@ onShow(() => {
current.value = 0;
}
});
onShow(() => {});
const goLogin = () => {
uni.reLaunch({
url: "/root/person/loginIndex",
@ -210,7 +203,6 @@ const updateType = (e) => {
</script>
<style lang="scss">
.i-items {
// border-bottom: 1px solid #eee;
position: relative;
@ -228,4 +220,4 @@ const updateType = (e) => {
// border-bottom: none;
// }
}
</style>
</style>

@ -4,14 +4,15 @@
<view class="m-info g_p_16 g_bg_f g_m_10 g_radius_8 g_position_rela u-skeleton" v-if="isLogin" hover-class="none" style="margin-top: 0">
<view class="g_flex_row_start">
<view class="g_w_71">
<view class="g_position_abso g_p_4 g_radius_13 g_bg_f u-skeleton-fillet" style="top: -20px">
<view class="u-skeleton-fillet g_w_71 g_h_71" v-if="loading"></view>
<view class="g_position_abso g_p_4 g_radius_13 g_bg_f" v-else style="top: -20px">
<g-panel-image :radius="13" :size="134" :url="userInfo.avatar" />
</view>
</view>
<view class="g_flex_1 g_ml_11">
<view class="g_flex_row_between flex_center">
<view class="g_flex_row_start flex_center" style="line-height: 22px">
<view class="g_flex_none g_ell_1 g_fs_20 g_fw_bold g_c_3 u-skeleton-fillet" style="max-width: 240rpx">
<view class="g_flex_none g_ell_1 g_fs_20 g_fw_bold g_c_3" style="max-width: 240rpx">
{{ userInfo.name ? userInfo.name : "-" }}
</view>
</view>
@ -53,7 +54,7 @@
</view>
<view class="g_mt_10">
<!-- v-if="userInfo.admin" -->
<view class="g_mt_10 g_position_rela" v-if="userInfo.agencyId">
<!-- <view class="g_mt_10 g_position_rela" v-if="userInfo.agencyId">
<div class="g_w_8 g_h_8 g_radius_50 g_bg_f0a g_position_abso" style="right: 18px; top: 10px" v-if="!readed"></div>
<g-panel-form-item
:list="[
@ -68,7 +69,7 @@
]"
@clickItem="goPage('/root/bind/order?active=0')"
/>
</view>
</view> -->
<view class="g_mt_10" v-if="userInfo.agencyId">
<g-panel-card-num :list="billDataList" titleNav="/root/person/applyIndex" :border="true" :speed="1" :marginBottom="16" cusType="num" :height="26" :num="5" cusTitle="报名工单(我报的)" @clickItem="goOrder" />
<!-- <g-panel-card-num :list="todayDataList"
@ -145,6 +146,7 @@
</view>
</scroll-view>
</u-popup>
<g-tabbar class="tabbar"></g-tabbar>
<u-skeleton :loading="loading" :animation="true" bgColor="#fff"></u-skeleton>
</view>
</template>

@ -10,7 +10,7 @@
</view>
<!-- #endif -->
<view class="g_h_all">
<view class="">
<!-- #ifdef APP-PLUS || H5 || MP-KUAISHOU || MP-WEIXIN -->
<view class="" style="height: calc(58px + 40px)"></view>
<!-- #endif -->
@ -35,6 +35,7 @@
<g-list-apply from="home" @uploadList="getList('concat')" bg="" class="" :tabActive="tabActive" :loading="loading" :speed="speed" :isLogin="isLogin" :query.sync="query" :navInfo="navInfo" />
</view>
</view>
<g-tabbar class="tabbar"></g-tabbar>
</view>
</template>

@ -1,5 +1,6 @@
$filter_bg_color: #e3ecfd;
$main_bg_color: #1890ff;
$page_bg_color: #ededed;
$main_color: #1890ff;
image {
vertical-align: top;
@ -359,7 +360,7 @@ scroll-view::-webkit-scrollbar {
&_09f {
color: #0091ff;
}
&_027 {
&_027 {
color: #027aff;
}
&_sub {
@ -429,6 +430,10 @@ scroll-view::-webkit-scrollbar {
&_trans_main {
background-color: $filter_bg_color;
}
&_page {
background-color: $page_bg_color;
min-height: 100vh;
}
}
&text {
//

@ -1,291 +1,265 @@
<template>
<div class="wrapper g_bg_f_5">
<!-- <NavBar :title="t('FriendPageText')" /> -->
<UserCard
:account="userInfo && userInfo.accountId"
:nick="userInfo && userInfo.name"
></UserCard>
<template v-if="relation === 'stranger'">
<div class="userInfo-item-wrapper">
<div class="userInfo-item">
<div class="item-left">{{ t('addBlacklist') }}</div>
<switch
:checked="isInBlacklist"
@change="(checked) => handleSwitchChange(checked)"
/>
</div>
</div>
<div class="wrapper bg_page">
<!-- <NavBar :title="t('FriendPageText')" /> -->
<UserCard :account="userInfo && userInfo.accountId" :nick="userInfo && userInfo.name"></UserCard>
<template v-if="relation === 'stranger'">
<div class="userInfo-item-wrapper">
<div class="userInfo-item">
<div class="item-left">{{ t("addBlacklist") }}</div>
<switch :checked="isInBlacklist" @change="(checked) => handleSwitchChange(checked)" />
</div>
</div>
<div class="button" :style="{ marginTop: '10px' }" @click="addFriend">
{{ t('addFriendText') }}
</div>
</template>
<template v-else>
<div class="userInfo-item-wrapper">
<div class="userInfo-item" @tap="handleAliasClick">
<div class="item-left">{{ t('remarkText') }}</div>
<div class="item-right">
<Icon iconClassName="more-icon" color="#999" type="icon-jiantou" />
</div>
</div>
<div class="userInfo-item">
<div class="item-left">{{ t('genderText') }}</div>
<div class="item-right">
{{
userInfo && userInfo.gender === 0
? t('unknow')
: userInfo && userInfo.gender === 1
? t('man')
: t('woman')
}}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t('birthText') }}</div>
<div class="item-right">
{{ (userInfo && userInfo.birthday) || t('unknow') }}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t('mobile') }}</div>
<div class="item-right">
{{ (userInfo && userInfo.mobile) || t('unknow') }}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t('email') }}</div>
<div class="item-right">
{{ (userInfo && userInfo.email) || t('unknow') }}
</div>
</div>
<div class="userInfo-item">
<div class="item-left">{{ t('sign') }}</div>
<div class="item-right">
{{ (userInfo && userInfo.sign) || t('unknow') }}
</div>
</div>
</div>
<div class="userInfo-item-wrapper">
<div class="userInfo-item">
<div class="item-left">{{ t('addBlacklist') }}</div>
<switch :checked="isInBlacklist" @change="handleSwitchChange" />
</div>
</div>
<div class="button" @click="gotoChat">{{ t('chatWithFriendText') }}</div>
<div class="box-shadow"></div>
<div class="button button-red" @click="deleteFriend">
{{ t('deleteFriendText') }}
</div>
</template>
</div>
<div class="button" :style="{ marginTop: '10px' }" @click="addFriend">
{{ t("addFriendText") }}
</div>
</template>
<template v-else>
<div class="userInfo-item-wrapper">
<div class="userInfo-item" @tap="handleAliasClick">
<div class="item-left">{{ t("remarkText") }}</div>
<div class="item-right">
<Icon iconClassName="more-icon" color="#999" type="icon-jiantou" />
</div>
</div>
<div class="userInfo-item">
<div class="item-left">{{ t("genderText") }}</div>
<div class="item-right">
{{ userInfo && userInfo.gender === 0 ? t("unknow") : userInfo && userInfo.gender === 1 ? t("man") : t("woman") }}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t("birthText") }}</div>
<div class="item-right">
{{ (userInfo && userInfo.birthday) || t("unknow") }}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t("mobile") }}</div>
<div class="item-right">
{{ (userInfo && userInfo.mobile) || t("unknow") }}
</div>
</div>
<div class="box-shadow"></div>
<div class="userInfo-item">
<div class="item-left">{{ t("email") }}</div>
<div class="item-right">
{{ (userInfo && userInfo.email) || t("unknow") }}
</div>
</div>
<div class="userInfo-item">
<div class="item-left">{{ t("sign") }}</div>
<div class="item-right">
{{ (userInfo && userInfo.sign) || t("unknow") }}
</div>
</div>
</div>
<div class="userInfo-item-wrapper">
<div class="userInfo-item">
<div class="item-left">{{ t("addBlacklist") }}</div>
<switch :checked="isInBlacklist" @change="handleSwitchChange" />
</div>
</div>
<div class="button" @click="gotoChat">{{ t("chatWithFriendText") }}</div>
<div class="box-shadow"></div>
<div class="button button-red" @click="deleteFriend">
{{ t("deleteFriendText") }}
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import UserCard from '../../../components/UserCard.vue'
import { onUnmounted, ref } from '../../../utils/transformVue'
import { t } from '../../../utils/i18n'
import NavBar from '../../../components/NavBar.vue'
import { autorun } from 'mobx'
import {
customRedirectTo,
customNavigateTo,
} from '../../../utils/customNavigate'
import { deepClone } from '../../../utils'
import type { Relation } from '@xkit-yx/im-store-v2'
import { V2NIMUser } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMUserService'
import { V2NIMConst } from 'nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK'
import Icon from '../../../components/Icon.vue'
import { onLoad } from "@dcloudio/uni-app";
import UserCard from "../../../components/UserCard.vue";
import { onUnmounted, ref } from "../../../utils/transformVue";
import { t } from "../../../utils/i18n";
import NavBar from "../../../components/NavBar.vue";
import { autorun } from "mobx";
import { customRedirectTo, customNavigateTo } from "../../../utils/customNavigate";
import { deepClone } from "../../../utils";
import type { Relation } from "@xkit-yx/im-store-v2";
import { V2NIMUser } from "nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK/V2NIMUserService";
import { V2NIMConst } from "nim-web-sdk-ng/dist/v2/NIM_UNIAPP_SDK";
import Icon from "../../../components/Icon.vue";
const userInfo = ref<V2NIMUser>()
const relation = ref<Relation>('stranger')
const isInBlacklist = ref(false)
const userInfo = ref<V2NIMUser>();
const relation = ref<Relation>("stranger");
const isInBlacklist = ref(false);
let account = ''
let account = "";
const handleAliasClick = () => {
customNavigateTo({
url: `/pages/Friend/friend-info-edit?id=${account}`,
})
}
let uninstallFriendWatch = () => {}
let uninstallRelationWatch = () => {}
customNavigateTo({
url: `/pages/Friend/friend-info-edit?id=${account}`,
});
};
let uninstallFriendWatch = () => {};
let uninstallRelationWatch = () => {};
onLoad((props) => {
account = props ? props.account : ''
uni.$UIKitStore.userStore.getUserForceActive(account)
uninstallFriendWatch = autorun(() => {
userInfo.value = deepClone(
uni.$UIKitStore.uiStore.getFriendWithUserNameCard(account)
)
})
account = props ? props.account : "";
uni.$UIKitStore.userStore.getUserForceActive(account);
uninstallFriendWatch = autorun(() => {
userInfo.value = deepClone(uni.$UIKitStore.uiStore.getFriendWithUserNameCard(account));
});
uninstallRelationWatch = autorun(() => {
const { relation: _relation, isInBlacklist: _isInBlacklist } =
uni.$UIKitStore.uiStore.getRelation(account)
relation.value = _relation
isInBlacklist.value = _isInBlacklist
})
})
uninstallRelationWatch = autorun(() => {
const { relation: _relation, isInBlacklist: _isInBlacklist } = uni.$UIKitStore.uiStore.getRelation(account);
relation.value = _relation;
isInBlacklist.value = _isInBlacklist;
});
});
const handleSwitchChange = async (e: any) => {
const isAdd = e.detail.value
try {
if (isAdd) {
await uni.$UIKitStore.relationStore.addUserToBlockListActive(account)
} else {
await uni.$UIKitStore.relationStore.removeUserFromBlockListActive(account)
}
} catch (error) {
uni.showToast({
title: isAdd ? t('setBlackFailText') : t('removeBlackFailText'),
icon: 'error',
})
}
}
const isAdd = e.detail.value;
try {
if (isAdd) {
await uni.$UIKitStore.relationStore.addUserToBlockListActive(account);
} else {
await uni.$UIKitStore.relationStore.removeUserFromBlockListActive(account);
}
} catch (error) {
uni.showToast({
title: isAdd ? t("setBlackFailText") : t("removeBlackFailText"),
icon: "error",
});
}
};
const deleteFriend = () => {
uni.showModal({
title: t('deleteFriendText'),
content:
t('deleteFriendConfirmText') +
'“' +
uni.$UIKitStore.uiStore.getAppellation({ account }) +
'”?',
success: (res) => {
if (res.confirm) {
uni.$UIKitStore.friendStore
.deleteFriendActive(account)
.then(() => {
uni.showToast({
title: t('deleteFriendSuccessText'),
icon: 'success',
})
})
.then(() => {
const conversationId =
uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(account)
return uni.$UIKitStore.conversationStore.deleteConversationActive(
conversationId
)
})
.catch(() => {
uni.showToast({
title: t('deleteFriendFailText'),
icon: 'error',
})
})
} else if (res.cancel) {
console.log('用户点击取消')
}
},
})
}
uni.showModal({
title: t("deleteFriendText"),
content: t("deleteFriendConfirmText") + "“" + uni.$UIKitStore.uiStore.getAppellation({ account }) + "”?",
success: (res) => {
if (res.confirm) {
uni.$UIKitStore.friendStore
.deleteFriendActive(account)
.then(() => {
uni.showToast({
title: t("deleteFriendSuccessText"),
icon: "success",
});
})
.then(() => {
const conversationId = uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(account);
return uni.$UIKitStore.conversationStore.deleteConversationActive(conversationId);
})
.catch(() => {
uni.showToast({
title: t("deleteFriendFailText"),
icon: "error",
});
});
} else if (res.cancel) {
console.log("用户点击取消");
}
},
});
};
const addFriend = async () => {
try {
await uni.$UIKitStore.friendStore.addFriendActive(account, {
addMode: V2NIMConst.V2NIMFriendAddMode.V2NIM_FRIEND_MODE_TYPE_APPLY,
postscript: '',
})
try {
await uni.$UIKitStore.friendStore.addFriendActive(account, {
addMode: V2NIMConst.V2NIMFriendAddMode.V2NIM_FRIEND_MODE_TYPE_APPLY,
postscript: "",
});
//
await uni.$UIKitStore.relationStore.removeUserFromBlockListActive(account)
uni.showToast({
title: t('applyFriendSuccessText'),
icon: 'success',
})
} catch (error) {
uni.showToast({
title: t('applyFriendFailText'),
icon: 'error',
})
}
}
//
await uni.$UIKitStore.relationStore.removeUserFromBlockListActive(account);
uni.showToast({
title: t("applyFriendSuccessText"),
icon: "success",
});
} catch (error) {
uni.showToast({
title: t("applyFriendFailText"),
icon: "error",
});
}
};
const gotoChat = async () => {
const conversationId =
uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(
userInfo.value?.accountId || ''
)
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
customRedirectTo({
url: '/pages/Chat/index',
})
}
const conversationId = uni.$UIKitNIM.V2NIMConversationIdUtil.p2pConversationId(userInfo.value?.accountId || "");
await uni.$UIKitStore.uiStore.selectConversation(conversationId);
customRedirectTo({
url: "/pages/Chat/index",
});
};
onUnmounted(() => {
uninstallFriendWatch()
uninstallRelationWatch()
})
uninstallFriendWatch();
uninstallRelationWatch();
});
</script>
<style lang="scss" scoped>
.bg_page {
background-color: #ededed;
min-height: 100vh;
}
page {
padding-top: var(--status-bar-height);
height: 100vh;
overflow: hidden;
padding-top: var(--status-bar-height);
height: 100vh;
overflow: hidden;
}
.wrapper {
// background-color: rgb(245, 246, 247);
height: 100vh;
box-sizing: border-box;
padding-bottom: 50px;
// background-color: rgb(245, 246, 247);
height: 100vh;
box-sizing: border-box;
padding-bottom: 50px;
.userInfo-item-wrapper {
background-color: #fff;
margin: 10px 0;
.userInfo-item-wrapper {
background-color: #fff;
margin: 10px 0;
.userInfo-item {
display: flex;
height: 50px;
align-items: center;
justify-content: space-between;
padding: 0 16px;
.userInfo-item {
display: flex;
height: 50px;
align-items: center;
justify-content: space-between;
padding: 0 16px;
.item-left {
font-size: 16px;
}
.item-left {
font-size: 16px;
}
.item-right {
font-size: 15px;
width: 200px;
text-align: right;
color: #a6adb6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.item-right {
font-size: 15px;
width: 200px;
text-align: right;
color: #a6adb6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.button {
// margin-top: 150px;
display: block;
width: 100%;
background-color: #fff;
color: #1890ff;
text-align: center;
height: 50px;
font-size: 16px;
line-height: 50px;
}
.button {
// margin-top: 150px;
display: block;
width: 100%;
background-color: #fff;
color: #1890ff;
text-align: center;
height: 50px;
font-size: 16px;
line-height: 50px;
}
.button-red {
color: #e6605c;
}
.button-red {
color: #e6605c;
}
.box-shadow {
height: 1px;
background: none;
padding: 0 25px;
box-shadow: 0 0.5px 0 rgb(247, 244, 244);
}
.box-shadow {
height: 1px;
background: none;
padding: 0 25px;
box-shadow: 0 0.5px 0 rgb(247, 244, 244);
}
}
</style>

File diff suppressed because one or more lines are too long

@ -0,0 +1,19 @@
## 1.0.2503312025-03-31
增加RecordApp.UniNativeUtsPlugin_OnJsCall接口App端搭配原生插件使用时可绑定接收配套原生录音插件事件原生插件新增PcmPlayer播放器支持流式播放、完整播放App端边录音边播放更流畅
## 1.0.2501112025-01-11
修复vue3 Fragments(multi-root 多个根节点)的兼容性问题修复uniapp Android自带的XXPermissions库在后台无法请求权限的问题仅限搭配原生录音插件可用
## 1.0.2410202024-10-20
适配HBuilder4.28 vue3 setup编译环境下$root.$scope无法读取的bugHBuilder4.29已修复此编译bug但似乎还是有不能使用的问题。如果setup内不能使用可尝试新建个vue组件然后使用选项式api来调用录音功能页面的setup内使用此vue组件
## 1.0.2409102024-09-10
- 新增RecordApp.UniMainCallBack_Register接口允许App renderjs层多次回调数据给逻辑层
- iOS App请求权限时会预先检查NSMicrophoneUsageDescription是否声明避免无声明时调用录音会崩溃
- 新增appNativePlugin_sampleRate原生插件录音选项
- Android App已提供后台录音保活功能启用后App在后台或锁屏后可继续正常录音
## 1.0.2406252024-06-25
调整UniWebViewCallAsync调用失败时返回更详细信息。android_audioSource默认值由1改成0新增ios_categoryOptions原生插件录音选项
## 1.0.2404092024-04-09
增加功能调用完善demo项目
## 1.0.2312082023-12-08
完善文档增加asr语音识别示例
## 1.0.2312012023-12-04
第一次发布

@ -0,0 +1,6 @@
<template>
<view>
<view style="font-weight: bold;">Recorder-UniCore Vue Component</view>
<view style="font-size:14px; color:#f60">无需手动显示本UI组件只需在script中正常引入 RecordApp + app-uni-support.js 即可实现 H5iOS Android App微信小程序 多端录音</view>
</view>
</template>

@ -0,0 +1,19 @@
《许可及服务协议》
**您以下称“用户”下载、使用我以下称“作者”提供的Recorder-UniCore组件含原生录音插件、uts插件以下统称“本组件”应当阅读并遵守本许可协议。请用户务必审慎阅读、充分理解各条款内容特别是免除或者限制责任的条款并选择接受或不接受。除非用户已阅读并接受本协议所有条款否则用户无权下载、使用本组件及相关服务用户的下载、使用等行为即视为用户已阅读并同意本许可协议的约束。**
1. 用户应当直接从作者许可的途径如作者的GitHub、Gitee仓库、已上架的DCloud插件市场、QQ群等途径中获取本组件其他途径获取到的组件代码是未经过作者授权的存在安全隐患可能会导致你的程序、资产受到侵害作者对因此给用户造成的损失不予负责。
2. 作者将积极并采取措施保护用户的信息和隐私;组件本身不会搜集存储任何用户信息。
3. 除法律法规有明确规定外,作者将尽最大努力确保本组件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解作者不能对此进行担保。
4. 用户理解,对于不可抗力及第三方原因导致的您的直接或间接损失,作者无法承担责任。
5. 用户因使用本组件进行生成、处理数据,由此引起或与有关的包括但不限于利润损失、资料损失、业务中断的损害赔偿或其它商业损害赔偿或损失,需由用户自行承担。
6. 如若发生赔偿、退款等行为,赔偿、退款等累计金额不得超过用户实际支付给作者的总金额。
7. 已授予的授权许可包括免费授权和已购买的原生录音插件、uts插件均仅限在授权指定的uni-app的应用标识AppID对应的项目上使用不可在其他项目上使用用户不得对本组件及其中的相关信息擅自出租、出借、销售、逆向工程、破解不得在未取得作者授权的情况下借助本组件发展与本组件有关联的衍生软件产品、服务、插件、外挂等。
8. 用户不得使用本组件从事违反法律法规政策、破坏公序良俗、损害公共利益的行为。

@ -0,0 +1,88 @@
{
"id": "Recorder-UniCore",
"displayName": "跨平台Recorder录音插件支持多种格式、音频可视化、实时上传、语音识别",
"version": "1.0.250331",
"description": "支持H5、Android iOS App、微信小程序mp3 wav pcm g711a g711u ogg amr 录音格式;实时帧回调处理 音频转码 波形动画显示 ASR语音转文字 无录制时长限制",
"keywords": [
"Recorder-UniCore",
"recorder-core",
"RecordApp",
"record",
"recording"
],
"repository": "https://github.com/xiangyuecn/Recorder",
"engines": {
"HBuilderX": "^3.6.11"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "753610399"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "录音权限"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "n",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,425 @@
**
**
# Recorder-UniCore组件uni-app内使用RecordApp录音
本组件使用`Recorder`开源库来进行录音和音频数据处理,使用`RecordApp`和本组件内的`app-uni-support.js`来适配到不同平台环境下进行录音。
- 支持vue2、vue3、nvue
- 支持编译成H5、Android App、iOS App、微信小程序
- 支持已有的大部分录音格式mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;**注意:不支持通话时录音**
- 支持PCM音频流式播放、完整播放App端用原生插件边录音边播放更流畅
- 支持离线使用,本组件和配套原生插件均不依赖网络
- App端有配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可供搭配使用,兼容性和体验更好
**详细文档(含Demo项目)** [https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp](https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)
**Recorder开源库地址** [https://github.com/xiangyuecn/Recorder](https://github.com/xiangyuecn/Recorder)
如果github打不开可以[点此访问Gitee仓库地址](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp) 。
**
## 测试方法
**示例项目如果在HBuilder中编译失败请删掉node_modules目录重新手动执行npm install偶尔出现HBuilder自动创建项目依赖包不完整导致无法编译**
1. 在本插件市场页面右侧下载或导入示例项目或打开上面详细文档链接中的Demo源码
2. 在测试项目根目录执行 `npm install --registry=https://registry.npmmirror.com/` ,完成`recorder-core`依赖的安装
3. 在HBuilder中打开本测试项目文件夹
4. 在HBuilder中运行到浏览器、手机、微信小程序即可在不同环境下测试
5. 测试中提供了基础录音、播放、上传、WebSocket实时语音通话对讲、ASR语音识别等功能
**
**
# 集成到自己项目中
你可以直接参考上面的测试示例项目源码,里面的`main_recTest.vue`更容易入门示例项目中已经实现了很多功能简单使用可直接照抄Demo代码到你的项目中。
## 一、引入js文件
1. 在你的项目根目录安装`recorder-core``npm install recorder-core --registry=https://registry.npmmirror.com/`
2. 导入Recorder-UniCore组件插件市场下载本组件然后添加到你的项目中 `/uni_modules/Recorder-UniCore`
3. 项目配置好录音权限,参考下面的录音权限配置章节,**特别注意App后台录音配置、小程序权限声明**
4. 在需要录音的vue文件script内编写以下代码按需引入需要的js
``` html
<template>
<view>
... 建议template下只有一个根节点最外面套一层view如果不小心踩到了vue3的Fragments(multi-root 多个根节点)特性vue2编译会报错vue3不会可能会出现奇奇怪怪的兼容性问题
</view>
</template>
<script> /****/
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行
import Recorder from 'recorder-core' //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js
import RecordApp from 'recorder-core/src/app-support/app'
//所有平台必须引入的uni-app支持文件如果编译出现路径错误请把@换成 ../../ 这种)
<!-- import '@/uni_modules/Recorder-UniCore/app-uni-support.js' -->
/** 需要编译成微信小程序时,引入微信小程序支持文件 **/
// #ifdef MP-WEIXIN
import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
// #endif
/** H5、小程序环境中引入需要的格式编码器、可视化插件App环境中在renderjs中引入 **/
// 注意如果App中需要在逻辑层中调用Recorder的编码/转码功能,需要去掉此条件编译,否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js的话必须要加上
//可选的插件支持项,把需要的插件按需引入进来即可
import 'recorder-core/src/extensions/waveview'
// #endif
// ... 这后面写页面代码用选项式API风格vue2、vue3、setup组合式API风格仅vue3都可以
</script>
```
5. 编译成app时默认需要额外提供一个renderjs模块请照抄下面这段代码放到vue文件末尾
``` html
<!-- #ifdef APP -->
<script module="yourModuleName" lang="renderjs"> //APIvue2vue3setupAPIimport vue
/**需要编译成App时你需要添加一个renderjs模块然后一模一样的import上面那些js微信的js除外
因为App中默认是在renderjsWebView中进行录音和音频编码
。如果配置了 RecordApp.UniWithoutAppRenderjs=true 且未调用依赖renderjs的功能时如nvue、可视化、仅H5中可用的插件
可不提供此renderjs模块同时逻辑层中需要将相关import的条件编译去掉**/
import 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
// import '../../uni_modules/Recorder-UniCore/app-uni-support.js' //renderjs中似乎不支持"@/"打头的路径,如果编译路径错误请改正路径即可
//按需引入你需要的录音格式支持文件,和插件
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import 'recorder-core/src/extensions/waveview'
export default {
mounted(){
//App的renderjs必须调用的函数传入当前模块this
RecordApp.UniRenderjsRegister(this);
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用二进制数据需转成base64来传递
}
}
</script>
<!-- #endif -->
```
**
**
## 二、调用录音
``` javascript
/**在逻辑层中编写**/
//import ... 上面那些import代码
//var vue3This=getCurrentInstance().proxy; //当用vue3 setup组合式 API (Composition API) 编写时直接在import后面取到当前实例this在需要this的地方传vue3This变量即可其他的和选项式 API (Options API) 没有任何区别import {getCurrentInstance} from 'vue'详细可以参考Demo项目中的 page_vue3____composition_api.vue
//RecordApp.UniNativeUtsPlugin={ nativePlugin:true }; //App中启用配套的原生录音插件支持配置后会使用原生插件进行录音没有原生插件时依旧使用renderjs H5录音
//App中提升后台录音的稳定性配置了原生插件后可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码WebWorker加速变成逻辑层中直接编码但会降低逻辑层性能后台运行时可避免部分手机WebView运行受限的影响
//App中提升后台录音的稳定性需要启用后台录音保活服务iOS不需要参考录音权限配置Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音也受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
export default {
data() { return {} } //视图没有引用到的变量无需放data里直接this.xxx使用
,mounted() {
this.isMounted=true;
//页面onShow时【必须调用】的函数传入当前组件this
RecordApp.UniPageOnShow(this);
}
,onShow(){ //onShow可能比mounted先执行页面可能还未准备好
if(this.isMounted) RecordApp.UniPageOnShow(this);
}
,methods:{
//请求录音权限
recReq(){
//编译成App时提供的授权许可编译成H5、小程序为免费授权可不填写如果未填写授权许可将会在App打开后第一次调用请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框
//RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';
//RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置在h5H5、App+renderjs中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.RequestPermission(()=>{
console.log("已获得录音权限,可以开始录音了");
},(msg,isUserNotAllow)=>{
if(isUserNotAllow){//用户拒绝了录音权限
//这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
}
console.error("请求录音权限失败:"+msg);
});
}
//开始录音
,recStart(){
//Android App如果要后台录音需要启用后台录音保活服务iOS不需要需使用配套原生插件、或使用第三方保活插件
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ title:"正在录音" ,content:"正在录音中请勿关闭App运行" }).then(()=>{...}).catch((e)=>{...}) 注意必须RecordApp.RequestPermission得到权限后调用
//录音配置信息
var set={
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除并不一定会生效打开后声音可能会从听筒播放部分环境下如小程序、App原生插件可调用接口切换成扬声器外放
//注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
//全平台通用可实时上传发送数据配合Recorder.SampleData方法将buffers中的新数据连续的转换成pcm上传或使用mock方法将新数据连续的转码成其他格式上传可以参考Recorder文档里面的Demo片段列表 -> 实时转码并上传-通用版基于本功能可以做到实时转发数据、实时保存数据、实时语音识别ASR
//注意App里面是在renderjs中进行实际的音频格式编码操作此处的buffers数据是renderjs实时转发过来的修改此处的buffers数据不会改变renderjs中buffers所以不会改变生成的音频文件可在onProcess_renderjs中进行修改操作就没有此问题了如需清理buffers内存此处和onProcess_renderjs中均需要进行清理H5、小程序中无此限制
//注意如果你要用只支持在浏览器中使用的Recorder扩展插件App里面请在renderjs中引入此扩展插件然后在onProcess_renderjs中调用这个插件H5可直接在这里进行调用小程序不支持这类插件如果调用插件的逻辑比较复杂建议封装成js文件这样逻辑层、renderjs中直接import不需要重复编写
//H5、小程序等可视化图形绘制直接运行在逻辑层App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
// #endif
/*实时释放清理内存用于支持长时间录音在指定了有效的type时编码器内部可能还会有其他缓冲必须同时提供takeoffEncodeChunk才能清理内存否则type需要提供unknown格式来阻止编码器内部缓冲App的onProcess_renderjs中需要进行相同操作
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}
,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中在这里修改buffers会改变生成的音频文件但注意buffers会先转发到逻辑层onProcess后才会调用本方法因此在逻辑层的onProcess中需要重新修改一遍
//本方法可以返回truerenderjs中的onProcess将开启异步模式处理完后调用asyncEnd结束异步注意这里异步修改的buffers一样的不会在逻辑层的onProcess中生效
//App中是在renderjs中进行的可视化图形绘制因此需要写在这里this是renderjs模块的this也可以用This变量如果代码比较复杂请直接在renderjs的methods里面放个方法xxxFunc这里直接使用this.xxxFunc(args)进行调用
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
/*和onProcess中一样进行释放清理内存用于支持长时间录音
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}`
,onProcessBefore_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx){
//App中本方法会在逻辑层onProcess之前调用因此修改的buffers会转发给逻辑层onProcess本方法没有asyncEnd参数不支持异步处理
//一般无需提供本方法只用onProcess_renderjs就行renderjs的onProcess内部调用过程onProcessBefore_renderjs -> 转发给逻辑层onProcess -> onProcess_renderjs
}`
,takeoffEncodeChunk:true?null:(chunkBytes)=>{
//全平台通用实时接收到编码器编码出来的音频片段数据chunkBytes是Uint8Array二进制数据可以实时上传发送出去
//App中如果未配置RecordApp.UniWithoutAppRenderjs时建议提供此回调因为录音结束后会将整个录音文件从renderjs传回逻辑层由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了大点的文件传输会比较慢提供此回调后可避免Stop时产生超大数据回传
//App中使用原生插件时可方便的将数据实时保存到同一文件第一帧时append:false新建文件后面的append:true追加到文件
//RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
}
,takeoffEncodeChunk_renderjs:true?null:`function(chunkBytes){
//App中这里可以做一些仅在renderjs中才生效的事情不提供也行this是renderjs模块的this也可以用This变量
}`
,start_renderjs:`function(){
//App中可以放一个函数在Start成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情比如初始化不提供也行
}`
,stop_renderjs:`function(arrayBuffer,duration,mime){
//App中可以放一个函数在Stop成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情不提供也行
}`
};
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.Start(set,()=>{
console.log("已开始录音");
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
//var wdt=this.watchDogTimer=setInterval ... 请参考示例Demo的main_recTest.vue中的watchDogTimer实现
//创建音频可视化图形绘制App环境下是在renderjs中绘制H5、小程序等是在逻辑层中绘制因此需要提供两段相同的代码
//view里面放一个canvascanvas需要指定宽高下面style里指定了300*100
//<canvas type="2d" class="recwave-WaveView" style="width:300px;height:100px"></canvas>
RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
`,(canvas1)=>{
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
});
},(msg)=>{
console.error("开始录音失败:"+msg);
});
}
//暂停录音
,recPause(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Pause();
console.log("已暂停");
}
}
//继续录音
,recResume(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Resume();
console.log("继续录音中...");
}
}
//停止录音
,recStop(){
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ close:true }) //关闭Android App后台录音保活服务
RecordApp.Stop((arrayBuffer,duration,mime)=>{
//全平台通用arrayBuffer是音频文件二进制数据可以保存成文件或者发送给服务器
//App中如果在Start参数中提供了stop_renderjsrenderjs中的函数会比这个函数先执行
//注意当Start时提供了takeoffEncodeChunk后你需要自行实时保存录音文件数据因此Stop时返回的arrayBuffer的长度将为0字节
//如果是H5环境也可以直接构造成Blob/File文件对象和Recorder使用一致
// #ifdef H5
var blob=new Blob([arrayBuffer],{type:mime});
console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
var file=new File([arrayBuffer],"recorder.mp3");
//uni.uploadFile({file:file, ...}) //参考demo中的test_upload_saveFile.vue
// #endif
//如果是App、小程序环境可以直接保存到本地文件然后调用相关网络接口上传
// #ifdef APP || MP-WEIXIN
RecordApp.UniSaveLocalFile("recorder.mp3",arrayBuffer,(savePath)=>{
console.log(savePath); //app保存的文件夹为`plus.io.PUBLIC_DOWNLOADS`,小程序为 `wx.env.USER_DATA_PATH` 路径
//uni.uploadFile({filePath:savePath, ...}) //参考demo中的test_upload_saveFile.vue
},(errMsg)=>{ console.error(errMsg) });
// #endif
},(msg)=>{
console.error("结束录音失败:"+msg);
});
}
}
}
```
**
**
**
**
# 录音权限配置、需要注意的细节
## 编译成H5时录音和权限
编译成H5时录音功能由Recorder H5提供无需额外处理录音权限。
**
## 编译成微信小程序时录音和权限
编译成微信小程序时,录音功能由小程序的`RecorderManager`提供,屏蔽了微信原有的底层细节(无录音时长限制)。
小程序录音需要用户授予录音权限,调用`RecordApp.RequestPermission`的时候会检查是否能正常录音,如果用户拒绝了录音权限,会进入错误回调,回调里面你应当编写代码检查`wx.getSetting`中的`scope.record`录音权限,然后引导用户进行授权(可调用`wx.openSetting`打开设置页面,方便用户给权限)。
**注意:上架小程序需要到小程序管理后台《[用户隐私保护指引](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html)》中声明录音权限,否则正式版将无法调用录音功能(请求权限时会直接走错误回调)。**
更多细节请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 测试项目文档。
**
## 编译成App时录音和权限
编译成App录音时分两种情况
1. 默认未配置`RecordApp.UniNativeUtsPlugin`未使用原生录音插件和uts插件会在renderjs中使用Recorder H5进行录音录音数据会实时回传到逻辑层。
2. 配置了`RecordApp.UniNativeUtsPlugin`使用原生录音插件或uts插件时会直接调用原生插件进行录音录音数据默认会传递到renderjs中进行音频编码处理WebWorker加速然后再实时回传到逻辑层如果配置了`RecordApp.UniWithoutAppRenderjs=true`时,音频编码处理将会在逻辑层中直接处理。
当App是在renderjs中使用H5进行录音时未使用原生录音插件和uts插件iOS上只支持14.3以上版本,**且iOS上每次进入页面后第一次请求录音权限时、或长时间无操作再请求录音权限时WebView均会弹出录音权限对话框**不同旧iOS版本低于iOS17下H5录音可能存在的问题在App中同样会存在使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)或uts插件时无以上问题和版本限制uts插件开发中暂不可用Android也无以上问题。
当音频编码是在renderjs中进行处理时录音结束后会将整个录音文件传回逻辑层由于uni-app的逻辑层和renderjs层大点的文件传输会比较慢**建议Start时使用takeoffEncodeChunk实时获取音频文件数据可避免Stop时产生超大数据回传**;配置了`RecordApp.UniWithoutAppRenderjs=true`后,因为音频编码直接是在逻辑层中进行,将不存在传输性能损耗,但会影响逻辑层的性能(正常情况轻微不明显),需要配套使用原生录音插件才可以进行此项配置。
在调用`RecordApp.RequestPermission`的时候,`Recorder-UniCore`组件会自动处理好App的系统录音权限只需要在uni-app项目的 `manifest.json` 中配置好Android和iOS的录音权限声明。
```
//Android需要勾选的权限第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音需要启用后台录音保活服务Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音、原生插件录音均受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置不然App切到后台后立马会停止录音
```
**
## PCM音频流式播放、语音通话、回声消除、声音外放
在App、H5中均可使用H5版的[BufferStreamPlayer](https://gitee.com/xiangyuecn/Recorder/blob/master/src/extensions/buffer_stream.player.js)来实时流式播放语音其中App中需要在renderjs中加载BufferStreamPlayer在逻辑层中调用`RecordApp.UniWebViewVueCall`等方法将逻辑层中接收到的实时语音数据发送到renderjs中播放播放声音的同时进行录音声音可能会被录进去产生回声因此一般需要打开回声消除调用代码参考demo中的[test_realtime_voice.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_realtime_voice.vue)。
App中如果搭配使用了配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可以调用原生实现的PcmPlayer播放器实时流式播放PCM音频边录音边播放更流畅同时也支持完整播放比如AI语音合成的播放调用代码参考demo中的[test_player_nativePlugin_pcmPlayer.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_player_nativePlugin_pcmPlayer.vue)。
微信小程序请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 文档里面的同名章节使用WebAudioContext播放。
配置audioTrackSet可尝试打开回声消除或者切换听筒播放或外放打开回声消除时一般会转为听筒播放显著降低回声。
``` js
//打开回声消除
RecordApp.Start({
... 更多配置参数请参考RecordApp文档
//此配置App、H5、小程序均可打开回声消除注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
,audioTrackSet:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}
//Android指定麦克风源App搭配原生插件、小程序可用0 DEFAULT 默认音频源1 MIC 主麦克风5 CAMCORDER 相机方向的麦6 VOICE_RECOGNITION 语音识别7 VOICE_COMMUNICATION 语音通信(带回声消除)
,android_audioSource:7 //提供此配置时优先级比audioTrackSet更高默认值为0
//iOS的AVAudioSession setCategory的withOptions参数值App搭配原生插件可用取值请参考配套原生插件文档中的iosSetDefault_categoryOptions
//,ios_categoryOptions:0x1|0x4 //默认值为5(0x1|0x4)
});
//App搭配原生插件时尝试切换听筒播放或外放
await RecordApp.UniNativeUtsPluginCallAsync("setSpeakerOff",{off:true或false});
//小程序尝试切换
wx.setInnerAudioOption({ speakerOn:false或true })
//H5不支持切换
```
**
**
**
# 详细文档、RecordApp方法、属性文档
请先阅读 [demo_UniApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)含Demo项目更高级使用还需深入阅读 [Recorder文档](https://gitee.com/xiangyuecn/Recorder)、[RecordApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample) 均为完整的一个README.md文档Recorder文档中包含了更丰富的示例代码基础录音、实时处理、格式转码、音频分析、音频混音、音频生成 等等大部分能在uniapp中直接使用。
**
**
**
# 本组件的授权许可限制
**本组件内的app-uni-support.js文件在uni-app中编译到App平台时仅供测试用App平台包括Android App、iOS App不可用于正式发布或商用正式发布或商用需先到DCloud插件市场购买[此带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)费用为¥199元赠送Android版原生插件即可获得授权许可**编译到其他平台时无此授权限制比如H5、小程序均为免费授权。
在App中如果未获得授权许可将会在App打开后第一次调用`RecordApp.RequestPermission`请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框。
在DCloud插件市场购买了[带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)获得了授权后,请在调用`RecordApp.RequestPermission`请求录音权限前,赋值`RecordApp.UniAppUseLicense="我已获得UniAppID=***的商用授权"`星号为你项目的uni-app应用标识就不会弹提示框了或者直接使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin),设置`RecordApp.UniNativeUtsPlugin`参数后,也不会弹提示框;其他情况请联系作者咨询,更多细节请参考[本组件的GitHub文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)。
获取授权、需要技术支持、或有不清楚的地方可以联系我们客服联系方式QQ 1251654593 或者直接联系作者QQ 753610399 (回复可能没有客服及时)。
插件开发维护不易,感谢支持~
**
**

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,19 @@
## 1.0.2503312025-03-31
增加RecordApp.UniNativeUtsPlugin_OnJsCall接口App端搭配原生插件使用时可绑定接收配套原生录音插件事件原生插件新增PcmPlayer播放器支持流式播放、完整播放App端边录音边播放更流畅
## 1.0.2501112025-01-11
修复vue3 Fragments(multi-root 多个根节点)的兼容性问题修复uniapp Android自带的XXPermissions库在后台无法请求权限的问题仅限搭配原生录音插件可用
## 1.0.2410202024-10-20
适配HBuilder4.28 vue3 setup编译环境下$root.$scope无法读取的bugHBuilder4.29已修复此编译bug但似乎还是有不能使用的问题。如果setup内不能使用可尝试新建个vue组件然后使用选项式api来调用录音功能页面的setup内使用此vue组件
## 1.0.2409102024-09-10
- 新增RecordApp.UniMainCallBack_Register接口允许App renderjs层多次回调数据给逻辑层
- iOS App请求权限时会预先检查NSMicrophoneUsageDescription是否声明避免无声明时调用录音会崩溃
- 新增appNativePlugin_sampleRate原生插件录音选项
- Android App已提供后台录音保活功能启用后App在后台或锁屏后可继续正常录音
## 1.0.2406252024-06-25
调整UniWebViewCallAsync调用失败时返回更详细信息。android_audioSource默认值由1改成0新增ios_categoryOptions原生插件录音选项
## 1.0.2404092024-04-09
增加功能调用完善demo项目
## 1.0.2312082023-12-08
完善文档增加asr语音识别示例
## 1.0.2312012023-12-04
第一次发布

@ -0,0 +1,6 @@
<template>
<view>
<view style="font-weight: bold;">Recorder-UniCore Vue Component</view>
<view style="font-size:14px; color:#f60">无需手动显示本UI组件只需在script中正常引入 RecordApp + app-uni-support.js 即可实现 H5iOS Android App微信小程序 多端录音</view>
</view>
</template>

@ -0,0 +1,19 @@
《许可及服务协议》
**您以下称“用户”下载、使用我以下称“作者”提供的Recorder-UniCore组件含原生录音插件、uts插件以下统称“本组件”应当阅读并遵守本许可协议。请用户务必审慎阅读、充分理解各条款内容特别是免除或者限制责任的条款并选择接受或不接受。除非用户已阅读并接受本协议所有条款否则用户无权下载、使用本组件及相关服务用户的下载、使用等行为即视为用户已阅读并同意本许可协议的约束。**
1. 用户应当直接从作者许可的途径如作者的GitHub、Gitee仓库、已上架的DCloud插件市场、QQ群等途径中获取本组件其他途径获取到的组件代码是未经过作者授权的存在安全隐患可能会导致你的程序、资产受到侵害作者对因此给用户造成的损失不予负责。
2. 作者将积极并采取措施保护用户的信息和隐私;组件本身不会搜集存储任何用户信息。
3. 除法律法规有明确规定外,作者将尽最大努力确保本组件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解作者不能对此进行担保。
4. 用户理解,对于不可抗力及第三方原因导致的您的直接或间接损失,作者无法承担责任。
5. 用户因使用本组件进行生成、处理数据,由此引起或与有关的包括但不限于利润损失、资料损失、业务中断的损害赔偿或其它商业损害赔偿或损失,需由用户自行承担。
6. 如若发生赔偿、退款等行为,赔偿、退款等累计金额不得超过用户实际支付给作者的总金额。
7. 已授予的授权许可包括免费授权和已购买的原生录音插件、uts插件均仅限在授权指定的uni-app的应用标识AppID对应的项目上使用不可在其他项目上使用用户不得对本组件及其中的相关信息擅自出租、出借、销售、逆向工程、破解不得在未取得作者授权的情况下借助本组件发展与本组件有关联的衍生软件产品、服务、插件、外挂等。
8. 用户不得使用本组件从事违反法律法规政策、破坏公序良俗、损害公共利益的行为。

@ -0,0 +1,88 @@
{
"id": "Recorder-UniCore",
"displayName": "跨平台Recorder录音插件支持多种格式、音频可视化、实时上传、语音识别",
"version": "1.0.250331",
"description": "支持H5、Android iOS App、微信小程序mp3 wav pcm g711a g711u ogg amr 录音格式;实时帧回调处理 音频转码 波形动画显示 ASR语音转文字 无录制时长限制",
"keywords": [
"Recorder-UniCore",
"recorder-core",
"RecordApp",
"record",
"recording"
],
"repository": "https://github.com/xiangyuecn/Recorder",
"engines": {
"HBuilderX": "^3.6.11"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "753610399"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "录音权限"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "n",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,425 @@
**
**
# Recorder-UniCore组件uni-app内使用RecordApp录音
本组件使用`Recorder`开源库来进行录音和音频数据处理,使用`RecordApp`和本组件内的`app-uni-support.js`来适配到不同平台环境下进行录音。
- 支持vue2、vue3、nvue
- 支持编译成H5、Android App、iOS App、微信小程序
- 支持已有的大部分录音格式mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;**注意:不支持通话时录音**
- 支持PCM音频流式播放、完整播放App端用原生插件边录音边播放更流畅
- 支持离线使用,本组件和配套原生插件均不依赖网络
- App端有配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可供搭配使用,兼容性和体验更好
**详细文档(含Demo项目)** [https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp](https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)
**Recorder开源库地址** [https://github.com/xiangyuecn/Recorder](https://github.com/xiangyuecn/Recorder)
如果github打不开可以[点此访问Gitee仓库地址](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp) 。
**
## 测试方法
**示例项目如果在HBuilder中编译失败请删掉node_modules目录重新手动执行npm install偶尔出现HBuilder自动创建项目依赖包不完整导致无法编译**
1. 在本插件市场页面右侧下载或导入示例项目或打开上面详细文档链接中的Demo源码
2. 在测试项目根目录执行 `npm install --registry=https://registry.npmmirror.com/` ,完成`recorder-core`依赖的安装
3. 在HBuilder中打开本测试项目文件夹
4. 在HBuilder中运行到浏览器、手机、微信小程序即可在不同环境下测试
5. 测试中提供了基础录音、播放、上传、WebSocket实时语音通话对讲、ASR语音识别等功能
**
**
# 集成到自己项目中
你可以直接参考上面的测试示例项目源码,里面的`main_recTest.vue`更容易入门示例项目中已经实现了很多功能简单使用可直接照抄Demo代码到你的项目中。
## 一、引入js文件
1. 在你的项目根目录安装`recorder-core``npm install recorder-core --registry=https://registry.npmmirror.com/`
2. 导入Recorder-UniCore组件插件市场下载本组件然后添加到你的项目中 `/uni_modules/Recorder-UniCore`
3. 项目配置好录音权限,参考下面的录音权限配置章节,**特别注意App后台录音配置、小程序权限声明**
4. 在需要录音的vue文件script内编写以下代码按需引入需要的js
``` html
<template>
<view>
... 建议template下只有一个根节点最外面套一层view如果不小心踩到了vue3的Fragments(multi-root 多个根节点)特性vue2编译会报错vue3不会可能会出现奇奇怪怪的兼容性问题
</view>
</template>
<script> /****/
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行
import Recorder from 'recorder-core' //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js
import RecordApp from 'recorder-core/src/app-support/app'
//所有平台必须引入的uni-app支持文件如果编译出现路径错误请把@换成 ../../ 这种)
<!-- import '@/uni_modules/Recorder-UniCore/app-uni-support.js' -->
/** 需要编译成微信小程序时,引入微信小程序支持文件 **/
// #ifdef MP-WEIXIN
import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
// #endif
/** H5、小程序环境中引入需要的格式编码器、可视化插件App环境中在renderjs中引入 **/
// 注意如果App中需要在逻辑层中调用Recorder的编码/转码功能,需要去掉此条件编译,否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js的话必须要加上
//可选的插件支持项,把需要的插件按需引入进来即可
import 'recorder-core/src/extensions/waveview'
// #endif
// ... 这后面写页面代码用选项式API风格vue2、vue3、setup组合式API风格仅vue3都可以
</script>
```
5. 编译成app时默认需要额外提供一个renderjs模块请照抄下面这段代码放到vue文件末尾
``` html
<!-- #ifdef APP -->
<script module="yourModuleName" lang="renderjs"> //APIvue2vue3setupAPIimport vue
/**需要编译成App时你需要添加一个renderjs模块然后一模一样的import上面那些js微信的js除外
因为App中默认是在renderjsWebView中进行录音和音频编码
。如果配置了 RecordApp.UniWithoutAppRenderjs=true 且未调用依赖renderjs的功能时如nvue、可视化、仅H5中可用的插件
可不提供此renderjs模块同时逻辑层中需要将相关import的条件编译去掉**/
import 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
// import '../../uni_modules/Recorder-UniCore/app-uni-support.js' //renderjs中似乎不支持"@/"打头的路径,如果编译路径错误请改正路径即可
//按需引入你需要的录音格式支持文件,和插件
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import 'recorder-core/src/extensions/waveview'
export default {
mounted(){
//App的renderjs必须调用的函数传入当前模块this
RecordApp.UniRenderjsRegister(this);
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用二进制数据需转成base64来传递
}
}
</script>
<!-- #endif -->
```
**
**
## 二、调用录音
``` javascript
/**在逻辑层中编写**/
//import ... 上面那些import代码
//var vue3This=getCurrentInstance().proxy; //当用vue3 setup组合式 API (Composition API) 编写时直接在import后面取到当前实例this在需要this的地方传vue3This变量即可其他的和选项式 API (Options API) 没有任何区别import {getCurrentInstance} from 'vue'详细可以参考Demo项目中的 page_vue3____composition_api.vue
//RecordApp.UniNativeUtsPlugin={ nativePlugin:true }; //App中启用配套的原生录音插件支持配置后会使用原生插件进行录音没有原生插件时依旧使用renderjs H5录音
//App中提升后台录音的稳定性配置了原生插件后可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码WebWorker加速变成逻辑层中直接编码但会降低逻辑层性能后台运行时可避免部分手机WebView运行受限的影响
//App中提升后台录音的稳定性需要启用后台录音保活服务iOS不需要参考录音权限配置Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音也受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
export default {
data() { return {} } //视图没有引用到的变量无需放data里直接this.xxx使用
,mounted() {
this.isMounted=true;
//页面onShow时【必须调用】的函数传入当前组件this
RecordApp.UniPageOnShow(this);
}
,onShow(){ //onShow可能比mounted先执行页面可能还未准备好
if(this.isMounted) RecordApp.UniPageOnShow(this);
}
,methods:{
//请求录音权限
recReq(){
//编译成App时提供的授权许可编译成H5、小程序为免费授权可不填写如果未填写授权许可将会在App打开后第一次调用请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框
//RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';
//RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置在h5H5、App+renderjs中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.RequestPermission(()=>{
console.log("已获得录音权限,可以开始录音了");
},(msg,isUserNotAllow)=>{
if(isUserNotAllow){//用户拒绝了录音权限
//这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
}
console.error("请求录音权限失败:"+msg);
});
}
//开始录音
,recStart(){
//Android App如果要后台录音需要启用后台录音保活服务iOS不需要需使用配套原生插件、或使用第三方保活插件
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ title:"正在录音" ,content:"正在录音中请勿关闭App运行" }).then(()=>{...}).catch((e)=>{...}) 注意必须RecordApp.RequestPermission得到权限后调用
//录音配置信息
var set={
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除并不一定会生效打开后声音可能会从听筒播放部分环境下如小程序、App原生插件可调用接口切换成扬声器外放
//注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
//全平台通用可实时上传发送数据配合Recorder.SampleData方法将buffers中的新数据连续的转换成pcm上传或使用mock方法将新数据连续的转码成其他格式上传可以参考Recorder文档里面的Demo片段列表 -> 实时转码并上传-通用版基于本功能可以做到实时转发数据、实时保存数据、实时语音识别ASR
//注意App里面是在renderjs中进行实际的音频格式编码操作此处的buffers数据是renderjs实时转发过来的修改此处的buffers数据不会改变renderjs中buffers所以不会改变生成的音频文件可在onProcess_renderjs中进行修改操作就没有此问题了如需清理buffers内存此处和onProcess_renderjs中均需要进行清理H5、小程序中无此限制
//注意如果你要用只支持在浏览器中使用的Recorder扩展插件App里面请在renderjs中引入此扩展插件然后在onProcess_renderjs中调用这个插件H5可直接在这里进行调用小程序不支持这类插件如果调用插件的逻辑比较复杂建议封装成js文件这样逻辑层、renderjs中直接import不需要重复编写
//H5、小程序等可视化图形绘制直接运行在逻辑层App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
// #endif
/*实时释放清理内存用于支持长时间录音在指定了有效的type时编码器内部可能还会有其他缓冲必须同时提供takeoffEncodeChunk才能清理内存否则type需要提供unknown格式来阻止编码器内部缓冲App的onProcess_renderjs中需要进行相同操作
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}
,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中在这里修改buffers会改变生成的音频文件但注意buffers会先转发到逻辑层onProcess后才会调用本方法因此在逻辑层的onProcess中需要重新修改一遍
//本方法可以返回truerenderjs中的onProcess将开启异步模式处理完后调用asyncEnd结束异步注意这里异步修改的buffers一样的不会在逻辑层的onProcess中生效
//App中是在renderjs中进行的可视化图形绘制因此需要写在这里this是renderjs模块的this也可以用This变量如果代码比较复杂请直接在renderjs的methods里面放个方法xxxFunc这里直接使用this.xxxFunc(args)进行调用
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
/*和onProcess中一样进行释放清理内存用于支持长时间录音
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}`
,onProcessBefore_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx){
//App中本方法会在逻辑层onProcess之前调用因此修改的buffers会转发给逻辑层onProcess本方法没有asyncEnd参数不支持异步处理
//一般无需提供本方法只用onProcess_renderjs就行renderjs的onProcess内部调用过程onProcessBefore_renderjs -> 转发给逻辑层onProcess -> onProcess_renderjs
}`
,takeoffEncodeChunk:true?null:(chunkBytes)=>{
//全平台通用实时接收到编码器编码出来的音频片段数据chunkBytes是Uint8Array二进制数据可以实时上传发送出去
//App中如果未配置RecordApp.UniWithoutAppRenderjs时建议提供此回调因为录音结束后会将整个录音文件从renderjs传回逻辑层由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了大点的文件传输会比较慢提供此回调后可避免Stop时产生超大数据回传
//App中使用原生插件时可方便的将数据实时保存到同一文件第一帧时append:false新建文件后面的append:true追加到文件
//RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
}
,takeoffEncodeChunk_renderjs:true?null:`function(chunkBytes){
//App中这里可以做一些仅在renderjs中才生效的事情不提供也行this是renderjs模块的this也可以用This变量
}`
,start_renderjs:`function(){
//App中可以放一个函数在Start成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情比如初始化不提供也行
}`
,stop_renderjs:`function(arrayBuffer,duration,mime){
//App中可以放一个函数在Stop成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情不提供也行
}`
};
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.Start(set,()=>{
console.log("已开始录音");
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
//var wdt=this.watchDogTimer=setInterval ... 请参考示例Demo的main_recTest.vue中的watchDogTimer实现
//创建音频可视化图形绘制App环境下是在renderjs中绘制H5、小程序等是在逻辑层中绘制因此需要提供两段相同的代码
//view里面放一个canvascanvas需要指定宽高下面style里指定了300*100
//<canvas type="2d" class="recwave-WaveView" style="width:300px;height:100px"></canvas>
RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
`,(canvas1)=>{
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
});
},(msg)=>{
console.error("开始录音失败:"+msg);
});
}
//暂停录音
,recPause(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Pause();
console.log("已暂停");
}
}
//继续录音
,recResume(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Resume();
console.log("继续录音中...");
}
}
//停止录音
,recStop(){
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ close:true }) //关闭Android App后台录音保活服务
RecordApp.Stop((arrayBuffer,duration,mime)=>{
//全平台通用arrayBuffer是音频文件二进制数据可以保存成文件或者发送给服务器
//App中如果在Start参数中提供了stop_renderjsrenderjs中的函数会比这个函数先执行
//注意当Start时提供了takeoffEncodeChunk后你需要自行实时保存录音文件数据因此Stop时返回的arrayBuffer的长度将为0字节
//如果是H5环境也可以直接构造成Blob/File文件对象和Recorder使用一致
// #ifdef H5
var blob=new Blob([arrayBuffer],{type:mime});
console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
var file=new File([arrayBuffer],"recorder.mp3");
//uni.uploadFile({file:file, ...}) //参考demo中的test_upload_saveFile.vue
// #endif
//如果是App、小程序环境可以直接保存到本地文件然后调用相关网络接口上传
// #ifdef APP || MP-WEIXIN
RecordApp.UniSaveLocalFile("recorder.mp3",arrayBuffer,(savePath)=>{
console.log(savePath); //app保存的文件夹为`plus.io.PUBLIC_DOWNLOADS`,小程序为 `wx.env.USER_DATA_PATH` 路径
//uni.uploadFile({filePath:savePath, ...}) //参考demo中的test_upload_saveFile.vue
},(errMsg)=>{ console.error(errMsg) });
// #endif
},(msg)=>{
console.error("结束录音失败:"+msg);
});
}
}
}
```
**
**
**
**
# 录音权限配置、需要注意的细节
## 编译成H5时录音和权限
编译成H5时录音功能由Recorder H5提供无需额外处理录音权限。
**
## 编译成微信小程序时录音和权限
编译成微信小程序时,录音功能由小程序的`RecorderManager`提供,屏蔽了微信原有的底层细节(无录音时长限制)。
小程序录音需要用户授予录音权限,调用`RecordApp.RequestPermission`的时候会检查是否能正常录音,如果用户拒绝了录音权限,会进入错误回调,回调里面你应当编写代码检查`wx.getSetting`中的`scope.record`录音权限,然后引导用户进行授权(可调用`wx.openSetting`打开设置页面,方便用户给权限)。
**注意:上架小程序需要到小程序管理后台《[用户隐私保护指引](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html)》中声明录音权限,否则正式版将无法调用录音功能(请求权限时会直接走错误回调)。**
更多细节请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 测试项目文档。
**
## 编译成App时录音和权限
编译成App录音时分两种情况
1. 默认未配置`RecordApp.UniNativeUtsPlugin`未使用原生录音插件和uts插件会在renderjs中使用Recorder H5进行录音录音数据会实时回传到逻辑层。
2. 配置了`RecordApp.UniNativeUtsPlugin`使用原生录音插件或uts插件时会直接调用原生插件进行录音录音数据默认会传递到renderjs中进行音频编码处理WebWorker加速然后再实时回传到逻辑层如果配置了`RecordApp.UniWithoutAppRenderjs=true`时,音频编码处理将会在逻辑层中直接处理。
当App是在renderjs中使用H5进行录音时未使用原生录音插件和uts插件iOS上只支持14.3以上版本,**且iOS上每次进入页面后第一次请求录音权限时、或长时间无操作再请求录音权限时WebView均会弹出录音权限对话框**不同旧iOS版本低于iOS17下H5录音可能存在的问题在App中同样会存在使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)或uts插件时无以上问题和版本限制uts插件开发中暂不可用Android也无以上问题。
当音频编码是在renderjs中进行处理时录音结束后会将整个录音文件传回逻辑层由于uni-app的逻辑层和renderjs层大点的文件传输会比较慢**建议Start时使用takeoffEncodeChunk实时获取音频文件数据可避免Stop时产生超大数据回传**;配置了`RecordApp.UniWithoutAppRenderjs=true`后,因为音频编码直接是在逻辑层中进行,将不存在传输性能损耗,但会影响逻辑层的性能(正常情况轻微不明显),需要配套使用原生录音插件才可以进行此项配置。
在调用`RecordApp.RequestPermission`的时候,`Recorder-UniCore`组件会自动处理好App的系统录音权限只需要在uni-app项目的 `manifest.json` 中配置好Android和iOS的录音权限声明。
```
//Android需要勾选的权限第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音需要启用后台录音保活服务Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音、原生插件录音均受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置不然App切到后台后立马会停止录音
```
**
## PCM音频流式播放、语音通话、回声消除、声音外放
在App、H5中均可使用H5版的[BufferStreamPlayer](https://gitee.com/xiangyuecn/Recorder/blob/master/src/extensions/buffer_stream.player.js)来实时流式播放语音其中App中需要在renderjs中加载BufferStreamPlayer在逻辑层中调用`RecordApp.UniWebViewVueCall`等方法将逻辑层中接收到的实时语音数据发送到renderjs中播放播放声音的同时进行录音声音可能会被录进去产生回声因此一般需要打开回声消除调用代码参考demo中的[test_realtime_voice.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_realtime_voice.vue)。
App中如果搭配使用了配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可以调用原生实现的PcmPlayer播放器实时流式播放PCM音频边录音边播放更流畅同时也支持完整播放比如AI语音合成的播放调用代码参考demo中的[test_player_nativePlugin_pcmPlayer.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_player_nativePlugin_pcmPlayer.vue)。
微信小程序请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 文档里面的同名章节使用WebAudioContext播放。
配置audioTrackSet可尝试打开回声消除或者切换听筒播放或外放打开回声消除时一般会转为听筒播放显著降低回声。
``` js
//打开回声消除
RecordApp.Start({
... 更多配置参数请参考RecordApp文档
//此配置App、H5、小程序均可打开回声消除注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
,audioTrackSet:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}
//Android指定麦克风源App搭配原生插件、小程序可用0 DEFAULT 默认音频源1 MIC 主麦克风5 CAMCORDER 相机方向的麦6 VOICE_RECOGNITION 语音识别7 VOICE_COMMUNICATION 语音通信(带回声消除)
,android_audioSource:7 //提供此配置时优先级比audioTrackSet更高默认值为0
//iOS的AVAudioSession setCategory的withOptions参数值App搭配原生插件可用取值请参考配套原生插件文档中的iosSetDefault_categoryOptions
//,ios_categoryOptions:0x1|0x4 //默认值为5(0x1|0x4)
});
//App搭配原生插件时尝试切换听筒播放或外放
await RecordApp.UniNativeUtsPluginCallAsync("setSpeakerOff",{off:true或false});
//小程序尝试切换
wx.setInnerAudioOption({ speakerOn:false或true })
//H5不支持切换
```
**
**
**
# 详细文档、RecordApp方法、属性文档
请先阅读 [demo_UniApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)含Demo项目更高级使用还需深入阅读 [Recorder文档](https://gitee.com/xiangyuecn/Recorder)、[RecordApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample) 均为完整的一个README.md文档Recorder文档中包含了更丰富的示例代码基础录音、实时处理、格式转码、音频分析、音频混音、音频生成 等等大部分能在uniapp中直接使用。
**
**
**
# 本组件的授权许可限制
**本组件内的app-uni-support.js文件在uni-app中编译到App平台时仅供测试用App平台包括Android App、iOS App不可用于正式发布或商用正式发布或商用需先到DCloud插件市场购买[此带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)费用为¥199元赠送Android版原生插件即可获得授权许可**编译到其他平台时无此授权限制比如H5、小程序均为免费授权。
在App中如果未获得授权许可将会在App打开后第一次调用`RecordApp.RequestPermission`请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框。
在DCloud插件市场购买了[带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)获得了授权后,请在调用`RecordApp.RequestPermission`请求录音权限前,赋值`RecordApp.UniAppUseLicense="我已获得UniAppID=***的商用授权"`星号为你项目的uni-app应用标识就不会弹提示框了或者直接使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin),设置`RecordApp.UniNativeUtsPlugin`参数后,也不会弹提示框;其他情况请联系作者咨询,更多细节请参考[本组件的GitHub文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)。
获取授权、需要技术支持、或有不清楚的地方可以联系我们客服联系方式QQ 1251654593 或者直接联系作者QQ 753610399 (回复可能没有客服及时)。
插件开发维护不易,感谢支持~
**
**

@ -0,0 +1,38 @@
<template>
<view class="g-components-chat-gree g_fs_16">
<div class="conet">
<div class="g_c_4 g_fs_16 g_c_0">
你好我是伯才智能匹配AI<text class="g_fw_bold">大鹏</text>可以帮老乡快速匹配工作支持语音输入为了匹配更准确需多提供老乡需求信息例如:
</div>
<div class="g_pt_24 g_pb_24">
<div class="g_flex_row_start g_mb_4">
<div class="g_fs_16 g_fw_600 g_mr_4 g_flex_none g_c_0">1. 性别</div>
</div>
<div class="g_flex_row_start g_mb_4">
<div class="g_fs_16 g_fw_600 g_mr_4 g_flex_none g_c_0">2. 年龄</div>
</div>
<div class="g_flex_row_start g_mb_4">
<div class="g_fs_16 g_fw_600 g_mr_4 g_flex_none g_c_0">3. 意向城市</div>
</div>
<div class="g_flex_row_start g_mb_0">
<div class="g_fs_16 g_fw_600 g_mr_4 g_flex_none g_c_0">4. 工作要求(如吃住班次等)</div>
</div>
</div>
<view class="g_pb_24" >
<div class="">
<text class="g_fs_16 g_flex_none g_c_0">示例</text>
<text class="g_fs_16 g_fw_600 g_mr_4 g_flex_none g_c_0">有位32岁大姐想去常州找个长白班的工作</text>
</div>
</view>
<div class="g_c_4 g_fs_16 g_c_0">
快告诉我老乡需求开始匹配吧!
</div>
</div>
</view>
</template>
<script>
</script>
<style>
</style>

@ -0,0 +1,205 @@
<template>
<view class="g-components-chat-markdown g_fs_17">
<div class="g-tip-title g_flex_row_between"
style="background-color: #d6e9ff;
padding: 10px 10px 22px 10px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
position: relative;
bottom: -16px;
margin-top: -16px;
"
@click="handleMore"
>
<div class="g_flex_row_between g_w_all">
<div class="g_flex_1 g_ell_1" style="font-size: 18px;color: #576b95;font-weight: 600;">
智能匹配方案推荐
</div>
<div class="g_flex_none g_ml_12 g_flex_column_center">
<i class="iconfont icon-gengduo11" style="color: #666666;"></i>
</div>
</div>
<div class="g_mt_2" style="font-size: 14px;color: #888888;">
更多信息可点击卡片查看详情
</div>
</div>
<div v-if="cusList && cusList.length > 0">
<div class="item" v-for="(item,index) in cusList" :key="index">
<view class="itema g_position_rela g_bg_f"
@click="handleCard(item, index)"
:class="isbor == 1 ? '' : 'bor8'"
>
<view class="g_pt_10 g_pb_10 g_border_e_b issa" style="width: calc(100% - 20px);margin: 0 auto;position: relative;">
<i class="iconfont icon-yitingzhao" v-if="item.recruitment == 2"
style="position: absolute; right: 30px; top: 50%; transform: translateY(-50%);color: #ff4d4f;font-size: 80px;z-index: 1;"
></i>
<!-- 基础信息 -->
<view class="m-top g_flex_row_between">
<view class="g_flex_1 g_flex_column_between">
<view class="g_flex_1 g_flex_row_between g_lh_1">
<view class="g_flex_none g_flex_row_start flex_center">
<img class="g_w_32 g_h_16 g_mr_4" v-if="item.picked == 1" src="https://matripe-cms.oss-cn-beijing.aliyuncs.com/bocaigongyinglian/zhen.svg" alt="" />
<view class="g_ell_1 g_fs_18 g_fw_600" style="color: rgba(0, 0, 0, 0.85); max-width: 500rpx; line-height: 1.3">{{ item.jobName }}</view>
</view>
<view class="g_fs_14 g_flex_row_end" style="color: rgba(0, 0, 0, 0.45)">
<view>{{ timeShowXXX(item.updateTime) }}</view>
</view>
</view>
<view class="g_flex_none g_flex_row_between g_mt_2">
<view class="g_flex_none g_flex_row_start g_fs_14" style="color: rgba(0, 0, 0, 0.45)">
<view class="g_ell_1" style="max-width: 136px; margin-right: 2px"> {{ item.district }}{{ item.district ? "丨" : "" }} </view>
{{ item.gender + "丨" + item.age }}
</view>
</view>
</view>
</view>
<!-- 标签 -->
<view class="m-bottom g_flex_row_between g_mt_2">
<view class="g_flex_1 g_flex_row_between flex_center">
<view class="g_flex_1 g_flex_row_between">
<view class="g_flex_row_start_none g_flex_1" style="max-width: 75%; flex-wrap: wrap; height: 22px; overflow: hidden"
v-if="item.jobSpecialLabelNames"
>
<view class="label_btn" v-for="(itm, inx) in item.jobSpecialLabelNames.split(',')" :key="inx">{{ itm }}</view>
<view class="label_btn" v-if="item.jobSpecialLabelNameArray && item.jobSpecialLabelNameArray.length == 0"> </view>
</view>
</view>
</view>
</view>
<!-- 费用 -->
<view class="g_mt_2">
<view class="g_flex_row_between flex_center g_fs_22 g_c_f40 g_fw_600">
<rich-text class="g_lh_1" :nodes="item.cus_price" v-if="item.cus_price != '月薪'"></rich-text>
<view v-else>{{ (item.minMonthlyPay / 100) + '-' + (item.maxMonthlyPay / 100) }}<span class="g_fs_14"> / </span> </view>
<view class="g_flex_row_start g_fs_12 g_pr_4 g_h_20 g_fw_400" style="background: linear-gradient(138deg, #fde0ad 22%, #fac474); border-radius: 2px; color: #754300; line-height: 20px">
<view class>
<image class="g_mr_4" style="width: 20px; height: 20px; display: block" src="https://matripe-cms.oss-cn-beijing.aliyuncs.com/1shoudan/fee.svg" mode="aspecFill" lazy-load="false"></image>
</view>
<view class="g_fs_13" v-if="userinfo.agencyStatus == 1">{{ item.fuWuFei || "" }}</view>
<view class="biggerSize2 g_fs_13" v-else>{{ "" }}</view>
</view>
</view>
</view>
</view>
<view class="g_border_e_t g_p_8 g_flex_row_between flex_center" v-if="userinfo.corpUserFlag && false">
<view class="g_c_6 g_fs_12 g_pr_4 g_radius_4 g_flex_row_start flex_center" style="background: #f1faff">
<img class="g_w_33 g_h_22 g_mr_4" src="https://matripe-cms.oss-cn-beijing.aliyuncs.com/bocaigongyinglian/xmf.svg" alt="" />
{{ item.supplierName || "-" }}
</view>
</view>
</view>
</div>
</div>
<div v-else class="g_p_10 g_bg_f g_flex_c g_mt_12"
style="border-radius: 12px;padding: 24px 10px;"
:style="{
'border-radius': isbor == 1 ? '0' : '12px'
}"
>
<u-empty
text="暂无更多职位"
:src="cdnBaseImg + 'noData.svg'"
>
</u-empty>
</div>
</view>
</template>
<script>
export default {
data(){
return {
cdnBaseImg:this.G.store().cdnBaseImg,
userinfo: uni.getStorageSync("apply-userinfo"),
}
},
props: {
cusList: {
default: () => {
return [];
},
},
isbor: {
default: () => {
return 0;
},
},
},
onHide(){
uni.removeStorageSync('test_file')
},
onUnload() {
uni.removeStorageSync('test_file')
},
methods:{
handleMore(){
this.$emit('exportMore')
},
handleCard($item) {
uni.navigateTo({
url: "/root/detail/work?id=" + $item.id,
});
},
timeShowXXX(updateTime) {
const now = Date.now();
const diff = now - updateTime;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days < 2) {
if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else {
return `刚刚`;
}
} else {
return `${days}天前`;
}
},
}
};
</script>
<style lang="scss">
.g-components-chat-markdown{
.label_btn {
display: inline-block;
height: 20px;
box-sizing: border-box;
font-size: 12px;
font-weight: 400;
color: #666;
background: #f1faff;
border-radius: 2px;
padding: 0 4px 0px;
line-height: 20px;
margin-right: 6px;
margin-bottom: 0px;
position: relative;
top: 1px;
}
.item{
&:nth-child(1){
.itema{
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
}
&:last-child{
.issa{
border-bottom: none;
}
.bor8{
border-bottom-left-radius: 8px !important;
border-bottom-right-radius: 8px !important;
}
}
}
}
</style>

@ -0,0 +1,65 @@
<template>
<view class="g-components-chat-message g_fs_17 g_flex_row_start" >
<div class="g_flex_column_center">
{{displayedMessage}}
</div>
</view>
</template>
<script>
export default {
props: {
cusMessage: {
type: String,
default: () => {
return '';
},
},
isRender:{
default: () => {
return 0;
},
}
},
data(){
return {
isShow:0,
displayedMessage: ''
}
},
watch: {
cusMessage: {
handler(newVal) {
console.log('newVal',newVal)
const hasDot = newVal.endsWith('。');
const messageWithoutDot = hasDot ? newVal.slice(0, -1) : newVal;
this.displayedMessage = messageWithoutDot;
if (this.isShow) {
this.displayedMessage = messageWithoutDot + '...';
} else {
this.displayedMessage = hasDot ? messageWithoutDot + '。' : messageWithoutDot;
}
},
immediate: true
},
'isRender': {
handler(newVal) {
this.isShow = newVal;
const hasDot = this.cusMessage.endsWith('。');
const messageWithoutDot = hasDot ? this.cusMessage.slice(0, -1) : this.cusMessage;
this.displayedMessage = this.isShow ? messageWithoutDot + '...' : (hasDot ? messageWithoutDot + '。' : messageWithoutDot);
},
immediate: true
}
},
created(){
this.isShow = this.isRender;
},
methods:{
}
};
</script>
<style>
</style>

@ -0,0 +1,591 @@
import pako from 'pako';
export class AsrClient {
constructor (url) {
this.URL = url;
// 柚子
// this.appid = "4200510043";
// this.token = "BWP-R4zL035PE0rtyzdzU1UPxjc-9VTJ";
// 测试
this.appid = "5592672364";
this.token = "wdoyVaxUik1NqR3Ei3L7ACeNmqlkpMEl";
this.sk = "TIlmvF4EeKEEfFs74eyyZ8Wn-kXn-WwJ";
this.cluster = "volcengine_input_common";
this.workflow = "audio_in,resample,partition,vad,fe,decode,itn";
this.uid = "user_id";
this.nbest = 1;
this.show_utterances = true;
this.result_type = "full";
this.format = "wav";
this.codec = "raw";
this.sample_rate = 16000;
this.channels = 1;
this.bits = 16;
this.authType = "TOKEN";
this.socket = null;
this.recv_latch = null;
this.recv_timeout = 5;
this.recv_suc = true;
this.asr_response = new ASRResponse();
}
asr_sync_connect ($form='') {
console.log('初始化接受参数:',$form)
if($form){
uni.setStorageSync('asytip',$form)
}else{
$form = uni.getStorageSync('asytip')
}
const self = this;
try {
// 构造参数消息
const params_msg = self.construct_param();
// 创建 WebSocket 连接
self.socket = uni.connectSocket({
// url: 'wss://openspeech.bytedance.com/api/v2/asr',
url: self.URL + "?api_jwt=" + self.token,
header: {
'Authorization': 'Bearer; ' + self.token
},
binaryType: 'arraybuffer',
success: () => {
console.log('WebSocket 连接成功');
},
fail: (err) => {
console.error('WebSocket 连接失败', err);
}
});
console.log("1------------------------", self.socket);
console.log("2------------------------", self.socket.readyState);
// 监听WebSocket接收到的消息事件
self.socket.onMessage(function (res) {
console.log('收到服务器消息', res,' whywhy:',$form);
if (res.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(res.data);
self.parse_response(bytes,$form)
}
// 处理接收到的消息
});
// 监听WebSocket连接打开事件
self.socket.onOpen(function () {
console.log('WebSocket连接已打开');
console.log("3------------------------", self.socket.readyState);
self.socket.send({
data: params_msg.buffer,
success: () => {
console.log('请求头发送成功');
},
fail: (err) => {
console.error('完整的客户端请求发送失败', err);
uni.$emit("asrEvent_close", {
data: err.errMsg
});
}
});
// 连接打开后的操作
});
// 监听WebSocket连接关闭事件
self.socket.onClose(function (res) {
console.log('WebSocket已关闭');
// 连接关闭后的操作
});
} catch (error) {
console.error("Error in asr_sync_connect:", error);
return false;
}
}
async asr_send (audio, is_last) {
try {
const self = this;
const payload = self.construct_audio_payload(audio, is_last);
return new Promise((resolve) => {
if (!self.socket || self.socket.readyState !== 1) {
console.error("WebSocket is not open");
resolve(null);
return;
}
self.socket.send({
data: payload.buffer,
success: () => {
// console.log('完整的客户端请求发送成功',connected);
},
fail: (err) => {
console.error('完整的客户端请求发送失败', err);
}
});
});
} catch (error) {
console.error("Error in asr_send:", error);
return null;
}
}
asr_close () {
console.log('inner asr_close');
uni.$emit("ws_send_ready", {
data: false,
event: '主动关闭'
});
if (this.socket && this.socket.readyState === 1) {
console.log('inner asr_closeasr_close');
this.socket.close();
}
}
gzip_compress (content) {
// 使用 pako 库进行 GZIP 压缩
return pako.gzip(content);
}
gzip_decompress (content) {
// 使用 pako 库进行 GZIP 解压缩
return pako.ungzip(content);
}
construct_param () {
try {
// 构造请求 ID
const reqid = this.generateUUID();
// 构造 ASR 参数对象
const app = new App(this.appid, this.cluster, this.token);
const user = new User(this.uid);
const request = new Request(reqid, this.workflow, this.nbest, this.show_utterances, this.result_type, 1);
const audio = new Audio(this.format, this.sample_rate, this.bits, this.channels);
const asr_params = new ASRParams(app, user, request, audio);
// 将对象序列化为 JSON 字符串
const json_data = JSON.stringify(asr_params);
// 将字符串转换为字节数组
const bytes = [];
for (let i = 0; i < json_data.length; i++) {
const charCode = json_data.charCodeAt(i);
bytes.push(charCode & 0xFF); // 获取字符的字节值(低 8 位)
}
// 创建 Int8Array
const int8Array = new Int8Array(bytes);
// 对数据进行 GZIP 压缩
// const compressed = this.gzip_compress(int8Array);
const compressed = int8Array;
// 构造消息头部
const header = new Int8Array(4);
header[0] = (0b0001 << 4) | (4 >> 2); // 协议版本和头部长度
header[1] = (0b0001 << 4) | 0b0000; // 消息类型和序列号标志
// header[2] = (0b0001 << 4) | 0b0001; // 序列化方式和压缩方式
header[2] = (0b0001 << 4) | 0b0000; // 序列化方式和压缩方式
header[3] = 0; // 保留字节
// 构造 payload 长度部分
const payload_len = compressed.length;
const pl_byte = new Int8Array(4);
new DataView(pl_byte.buffer).setUint32(0, payload_len, false); // 大端模式
// 拼接所有部分
const header_part = new Int8Array(header.buffer);
const pl_part = new Int8Array(pl_byte.buffer);
const compressed_part = new Int8Array(compressed);
return this.concat_byte(header_part, pl_part, compressed);
} catch (error) {
console.error("Error constructing param:", error);
return new Int8Array();
}
}
parse_response (bytes,$form='') {
try {
var self = this;
// 解析头部
const header_len = (bytes[0] & 0x0f) << 2;
const message_type = (bytes[1] & 0xf0) >>> 4;
const message_type_flag = bytes[1] & 0x0f;
const message_serial = (bytes[2] & 0xf0) >>> 4;
const message_compress = bytes[2] & 0x0f;
let payload_offset = header_len;
let payload_len = 0;
let payload = null;
if (message_type === 0b1001) { // FULL_SERVER_RESPONSE
// 读取 payload 长度
const len_bytes = bytes.subarray(payload_offset, payload_offset + 4);
payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式
payload_offset += 4;
} else if (message_type === 0b1011) { // SERVER_ACK
// 读取序列号
const seq_bytes = bytes.subarray(payload_offset, payload_offset + 4);
const seq = new DataView(seq_bytes.buffer).getUint32(0, false); // 大端模式
payload_offset += 4;
// 如果还有数据,读取 payload 长度
if (bytes.length > payload_offset + 4) {
const len_bytes = bytes.subarray(payload_offset, payload_offset + 4);
payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式
payload_offset += 4;
}
} else if (message_type === 0b1111) { // ERROR_MESSAGE_FROM_SERVER
// 读取错误码
const error_code_bytes = bytes.subarray(payload_offset, payload_offset + 4);
const error_code = new DataView(error_code_bytes.buffer).getUint32(0, false); // 大端模式
payload_offset += 4;
// 读取 payload 长度
const len_bytes = bytes.subarray(payload_offset, payload_offset + 4);
payload_len = new DataView(len_bytes.buffer).getUint32(0, false); // 大端模式
payload_offset += 4;
} else {
console.error("Unsupported message type:", message_type);
self.asr_close();
return -1;
}
// 提取 payload 数据
payload = bytes.subarray(payload_offset, payload_offset + payload_len);
// 解压缩 payload
if (message_compress === 0b0001) { // GZIP
try {
payload = this.gzip_decompress(payload);
} catch (error) {
console.error("Failed to decompress payload:", error);
self.asr_close();
return -1;
}
}
// 反序列化 JSON 数据
if (message_serial === 0b0001) { // JSON
try {
// const payload_text = new TextDecoder().decode(payload);
// 将 Uint8Array 转换为字符串
let jsonString = '';
for (let i = 0; i < payload.length; i++) {
jsonString += String.fromCharCode(payload[i]);
}
jsonString = decodeURIComponent(escape(jsonString));
this.asr_response = JSON.parse(jsonString);
// this.asr_response = JSON.parse(payload_text);
} catch (error) {
console.error("Failed to parse JSON payload:", error);
self.asr_close();
return -1;
}
}
// 检查响应码
console.error("ASR response code:", '--code:', this.asr_response.code, '--websokect status:', this.socket.readyState);
if (this.asr_response.code == 1000 && this.asr_response.sequence == 1) {
uni.$emit("ws_send_ready", {
data: true,
event: '1000'
});
} else if (this.asr_response.code == 400) {
uni.$emit("ws_send_ready", {
data: false,
event: '400'
});
} else if (this.asr_response.code !== 1000) {
uni.$emit("ws_send_ready", {
data: false,
event: '!=1000'
});
console.error("ASR response error:", this.asr_response, '--code:', this.asr_response.code, '--websokect status:', this.socket.readyState);
//连接数太多
let codeGroup = [
{ code: 1000, message: '成功' },
{ code: 1001, message: '请求参数无效' },
{ code: 1002, message: '无访问权限' },
{ code: 1003, message: '访问超频' },
{ code: 1004, message: '访问超额' },
{ code: 1005, message: '服务器繁忙' },
{ code: 1008, message: '保留号段' },
{ code: 1009, message: '保留号段' },
{ code: 1010, message: '音频过长' },
{ code: 1011, message: '音频过大' },
{ code: 1012, message: '音频格式无效' },
{ code: 1013, message: '音频静音' },
{ code: 1014, message: '保留号段' },
{ code: 1015, message: '保留号段' },
{ code: 1016, message: '保留号段' },
{ code: 1017, message: '保留号段' },
{ code: 1018, message: '保留号段' },
{ code: 1019, message: '保留号段' },
{ code: 1020, message: '识别等待超时' },
{ code: 1021, message: '识别处理超时' },
{ code: 1022, message: '识别错误' },
{ code: 1023, message: '保留号段' },
{ code: 1024, message: '保留号段' },
{ code: 1025, message: '保留号段' },
{ code: 1026, message: '保留号段' },
{ code: 1027, message: '保留号段' },
{ code: 1028, message: '保留号段' },
{ code: 1029, message: '保留号段' },
{ code: 1030, message: '保留号段' },
{ code: 1031, message: '保留号段' },
{ code: 1032, message: '保留号段' },
{ code: 1033, message: '保留号段' },
{ code: 1034, message: '保留号段' },
{ code: 1035, message: '保留号段' },
{ code: 1036, message: '保留号段' },
{ code: 1037, message: '保留号段' },
{ code: 1038, message: '保留号段' },
{ code: 1039, message: '保留号段' },
{ code: 1099, message: '未知错误' },
{ code: 400, message: '识别中' }
];
uni.$emit("asrEvent_close", {
data: codeGroup.filter(item => { return item.code == self.asr_response.code })[0].message,
code: self.asr_response.code
});
self.asr_close();
return -1;
}
//最后一条消息关闭ws
if (this.asr_response['sequence'] < 0) {
self.asr_close();
console.log("ASR response:", this.asr_response,' asr来源',$form);
uni.$emit('asrEvent_message', {
data: JSON.stringify(self.asr_response)
});
}
return 0;
} catch (error) {
console.error("Error parsing response:", error);
self.asr_close();
//开启webSocket
return -1;
}
}
generateHeader (options) {
const header = new Int8Array(4);
header[0] = (options.version << 4) | options.headerSize;
header[1] = (options.messageType << 4) | options.flags;
header[2] = (options.serialMethod << 4) | options.compression;
header[3] = options.reserved;
return header;
}
construct_audio_payload (audio, is_last) {
try {
// 构造消息头部
const header = new Int8Array(4);
header[0] = (0b0001 << 4) | (4 >> 2); // 协议版本和头部长度
header[1] = is_last
? (0b0010 << 4) | 0b0010 // AUDIO_ONLY_CLIENT_REQUEST 和 NEGATIVE_SEQUENCE_SERVER_ASSGIN
: (0b0010 << 4) | 0b0000; // AUDIO_ONLY_CLIENT_REQUEST 和 NO_SEQUENCE_NUMBER
header[2] = (0b0001 << 4) | 0b0001; // 序列化方式和压缩方式
header[2] = (0b0001 << 4) | 0b0000; // 序列化方式和压缩方式
header[3] = 0; // 保留字节
// 对音频数据进行 GZIP 压缩
// const compressed_audio = this.gzip_compress(audio);
const compressed_audio = audio;
console.log(is_last, audio, compressed_audio)
// 构造 payload 长度部分
const payload_len = compressed_audio.length;
const pl_byte = new ArrayBuffer(4);
new DataView(pl_byte).setUint32(0, payload_len, false); // 大端模式
// 拼接所有部分
const header_part = new Int8Array(header.buffer);
const pl_part = new Int8Array(pl_byte);
const compressed_part = new Int8Array(compressed_audio);
return this.concat_byte(header_part, pl_part, compressed_part);
} catch (error) {
console.error("Error constructing audio payload:", error);
return new Int8Array();
}
}
generateUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
concat_byte (...arrays) {
const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
const result = new Int8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
// 设置各种参数的方法
setAppid (appid) {
this.appid = appid;
}
setToken (token) {
this.token = token;
}
setSk (sk) {
this.sk = sk;
}
setCluster (cluster) {
this.cluster = cluster;
}
setWorkflow (workflow) {
this.workflow = workflow;
}
setUid (uid) {
this.uid = uid;
}
setShow_utterances (show_utterances) {
this.show_utterances = show_utterances;
}
setResult_type (result_type) {
this.result_type = result_type;
}
setFormat (format) {
this.format = format;
}
setSample_rate (sample_rate) {
this.sample_rate = sample_rate;
}
setChannels (channels) {
this.channels = channels;
}
setBits (bits) {
this.bits = bits;
}
setAuthType (authType) {
this.authType = authType;
}
getAsrResponse () {
return this.asr_response;
}
}
export class ASRResponse {
constructor () {
this.reqid = "unknown";
this.code = 0;
this.message = "";
this.sequence = 0;
this.result = [];
this.addition = new Addition();
}
}
export class Result {
constructor () {
this.text = "";
this.confidence = 0;
this.language = "";
this.utterances = [];
this.global_confidence = 0;
}
}
export class Utterances {
constructor () {
this.text = "";
this.start_time = 0;
this.end_time = 0;
this.definite = false;
this.language = "";
this.words = [];
}
}
export class Words {
constructor () {
this.text = "";
this.start_time = 0;
this.end_time = 0;
this.blank_duration = 0;
}
}
export class Addition {
constructor () {
this.duration = "";
}
}
export class ASRParams {
constructor (app, user, request, audio) {
this.app = app;
this.user = user;
this.request = request;
this.audio = audio;
}
}
export class App {
constructor (appid, cluster, token) {
this.appid = appid;
this.cluster = cluster;
this.token = token;
}
}
export class User {
constructor (uid) {
this.uid = uid;
}
}
export class Request {
constructor (reqid, workflow, nbest, show_utterances, result_type, sequence) {
this.reqid = reqid;
this.workflow = workflow;
this.nbest = nbest;
this.show_utterances = show_utterances;
this.result_type = result_type;
this.sequence = sequence;
}
}
export class Audio {
constructor (format, rate, bits, channels) {
this.format = format;
this.rate = rate;
this.bits = bits;
this.channels = channels;
}
}
export default {
AsrClient
}

@ -0,0 +1,7 @@
export let chatMock = [
{
msg:'你好我是伯才智能匹配AI大鹏可以帮老乡快速匹配工作支持语音输入。为了匹配更准确需多提供老乡需求信息例如:1. 性别2. 年龄3. 意向城市4. 工作要求(如吃住、班次等)示例有位32岁大姐想去常州找个长白班的工作。快告诉我老乡需求开始匹配吧!',
chat_type:'text',
robotTag:1
}
]

@ -0,0 +1,95 @@
let ajaxUrl = "https://api.coze.cn/";
let requestTask;
let data = {
ajaxUrl: ajaxUrl,
startAbort ($bool = 0) {
if ($bool == 1) {
requestTask.abort();
}
},
coziGet ($url = '', $parmas = {}, callback = () => { }, failback = () => { }) {
let that = this,
params = {};
let promise = new Promise((resolve, reject) => {
params = $parmas;
resolve();
});
promise.then(() => {
that.postData($url, params, callback, failback, 'GET');
}).catch();
},
coziPost ($url = '', $parmas = {}, callback = () => { }, failback = () => { }) {
let that = this,
params = {};
let promise = new Promise((resolve, reject) => {
params = $parmas;
resolve();
});
promise.then(() => {
that.postData($url, params, callback, failback, 'POST');
}).catch();
},
// 发送请求
postData ($url = '', $parmas = {}, callback = () => { }, failback = () => { }, $method) {
let that = this,
$header = {};
if ($parmas == '') {
$parmas = {};
}
if(uni.getStorageSync("cozi_token")){
$header['Authorization'] = 'Bearer ' + uni.getStorageSync("cozi_token");
}
// 检查网络信息, 避免网络引起的错误码
uni.getNetworkType({
success: function (res) {
console.log(res);
if (res.networkType != 'none') {
requestTask = uni.request({
url: ajaxUrl + $url,
data: $parmas,
header: $header,
method: $method,
success: (res) => {
console.log('res before', res);
let resData = res.data;
console.log('res after', resData);
if($parmas.stream){
callback(resData);
}else{
if (resData.code == 0) {// 正常
callback(resData.data, resData.msg);
} else {// 其他异常
failback(resData.msg);
}
}
},
fail (error) {
console.log('请求失败', error);
if (error.errMsg == 'request:fail abort' || error.errMsg == 'request:fail timeout') {
} else {
uni.showToast({
title: error.errMsg,
icon: "none"
})
}
failback(error);
}
});
} else {
uni.showToast({
title: '网络异常,请检查网络',
icon: "none"
})
}
},
});
},
};
export default data;

File diff suppressed because one or more lines are too long

@ -0,0 +1,19 @@
## 1.0.2503312025-03-31
增加RecordApp.UniNativeUtsPlugin_OnJsCall接口App端搭配原生插件使用时可绑定接收配套原生录音插件事件原生插件新增PcmPlayer播放器支持流式播放、完整播放App端边录音边播放更流畅
## 1.0.2501112025-01-11
修复vue3 Fragments(multi-root 多个根节点)的兼容性问题修复uniapp Android自带的XXPermissions库在后台无法请求权限的问题仅限搭配原生录音插件可用
## 1.0.2410202024-10-20
适配HBuilder4.28 vue3 setup编译环境下$root.$scope无法读取的bugHBuilder4.29已修复此编译bug但似乎还是有不能使用的问题。如果setup内不能使用可尝试新建个vue组件然后使用选项式api来调用录音功能页面的setup内使用此vue组件
## 1.0.2409102024-09-10
- 新增RecordApp.UniMainCallBack_Register接口允许App renderjs层多次回调数据给逻辑层
- iOS App请求权限时会预先检查NSMicrophoneUsageDescription是否声明避免无声明时调用录音会崩溃
- 新增appNativePlugin_sampleRate原生插件录音选项
- Android App已提供后台录音保活功能启用后App在后台或锁屏后可继续正常录音
## 1.0.2406252024-06-25
调整UniWebViewCallAsync调用失败时返回更详细信息。android_audioSource默认值由1改成0新增ios_categoryOptions原生插件录音选项
## 1.0.2404092024-04-09
增加功能调用完善demo项目
## 1.0.2312082023-12-08
完善文档增加asr语音识别示例
## 1.0.2312012023-12-04
第一次发布

@ -0,0 +1,6 @@
<template>
<view>
<view style="font-weight: bold;">Recorder-UniCore Vue Component</view>
<view style="font-size:14px; color:#f60">无需手动显示本UI组件只需在script中正常引入 RecordApp + app-uni-support.js 即可实现 H5iOS Android App微信小程序 多端录音</view>
</view>
</template>

@ -0,0 +1,19 @@
《许可及服务协议》
**您以下称“用户”下载、使用我以下称“作者”提供的Recorder-UniCore组件含原生录音插件、uts插件以下统称“本组件”应当阅读并遵守本许可协议。请用户务必审慎阅读、充分理解各条款内容特别是免除或者限制责任的条款并选择接受或不接受。除非用户已阅读并接受本协议所有条款否则用户无权下载、使用本组件及相关服务用户的下载、使用等行为即视为用户已阅读并同意本许可协议的约束。**
1. 用户应当直接从作者许可的途径如作者的GitHub、Gitee仓库、已上架的DCloud插件市场、QQ群等途径中获取本组件其他途径获取到的组件代码是未经过作者授权的存在安全隐患可能会导致你的程序、资产受到侵害作者对因此给用户造成的损失不予负责。
2. 作者将积极并采取措施保护用户的信息和隐私;组件本身不会搜集存储任何用户信息。
3. 除法律法规有明确规定外,作者将尽最大努力确保本组件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解作者不能对此进行担保。
4. 用户理解,对于不可抗力及第三方原因导致的您的直接或间接损失,作者无法承担责任。
5. 用户因使用本组件进行生成、处理数据,由此引起或与有关的包括但不限于利润损失、资料损失、业务中断的损害赔偿或其它商业损害赔偿或损失,需由用户自行承担。
6. 如若发生赔偿、退款等行为,赔偿、退款等累计金额不得超过用户实际支付给作者的总金额。
7. 已授予的授权许可包括免费授权和已购买的原生录音插件、uts插件均仅限在授权指定的uni-app的应用标识AppID对应的项目上使用不可在其他项目上使用用户不得对本组件及其中的相关信息擅自出租、出借、销售、逆向工程、破解不得在未取得作者授权的情况下借助本组件发展与本组件有关联的衍生软件产品、服务、插件、外挂等。
8. 用户不得使用本组件从事违反法律法规政策、破坏公序良俗、损害公共利益的行为。

@ -0,0 +1,88 @@
{
"id": "Recorder-UniCore",
"displayName": "跨平台Recorder录音插件支持多种格式、音频可视化、实时上传、语音识别",
"version": "1.0.250331",
"description": "支持H5、Android iOS App、微信小程序mp3 wav pcm g711a g711u ogg amr 录音格式;实时帧回调处理 音频转码 波形动画显示 ASR语音转文字 无录制时长限制",
"keywords": [
"Recorder-UniCore",
"recorder-core",
"RecordApp",
"record",
"recording"
],
"repository": "https://github.com/xiangyuecn/Recorder",
"engines": {
"HBuilderX": "^3.6.11"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "753610399"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "录音权限"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "n",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "n",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

@ -0,0 +1,425 @@
**
**
# Recorder-UniCore组件uni-app内使用RecordApp录音
本组件使用`Recorder`开源库来进行录音和音频数据处理,使用`RecordApp`和本组件内的`app-uni-support.js`来适配到不同平台环境下进行录音。
- 支持vue2、vue3、nvue
- 支持编译成H5、Android App、iOS App、微信小程序
- 支持已有的大部分录音格式mp3、wav、pcm、amr、ogg、g711a、g711u等
- 支持实时处理包括变速变调、实时上传、ASR语音转文字
- 支持可视化波形显示;可配置回声消除、降噪;**注意:不支持通话时录音**
- 支持PCM音频流式播放、完整播放App端用原生插件边录音边播放更流畅
- 支持离线使用,本组件和配套原生插件均不依赖网络
- App端有配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可供搭配使用,兼容性和体验更好
**详细文档(含Demo项目)** [https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp](https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)
**Recorder开源库地址** [https://github.com/xiangyuecn/Recorder](https://github.com/xiangyuecn/Recorder)
如果github打不开可以[点此访问Gitee仓库地址](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp) 。
**
## 测试方法
**示例项目如果在HBuilder中编译失败请删掉node_modules目录重新手动执行npm install偶尔出现HBuilder自动创建项目依赖包不完整导致无法编译**
1. 在本插件市场页面右侧下载或导入示例项目或打开上面详细文档链接中的Demo源码
2. 在测试项目根目录执行 `npm install --registry=https://registry.npmmirror.com/` ,完成`recorder-core`依赖的安装
3. 在HBuilder中打开本测试项目文件夹
4. 在HBuilder中运行到浏览器、手机、微信小程序即可在不同环境下测试
5. 测试中提供了基础录音、播放、上传、WebSocket实时语音通话对讲、ASR语音识别等功能
**
**
# 集成到自己项目中
你可以直接参考上面的测试示例项目源码,里面的`main_recTest.vue`更容易入门示例项目中已经实现了很多功能简单使用可直接照抄Demo代码到你的项目中。
## 一、引入js文件
1. 在你的项目根目录安装`recorder-core``npm install recorder-core --registry=https://registry.npmmirror.com/`
2. 导入Recorder-UniCore组件插件市场下载本组件然后添加到你的项目中 `/uni_modules/Recorder-UniCore`
3. 项目配置好录音权限,参考下面的录音权限配置章节,**特别注意App后台录音配置、小程序权限声明**
4. 在需要录音的vue文件script内编写以下代码按需引入需要的js
``` html
<template>
<view>
... 建议template下只有一个根节点最外面套一层view如果不小心踩到了vue3的Fragments(multi-root 多个根节点)特性vue2编译会报错vue3不会可能会出现奇奇怪怪的兼容性问题
</view>
</template>
<script> /****/
//必须引入的Recorder核心文件路径是 /src/recorder-core.js 下同使用import、require都行
import Recorder from 'recorder-core' //注意如果未引用Recorder变量可能编译时会被优化删除如vue3 tree-shaking请改成 import 'recorder-core',或随便调用一下 Recorder.a=1 保证强引用
//必须引入的RecordApp核心文件文件路径是 /src/app-support/app.js
import RecordApp from 'recorder-core/src/app-support/app'
//所有平台必须引入的uni-app支持文件如果编译出现路径错误请把@换成 ../../ 这种)
<!-- import '@/uni_modules/Recorder-UniCore/app-uni-support.js' -->
/** 需要编译成微信小程序时,引入微信小程序支持文件 **/
// #ifdef MP-WEIXIN
import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
// #endif
/** H5、小程序环境中引入需要的格式编码器、可视化插件App环境中在renderjs中引入 **/
// 注意如果App中需要在逻辑层中调用Recorder的编码/转码功能,需要去掉此条件编译,否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
//按需引入你需要的录音格式支持文件如果需要多个格式支持把这些格式的编码引擎js文件统统引入进来即可
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码引擎(*-engine.js的话必须要加上
//可选的插件支持项,把需要的插件按需引入进来即可
import 'recorder-core/src/extensions/waveview'
// #endif
// ... 这后面写页面代码用选项式API风格vue2、vue3、setup组合式API风格仅vue3都可以
</script>
```
5. 编译成app时默认需要额外提供一个renderjs模块请照抄下面这段代码放到vue文件末尾
``` html
<!-- #ifdef APP -->
<script module="yourModuleName" lang="renderjs"> //APIvue2vue3setupAPIimport vue
/**需要编译成App时你需要添加一个renderjs模块然后一模一样的import上面那些js微信的js除外
因为App中默认是在renderjsWebView中进行录音和音频编码
。如果配置了 RecordApp.UniWithoutAppRenderjs=true 且未调用依赖renderjs的功能时如nvue、可视化、仅H5中可用的插件
可不提供此renderjs模块同时逻辑层中需要将相关import的条件编译去掉**/
import 'recorder-core'
import RecordApp from 'recorder-core/src/app-support/app'
// import '../../uni_modules/Recorder-UniCore/app-uni-support.js' //renderjs中似乎不支持"@/"打头的路径,如果编译路径错误请改正路径即可
//按需引入你需要的录音格式支持文件,和插件
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
import 'recorder-core/src/extensions/waveview'
export default {
mounted(){
//App的renderjs必须调用的函数传入当前模块this
RecordApp.UniRenderjsRegister(this);
},
methods: {
//这里定义的方法,在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
//调用逻辑层的方法,请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用二进制数据需转成base64来传递
}
}
</script>
<!-- #endif -->
```
**
**
## 二、调用录音
``` javascript
/**在逻辑层中编写**/
//import ... 上面那些import代码
//var vue3This=getCurrentInstance().proxy; //当用vue3 setup组合式 API (Composition API) 编写时直接在import后面取到当前实例this在需要this的地方传vue3This变量即可其他的和选项式 API (Options API) 没有任何区别import {getCurrentInstance} from 'vue'详细可以参考Demo项目中的 page_vue3____composition_api.vue
//RecordApp.UniNativeUtsPlugin={ nativePlugin:true }; //App中启用配套的原生录音插件支持配置后会使用原生插件进行录音没有原生插件时依旧使用renderjs H5录音
//App中提升后台录音的稳定性配置了原生插件后可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码WebWorker加速变成逻辑层中直接编码但会降低逻辑层性能后台运行时可避免部分手机WebView运行受限的影响
//App中提升后台录音的稳定性需要启用后台录音保活服务iOS不需要参考录音权限配置Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音也受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
export default {
data() { return {} } //视图没有引用到的变量无需放data里直接this.xxx使用
,mounted() {
this.isMounted=true;
//页面onShow时【必须调用】的函数传入当前组件this
RecordApp.UniPageOnShow(this);
}
,onShow(){ //onShow可能比mounted先执行页面可能还未准备好
if(this.isMounted) RecordApp.UniPageOnShow(this);
}
,methods:{
//请求录音权限
recReq(){
//编译成App时提供的授权许可编译成H5、小程序为免费授权可不填写如果未填写授权许可将会在App打开后第一次调用请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框
//RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';
//RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } }; //这个是Start中的audioTrackSet配置在h5H5、App+renderjs中必须提前配置因为h5中RequestPermission会直接打开录音
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.RequestPermission(()=>{
console.log("已获得录音权限,可以开始录音了");
},(msg,isUserNotAllow)=>{
if(isUserNotAllow){//用户拒绝了录音权限
//这里你应当编写代码进行引导用户给录音权限,不同平台分别进行编写
}
console.error("请求录音权限失败:"+msg);
});
}
//开始录音
,recStart(){
//Android App如果要后台录音需要启用后台录音保活服务iOS不需要需使用配套原生插件、或使用第三方保活插件
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ title:"正在录音" ,content:"正在录音中请勿关闭App运行" }).then(()=>{...}).catch((e)=>{...}) 注意必须RecordApp.RequestPermission得到权限后调用
//录音配置信息
var set={
type:"mp3",sampleRate:16000,bitRate:16 //mp3格式指定采样率hz、比特率kbps其他参数使用默认配置注意是数字的参数必须提供数字不要用字符串需要使用的type类型需提前把格式支持文件加载进来比如使用wav格式需要提前加载wav.js编码引擎
/*,audioTrackSet:{ //可选如果需要同时播放声音比如语音通话需要打开回声消除并不一定会生效打开后声音可能会从听筒播放部分环境下如小程序、App原生插件可调用接口切换成扬声器外放
//注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
echoCancellation:true,noiseSuppression:true,autoGainControl:true} */
,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
//全平台通用可实时上传发送数据配合Recorder.SampleData方法将buffers中的新数据连续的转换成pcm上传或使用mock方法将新数据连续的转码成其他格式上传可以参考Recorder文档里面的Demo片段列表 -> 实时转码并上传-通用版基于本功能可以做到实时转发数据、实时保存数据、实时语音识别ASR
//注意App里面是在renderjs中进行实际的音频格式编码操作此处的buffers数据是renderjs实时转发过来的修改此处的buffers数据不会改变renderjs中buffers所以不会改变生成的音频文件可在onProcess_renderjs中进行修改操作就没有此问题了如需清理buffers内存此处和onProcess_renderjs中均需要进行清理H5、小程序中无此限制
//注意如果你要用只支持在浏览器中使用的Recorder扩展插件App里面请在renderjs中引入此扩展插件然后在onProcess_renderjs中调用这个插件H5可直接在这里进行调用小程序不支持这类插件如果调用插件的逻辑比较复杂建议封装成js文件这样逻辑层、renderjs中直接import不需要重复编写
//H5、小程序等可视化图形绘制直接运行在逻辑层App里面需要在onProcess_renderjs中进行这些操作
// #ifdef H5 || MP-WEIXIN
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
// #endif
/*实时释放清理内存用于支持长时间录音在指定了有效的type时编码器内部可能还会有其他缓冲必须同时提供takeoffEncodeChunk才能清理内存否则type需要提供unknown格式来阻止编码器内部缓冲App的onProcess_renderjs中需要进行相同操作
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}
,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//App中在这里修改buffers会改变生成的音频文件但注意buffers会先转发到逻辑层onProcess后才会调用本方法因此在逻辑层的onProcess中需要重新修改一遍
//本方法可以返回truerenderjs中的onProcess将开启异步模式处理完后调用asyncEnd结束异步注意这里异步修改的buffers一样的不会在逻辑层的onProcess中生效
//App中是在renderjs中进行的可视化图形绘制因此需要写在这里this是renderjs模块的this也可以用This变量如果代码比较复杂请直接在renderjs的methods里面放个方法xxxFunc这里直接使用this.xxxFunc(args)进行调用
if(this.waveView) this.waveView.input(buffers[buffers.length-1],powerLevel,sampleRate);
/*和onProcess中一样进行释放清理内存用于支持长时间录音
if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
this.clearBufferIdx=newBufferIdx; */
}`
,onProcessBefore_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx){
//App中本方法会在逻辑层onProcess之前调用因此修改的buffers会转发给逻辑层onProcess本方法没有asyncEnd参数不支持异步处理
//一般无需提供本方法只用onProcess_renderjs就行renderjs的onProcess内部调用过程onProcessBefore_renderjs -> 转发给逻辑层onProcess -> onProcess_renderjs
}`
,takeoffEncodeChunk:true?null:(chunkBytes)=>{
//全平台通用实时接收到编码器编码出来的音频片段数据chunkBytes是Uint8Array二进制数据可以实时上传发送出去
//App中如果未配置RecordApp.UniWithoutAppRenderjs时建议提供此回调因为录音结束后会将整个录音文件从renderjs传回逻辑层由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了大点的文件传输会比较慢提供此回调后可避免Stop时产生超大数据回传
//App中使用原生插件时可方便的将数据实时保存到同一文件第一帧时append:false新建文件后面的append:true追加到文件
//RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
}
,takeoffEncodeChunk_renderjs:true?null:`function(chunkBytes){
//App中这里可以做一些仅在renderjs中才生效的事情不提供也行this是renderjs模块的this也可以用This变量
}`
,start_renderjs:`function(){
//App中可以放一个函数在Start成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情比如初始化不提供也行
}`
,stop_renderjs:`function(arrayBuffer,duration,mime){
//App中可以放一个函数在Stop成功时renderjs中会先调用这里的代码this是renderjs模块的this也可以用This变量
//放一些仅在renderjs中才生效的事情不提供也行
}`
};
RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
RecordApp.Start(set,()=>{
console.log("已开始录音");
//【稳如老狗WDT】可选的监控是否在正常录音有onProcess回调如果长时间没有回调就代表录音不正常
//var wdt=this.watchDogTimer=setInterval ... 请参考示例Demo的main_recTest.vue中的watchDogTimer实现
//创建音频可视化图形绘制App环境下是在renderjs中绘制H5、小程序等是在逻辑层中绘制因此需要提供两段相同的代码
//view里面放一个canvascanvas需要指定宽高下面style里指定了300*100
//<canvas type="2d" class="recwave-WaveView" style="width:300px;height:100px"></canvas>
RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
`,(canvas1)=>{
this.waveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
});
},(msg)=>{
console.error("开始录音失败:"+msg);
});
}
//暂停录音
,recPause(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Pause();
console.log("已暂停");
}
}
//继续录音
,recResume(){
if(RecordApp.GetCurrentRecOrNull()){
RecordApp.Resume();
console.log("继续录音中...");
}
}
//停止录音
,recStop(){
//RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{ close:true }) //关闭Android App后台录音保活服务
RecordApp.Stop((arrayBuffer,duration,mime)=>{
//全平台通用arrayBuffer是音频文件二进制数据可以保存成文件或者发送给服务器
//App中如果在Start参数中提供了stop_renderjsrenderjs中的函数会比这个函数先执行
//注意当Start时提供了takeoffEncodeChunk后你需要自行实时保存录音文件数据因此Stop时返回的arrayBuffer的长度将为0字节
//如果是H5环境也可以直接构造成Blob/File文件对象和Recorder使用一致
// #ifdef H5
var blob=new Blob([arrayBuffer],{type:mime});
console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
var file=new File([arrayBuffer],"recorder.mp3");
//uni.uploadFile({file:file, ...}) //参考demo中的test_upload_saveFile.vue
// #endif
//如果是App、小程序环境可以直接保存到本地文件然后调用相关网络接口上传
// #ifdef APP || MP-WEIXIN
RecordApp.UniSaveLocalFile("recorder.mp3",arrayBuffer,(savePath)=>{
console.log(savePath); //app保存的文件夹为`plus.io.PUBLIC_DOWNLOADS`,小程序为 `wx.env.USER_DATA_PATH` 路径
//uni.uploadFile({filePath:savePath, ...}) //参考demo中的test_upload_saveFile.vue
},(errMsg)=>{ console.error(errMsg) });
// #endif
},(msg)=>{
console.error("结束录音失败:"+msg);
});
}
}
}
```
**
**
**
**
# 录音权限配置、需要注意的细节
## 编译成H5时录音和权限
编译成H5时录音功能由Recorder H5提供无需额外处理录音权限。
**
## 编译成微信小程序时录音和权限
编译成微信小程序时,录音功能由小程序的`RecorderManager`提供,屏蔽了微信原有的底层细节(无录音时长限制)。
小程序录音需要用户授予录音权限,调用`RecordApp.RequestPermission`的时候会检查是否能正常录音,如果用户拒绝了录音权限,会进入错误回调,回调里面你应当编写代码检查`wx.getSetting`中的`scope.record`录音权限,然后引导用户进行授权(可调用`wx.openSetting`打开设置页面,方便用户给权限)。
**注意:上架小程序需要到小程序管理后台《[用户隐私保护指引](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html)》中声明录音权限,否则正式版将无法调用录音功能(请求权限时会直接走错误回调)。**
更多细节请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 测试项目文档。
**
## 编译成App时录音和权限
编译成App录音时分两种情况
1. 默认未配置`RecordApp.UniNativeUtsPlugin`未使用原生录音插件和uts插件会在renderjs中使用Recorder H5进行录音录音数据会实时回传到逻辑层。
2. 配置了`RecordApp.UniNativeUtsPlugin`使用原生录音插件或uts插件时会直接调用原生插件进行录音录音数据默认会传递到renderjs中进行音频编码处理WebWorker加速然后再实时回传到逻辑层如果配置了`RecordApp.UniWithoutAppRenderjs=true`时,音频编码处理将会在逻辑层中直接处理。
当App是在renderjs中使用H5进行录音时未使用原生录音插件和uts插件iOS上只支持14.3以上版本,**且iOS上每次进入页面后第一次请求录音权限时、或长时间无操作再请求录音权限时WebView均会弹出录音权限对话框**不同旧iOS版本低于iOS17下H5录音可能存在的问题在App中同样会存在使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)或uts插件时无以上问题和版本限制uts插件开发中暂不可用Android也无以上问题。
当音频编码是在renderjs中进行处理时录音结束后会将整个录音文件传回逻辑层由于uni-app的逻辑层和renderjs层大点的文件传输会比较慢**建议Start时使用takeoffEncodeChunk实时获取音频文件数据可避免Stop时产生超大数据回传**;配置了`RecordApp.UniWithoutAppRenderjs=true`后,因为音频编码直接是在逻辑层中进行,将不存在传输性能损耗,但会影响逻辑层的性能(正常情况轻微不明显),需要配套使用原生录音插件才可以进行此项配置。
在调用`RecordApp.RequestPermission`的时候,`Recorder-UniCore`组件会自动处理好App的系统录音权限只需要在uni-app项目的 `manifest.json` 中配置好Android和iOS的录音权限声明。
```
//Android需要勾选的权限第二个也必须勾选
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
【注意】Android如果需要在后台录音需要启用后台录音保活服务Android 9开始锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音renderjs中H5录音、原生插件录音均受影响请调用配套原生插件的`androidNotifyService`接口,或使用第三方保活插件
//iOS需要声明的权限
NSMicrophoneUsageDescription
【注意】iOS需要在 `App常用其它设置`->`后台运行能力`中提供`audio`配置不然App切到后台后立马会停止录音
```
**
## PCM音频流式播放、语音通话、回声消除、声音外放
在App、H5中均可使用H5版的[BufferStreamPlayer](https://gitee.com/xiangyuecn/Recorder/blob/master/src/extensions/buffer_stream.player.js)来实时流式播放语音其中App中需要在renderjs中加载BufferStreamPlayer在逻辑层中调用`RecordApp.UniWebViewVueCall`等方法将逻辑层中接收到的实时语音数据发送到renderjs中播放播放声音的同时进行录音声音可能会被录进去产生回声因此一般需要打开回声消除调用代码参考demo中的[test_realtime_voice.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_realtime_voice.vue)。
App中如果搭配使用了配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin)可以调用原生实现的PcmPlayer播放器实时流式播放PCM音频边录音边播放更流畅同时也支持完整播放比如AI语音合成的播放调用代码参考demo中的[test_player_nativePlugin_pcmPlayer.vue](https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_UniApp/pages/recTest/test_player_nativePlugin_pcmPlayer.vue)。
微信小程序请参考 [miniProgram-wx](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/miniProgram-wx) 文档里面的同名章节使用WebAudioContext播放。
配置audioTrackSet可尝试打开回声消除或者切换听筒播放或外放打开回声消除时一般会转为听筒播放显著降低回声。
``` js
//打开回声消除
RecordApp.Start({
... 更多配置参数请参考RecordApp文档
//此配置App、H5、小程序均可打开回声消除注意H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
,audioTrackSet:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}
//Android指定麦克风源App搭配原生插件、小程序可用0 DEFAULT 默认音频源1 MIC 主麦克风5 CAMCORDER 相机方向的麦6 VOICE_RECOGNITION 语音识别7 VOICE_COMMUNICATION 语音通信(带回声消除)
,android_audioSource:7 //提供此配置时优先级比audioTrackSet更高默认值为0
//iOS的AVAudioSession setCategory的withOptions参数值App搭配原生插件可用取值请参考配套原生插件文档中的iosSetDefault_categoryOptions
//,ios_categoryOptions:0x1|0x4 //默认值为5(0x1|0x4)
});
//App搭配原生插件时尝试切换听筒播放或外放
await RecordApp.UniNativeUtsPluginCallAsync("setSpeakerOff",{off:true或false});
//小程序尝试切换
wx.setInnerAudioOption({ speakerOn:false或true })
//H5不支持切换
```
**
**
**
# 详细文档、RecordApp方法、属性文档
请先阅读 [demo_UniApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)含Demo项目更高级使用还需深入阅读 [Recorder文档](https://gitee.com/xiangyuecn/Recorder)、[RecordApp文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample) 均为完整的一个README.md文档Recorder文档中包含了更丰富的示例代码基础录音、实时处理、格式转码、音频分析、音频混音、音频生成 等等大部分能在uniapp中直接使用。
**
**
**
# 本组件的授权许可限制
**本组件内的app-uni-support.js文件在uni-app中编译到App平台时仅供测试用App平台包括Android App、iOS App不可用于正式发布或商用正式发布或商用需先到DCloud插件市场购买[此带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)费用为¥199元赠送Android版原生插件即可获得授权许可**编译到其他平台时无此授权限制比如H5、小程序均为免费授权。
在App中如果未获得授权许可将会在App打开后第一次调用`RecordApp.RequestPermission`请求录音权限时弹出“未获得商用授权时App上仅供测试”提示框。
在DCloud插件市场购买了[带授权的插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin-Android)获得了授权后,请在调用`RecordApp.RequestPermission`请求录音权限前,赋值`RecordApp.UniAppUseLicense="我已获得UniAppID=***的商用授权"`星号为你项目的uni-app应用标识就不会弹提示框了或者直接使用配套的[原生录音插件](https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin),设置`RecordApp.UniNativeUtsPlugin`参数后,也不会弹提示框;其他情况请联系作者咨询,更多细节请参考[本组件的GitHub文档](https://gitee.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp)。
获取授权、需要技术支持、或有不清楚的地方可以联系我们客服联系方式QQ 1251654593 或者直接联系作者QQ 753610399 (回复可能没有客服及时)。
插件开发维护不易,感谢支持~
**
**

@ -138,9 +138,6 @@
</template>
<script>
import provinces from "../../libs/address/provinces.json";
import citys from "../../libs/address/citys.json";
import areas from "../../libs/address/areas.json";
import timeFormat from '../../libs/function/timeFormat.js';
/**
* picker picker弹出选择器
@ -321,9 +318,9 @@ export default {
startDate: "",
endDate: "",
valueArr: [],
provinces: provinces,
citys: citys[0],
areas: areas[0][0],
provinces: [],
citys: [],
areas: [],
province: 0,
city: 0,
area: 0,

@ -1,22 +1,14 @@
//
@import "./libs/css/color.scss";
// nvue
/* #ifndef APP-NVUE */
@import "./libs/css/style.vue.scss";
/* #endif */
// nvue
/* #ifdef APP-NVUE */
@import "./libs/css/style.nvue.scss";
/* #endif */
//
/* #ifdef MP */
@import "./libs/css/style.mp.scss";
/* #endif */
// H5
/* #ifdef H5 */
@import "./libs/css/style.h5.scss";
/* #endif */

@ -1,155 +0,0 @@
.u-type-primary-light {
color: $u-type-primary-light;
}
.u-type-warning-light {
color: $u-type-warning-light;
}
.u-type-success-light {
color: $u-type-success-light;
}
.u-type-error-light {
color: $u-type-error-light;
}
.u-type-info-light {
color: $u-type-info-light;
}
.u-type-primary-light-bg {
background-color: $u-type-primary-light;
}
.u-type-warning-light-bg {
background-color: $u-type-warning-light;
}
.u-type-success-light-bg {
background-color: $u-type-success-light;
}
.u-type-error-light-bg {
background-color: $u-type-error-light;
}
.u-type-info-light-bg {
background-color: $u-type-info-light;
}
.u-type-primary-dark {
color: $u-type-primary-dark;
}
.u-type-warning-dark {
color: $u-type-warning-dark;
}
.u-type-success-dark {
color: $u-type-success-dark;
}
.u-type-error-dark {
color: $u-type-error-dark;
}
.u-type-info-dark {
color: $u-type-info-dark;
}
.u-type-primary-dark-bg {
background-color: $u-type-primary-dark;
}
.u-type-warning-dark-bg {
background-color: $u-type-warning-dark;
}
.u-type-success-dark-bg {
background-color: $u-type-success-dark;
}
.u-type-error-dark-bg {
background-color: $u-type-error-dark;
}
.u-type-info-dark-bg {
background-color: $u-type-info-dark;
}
.u-type-primary-disabled {
color: $u-type-primary-disabled;
}
.u-type-warning-disabled {
color: $u-type-warning-disabled;
}
.u-type-success-disabled {
color: $u-type-success-disabled;
}
.u-type-error-disabled {
color: $u-type-error-disabled;
}
.u-type-info-disabled {
color: $u-type-info-disabled;
}
.u-type-primary {
color: $u-type-primary;
}
.u-type-warning {
color: $u-type-warning;
}
.u-type-success {
color: $u-type-success;
}
.u-type-error {
color: $u-type-error;
}
.u-type-info {
color: $u-type-info;
}
.u-type-primary-bg {
background-color: $u-type-primary;
}
.u-type-warning-bg {
background-color: $u-type-warning;
}
.u-type-success-bg {
background-color: $u-type-success;
}
.u-type-error-bg {
background-color: $u-type-error;
}
.u-type-info-bg {
background-color: $u-type-info;
}
.u-main-color {
color: $u-main-color;
}
.u-content-color {
color: $u-content-color;
}
.u-tips-color {
color: $u-tips-color;
}
.u-light-color {
color: $u-light-color;
}

@ -1,38 +0,0 @@
// uViewuni.scss
// uni.scss
// uni.scssscssmain.jsApp.vue
$u-main-color: #303133;
$u-content-color: #606266;
$u-tips-color: #909399;
$u-light-color: #c0c4cc;
$u-border-color: #e4e7ed;
$u-bg-color: #f3f4f6;
$u-type-primary: #2979ff;
$u-type-primary-light: #ecf5ff;
$u-type-primary-disabled: #a0cfff;
$u-type-primary-dark: #2b85e4;
$u-type-warning: #ff9900;
$u-type-warning-disabled: #fcbd71;
$u-type-warning-dark: #f29100;
$u-type-warning-light: #fdf6ec;
$u-type-success: #19be6b;
$u-type-success-disabled: #71d5a1;
$u-type-success-dark: #18b566;
$u-type-success-light: #dbf1e1;
$u-type-error: #fa3534;
$u-type-error-disabled: #fab6b6;
$u-type-error-dark: #dd6161;
$u-type-error-light: #fef0f0;
$u-type-info: #909399;
$u-type-info-disabled: #c8c9cc;
$u-type-info-dark: #82848a;
$u-type-info-light: #f4f4f5;
$u-form-item-height: 70rpx;
$u-form-item-border-color: #dcdfe6;

@ -7,7 +7,9 @@ let data = {
localBaseImg: "https://matripe-cms.oss-cn-beijing.aliyuncs.com/dailibaoming/APP/", // app图片前缀
cdnBaseImg: "https://matripe-cms.oss-cn-beijing.aliyuncs.com/dailibaoming/", // cdn图片公共前缀路径
v3BaseImg: "https://matripe-cms.oss-cn-beijing.aliyuncs.com/dailibaoming/v3/", // cdn图片公共前缀路径
fadanBaseImg: "https://matripe-cms.oss-cn-beijing.aliyuncs.com/dailibaoming/",
loginText: '请登录',
coziID:'7537572244600471579',
// #ifdef MP-WEIXIN
version: uni.getAccountInfoSync().miniProgram.version || "1.0.16",
// #endif

@ -0,0 +1,18 @@
import {
defineConfig
} from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
import {
visualizer
} from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
uni(),
visualizer({
emitFile: true,
filename: "111stats.html",
})
],
});
Loading…
Cancel
Save