PageDesignTop.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. <template>
  2. <div class="page-top-setting-wrap">
  3. <div class="logo-wrap item-wrap">
  4. <img
  5. class="menu-img"
  6. src="../BigScreenDesign/images/app.png"
  7. alt="返回"
  8. @click="goBackManage"
  9. >
  10. <span class="logo-text name-span">{{ pageInfo.name }}</span>
  11. </div>
  12. <div class="head-btn-group">
  13. <span style="margin-right:8px;font-size:12px">缩放</span>
  14. <el-input-number
  15. class="bs-el-input-number"
  16. style="margin-right:10px"
  17. :value="zoom"
  18. :min="1"
  19. label="描述文字"
  20. @change="changeZoom"
  21. />
  22. <CusBtn
  23. :loading="saveAndPreviewLoading"
  24. @click.native="changeZoom('auto')"
  25. >
  26. 自适应
  27. </CusBtn>
  28. <el-dropdown
  29. trigger="click"
  30. class="align-list-dropdown"
  31. >
  32. <CusBtn type="primary">
  33. 对齐方式<i class="el-icon-arrow-down el-icon--right" />
  34. </CusBtn>
  35. <el-dropdown-menu
  36. slot="dropdown"
  37. class="align-dropdown-menu"
  38. >
  39. <el-dropdown-item
  40. v-for="(mode, index) in alignList"
  41. :key="mode.value"
  42. @click.native="setAlign(mode.value)"
  43. >
  44. <icon-svg
  45. style="padding:3px 8px"
  46. :name="iconList[index]"
  47. />
  48. <span style="color: #bcc9d4">{{ mode.label }}</span>
  49. </el-dropdown-item>
  50. </el-dropdown-menu>
  51. </el-dropdown>
  52. <CusBtn
  53. :loading="saveAndPreviewLoading"
  54. @click.native="designAssign()"
  55. >
  56. 设计分工
  57. </CusBtn>
  58. <CusBtn @click.native="showHostory">
  59. 历史操作
  60. </CusBtn>
  61. <CusBtn
  62. :disabled="undoDisabled"
  63. @click.native="undo(true)"
  64. >
  65. <i class="iconfont-bigscreen icon-jiantouqianjin icon-reverse" />
  66. </CusBtn>
  67. <CusBtn
  68. :disabled="redoDisabled"
  69. @click.native="undo(false)"
  70. >
  71. <i class="iconfont-bigscreen icon-jiantouqianjin" />
  72. </CusBtn>
  73. <CusBtn
  74. :loading="saveAndPreviewLoading"
  75. @click.native="createdImg()"
  76. >
  77. 生成图片
  78. </CusBtn>
  79. <CusBtn
  80. :loading="saveAndPreviewLoading"
  81. @click.native="execRun()"
  82. >
  83. 预览
  84. </CusBtn>
  85. <CusBtn
  86. :loading="saveLoading"
  87. @click="save('saveLoading')"
  88. >
  89. 保存
  90. </CusBtn>
  91. <CusBtn @click="empty">
  92. 清空
  93. </CusBtn>
  94. <CusBtn @click="showPageInfo">
  95. 设置
  96. </CusBtn>
  97. <CusBtn @click="updateRightVisiable">
  98. <i
  99. class="iconfont-bigscreen"
  100. :class="rightFold ? 'icon-zhankaicaidan' : 'icon-shouqicaidan'"
  101. />
  102. </CusBtn>
  103. </div>
  104. <ChooseTemplateDialog
  105. ref="ChooseTemplateDialog"
  106. :has-create="false"
  107. :page-info="pageInfo"
  108. @replaceItByTemplate="replaceItByTemplate"
  109. />
  110. <AssignDialog ref="AssignDialog" />
  111. <HistoryList ref="HistoryList" />
  112. </div>
  113. </template>
  114. <script>
  115. import { EventBus } from 'data-room-ui/js/utils/eventBus'
  116. import { toJpeg, toPng } from 'html-to-image'
  117. import { mapMutations, mapActions, mapState } from 'vuex'
  118. import { saveScreen } from 'data-room-ui/js/api/bigScreenApi'
  119. import ChooseTemplateDialog from 'data-room-ui/BigScreenManagement/ChooseTemplateDialog.vue'
  120. // import _ from 'lodash'
  121. import cloneDeep from 'lodash/cloneDeep'
  122. import uniqBy from 'lodash/uniqBy'
  123. import { stringifyObjectFunctions } from 'data-room-ui/js/utils/evalFunctions'
  124. import AssignDialog from 'data-room-ui/BigScreenDesign/AssignDialog/index.vue'
  125. import HistoryList from 'data-room-ui/BigScreenDesign/HistoryList/index.vue'
  126. import CusBtn from './BtnLoading'
  127. import icons from 'data-room-ui/assets/images/alignIcon/export'
  128. import IconSvg from 'data-room-ui/SvgIcon'
  129. import {
  130. showSize,
  131. dataURLtoBlob,
  132. translateBlobToBase64
  133. } from 'data-room-ui/js/utils/compressImg'
  134. import * as imageConversion from 'image-conversion'
  135. export default {
  136. name: 'PageTopSetting',
  137. components: {
  138. IconSvg,
  139. ChooseTemplateDialog,
  140. AssignDialog,
  141. CusBtn,
  142. HistoryList
  143. },
  144. props: {
  145. code: {
  146. type: String,
  147. default: ''
  148. },
  149. rightFold: {
  150. type: Boolean,
  151. default: false
  152. }
  153. },
  154. data () {
  155. return {
  156. iconList: icons.getNameList(),
  157. alignList: [
  158. {
  159. label: '左侧对齐',
  160. value: 'left'
  161. },
  162. {
  163. label: '居中对齐',
  164. value: 'center'
  165. },
  166. {
  167. label: '右侧对齐',
  168. value: 'right'
  169. },
  170. {
  171. label: '顶部对齐',
  172. value: 'top'
  173. },
  174. {
  175. label: '中部对齐',
  176. value: 'middle'
  177. },
  178. {
  179. label: '底部对齐',
  180. value: 'bottom'
  181. },
  182. {
  183. label: '水平均分',
  184. value: 'levelAround'
  185. },
  186. {
  187. label: '垂直均分',
  188. value: 'verticalAround'
  189. }
  190. ],
  191. appInfo: '',
  192. saveLoading: false,
  193. createdImgLoading: false,
  194. saveAndPreviewLoading: false
  195. }
  196. },
  197. computed: {
  198. ...mapState({
  199. pageInfo: (state) => state.bigScreen.pageInfo,
  200. timelineStore: (state) => state.bigScreen.timelineStore,
  201. currentTimeLine: (state) => state.bigScreen.currentTimeLine,
  202. activeCodes: state => state.bigScreen.activeCodes,
  203. zoom: (state) => state.bigScreen.zoom
  204. }),
  205. pageCode () {
  206. return this.$route.query.code || this.code
  207. },
  208. undoDisabled () {
  209. return Boolean(this.currentTimeLine <= 1)
  210. },
  211. redoDisabled () {
  212. return Boolean(
  213. !this.timelineStore?.length ||
  214. (
  215. this.currentTimeLine &&
  216. this.currentTimeLine === this.timelineStore?.length
  217. )
  218. )
  219. }
  220. },
  221. methods: {
  222. ...mapActions({
  223. initLayout: 'bigScreen/initLayout'
  224. }),
  225. ...mapMutations({
  226. changeActiveCode: 'bigScreen/changeActiveCode',
  227. changeActiveItem: 'bigScreen/changeActiveItem',
  228. changePageInfo: 'bigScreen/changePageInfo',
  229. undoTimeLine: 'bigScreen/undoTimeLine',
  230. saveTimeLine: 'bigScreen/saveTimeLine'
  231. }),
  232. changeZoom (val) {
  233. this.$emit('changeZoom', val)
  234. },
  235. setAlign (command) {
  236. const pageInfo = cloneDeep(this.pageInfo)
  237. // 获取所有选中的组件
  238. let activeChartList = pageInfo.chartList.filter((chart) => {
  239. return this.activeCodes.some(code => (code === chart.code))
  240. })
  241. // 找到选中组件内的xy最大最小值
  242. const maxXW = Math.max.apply(Math, activeChartList.map(item => { return item.x + item.w }))
  243. let maxX = Math.max.apply(Math, activeChartList.map(item => { return item.x }))
  244. const minX = Math.min.apply(Math, activeChartList.map(item => { return item.x }))
  245. const maxYH = Math.max.apply(Math, activeChartList.map(item => { return item.y + item.h }))
  246. const maxY = Math.max.apply(Math, activeChartList.map(item => { return item.y }))
  247. const minY = Math.min.apply(Math, activeChartList.map(item => { return item.y }))
  248. const centerW = maxXW - minX
  249. const centerH = maxY - minY
  250. switch (command) {
  251. case 'left':
  252. activeChartList.forEach((chart) => {
  253. chart.x = minX
  254. })
  255. break
  256. case 'center':
  257. // eslint-disable-next-line no-case-declarations
  258. activeChartList.forEach((chart) => {
  259. chart.x = (centerW - chart.w) / 2 + minX
  260. })
  261. break
  262. case 'right':
  263. activeChartList.forEach((chart) => {
  264. chart.x = maxXW - chart.w
  265. })
  266. break
  267. case 'top':
  268. activeChartList.forEach((chart) => {
  269. chart.y = minY
  270. })
  271. break
  272. case 'middle':
  273. activeChartList.forEach((chart) => {
  274. chart.y = (centerH - chart.h) / 2 + minY
  275. })
  276. break
  277. case 'bottom':
  278. activeChartList.forEach((chart) => {
  279. chart.y = maxYH - chart.h
  280. })
  281. break
  282. case 'levelAround':
  283. // 先让数组根据x的属性进行排序
  284. activeChartList = activeChartList.sort(this.compare('x'))
  285. // eslint-disable-next-line no-case-declarations
  286. const minXW = activeChartList[0].x + activeChartList[0].w
  287. maxX = Math.max.apply(Math, activeChartList.map(item => { return item.x }))
  288. // 中间总的宽度
  289. // eslint-disable-next-line no-case-declarations
  290. let totalW = 0
  291. for (let i = 1; i < activeChartList.length - 1; i++) {
  292. totalW = totalW + activeChartList[i].w
  293. }
  294. // 中间剩余的空格
  295. // eslint-disable-next-line no-case-declarations
  296. const padding = (maxX - minXW - totalW) / (activeChartList.length - 1)
  297. // eslint-disable-next-line no-case-declarations
  298. let useW = 0
  299. for (let i = 1; i < activeChartList.length - 1; i++) {
  300. activeChartList[i].x = minXW + padding * i + useW
  301. useW = useW + activeChartList[i].w
  302. }
  303. break
  304. case 'verticalAround':
  305. // 先让数组根据y的属性进行排序
  306. activeChartList = activeChartList.sort(this.compare('y'))
  307. // eslint-disable-next-line no-case-declarations
  308. const minYH = activeChartList[0].y + activeChartList[0].h
  309. // eslint-disable-next-line no-case-declarations
  310. let totalH = 0
  311. for (let i = 1; i < activeChartList.length - 1; i++) {
  312. totalH = totalH + activeChartList[i].h
  313. }
  314. // eslint-disable-next-line no-case-declarations
  315. const paddingBottom = (maxY - minYH - totalH) / (activeChartList.length - 1)
  316. // eslint-disable-next-line no-case-declarations
  317. let useH = 0
  318. for (let i = 1; i < activeChartList.length - 1; i++) {
  319. activeChartList[i].y = minYH + paddingBottom * i + useH
  320. useH = useH + activeChartList[i].h
  321. }
  322. break
  323. }
  324. pageInfo.chartList = [...pageInfo.chartList, ...activeChartList]
  325. pageInfo.chartList = uniqBy(pageInfo.chartList, 'code')
  326. this.changePageInfo(pageInfo)
  327. },
  328. compare (property) {
  329. return function (obj1, obj2) {
  330. const value1 = obj1[property]
  331. const value2 = obj2[property]
  332. return value1 - value2 // 升序
  333. }
  334. },
  335. goBackManage () {
  336. this.$confirm('确定返回主页面吗?未保存的配置将会丢失。', '提示', {
  337. distinguishCancelAndClose: true,
  338. confirmButtonText: '保存后离开页面',
  339. cancelButtonText: '离开页面',
  340. cancelButtonClass: 'cancel-btn',
  341. type: 'warning',
  342. customClass: 'bs-el-message-box'
  343. }).then(async () => {
  344. await this.save()
  345. await this.backManagement()
  346. }).catch((action) => {
  347. if (action === 'cancel') {
  348. this.backManagement()
  349. }
  350. })
  351. },
  352. backManagement () {
  353. const data = { componentsManagementType: 'component' }
  354. this.$router.app.$options.globalData = data // 将数据存储在全局变量中
  355. this.$router.push({ path: this.pageInfo.type === 'component' ? (window.BS_CONFIG?.routers?.componentUrl || '/big-screen-components') : (window.BS_CONFIG?.routers?.pageManagementUrl || '/home') })
  356. },
  357. undo (isUndo) {
  358. this.undoTimeLine(isUndo)
  359. },
  360. // 清空
  361. empty () {
  362. this.changeActiveCode('')
  363. this.$emit('empty')
  364. },
  365. // 预览
  366. async execRun () {
  367. this.save('preview').then((res) => {
  368. this.preview(res)
  369. })
  370. },
  371. // 预览
  372. preview (previewCode) {
  373. const { href } = this.$router.resolve({
  374. path: window.BS_CONFIG?.routers?.previewUrl || '/big-screen/preview',
  375. query: {
  376. code: previewCode || this.pageCode
  377. }
  378. })
  379. window.open(href, '_blank')
  380. },
  381. // 保存
  382. async save (type, hasPageTemplateId = false) {
  383. const pageInfo = cloneDeep(this.handleSaveData())
  384. // 保存页面
  385. try {
  386. if (!hasPageTemplateId) {
  387. delete pageInfo.pageTemplateId
  388. }
  389. if (type === 'preview') {
  390. pageInfo.isPreview = true
  391. const res = await saveScreen(pageInfo)
  392. return res
  393. } else {
  394. pageInfo.isPreview = false
  395. this.saveLoading = true
  396. const node = document.querySelector('.render-theme-wrap')
  397. const dataUrl = await toJpeg(node, { quality: 0.2 })
  398. if (showSize(dataUrl) > 200) {
  399. const url = dataURLtoBlob(dataUrl)
  400. // 压缩到500KB,这里的500就是要压缩的大小,可自定义
  401. const imgRes = await imageConversion.compressAccurately(url, {
  402. size: 200, // 图片大小压缩到100kb
  403. width: 1280, // 宽度压缩到1280
  404. height: 720 // 高度压缩到720
  405. })
  406. const base64 = await translateBlobToBase64(imgRes)
  407. pageInfo.coverPicture = base64.result
  408. const res = await saveScreen(pageInfo)
  409. this.$message.success('保存成功')
  410. return res
  411. } else {
  412. pageInfo.coverPicture = dataUrl
  413. const res = await saveScreen(pageInfo)
  414. this.$message.success('保存成功')
  415. return res
  416. }
  417. }
  418. } catch (error) {
  419. console.error(error)
  420. this.saveLoading = false
  421. throw error
  422. } finally {
  423. this.saveLoading = false
  424. }
  425. },
  426. goBack (path) {
  427. this.$router.push({
  428. path: `/${path}`
  429. })
  430. },
  431. // 得到模板列表
  432. getTemplateList (type) {
  433. this.$nextTick(() => {
  434. this.$refs.ChooseTemplateDialog.init(undefined, type)
  435. })
  436. },
  437. // 选择模版后覆盖配置
  438. selectTemplate (template) {
  439. this.pageInfo.pageTemplateId = template.id
  440. this.save('saveLoading', true).then(() => {
  441. this.initLayout(this.pageCode)
  442. })
  443. },
  444. replaceItByTemplate (config) {
  445. this.changePageInfo(config)
  446. },
  447. // 处理保存数据
  448. handleSaveData () {
  449. const pageInfo = cloneDeep(this.pageInfo)
  450. const chartList = cloneDeep(this.pageInfo.chartList)
  451. pageInfo.pageConfig.cacheDataSets =
  452. pageInfo.pageConfig.cacheDataSets?.map((cache) => ({
  453. name: cache.name,
  454. dataSetId: cache.dataSetId
  455. })) || []
  456. const newChartList = chartList?.map((chart) => {
  457. // 如果是自定义组件,需要将option转换为json字符串,因为其中可能有函数
  458. if (['customComponent', 'remoteComponent', 'echartsComponent'].includes(chart.type)) {
  459. // chart.option.data = []
  460. chart.option = stringifyObjectFunctions(chart.option)
  461. }
  462. return chart
  463. })
  464. return cloneDeep({
  465. ...this.pageInfo,
  466. chartList: newChartList
  467. })
  468. },
  469. updateRightVisiable () {
  470. this.$emit('updateRightVisiable', !this.rightFold)
  471. },
  472. showPageInfo () {
  473. this.$emit('showPageInfo')
  474. },
  475. designAssign () {
  476. this.$refs.AssignDialog.init()
  477. },
  478. showHostory () {
  479. this.$refs.HistoryList.init()
  480. },
  481. createdImg () {
  482. this.saveAndPreviewLoading = true
  483. // 暂停跑马灯动画
  484. EventBus.$emit('stopMarquee')
  485. const node = document.querySelector('.render-theme-wrap')
  486. toPng(node)
  487. .then((dataUrl) => {
  488. const link = document.createElement('a')
  489. link.download = `${this.pageInfo.name}.png`
  490. link.href = dataUrl
  491. link.click()
  492. link.addEventListener('click', () => {
  493. link.remove()
  494. })
  495. this.saveAndPreviewLoading = false
  496. // 恢复跑马灯动画
  497. EventBus.$emit('startMarquee')
  498. })
  499. .catch(() => {
  500. this.$message.warning('出现未知错误,请重试')
  501. this.saveAndPreviewLoading = false
  502. })
  503. }
  504. }
  505. }
  506. </script>
  507. <style lang="scss" scoped>
  508. @import '../BigScreenDesign/fonts/iconfont.css';
  509. .default-layout-box {
  510. display: flex;
  511. flex-wrap: wrap;
  512. .default-layout-item {
  513. cursor: pointer;
  514. width: 42%;
  515. margin: 9px;
  516. display: flex;
  517. flex-direction: column;
  518. align-items: center;
  519. .component-name {
  520. font-size: 12px;
  521. }
  522. .sampleImg {
  523. margin: 0 auto;
  524. width: 102px;
  525. height: 73px;
  526. display: block;
  527. }
  528. .img_dispaly {
  529. margin: 0 auto;
  530. text-align: center;
  531. width: 100px;
  532. height: 70px;
  533. line-height: 70px;
  534. background-color: #d7d7d7;
  535. color: #999;
  536. }
  537. .demonstration {
  538. text-align: center;
  539. }
  540. }
  541. .default-layout-item:hover {
  542. cursor: pointer;
  543. }
  544. ::v-deep .el-radio__label {
  545. display: none;
  546. }
  547. }
  548. .page-top-setting-wrap {
  549. height: 40px;
  550. background-color: var(--bs-background-2);
  551. display: flex;
  552. align-items: center;
  553. justify-content: space-between;
  554. position: relative;
  555. color: #ffffff;
  556. padding: 0 5px;
  557. .app-name {
  558. cursor: pointer;
  559. }
  560. .head-btn-group {
  561. display: flex;
  562. margin-left: 50px;
  563. align-items: center;
  564. i {
  565. font-size: 14px;
  566. }
  567. .icon-reverse {
  568. transform: rotate(180deg);
  569. }
  570. }
  571. .item-wrap {
  572. display: flex;
  573. align-items: center;
  574. .menu-img {
  575. width: 18px;
  576. height: 18px;
  577. margin-left: 9px;
  578. margin-right: 15px;
  579. cursor: pointer;
  580. }
  581. .logo-text {
  582. user-select: none;
  583. margin-left: 9px;
  584. font-size: 14px;
  585. color: #ffffff;
  586. }
  587. .name-span {
  588. max-width: 300px;
  589. overflow: hidden;
  590. white-space: nowrap;
  591. text-overflow: ellipsis;
  592. }
  593. }
  594. .theme-switch {
  595. margin-right: 10px;
  596. /deep/.el-switch__label {
  597. color: #bcc9d4 !important;
  598. }
  599. /deep/.el-switch__label.is-active {
  600. color: var(--bs-el-color-primary) !important;
  601. }
  602. }
  603. .align-list-dropdown {
  604. width: 100px !important;
  605. color: #ffffff !important;
  606. }
  607. }
  608. // 自定义dropdown的样式
  609. .align-dropdown-menu {
  610. background-color: var(--bs-background-2) !important;
  611. border: 1px solid var(--bs-border-1);
  612. /deep/ .el-dropdown-menu__item {
  613. background-color: var(--bs-background-2) !important;
  614. &:hover {
  615. color: var(--bs-el-color-primary) !important;
  616. background-color: var(--bs-el-background-3) !important;
  617. }
  618. }
  619. }
  620. ::v-deep .el-input__inner,
  621. ::v-deep .el-color-picker__color-inner,
  622. ::v-deep .el-input-number--mini,
  623. ::v-deep .el-textarea__inner,
  624. ::v-deep .el-input-group__append {
  625. background: var(--bs-el-background-1);
  626. color: var(--bs-el-text);
  627. border: 0 !important;
  628. width: 100px;
  629. }
  630. // .bs-el-input-number{
  631. // }
  632. </style>