PageDesignTop.vue 17 KB

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