Designer.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330
  1. <template>
  2. <div
  3. v-loading.lock="loading"
  4. element-loading-background="rgba(0, 0, 0, 0.8)"
  5. class="l-flex--col c-designer"
  6. @contextmenu.prevent
  7. >
  8. <audio
  9. ref="audio"
  10. :src="bgmUrl"
  11. :muted.prop="muted"
  12. autoplay
  13. @ended="onAudioEnded"
  14. @error="onAudioError"
  15. />
  16. <div class="l-flex__none l-flex--row c-designer__header">
  17. <i
  18. class="c-designer__shortcut el-icon-arrow-left u-bold u-pointer"
  19. @click="onBack"
  20. />
  21. <span class="c-designer__name u-bold u-ellipsis">{{ program.name }}</span>
  22. <span class="c-sibling-item">{{ program.resolutionRatio }}</span>
  23. <div
  24. v-if="hasNext"
  25. class="c-sibling-item c-designer__shortcut u-pointer"
  26. @click="switchBgm"
  27. >
  28. <i class="o-next" />
  29. </div>
  30. <div
  31. v-if="hasAudio"
  32. class="c-sibling-item c-designer__shortcut u-pointer"
  33. @click="toggleMute"
  34. >
  35. <volume :muted="muted" />
  36. </div>
  37. <div class="l-flex__fill c-sibling-item" />
  38. <button
  39. v-if="hasWidgets"
  40. class="c-sibling-item o-button mini"
  41. @click="onClear"
  42. >
  43. <i class="o-button__icon el-icon-delete" />
  44. 清空
  45. </button>
  46. <button
  47. class="c-sibling-item o-button mini"
  48. @click="onSave"
  49. >
  50. <i class="o-button__icon iconfont iconsave" />
  51. 保存
  52. </button>
  53. </div>
  54. <div class="l-flex__fill l-flex">
  55. <div class="c-designer__side left c-side">
  56. <div class="c-side__tool">
  57. <div class="c-side__item">
  58. 组件
  59. </div>
  60. </div>
  61. <el-scrollbar
  62. class="c-side__scrollbar"
  63. native
  64. >
  65. <div class="c-side__content mini">
  66. <div
  67. v-for="(widget, index) in layers"
  68. ref="widgetElements"
  69. :key="widget.id"
  70. class="o-layer"
  71. :class="{ active: widget.id === selectedWidgetId }"
  72. @mousedown="onLayerClick($event, index)"
  73. >
  74. <widget-shortcut
  75. class="dark"
  76. :widget="widget"
  77. editable
  78. />
  79. </div>
  80. <div
  81. v-if="node"
  82. ref="rootElement"
  83. key="root"
  84. class="o-layer"
  85. :class="{ active: !selectedWidgetId }"
  86. @click="onRootClick"
  87. >
  88. <widget-shortcut
  89. class="dark"
  90. :widget="node"
  91. :custom-style="backgroundStyles"
  92. source-key="bgm"
  93. background
  94. editable
  95. />
  96. </div>
  97. </div>
  98. </el-scrollbar>
  99. </div>
  100. <div class="c-designer__main">
  101. <div class="l-flex--row c-designer__tool">
  102. <div class="o-scale-slider">
  103. <div class="l-flex--row o-scale-slider__wrapper">
  104. <i
  105. class="el-icon-zoom-out u-pointer"
  106. @click="scaleDown"
  107. />
  108. <el-slider
  109. v-model="scale"
  110. class="o-scale-slider__slider"
  111. :min="minScale"
  112. :max="maxScale"
  113. />
  114. <i
  115. class="el-icon-zoom-in u-pointer"
  116. @click="scaleUp"
  117. />
  118. </div>
  119. </div>
  120. <div
  121. class="l-flex__none l-flex--row"
  122. @dragstart="widgetOnDragStart"
  123. @dragend="widgetOnDragEnd"
  124. >
  125. <div
  126. v-for="cfg in widgetConfigs"
  127. :key="cfg.key"
  128. :data-type="cfg.type"
  129. class="o-widget-cfg"
  130. draggable
  131. >
  132. <span class="o-widget-cfg__icon">
  133. <svg-icon :icon-class="cfg.icon" />
  134. </span>
  135. <span class="o-widget-cfg__text">{{ cfg.label }}</span>
  136. </div>
  137. </div>
  138. </div>
  139. <div
  140. ref="wrapper"
  141. class="c-designer__content"
  142. @mousedown="onRootClick"
  143. >
  144. <div
  145. class="c-designer__wrapper"
  146. :style="wrapperStyles"
  147. >
  148. <div
  149. ref="canvas"
  150. class="c-designer__canvas"
  151. :style="[transformStyles, styles]"
  152. @dragover.prevent
  153. @drop.prevent="widgetOnDrop"
  154. >
  155. <div
  156. class="c-designer__background has-bg"
  157. :style="backgroundStyles"
  158. />
  159. <div
  160. v-show="grid"
  161. class="c-designer__grid"
  162. />
  163. <widget
  164. v-for="(item, index) in widgets"
  165. :key="item.id"
  166. ref="widgets"
  167. :scale="100 / scale"
  168. :node="item"
  169. :root="node"
  170. @focus="onWidgetFocus(index)"
  171. @will-move="onWidgetWillMove"
  172. @blur="onWidgetBlur"
  173. @menu="onWidgetMenu($event, index)"
  174. @dblclick.native="onWidgetDblclick"
  175. />
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. <div class="c-designer__side right c-side">
  181. <div class="c-side__tool">
  182. <div
  183. v-for="(tab, index) in dynamicOptions"
  184. :key="index"
  185. class="c-side__item u-pointer"
  186. :class="{ active: optionIndex === index }"
  187. @click="optionIndex = index"
  188. >
  189. {{ tab.label }}
  190. </div>
  191. </div>
  192. <div
  193. v-for="(tab, index) in dynamicOptions"
  194. v-show="optionIndex === index"
  195. :key="index"
  196. class="c-side__scrollbar c-side__content u-overflow-y--auto"
  197. :class="{ active: optionIndex === index }"
  198. >
  199. <dynamic-item
  200. v-for="item in tab.list"
  201. :key="item.key"
  202. class="c-sibling-item--v far"
  203. :root="node"
  204. :node="widget"
  205. :attr="item"
  206. @choose="onEditData"
  207. />
  208. </div>
  209. </div>
  210. </div>
  211. <content-menu
  212. :visible.sync="visibleContentMenu"
  213. :style-obj="styleObj"
  214. @delete="deleteLayer"
  215. @copy="copyLayer"
  216. @top="topLayer"
  217. @low="lowLayer"
  218. @up="upLayer"
  219. @down="downLayer"
  220. />
  221. <el-dialog
  222. :visible.sync="showAssets"
  223. :title="assetDialogType"
  224. custom-class="c-dialog large"
  225. :close-on-click-modal="false"
  226. :before-close="onCloseAssetsDialog"
  227. >
  228. <draggable
  229. v-if="showAssets"
  230. v-model="sources"
  231. class="l-grid--info mini"
  232. :class="{ dragging }"
  233. animation="300"
  234. @start="onSourceDragStart"
  235. @end="onSourceDragEnd"
  236. >
  237. <media-card
  238. v-for="(source, index) in sources"
  239. :key="index"
  240. class="o-card"
  241. :source="source"
  242. @click="onView"
  243. >
  244. <div
  245. v-if="needTypeTag"
  246. class="o-card__tag"
  247. >{{ source.tag }}</div>
  248. <i
  249. class="o-card__icon el-icon-delete has-active"
  250. @mousedown.stop
  251. @pointerdown.stop
  252. @click.stop="onDelAsset(index)"
  253. />
  254. </media-card>
  255. </draggable>
  256. <template #footer>
  257. <button
  258. class="o-button"
  259. @click="onAddAssets"
  260. >
  261. 新增
  262. </button>
  263. <button
  264. class="o-button"
  265. @click="onSaveAssets"
  266. >
  267. 确定
  268. </button>
  269. <button
  270. class="o-button cancel"
  271. @click="onCloseAssetsDialog"
  272. >
  273. 取消
  274. </button>
  275. </template>
  276. </el-dialog>
  277. <el-dialog
  278. :visible.sync="showServerAssets"
  279. :title="assetDialogTitle"
  280. custom-class="c-dialog"
  281. append-to-body
  282. >
  283. <template v-if="showServerAssets">
  284. <grid-table :schema="assetSchema">
  285. <grid-table-item v-slot="item">
  286. <media-card
  287. :source="item"
  288. @dblclick="onChoosenAsset"
  289. />
  290. </grid-table-item>
  291. </grid-table>
  292. </template>
  293. </el-dialog>
  294. <confirm-dialog
  295. ref="assetsDialog"
  296. :title="assetDialogTitle"
  297. size
  298. append-to-body
  299. @confirm="onChoosenAssets"
  300. @cancel="showServerAssetsMulti = false"
  301. >
  302. <template v-if="showServerAssetsMulti">
  303. <grid-table
  304. ref="gridTable"
  305. :schema="assetSchema"
  306. :custom="!!fileDir"
  307. >
  308. <template #header>
  309. <i
  310. v-if="!!fileDir"
  311. class="o-icon medium o-icon--back el-icon-arrow-left u-bold u-pointer"
  312. @click="onDirBack"
  313. />
  314. </template>
  315. <grid-table-item v-slot="item">
  316. <media-card
  317. :source="item"
  318. @click="onToggleGrid"
  319. >
  320. <el-checkbox
  321. v-model="item.selected"
  322. class="o-card__checkbox"
  323. />
  324. <i
  325. class="o-card__play el-icon-video-play has-active u-pointer"
  326. @click.stop="onView(item)"
  327. />
  328. <i
  329. v-if="item.files"
  330. class="o-card__grid el-icon-s-grid has-active u-pointer"
  331. @click.stop="onChooseDir(item)"
  332. />
  333. </media-card>
  334. </grid-table-item>
  335. <div
  336. v-if="fileDir"
  337. class="l-flex__self l-grid u-overflow-y--auto"
  338. >
  339. <media-card
  340. v-for="file in fileDir.files"
  341. :key="file.keyName"
  342. :source="file"
  343. @click="onToggleGrid"
  344. >
  345. <el-checkbox
  346. v-model="file.selected"
  347. class="o-card__checkbox"
  348. />
  349. <i
  350. class="o-card__play el-icon-video-play has-active u-pointer"
  351. @click.stop="onView(file)"
  352. />
  353. </media-card>
  354. </div>
  355. </grid-table>
  356. </template>
  357. </confirm-dialog>
  358. <preview-dialog
  359. ref="previewDialog"
  360. @close="onClosePreview"
  361. />
  362. <confirm-dialog
  363. ref="rich"
  364. title="文本编辑"
  365. size
  366. append-to-body
  367. @confirm="onRichConfirm"
  368. >
  369. <toolbar
  370. style="border-bottom: 1px solid #ccc"
  371. mode="simple"
  372. :editor="editor"
  373. :default-config="toolbarConfig"
  374. />
  375. <editor
  376. v-model="richHtml"
  377. mode="simple"
  378. style="height: 500px; overflow: hidden;"
  379. :default-config="editorConfig"
  380. @onCreated="onEditorCreated"
  381. @customPaste="onCustomPaste"
  382. />
  383. </confirm-dialog>
  384. </div>
  385. </template>
  386. <script>
  387. import domToImage from 'dom-to-image'
  388. import { updateProgram } from '@/api/program'
  389. import {
  390. getAssets,
  391. getThumbnailUrl
  392. } from '@/api/asset'
  393. import {
  394. State,
  395. AssetType,
  396. AssetTypeInfo,
  397. AssetTag,
  398. AssetTagInfo
  399. } from '@/constant'
  400. import {
  401. widgets,
  402. isMediaWidget,
  403. normalize,
  404. getOptions,
  405. copy,
  406. fix,
  407. getDuration,
  408. getUsedAssets,
  409. toJSON
  410. } from './core/utils'
  411. import mixin from './mixin'
  412. import Draggable from 'vuedraggable'
  413. import Widget from './core/widget/Widget.vue'
  414. import ContentMenu from './components/ContentMenu.vue'
  415. import DynamicItem from './components/DynamicItem.vue'
  416. import {
  417. Editor,
  418. Toolbar
  419. } from '@wangeditor/editor-for-vue'
  420. import { DomEditor } from '@wangeditor/editor'
  421. import '@wangeditor/editor/dist/css/style.css'
  422. export default {
  423. name: 'BigScreenDesigner',
  424. components: {
  425. Draggable,
  426. Widget,
  427. ContentMenu,
  428. DynamicItem,
  429. Editor,
  430. Toolbar
  431. },
  432. mixins: [mixin],
  433. data () {
  434. return {
  435. loading: false,
  436. snapping: false,
  437. padding: 16,
  438. optionIndex: 0,
  439. widgetConfigs: widgets,
  440. grid: false,
  441. visibleContentMenu: false,
  442. styleObj: null,
  443. widgetAttr: null,
  444. showAssets: false,
  445. sources: null,
  446. dragging: false,
  447. dragScale: false,
  448. showServerAssets: false,
  449. showServerAssetsMulti: false,
  450. fileDir: null,
  451. editor: null,
  452. richHtml: '',
  453. toolbarConfig: {
  454. toolbarKeys: [
  455. 'headerSelect',
  456. '|',
  457. 'bold',
  458. // 'color',
  459. // '|',
  460. // 'fontSize',
  461. // 'lineHeight',
  462. '|',
  463. 'justifyLeft',
  464. 'justifyCenter',
  465. 'justifyRight'
  466. ]
  467. },
  468. editorConfig: {
  469. placeholder: '请输入内容...'
  470. }
  471. }
  472. },
  473. computed: {
  474. hasWidgets () {
  475. return this.widgets.length > 0
  476. },
  477. dynamicOptions () {
  478. if (this.widget) {
  479. return getOptions(this.widget.type)
  480. }
  481. return []
  482. },
  483. layers () {
  484. return this.widgets.slice().reverse()
  485. },
  486. assetType () {
  487. if (this.widgetAttr?.type === 'data') {
  488. return this.widgetAttr.options.type
  489. }
  490. return []
  491. },
  492. assetSchema () {
  493. return {
  494. condition: { status: State.AVAILABLE, tag: AssetTag.AD, type: this.assetType[0], originalName: '' },
  495. list: getAssets,
  496. transform: this.transformAsset,
  497. filters: [
  498. { key: 'tag', type: 'select', options: [
  499. { value: AssetTag.AD, label: AssetTagInfo[AssetTag.AD] },
  500. { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
  501. { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] }
  502. ] },
  503. this.assetType.length > 1
  504. ? {
  505. key: 'type', type: 'select', options: this.assetType.map(type => {
  506. return {
  507. value: type,
  508. label: this.getAssetType(type)
  509. }
  510. })
  511. }
  512. : null,
  513. { key: 'originalName', type: 'search', placeholder: '媒资名称' }
  514. ].filter(Boolean)
  515. }
  516. },
  517. needTypeTag () {
  518. return this.assetType.length > 1
  519. },
  520. assetDialogType () {
  521. return this.assetType.length
  522. ? this.assetType.length > 1
  523. ? '媒资'
  524. : this.getAssetType(this.assetType[0])
  525. : ''
  526. },
  527. assetDialogTitle () {
  528. return this.fileDir
  529. ? this.fileDir.name
  530. : `请选择${this.assetDialogType}`
  531. },
  532. selectedWidgetId () {
  533. return this.widget?.id
  534. },
  535. wrapperStyles () {
  536. return this.node
  537. ? {
  538. width: `${this.node.width * this.scale / 100}px`,
  539. height: `${this.node.height * this.scale / 100}px`
  540. }
  541. : null
  542. }
  543. },
  544. watch: {
  545. selectedWidgetIndex (val, old) {
  546. if (old > -1) {
  547. const id = this.widgets[old]?.id
  548. id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(false)
  549. }
  550. if (val > -1) {
  551. const id = this.widgets[val]?.id
  552. id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(true)
  553. }
  554. this.optionIndex = 0
  555. },
  556. widget () {
  557. this.node && setTimeout(() => {
  558. if (this.selectedWidgetIndex === -1) {
  559. this.$refs.rootElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
  560. } else {
  561. this.$refs.widgetElements.find(element => element.classList.contains('active')).scrollIntoView({ behavior: 'smooth', block: 'start' })
  562. }
  563. })
  564. }
  565. },
  566. beforeDestroy () {
  567. if (this.editor) {
  568. this.editor.destroy()
  569. }
  570. },
  571. methods: {
  572. onEditorCreated (editor) {
  573. this.editor = Object.seal(editor)
  574. this.$nextTick(() => {
  575. console.log(DomEditor.getToolbar(editor).getConfig())
  576. })
  577. },
  578. onCustomPaste (editor, event, callback) {
  579. // 返回 true ,继续默认的粘贴行为
  580. callback(true)
  581. this.$nextTick(() => {
  582. editor.setHtml(editor.getHtml().replace(/text-indent[^;]+;/g, ''))
  583. })
  584. },
  585. onRichConfirm (done) {
  586. this.widget[this.widgetAttr.key] = this.richHtml
  587. done()
  588. },
  589. getAssetType (type) {
  590. return AssetTypeInfo[type] || '媒资'
  591. },
  592. onClear () {
  593. this.$confirm(
  594. '清空后所有数据将丢失',
  595. '清空画布',
  596. { type: 'warning' }
  597. ).then(() => {
  598. this.selectedWidgetIndex = -1
  599. const { width, height } = this.node
  600. this.initCanvas({ width, height })
  601. })
  602. },
  603. widgetOnDragStart (evt) {
  604. evt.dataTransfer.setData('type', evt.target.dataset.type)
  605. this.dragging = true
  606. },
  607. widgetOnDragEnd () {
  608. this.dragging = false
  609. },
  610. widgetOnDrop (evt) {
  611. const type = evt.dataTransfer.getData('type')
  612. if (type) {
  613. const node = normalize({ type })
  614. if (this.widgets.length || !isMediaWidget(type)) {
  615. let { offsetX: left, offsetY: top } = evt
  616. top -= node.height / 2
  617. if (top < 0) {
  618. top = 0
  619. }
  620. left -= node.width / 2
  621. if (left < 0) {
  622. left = 0
  623. }
  624. node.top = top
  625. node.left = left
  626. } else {
  627. node.top = 0
  628. node.left = 0
  629. node.width = this.node.width
  630. node.height = this.node.height
  631. }
  632. this.node.widgets.push(node)
  633. this.$nextTick(() => {
  634. this.selectedWidgetIndex = this.widgets.length - 1
  635. })
  636. }
  637. },
  638. onWidgetFocus (index) {
  639. this.selectedWidgetIndex = index
  640. this.visibleContentMenu = false
  641. },
  642. onWidgetWillMove () {
  643. this.grid = true
  644. },
  645. onWidgetBlur () {
  646. this.grid = false
  647. },
  648. onWidgetMenu (evt, index) {
  649. this.selectedWidgetIndex = index
  650. this.onRightClick(evt)
  651. },
  652. onWidgetDblclick () {
  653. const attr = this.dynamicOptions[0]?.list.find(({ type }) => type === 'data')
  654. if (attr) {
  655. this.onEditData(attr)
  656. }
  657. },
  658. onLayerClick (evt, index) {
  659. this.selectedWidgetIndex = this.widgets.length - 1 - index
  660. this.onRightClick(evt)
  661. },
  662. onRootClick () {
  663. this.selectedWidgetIndex = -1
  664. },
  665. onRightClick (evt) {
  666. const { button, clientY: top, clientX: left } = evt
  667. if (button !== 2) {
  668. return
  669. }
  670. evt.stopPropagation()
  671. if (top || left) {
  672. this.styleObj = {
  673. display: 'block',
  674. top: `${top}px`,
  675. left: `${left}px`
  676. }
  677. }
  678. this.visibleContentMenu = true
  679. },
  680. // 删除
  681. deleteLayer () {
  682. this.widgets.splice(this.selectedWidgetIndex, 1)
  683. this.$nextTick(() => {
  684. this.selectedWidgetIndex = -1
  685. })
  686. },
  687. // 复制
  688. copyLayer () {
  689. this.widgets.splice(this.selectedWidgetIndex + 1, 0, copy(this.widget))
  690. this.$message({
  691. type: 'success',
  692. message: '复制成功,已为您选择复制后的组件'
  693. })
  694. this.$nextTick(() => {
  695. this.selectedWidgetIndex += 1
  696. })
  697. },
  698. // 置顶
  699. topLayer () {
  700. if (this.selectedWidgetIndex < this.widgets.length - 1) {
  701. this.widgets.push(this.widgets.splice(
  702. this.selectedWidgetIndex,
  703. 1
  704. )[0])
  705. this.$nextTick(() => {
  706. this.selectedWidgetIndex = this.widgets.length - 1
  707. })
  708. }
  709. },
  710. // 置底
  711. lowLayer () {
  712. if (this.selectedWidgetIndex > 0) {
  713. this.widgets.unshift(this.widgets.splice(
  714. this.selectedWidgetIndex,
  715. 1
  716. )[0])
  717. this.$nextTick(() => {
  718. this.selectedWidgetIndex = 0
  719. })
  720. }
  721. },
  722. // 上移一层
  723. upLayer () {
  724. if (this.selectedWidgetIndex < this.widgets.length - 1) {
  725. this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
  726. this.selectedWidgetIndex + 1,
  727. 1,
  728. this.widgets[this.selectedWidgetIndex]
  729. )[0]
  730. this.$nextTick(() => {
  731. this.selectedWidgetIndex += 1
  732. })
  733. }
  734. },
  735. // 下移一层
  736. downLayer () {
  737. if (this.selectedWidgetIndex > 0) {
  738. this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
  739. this.selectedWidgetIndex - 1,
  740. 1,
  741. this.widgets[this.selectedWidgetIndex]
  742. )[0]
  743. this.$nextTick(() => {
  744. this.selectedWidgetIndex -= 1
  745. })
  746. }
  747. },
  748. changeAttr (value) {
  749. const { key, options } = this.widgetAttr
  750. this.widget[key] = value
  751. if (options) {
  752. const { callback } = options
  753. callback && callback.call(this.widget, this.node)
  754. }
  755. },
  756. onEditData (attr) {
  757. this.widgetAttr = attr
  758. switch (attr.type) {
  759. case 'data':
  760. if (attr.options && attr.options.solo) {
  761. this.showServerAssets = true
  762. } else {
  763. this.sources = this.widget[attr.key].map(this.transformDataToSource)
  764. this.showAssets = true
  765. }
  766. break
  767. case 'rich':
  768. this.richHtml = this.widget[attr.key]
  769. this.$refs.rich.show()
  770. break
  771. default:
  772. break
  773. }
  774. },
  775. onCloseAssetsDialog () {
  776. this.widgetAttr = null
  777. this.showAssets = false
  778. this.showServerAssets = false
  779. },
  780. transformAsset ({ type, originalName, keyName, thumbnail, duration, size, md5, childrenData }) {
  781. const asset = {
  782. selected: false,
  783. type,
  784. name: originalName,
  785. keyName,
  786. size,
  787. md5,
  788. thumbnail
  789. }
  790. switch (type) {
  791. case AssetType.IMAGE:
  792. asset.duration = this.widget.interval
  793. asset.thumb = getThumbnailUrl(thumbnail)
  794. break
  795. case AssetType.PPT:
  796. case AssetType.PDF:
  797. case AssetType.DOC:
  798. asset.thumb = getThumbnailUrl(childrenData[0].keyName)
  799. asset.files = (childrenData || []).map(({ type, keyName, size, md5 }, index) => {
  800. return {
  801. type, originalName, keyName, size, md5,
  802. selected: false,
  803. name: `第${index + 1}页`,
  804. url: keyName,
  805. thumb: getThumbnailUrl(keyName)
  806. }
  807. })
  808. break
  809. default:
  810. asset.duration = Number(duration) || 0
  811. if (thumbnail) {
  812. asset.thumb = getThumbnailUrl(thumbnail)
  813. } else {
  814. asset.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
  815. }
  816. break
  817. }
  818. return this.addTag(asset)
  819. },
  820. transformToData ({ type, name, keyName, thumbnail, duration, size, md5 }) {
  821. const source = {
  822. type,
  823. name,
  824. keyName,
  825. size,
  826. md5,
  827. duration
  828. }
  829. if (type !== AssetType.IMAGE && thumbnail) {
  830. source.thumbnail = thumbnail
  831. }
  832. return source
  833. },
  834. transformDataToSource (data) {
  835. const source = { ...data }
  836. if (data.thumbnail) {
  837. source.thumb = getThumbnailUrl(data.thumbnail)
  838. } else if (source.type === AssetType.IMAGE) {
  839. source.thumb = getThumbnailUrl(data.keyName)
  840. } else {
  841. switch (source.type) {
  842. case AssetType.VIDEO:
  843. source.icon = 'video-bg'
  844. break
  845. case AssetType.AUDIO:
  846. source.icon = 'audio-bg'
  847. break
  848. default:
  849. break
  850. }
  851. }
  852. return this.addTag(source)
  853. },
  854. addTag (data) {
  855. if (this.needTypeTag) {
  856. data.tag = this.getAssetType(data.type)
  857. }
  858. return data
  859. },
  860. onChoosenAsset (asset) {
  861. this.changeAttr([this.transformToData(asset)])
  862. this.onCloseAssetsDialog()
  863. },
  864. onAddAssets () {
  865. this.fileDir = null
  866. this.showServerAssetsMulti = true
  867. this.$refs.assetsDialog.show()
  868. },
  869. onToggleGrid (asset) {
  870. asset.selected = !asset.selected
  871. },
  872. transformFileToAsset ({ type, originalName, name, keyName, thumb, size, md5 }) {
  873. const asset = {
  874. type, size, md5, keyName, thumb,
  875. name: `${originalName}${name}`,
  876. duration: this.widget.interval
  877. }
  878. this.addTag(asset)
  879. return asset
  880. },
  881. onChoosenAssets (done) {
  882. const assets = this.fileDir
  883. ? this.fileDir.files.filter(({ selected }) => selected).map(this.transformFileToAsset)
  884. : this.$refs.gridTable.getData().filter(({ selected }) => selected).map(asset => asset.files?.map(this.transformFileToAsset) || asset)
  885. if (assets.length) {
  886. this.sources = this.sources.concat(...assets)
  887. }
  888. this.showServerAssetsMulti = false
  889. done()
  890. },
  891. onChooseDir (fileDir) {
  892. fileDir.files.forEach(file => {
  893. file.selected = false
  894. })
  895. this.fileDir = fileDir
  896. },
  897. onDirBack () {
  898. this.fileDir = null
  899. },
  900. onSaveAssets () {
  901. this.changeAttr(this.sources.map(this.transformToData))
  902. this.onCloseAssetsDialog()
  903. },
  904. onView ({ type, keyName, files }) {
  905. this.onViewAsset({ type: type || this.widgetAttr.options.type, url: keyName, files })
  906. },
  907. onViewAsset (asset) {
  908. this.$muted = this.muted
  909. if (!this.muted) {
  910. this.muted = true
  911. }
  912. this.$refs.previewDialog.show(asset)
  913. },
  914. onClosePreview () {
  915. this.muted = this.$muted
  916. },
  917. onDelAsset (index) {
  918. this.sources.splice(index, 1)
  919. },
  920. onSourceDragStart () {
  921. this.dragging = true
  922. },
  923. onSourceDragEnd () {
  924. this.dragging = false
  925. },
  926. scaleDown () {
  927. this.scale = Math.max(this.scale - 10, this.minScale)
  928. },
  929. scaleUp () {
  930. this.scale = Math.min(this.scale + 10, this.maxScale)
  931. },
  932. onSave () {
  933. this.check().then(status => {
  934. this.selectedWidgetIndex = -1
  935. this.loading = true
  936. setTimeout(() => {
  937. this._save(status).finally(() => {
  938. this.loading = false
  939. })
  940. }, 100)
  941. })
  942. },
  943. check () {
  944. const { state, message } = fix(this.node)
  945. if (message) {
  946. return this.$confirm(
  947. message,
  948. '继续保存',
  949. { type: 'warning' }
  950. ).then(() => state)
  951. }
  952. return Promise.resolve(state)
  953. },
  954. async _save (status) {
  955. try {
  956. const base64 = await this.snap()
  957. const result = await updateProgram({
  958. id: this.program.id,
  959. duration: getDuration(this.node),
  960. itemJsonStr: JSON.stringify(toJSON(this.node)),
  961. keyNameList: getUsedAssets(this.node),
  962. base64,
  963. status
  964. })
  965. if (result) {
  966. if (window.opener) {
  967. window.opener.postMessage({
  968. type: 'PROGRAM_UPDATED',
  969. id: this.program.id
  970. })
  971. this.$message({
  972. type: 'success',
  973. dangerouslyUseHTMLString: true,
  974. duration: 5000,
  975. message: `保存成功, <span style="text-decoration: underline; cursor: pointer;" onclick="window.close()">返回节目列表</span>?`
  976. })
  977. } else {
  978. this.$message({
  979. type: 'success',
  980. message: '保存成功'
  981. })
  982. }
  983. } else {
  984. this.$message({
  985. type: 'warning',
  986. message: '保存失败'
  987. })
  988. }
  989. } catch (e) {
  990. console.warn(e)
  991. this.$message({
  992. type: 'warning',
  993. message: '保存失败'
  994. })
  995. }
  996. },
  997. snap () {
  998. this.snapping = true
  999. return domToImage.toJpeg(this.$refs.canvas, {
  1000. filter (node) {
  1001. const { tagName } = node
  1002. if (tagName === 'CANVAS') {
  1003. return /^data:.+;base64,.+/.test(node.toDataURL())
  1004. }
  1005. return tagName !== 'VIDEO' && tagName !== 'IFRAME'
  1006. },
  1007. width: this.node.width * this.scale / 100 | 0,
  1008. height: this.node.height * this.scale / 100 | 0,
  1009. quality: 0.1
  1010. }).finally(() => {
  1011. this.snapping = false
  1012. })
  1013. }
  1014. }
  1015. }
  1016. </script>
  1017. <style lang="scss" scoped>
  1018. $theme: #181b23;
  1019. $dark: #121418;
  1020. $light: #1f232e;
  1021. $hover: lighten($theme, 10%);
  1022. $active: darken($theme, 5%);
  1023. $border: #242835;
  1024. .c-designer {
  1025. height: 100%;
  1026. min-width: 1080px;
  1027. min-height: 600px;
  1028. background-color: $theme;
  1029. overflow: hidden;
  1030. &__header {
  1031. height: 53px;
  1032. padding: 0 24px 0 10px;
  1033. color: $info;
  1034. font-size: 12px;
  1035. line-height: 1;
  1036. border-bottom: 1px solid $border;
  1037. }
  1038. &__shortcut {
  1039. display: inline-flex;
  1040. justify-content: center;
  1041. align-items: center;
  1042. width: 36px;
  1043. height: 36px;
  1044. font-size: 18px;
  1045. border-radius: 50%;
  1046. transition: background-color 0.4s;
  1047. &:hover {
  1048. background-color: $hover;
  1049. }
  1050. &:active {
  1051. background-color: $active;
  1052. }
  1053. }
  1054. &__name {
  1055. max-width: 400px;
  1056. padding: 0 $spacing 0 6px;
  1057. font-size: 18px;
  1058. }
  1059. &__side {
  1060. flex: none;
  1061. min-height: 0;
  1062. &.left {
  1063. width: 144px;
  1064. }
  1065. &.right {
  1066. width: 196px;
  1067. }
  1068. }
  1069. &__main {
  1070. flex: 1 0 0;
  1071. display: flex;
  1072. flex-direction: column;
  1073. position: relative;
  1074. min-width: 0;
  1075. }
  1076. &__content {
  1077. flex: 1 0 0;
  1078. position: relative;
  1079. padding: 16px;
  1080. overflow: auto;
  1081. background: $dark url("~@/assets/dot.png") 0 0 / 16px 16px repeat;
  1082. }
  1083. &__tool {
  1084. flex: 0 0 60px;
  1085. color: $info;
  1086. border-left: 1px solid $border;
  1087. border-right: 1px solid $border;
  1088. }
  1089. &__wrapper {
  1090. min-width: 100%;
  1091. min-height: 100%;
  1092. }
  1093. &__canvas {
  1094. position: relative;
  1095. background-color: #000;
  1096. overflow: visible;
  1097. }
  1098. &__background {
  1099. position: absolute;
  1100. top: 0;
  1101. left: 0;
  1102. right: 0;
  1103. bottom: 0;
  1104. background-color: currentColor;
  1105. z-index: -1;
  1106. }
  1107. &__grid {
  1108. position: absolute;
  1109. top: 0;
  1110. left: 0;
  1111. right: 0;
  1112. bottom: 0;
  1113. background-size: 30px 30px, 30px 30px;
  1114. background-image: linear-gradient(hsla(0, 0%, 100%, 0.1) 1px, transparent 0),
  1115. linear-gradient(90deg, hsla(0, 0%, 100%, 0.1) 1px, transparent 0);
  1116. }
  1117. }
  1118. .c-side {
  1119. display: flex;
  1120. flex-direction: column;
  1121. &__tool {
  1122. flex: none;
  1123. display: flex;
  1124. border-bottom: 1px solid $border;
  1125. background-color: $light;
  1126. }
  1127. &__item {
  1128. flex: 1 0 0;
  1129. color: #9fa6b8;
  1130. font-size: 16px;
  1131. text-align: center;
  1132. line-height: 60px;
  1133. user-select: none;
  1134. &.active {
  1135. color: $blue;
  1136. background-color: $theme;
  1137. }
  1138. }
  1139. &__content {
  1140. padding: 10px 16px;
  1141. &.mini {
  1142. padding: 10px;
  1143. }
  1144. }
  1145. &__scrollbar {
  1146. flex: 1 1 0;
  1147. display: flex;
  1148. flex-direction: column;
  1149. min-height: 0;
  1150. ::v-deep {
  1151. .el-scrollbar__wrap {
  1152. flex: 1 1 0;
  1153. min-height: 0;
  1154. margin-bottom: 0 !important;
  1155. }
  1156. }
  1157. }
  1158. }
  1159. .o-widget-cfg {
  1160. display: inline-flex;
  1161. flex-direction: column;
  1162. align-items: center;
  1163. position: relative;
  1164. user-select: none;
  1165. cursor: pointer;
  1166. & + & {
  1167. margin-left: 32px;
  1168. }
  1169. &.active {
  1170. background-color: #31455d;
  1171. }
  1172. &__icon {
  1173. flex: none;
  1174. display: inline-flex;
  1175. justify-content: center;
  1176. align-items: center;
  1177. width: 32px;
  1178. height: 32px;
  1179. color: #fff;
  1180. font-size: 22px;
  1181. border-radius: $radius;
  1182. background-color: $blue;
  1183. }
  1184. &__text {
  1185. margin-top: 6px;
  1186. color: #9fa5b8;
  1187. font-size: 12px;
  1188. line-height: 1;
  1189. }
  1190. }
  1191. .o-layer {
  1192. color: $gray--dark;
  1193. user-select: none;
  1194. border-radius: $radius;
  1195. background-color: #252d42;
  1196. overflow: hidden;
  1197. cursor: pointer;
  1198. & + & {
  1199. margin-top: 10px;
  1200. }
  1201. &.active {
  1202. color: #fff;
  1203. outline: 2px solid $blue;
  1204. background-color: $blue;
  1205. }
  1206. }
  1207. .l-grid--info.dragging .o-card .o-card__icon {
  1208. display: none;
  1209. }
  1210. .o-card {
  1211. &:hover &__icon {
  1212. display: inline-block;
  1213. }
  1214. &__icon {
  1215. display: none;
  1216. position: absolute;
  1217. top: 0;
  1218. right: 0;
  1219. padding: 6px;
  1220. font-size: 16px;
  1221. border-radius: 50%;
  1222. background-color: rgba(#000, 0.4);
  1223. z-index: 9;
  1224. }
  1225. &__tag {
  1226. position: absolute;
  1227. top: 0;
  1228. left: 0;
  1229. padding: 4px 6px;
  1230. font-size: 12px;
  1231. line-height: 1;
  1232. border-bottom-right-radius: $radius;
  1233. background-color: rgba(#000, 0.4);
  1234. }
  1235. &__checkbox {
  1236. position: absolute;
  1237. top: 10px;
  1238. left: 10px;
  1239. pointer-events: none;
  1240. z-index: 9;
  1241. }
  1242. &__play {
  1243. position: absolute;
  1244. top: 50%;
  1245. left: 50%;
  1246. padding: 2px;
  1247. font-size: 24px;
  1248. border-radius: 50%;
  1249. background-color: rgba(#000, 0.4);
  1250. transform: translate(-50%, -50%);
  1251. }
  1252. &__grid {
  1253. position: absolute;
  1254. top: 10px;
  1255. right: 10px;
  1256. font-size: 18px;
  1257. z-index: 9;
  1258. }
  1259. }
  1260. .o-scale-slider {
  1261. justify-content: flex-start;
  1262. min-width: 48px;
  1263. width: auto;
  1264. padding: 0 $spacing;
  1265. font-size: 20px;
  1266. &__wrapper {
  1267. width: 184px;
  1268. transition: width 0.1s;
  1269. overflow: hidden;
  1270. }
  1271. &__slider {
  1272. width: 100px;
  1273. margin: 0 16px;
  1274. }
  1275. .el-icon-zoom-in + .svg-icon {
  1276. margin-left: 16px;
  1277. }
  1278. .svg-icon {
  1279. font-size: 16px;
  1280. }
  1281. }
  1282. </style>