index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <!-- 添加一个类似鼠标hover事件 -->
  3. <div class="marquee-box">
  4. <div class="scroll-area">
  5. <audio
  6. :ref="`audioPlayer${config.code}`"
  7. muted
  8. autoplay
  9. crossorigin="anonymous"
  10. />
  11. <!-- 设置margin,使内容 有从无到有的出现效果 -->
  12. <div
  13. class="marquee-container"
  14. @mouseenter.stop="mouseenter"
  15. @mouseleave.stop="mouseleave"
  16. >
  17. <div class="icon">
  18. <icon-svg
  19. v-if="config.customize.icon.name && config.customize.icon.position === 'left'"
  20. :name="config.customize.icon.name"
  21. :style="{ color: config.customize.icon.color, width: config.customize.fontSize + 'px',height: config.customize.fontSize + 'px' }"
  22. />
  23. </div>
  24. <svg class="svg-container">
  25. <defs>
  26. <linearGradient
  27. :id="'backgroundGradient-' + config.code"
  28. :x1="0"
  29. :y1="['to top right'].includes(config.customize.bgGradientDirection) ? '100%' : '0'"
  30. :x2="['to right', 'to bottom right', 'to top right'].includes(config.customize.bgGradientDirection) ? '100%' : '0'"
  31. :y2="['to bottom', 'to bottom right'].includes(config.customize.bgGradientDirection) ? '100%' : '0'"
  32. >
  33. <stop
  34. offset="0%"
  35. :stop-color="config.customize.backgroundColorType === 'pure' ? config.customize.backgroundColor : config.customize.bgGradientColor0"
  36. />
  37. <stop
  38. offset="100%"
  39. :stop-color="config.customize.backgroundColorType === 'pure' ? config.customize.backgroundColor : config.customize.bgGradientColor1"
  40. />
  41. </linearGradient>
  42. <linearGradient
  43. :id="'textGradient-' + config.code"
  44. :x1="0"
  45. :y1="['to top right'].includes(config.customize.textGradientDirection) ? '100%' : '0'"
  46. :x2="['to right', 'to bottom right', 'to top right'].includes(config.customize.textGradientDirection) ? '100%' : '0'"
  47. :y2="['to bottom', 'to bottom right'].includes(config.customize.textGradientDirection) ? '100%' : '0'"
  48. >
  49. <stop
  50. offset="0%"
  51. :stop-color="config.customize.textColorType === 'pure' ? config.customize.textColor : config.customize.textGradientColor0"
  52. />
  53. <stop
  54. offset="100%"
  55. :stop-color="config.customize.textColorType === 'pure' ? config.customize.textColor : config.customize.textGradientColor1"
  56. />
  57. </linearGradient>
  58. </defs>
  59. <rect
  60. v-if="config.customize.backgroundColorType !== 'transparent'"
  61. width="100%"
  62. height="100%"
  63. :fill="`url(#backgroundGradient-${config.code})`"
  64. />
  65. <text
  66. :x="10"
  67. :y="config.customize.fontSize"
  68. :style="{ fontSize: config.customize.fontSize + 'px', fontWeight: config.customize.fontWeight }"
  69. :fill="`url(#textGradient-${config.code})`"
  70. >
  71. <animate
  72. v-if="isAnimate"
  73. :attributeName="attributeName[config.customize.direction]"
  74. :from="from[config.customize.direction]"
  75. :to="to[config.customize.direction]"
  76. :dur="config.customize.dur + 's'"
  77. repeatCount="indefinite"
  78. />
  79. {{ config.customize.title }}
  80. </text>
  81. </svg>
  82. <div class="icon">
  83. <icon-svg
  84. v-if="config.customize.icon.name && config.customize.icon.position === 'right'"
  85. :name="config.customize.icon.name"
  86. :style="{ color: config.customize.icon.color, width: config.customize.fontSize + 'px',height: config.customize.fontSize + 'px' }"
  87. />
  88. </div>
  89. </div>
  90. </div>
  91. <div
  92. v-show="config.customize.voiceBroadcast && showVoiceSwitch"
  93. class="voice-switch"
  94. :style="{fontSize:config.customize.fontSize + 'px',right:config.customize.fontSize + 5 + 'px',}"
  95. @mouseenter.stop="mouseenter"
  96. >
  97. <i
  98. :class="voiceSwitchValue ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'"
  99. @click="voiceSwitch"
  100. />
  101. </div>
  102. </div>
  103. </template>
  104. <script>
  105. import Speech from 'speak-tts'
  106. import { EventBus } from 'data-room-ui/js/utils/eventBus'
  107. import commonMixins from 'data-room-ui/js/mixins/commonMixins'
  108. import paramsMixins from 'data-room-ui/js/mixins/paramsMixins'
  109. import linkageMixins from 'data-room-ui/js/mixins/linkageMixins'
  110. import { settingToTheme } from 'data-room-ui/js/utils/themeFormatting'
  111. import cloneDeep from 'lodash/cloneDeep'
  112. import IconSvg from 'data-room-ui/SvgIcon'
  113. import { get } from 'sortablejs'
  114. export default {
  115. name: 'Marquee',
  116. props: {
  117. // 卡片的属性
  118. config: {
  119. type: Object,
  120. default: () => ({})
  121. }
  122. },
  123. components: {
  124. IconSvg
  125. },
  126. data () {
  127. return {
  128. showVoiceSwitch: false,
  129. visibilityState: false,
  130. voiceSwitchValue: true,
  131. customClass: {},
  132. attributeName: {
  133. right: 'x',
  134. left: 'x',
  135. top: 'y',
  136. bottom: 'y'
  137. },
  138. // 动画开始
  139. from: {
  140. left: '-100%',
  141. right: '100%',
  142. top: '-100%',
  143. bottom: '100%'
  144. },
  145. // 动画结束
  146. to: {
  147. left: '100%',
  148. right: '-100%',
  149. top: '100%',
  150. bottom: '-100%'
  151. },
  152. isAnimate: true,
  153. // 组件内部数据
  154. innerData: null,
  155. // 音频播放
  156. audio: null,
  157. // 音频地址
  158. isPlayAudio: null,
  159. // 语音播报
  160. speech: null,
  161. isInit: false,
  162. firstSpeech: true,
  163. numberBroadcasts: 0
  164. }
  165. },
  166. computed: {
  167. // speechText
  168. speechText () {
  169. return this.config.customize.title || ''
  170. },
  171. audioSrc: {
  172. get () {
  173. return this.config?.option?.data?.[this.config?.dataSource?.metricField] || ''
  174. },
  175. set (val) {
  176. this.config.option.data[this.config.dataSource.metricField] = val
  177. }
  178. }
  179. },
  180. watch: {
  181. speechText (val) {
  182. if (!this.isPreview && this.config.customize.voiceBroadcast && !this.isInit && !this.firstSpeech) {
  183. this.speechBroadcast(val)
  184. } else {
  185. if (this.speech) {
  186. this.speech = null
  187. }
  188. }
  189. },
  190. deep: true,
  191. audioSrc (val) {
  192. if (this.config.customize.voiceBroadcast) {
  193. if (this.audio) {
  194. this.audio.src = val
  195. this.audio.play()
  196. }
  197. } else {
  198. if (this.aduio) {
  199. this.aduio.pause()
  200. this.aduio = null
  201. }
  202. }
  203. }
  204. },
  205. mixins: [paramsMixins, commonMixins, linkageMixins],
  206. mounted () {
  207. this.chartInit()
  208. // 如果点击了生成图片,则先关闭动画
  209. EventBus.$on('stopMarquee', () => {
  210. this.isAnimate = false
  211. })
  212. // 图片生成完成后,再开启动画
  213. EventBus.$on('startMarquee', () => {
  214. this.isAnimate = true
  215. })
  216. // 如果删除了组件
  217. EventBus.$on('deleteComponent', (codes) => {
  218. if (codes.includes(this.config.code)) {
  219. if (this.audio) {
  220. this.audio.pause()
  221. this.audio = null
  222. }
  223. if (this.speech) {
  224. this.speech = null
  225. }
  226. }
  227. })
  228. this.speech = null
  229. this.isInit = true
  230. // 如果是预览模式的话,则弹出对话框,当前大屏存在语音播报,是否开启语音播报
  231. if (this.isPreview && this.config.customize.voiceBroadcast) {
  232. this.$confirm('当前大屏存在语音播报,是否开启语音播报?若开启请点击确认或者回车', '提示', {
  233. confirmButtonText: '确定',
  234. cancelButtonText: '取消',
  235. type: 'warning',
  236. customClass: 'bs-el-message-box'
  237. }).then(() => {
  238. if (this.audioSrc) {
  239. this.audio.play()
  240. } else {
  241. this.speech = null
  242. this.speechBroadcast(this.config.customize.title)
  243. this.isInit = false
  244. }
  245. }).catch(() => { })
  246. }
  247. document.addEventListener('visibilitychange', this.handleVisibilityChange)
  248. },
  249. beforeDestroy () {
  250. EventBus.$off('stopMarquee')
  251. EventBus.$off('startMarquee')
  252. EventBus.$off('deleteComponent')
  253. },
  254. methods: {
  255. dataFormatting (config, data) {
  256. // 数据返回成功则赋值
  257. if (data.success) {
  258. data = data.data
  259. // 获取到后端返回的数据,有则赋值
  260. if (config.dataHandler) {
  261. try {
  262. // 此处函数处理data
  263. eval(config.dataHandler)
  264. } catch (e) {
  265. console.info(e)
  266. }
  267. }
  268. config.option.data = data
  269. config.customize.title = config.option.data[config.dataSource.dimensionField] || config.customize.title
  270. this.innerData = config
  271. // 语音播报
  272. } else {
  273. // 数据返回失败则赋前端的模拟数据
  274. config.option.data = []
  275. }
  276. // 清除上一个visibilitychange监听,重新开始监听
  277. if (this.voiceSwitchValue && !this.visibilityState && this.isInit) {
  278. this.voiceBroadcast(config)
  279. }
  280. return config
  281. },
  282. // 语音播报
  283. voiceBroadcast (config) {
  284. const innerData = this.innerData || config
  285. if (innerData) {
  286. if (config.customize.voiceBroadcast) {
  287. if (innerData?.dataSource?.businessKey && innerData?.option?.data[this.innerData.dataSource.metricField]) {
  288. // 如果aduio存在,先销毁这个实例,或者替换它的URL
  289. if (this.aduio) {
  290. this.aduio.pause()
  291. this.aduio = null
  292. }
  293. // 获取音频元素
  294. this.audio = this.$refs[`audioPlayer${config.code}`]
  295. this.audio.src = innerData.option.data[this.innerData.dataSource.metricField]
  296. this.audio.play()
  297. } else if (config.customize.title) {
  298. // 页面初始化不执行
  299. if (!this.isInit) {
  300. this.speechBroadcast(config.customize.title)
  301. }
  302. }
  303. } else {
  304. if (this.aduio) {
  305. this.aduio.pause()
  306. this.aduio = null
  307. }
  308. }
  309. }
  310. },
  311. // 语音播报
  312. speechBroadcast (text) {
  313. this.numberBroadcasts = 0
  314. this.speech = new Speech()
  315. this.speech.setLanguage('zh-CN')
  316. this.speech.pitch = 1
  317. this.speech.init()
  318. if (this.speech.hasBrowserSupport()) {
  319. if (this.numberBroadcasts < 1) {
  320. this.speech.speak({ text: text })
  321. this.numberBroadcasts += 1
  322. }
  323. } else {
  324. this.$message({
  325. message: '您的浏览器不支持语音播报',
  326. type: 'warning'
  327. })
  328. }
  329. },
  330. changeStyle (config) {
  331. config = { ...this.config, ...config }
  332. if (config.customize.voiceBroadcast && this.isInit && !config?.option?.data?.[this.config?.dataSource?.metricField]) {
  333. this.isInit = false
  334. this.speechBroadcast(config.customize.title)
  335. this.$nextTick(() => {
  336. this.firstSpeech = false
  337. })
  338. }
  339. // 样式改变时更新主题配置
  340. config.theme = settingToTheme(cloneDeep(config), this.customTheme)
  341. this.changeChartConfig(config)
  342. if (config.code === this.activeCode) {
  343. this.changeActiveItemConfig(config)
  344. }
  345. },
  346. // 监听页面是否可见
  347. handleVisibilityChange () {
  348. if (document.visibilityState === 'hidden') {
  349. this.visibilityState = true
  350. if (this.audio) {
  351. this.audio.pause()
  352. }
  353. if (this.speech) {
  354. this.speech = null
  355. }
  356. } else {
  357. this.visibilityState = false
  358. if (this.audio) {
  359. this.audio.play()
  360. }
  361. if (this.speech) {
  362. this.speech.resume()
  363. }
  364. }
  365. },
  366. voiceSwitch () {
  367. this.voiceSwitchValue = !this.voiceSwitchValue
  368. if (this.voiceSwitchValue) {
  369. if (this.audio) {
  370. try {
  371. this.audio.play()
  372. } catch (e) {
  373. console.info(e)
  374. }
  375. }
  376. if (this.speech) {
  377. this.speech.resume()
  378. }
  379. } else {
  380. if (this.audio) {
  381. try {
  382. this.audio.pause()
  383. } catch (e) {
  384. console.info(e)
  385. }
  386. }
  387. if (this.speech) {
  388. this.speech.pause()
  389. }
  390. }
  391. },
  392. mouseenter () {
  393. this.showVoiceSwitch = true
  394. },
  395. mouseleave () {
  396. this.showVoiceSwitch = false
  397. }
  398. }
  399. }
  400. </script>
  401. <style lang="scss" scoped>
  402. .marquee-box {
  403. width: 100%;
  404. height: 100%;
  405. user-select: none;
  406. white-space: nowrap;
  407. overflow: hidden;
  408. position: relative;
  409. .scroll-area {
  410. width: 100%;
  411. height: 100%;
  412. .marquee-container {
  413. width: 100%;
  414. height: 100%;
  415. display: flex;
  416. .svg-container {
  417. width: 100%;
  418. height: 100%;
  419. }
  420. }
  421. }
  422. .icon {
  423. position: relative;
  424. top: 0;
  425. // 清除浮动
  426. }
  427. }
  428. .voice-switch{
  429. position: absolute;
  430. cursor: pointer;
  431. bottom: 5px;
  432. color: #fff;
  433. }
  434. </style>