PageDesignTop.vue 17 KB

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