volume.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <template>
  2. <view class="settings-container">
  3. <!-- 设置选项组 -->
  4. <view class="settings-section">
  5. <u-cell-group :border="false">
  6. <!-- 语音播报设置 -->
  7. <u-cell title="语音播报" label="控制除app启动提示外所有语音播报的开关" :border-bottom="true">
  8. <template #right-icon>
  9. <u-switch v-model="settings.voiceBroadcast" activeColor="#4cd964"></u-switch>
  10. </template>
  11. </u-cell>
  12. <!-- 点击音效设置 -->
  13. <u-cell title="点击音效" :border-bottom="true">
  14. <template #right-icon>
  15. <u-switch v-model="settings.clickSound" activeColor="#4cd964"></u-switch>
  16. </template>
  17. </u-cell>
  18. <!-- 启动提示语设置 -->
  19. <u-cell title="启动提示语" :border-bottom="true">
  20. <template #right-icon>
  21. <u-switch v-model="settings.startupPrompt" activeColor="#4cd964"></u-switch>
  22. </template>
  23. </u-cell>
  24. </u-cell-group>
  25. <!-- 启动提示语输入框 -->
  26. <view class="input-section">
  27. <u-textarea v-model="settings.startupText" placeholder="输入启动时要说的内容,比如:欢迎使用书嗨/很开心又见到你啦"
  28. height="100"></u-textarea>
  29. </view>
  30. <!-- 语速设置 -->
  31. <view class="slider-section">
  32. <view class="slider-title">
  33. <text>设置语速</text>
  34. <text class="subtitle">(部分机型不支持)</text>
  35. </view>
  36. <u-slider v-model="settings.speechRate" :min="0" :max="10" :step="1" showValue></u-slider>
  37. </view>
  38. <!-- 音调设置 -->
  39. <view class="slider-section">
  40. <view class="slider-title">
  41. <text>设置音调</text>
  42. <text class="subtitle">(部分机型不支持)</text>
  43. </view>
  44. <u-slider v-model="settings.pitch" :min="0" :max="10" :step="1" showValue></u-slider>
  45. </view>
  46. <!-- 操作按钮 -->
  47. <view class="button-group">
  48. <u-button type="primary" text="保存设置并试听" @click="saveAndTest"></u-button>
  49. <u-button type="warning" text="停止播放" @click="stopPlayback" :plain="true"></u-button>
  50. <u-button type="error" text="重启app" @click="restartApp" :plain="true"></u-button>
  51. </view>
  52. </view>
  53. </view>
  54. </template>
  55. <script setup>
  56. import {
  57. reactive,
  58. onMounted
  59. } from 'vue'
  60. // TTS 引擎实例
  61. let innerAudioContext = null;
  62. // 设置状态
  63. const settings = reactive({
  64. voiceBroadcast: true,
  65. clickSound: true,
  66. startupPrompt: true,
  67. startupText: '书嗨,不辜负每一个爱书的人',
  68. speechRate: 10,
  69. pitch: 10
  70. })
  71. // 检查并请求权限
  72. async function checkAndRequestPermissions() {
  73. // #ifdef APP-PLUS
  74. try {
  75. const status = await requestPermissions()
  76. if (!status) {
  77. uni.showModal({
  78. title: '提示',
  79. content: '请在系统设置中允许应用访问麦克风等权限,以便使用语音功能',
  80. confirmText: '去设置',
  81. success: (res) => {
  82. if (res.confirm) {
  83. // 跳转到应用设置页面
  84. if (uni.getSystemInfoSync().platform === 'android') {
  85. const main = plus.android.runtimeMainActivity()
  86. const Intent = plus.android.importClass('android.content.Intent')
  87. const Settings = plus.android.importClass('android.provider.Settings')
  88. const Uri = plus.android.importClass('android.net.Uri')
  89. const intent = new Intent()
  90. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
  91. intent.setData(Uri.fromParts('package', main.getPackageName(), null))
  92. main.startActivity(intent)
  93. } else {
  94. // iOS
  95. const UIApplication = plus.ios.import("UIApplication")
  96. const NSURL = plus.ios.import("NSURL")
  97. const setting = NSURL.URLWithString("app-settings:")
  98. const application = UIApplication.sharedApplication()
  99. application.openURL(setting)
  100. }
  101. }
  102. }
  103. })
  104. return false
  105. }
  106. return true
  107. } catch (error) {
  108. console.error('权限请求失败:', error)
  109. return false
  110. }
  111. // #endif
  112. // #ifdef H5
  113. // H5端检查Web Speech API权限
  114. if ('speechSynthesis' in window) {
  115. return true
  116. }
  117. uni.showToast({
  118. title: '当前浏览器不支持语音功能',
  119. icon: 'none'
  120. })
  121. return false
  122. // #endif
  123. // #ifdef MP-WEIXIN
  124. // 微信小程序一般不需要特殊权限
  125. return true
  126. // #endif
  127. }
  128. // 请求权限
  129. function requestPermissions() {
  130. return new Promise((resolve, reject) => {
  131. // #ifdef APP-PLUS
  132. plus.android.requestPermissions(
  133. [
  134. 'android.permission.RECORD_AUDIO',
  135. 'android.permission.MODIFY_AUDIO_SETTINGS',
  136. 'android.permission.WRITE_EXTERNAL_STORAGE',
  137. 'android.permission.READ_EXTERNAL_STORAGE'
  138. ],
  139. function(resultObj) {
  140. if (resultObj.granted.length === 4) {
  141. resolve(true)
  142. } else {
  143. resolve(false)
  144. }
  145. },
  146. function(error) {
  147. reject(error)
  148. }
  149. )
  150. // #endif
  151. // 其他平台直接返回true
  152. resolve(true)
  153. })
  154. }
  155. // 初始化 TTS 时检查权限
  156. async function initTTS() {
  157. const hasPermission = await checkAndRequestPermissions()
  158. if (!hasPermission) {
  159. return
  160. }
  161. // 检查平台是否支持 TTS
  162. if (!uni.createInnerAudioContext) {
  163. uni.showToast({
  164. title: '当前设备不支持语音功能',
  165. icon: 'none'
  166. })
  167. return
  168. }
  169. try {
  170. // 创建音频实例
  171. innerAudioContext = uni.createInnerAudioContext()
  172. // 监听错误
  173. innerAudioContext.onError((res) => {
  174. console.error('音频播放错误:', res.errMsg)
  175. uni.showToast({
  176. title: '语音播放失败',
  177. icon: 'none'
  178. })
  179. })
  180. } catch (error) {
  181. console.error('TTS初始化失败:', error)
  182. }
  183. }
  184. // 文字转语音
  185. async function textToSpeech(text) {
  186. const hasPermission = await checkAndRequestPermissions()
  187. if (!hasPermission) {
  188. return
  189. }
  190. if (!settings.voiceBroadcast) return
  191. try {
  192. // 构建 TTS 参数
  193. const ttsParams = {
  194. lang: 'zh-CN',
  195. speed: settings.speechRate / 10, // 将 0-10 转换为 0-1
  196. pitch: settings.pitch / 10, // 将 0-10 转换为 0-1
  197. volume: 1,
  198. text: text
  199. }
  200. // #ifdef APP-PLUS
  201. // App 端使用原生 TTS
  202. const TTSModule = uni.requireNativePlugin('nrb-tts-plugin')
  203. TTSModule && TTSModule.init({
  204. "lang": "ZH",
  205. "country": "CN"
  206. }, res => {
  207. if (res.success == 0) {
  208. TTSModule.speak(text, ttsParams, (e) => {
  209. console.log(e, 'dsadsads')
  210. })
  211. }
  212. })
  213. // #endif
  214. // #ifdef H5
  215. // H5端使用Web Speech API
  216. if ('speechSynthesis' in window) {
  217. const utterance = new SpeechSynthesisUtterance(text)
  218. utterance.lang = ttsParams.lang
  219. utterance.rate = ttsParams.speed
  220. utterance.pitch = ttsParams.pitch
  221. utterance.volume = ttsParams.volume
  222. speechSynthesis.speak(utterance)
  223. }
  224. // #endif
  225. // #ifdef MP-WEIXIN
  226. // 微信小程序使用微信同声传译插件
  227. const plugin = requirePlugin("WechatSI")
  228. plugin.textToSpeech({
  229. lang: "zh_CN",
  230. tts: text,
  231. success: function(res) {
  232. innerAudioContext.src = res.filename
  233. innerAudioContext.play()
  234. },
  235. fail: function(res) {
  236. console.error("转换失败", res)
  237. }
  238. })
  239. // #endif
  240. } catch (error) {
  241. console.error('TTS播放错误:', error)
  242. uni.showToast({
  243. title: '语音播放失败',
  244. icon: 'none'
  245. })
  246. }
  247. }
  248. // 保存设置并试听
  249. function saveAndTest() {
  250. // 保存设置到本地存储
  251. uni.setStorageSync('tts_settings', settings)
  252. // 试听当前设置
  253. textToSpeech(settings.startupText)
  254. uni.showToast({
  255. title: '设置已保存',
  256. icon: 'success'
  257. })
  258. }
  259. // 停止播放
  260. function stopPlayback() {
  261. // #ifdef APP-PLUS
  262. const TTSModule = uni.requireNativePlugin('nrb-tts-plugin')
  263. if (TTSModule && TTSModule.stop) {
  264. TTSModule.stop()
  265. }
  266. // #endif
  267. // #ifdef H5
  268. if ('speechSynthesis' in window) {
  269. speechSynthesis.cancel()
  270. }
  271. // #endif
  272. // #ifdef MP-WEIXIN
  273. if (innerAudioContext) {
  274. innerAudioContext.stop()
  275. }
  276. // #endif
  277. uni.showToast({
  278. title: '已停止播放',
  279. icon: 'none'
  280. })
  281. }
  282. // 重启应用
  283. function restartApp() {
  284. uni.showModal({
  285. title: '提示',
  286. content: '确定要重启应用吗?',
  287. success: function(res) {
  288. if (res.confirm) {
  289. // #ifdef APP-PLUS
  290. plus.runtime.restart()
  291. // #endif
  292. // #ifdef H5 || MP-WEIXIN
  293. uni.reLaunch({
  294. url: '/pages/index/index'
  295. })
  296. // #endif
  297. }
  298. }
  299. })
  300. }
  301. // 加载保存的设置
  302. function loadSettings() {
  303. try {
  304. const savedSettings = uni.getStorageSync('tts_settings')
  305. if (savedSettings) {
  306. Object.assign(settings, savedSettings)
  307. }
  308. } catch (error) {
  309. console.error('加载设置失败:', error)
  310. }
  311. }
  312. // 播放点击音效
  313. function playClickSound() {
  314. if (!settings.clickSound) return
  315. const audio = uni.createInnerAudioContext()
  316. audio.src = '/static/audio/click.mp3' // 确保添加点击音效文件
  317. audio.play()
  318. }
  319. // 生命周期钩子
  320. onMounted(async () => {
  321. await initTTS()
  322. loadSettings()
  323. })
  324. </script>
  325. <style lang="scss" scoped>
  326. .settings-container {
  327. min-height: 100vh;
  328. background-color: #f5f5f5;
  329. padding-bottom: 40rpx;
  330. .card-head {
  331. display: flex;
  332. align-items: center;
  333. padding: 10rpx 0;
  334. .head-title {
  335. margin-left: 10rpx;
  336. font-size: 32rpx;
  337. font-weight: bold;
  338. }
  339. }
  340. .settings-section {
  341. margin: 20rpx;
  342. background-color: #ffffff;
  343. border-radius: 12rpx;
  344. padding: 20rpx;
  345. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  346. }
  347. .input-section {
  348. margin: 30rpx 0;
  349. padding: 0 20rpx;
  350. }
  351. .slider-section {
  352. margin: 30rpx 0;
  353. padding: 0 20rpx;
  354. .slider-title {
  355. margin-bottom: 20rpx;
  356. font-size: 28rpx;
  357. color: #333;
  358. .subtitle {
  359. font-size: 24rpx;
  360. color: #999;
  361. margin-left: 10rpx;
  362. }
  363. }
  364. }
  365. .button-group {
  366. margin-top: 50rpx;
  367. padding: 0 20rpx;
  368. :deep(.u-button) {
  369. margin-bottom: 20rpx;
  370. }
  371. }
  372. }
  373. </style>