Designer.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. <template>
  2. <div
  3. v-loading.lock="loading"
  4. element-loading-background="rgba(0, 0, 0, 0.8)"
  5. class="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="c-designer__side c-side">
  17. <div class="c-side__tool">
  18. <div
  19. class="c-side__item"
  20. :class="{ active: tabIndex === 0 }"
  21. @click="tabIndex = 0"
  22. >
  23. 工具栏
  24. </div>
  25. <div
  26. v-if="hasWidgets"
  27. class="c-side__item"
  28. :class="{ active: tabIndex === 1 }"
  29. @click="tabIndex = 1"
  30. >
  31. 图层
  32. </div>
  33. </div>
  34. <div
  35. v-show="tabIndex === 0"
  36. class="c-side__content"
  37. @dragstart="widgetOnDragStart"
  38. @dragend="widgetOnDragEnd"
  39. >
  40. <div
  41. v-for="cfg in widgetConfigs"
  42. :key="cfg.key"
  43. :data-type="cfg.type"
  44. class="c-widget"
  45. draggable
  46. >
  47. <span class="c-widget__icon">
  48. <i :class="cfg.icon" />
  49. </span>
  50. <span class="c-widget__text u-ellipsis">{{ cfg.label }}</span>
  51. </div>
  52. </div>
  53. <div
  54. v-show="tabIndex === 1"
  55. class="c-side__content"
  56. >
  57. <div
  58. v-for="(layer, index) in layers"
  59. :key="layer.id"
  60. class="c-widget"
  61. :class="{ active: layer.id === selectedWidgetId }"
  62. @mousedown="onLayerClick($event, index)"
  63. >
  64. <span class="c-widget__icon">
  65. <i :class="layer.icon" />
  66. </span>
  67. <span
  68. class="c-widget__text"
  69. :title="layer.name"
  70. >
  71. {{ layer.name }}
  72. </span>
  73. </div>
  74. </div>
  75. </div>
  76. <div class="c-designer__main">
  77. <div class="l-flex--row c-designer__tool">
  78. <div
  79. v-if="program"
  80. class="l-flex--row c-designer__tip"
  81. >
  82. <span class="c-sibling-item c-designer__name u-ellipsis">{{ program.name }}</span>
  83. <span class="c-sibling-item c-designer__ratio">{{ program.resolutionRatio }}</span>
  84. </div>
  85. <el-tooltip
  86. class="c-designer__btn"
  87. content="保存"
  88. :hide-after="2000"
  89. >
  90. <i
  91. class="iconfont iconsave"
  92. @click="onSave"
  93. />
  94. </el-tooltip>
  95. <el-tooltip
  96. v-if="hasWidgets"
  97. class="c-designer__btn"
  98. content="清空"
  99. :hide-after="2000"
  100. >
  101. <i
  102. class="iconfont iconlajitong"
  103. @click="clear"
  104. />
  105. </el-tooltip>
  106. <el-tooltip
  107. class="c-designer__btn"
  108. content="缩放"
  109. :hide-after="2000"
  110. >
  111. <div
  112. class="o-scale-slider"
  113. @click="toScale"
  114. >
  115. <div
  116. class="l-flex--row o-scale-slider__wrapper"
  117. :class="{ expand: dragScale }"
  118. >
  119. <i
  120. class="el-icon-zoom-out"
  121. @click="scaleDown"
  122. />
  123. <el-slider
  124. v-model="scale"
  125. class="o-scale-slider__slider"
  126. :min="minScale"
  127. :max="maxScale"
  128. @change="toScale"
  129. />
  130. <i
  131. class="el-icon-zoom-in"
  132. @click="scaleUp"
  133. />
  134. </div>
  135. <svg-icon
  136. v-if="dragScale"
  137. icon-class="exit-fullscreen"
  138. @click.stop="hideScale"
  139. />
  140. <svg-icon
  141. v-else
  142. icon-class="fullscreen"
  143. />
  144. </div>
  145. </el-tooltip>
  146. <el-tooltip
  147. v-if="hasAudio"
  148. class="c-designer__btn"
  149. content="音效"
  150. :hide-after="2000"
  151. >
  152. <div @click="toggleMute">
  153. <div
  154. class="o-wave"
  155. :class="{ muted }"
  156. >
  157. <span class="o-wave__line" />
  158. <span class="o-wave__line" />
  159. <span class="o-wave__line" />
  160. <span class="o-wave__line" />
  161. </div>
  162. </div>
  163. </el-tooltip>
  164. <el-tooltip
  165. v-if="hasNext"
  166. class="c-designer__btn"
  167. content="切歌"
  168. :hide-after="2000"
  169. >
  170. <div @click="chooseBgm">
  171. <i class="o-next" />
  172. </div>
  173. </el-tooltip>
  174. </div>
  175. <div
  176. ref="wrapper"
  177. class="c-designer__content"
  178. @mousedown="onScreenClick"
  179. >
  180. <div
  181. class="c-designer__wrapper"
  182. :style="wrapperStyles"
  183. >
  184. <div
  185. ref="canvas"
  186. class="c-designer__canvas"
  187. :style="[transformStyles, styles]"
  188. @dragover.prevent
  189. @drop.prevent="widgetOnDrop"
  190. >
  191. <div
  192. class="c-designer__background has-bg"
  193. :style="backgroundStyles"
  194. />
  195. <div
  196. v-show="grid"
  197. class="c-designer__grid"
  198. />
  199. <widget
  200. v-for="(item, index) in widgets"
  201. :key="item.id"
  202. ref="widgets"
  203. :scale="100 / scale"
  204. :node="item"
  205. :root="node"
  206. @focus="onWidgetFocus(index)"
  207. @blur="onWidgetBlur"
  208. @menu="onWidgetMenu($event, index)"
  209. />
  210. </div>
  211. </div>
  212. </div>
  213. </div>
  214. <div class="c-designer__side large c-side">
  215. <div class="c-side__tool">
  216. <div
  217. v-for="(tab, index) in dynamicOptions"
  218. :key="index"
  219. class="c-side__item"
  220. :class="{ active: optionIndex === index }"
  221. @click="optionIndex = index"
  222. >{{ tab.label }}</div>
  223. </div>
  224. <div
  225. v-for="(tab, index) in dynamicOptions"
  226. v-show="optionIndex === index"
  227. :key="index"
  228. class="c-side__content"
  229. :class="{ active: optionIndex === index }"
  230. >
  231. <dynamic-item
  232. v-for="item in tab.list"
  233. :key="item.key"
  234. :root="node"
  235. :node="widget"
  236. :attr="item"
  237. @choose="onEditAssets"
  238. />
  239. </div>
  240. </div>
  241. <content-menu
  242. :visible.sync="visibleContentMenu"
  243. :style-obj="styleObj"
  244. @delete="deleteLayer"
  245. @copy="copyLayer"
  246. @top="topLayer"
  247. @low="lowLayer"
  248. @up="upLayer"
  249. @down="downLayer"
  250. />
  251. <el-dialog
  252. title="数据"
  253. :visible.sync="dialogVisibleAssets"
  254. custom-class="c-dialog"
  255. :close-on-click-modal="false"
  256. :before-close="onCloseAssetsDialog"
  257. >
  258. <draggable
  259. v-if="sources"
  260. v-model="sources"
  261. class="c-grid"
  262. :class="{ dragging }"
  263. animation="300"
  264. @start="onSourceDragStart"
  265. @end="onSourceDragEnd"
  266. >
  267. <div
  268. v-for="(source, index) in sources"
  269. :key="index"
  270. class="c-card u-pointer"
  271. @click="onView(source)"
  272. >
  273. <div class="c-card__content">
  274. <i
  275. class="c-card__icon iconfont iconlajitong"
  276. @mousedown.stop
  277. @pointerdown.stop
  278. @click.stop="onDelAsset(index)"
  279. />
  280. <i
  281. v-if="source.thumbnailUrl"
  282. class="o-image"
  283. :style="{ 'background-image': `url('${source.thumbnailUrl}')` }"
  284. />
  285. <div class="c-card__text u-ellipsis">{{ source.name }}</div>
  286. </div>
  287. </div>
  288. </draggable>
  289. <template #footer>
  290. <button
  291. class="o-button"
  292. @click="onAddAsset"
  293. >
  294. 新增
  295. </button>
  296. <button
  297. class="o-button"
  298. @click="onSaveAssets"
  299. >
  300. 确定
  301. </button>
  302. <button
  303. class="o-button cancel"
  304. @click="onCloseAssetsDialog"
  305. >
  306. 取消
  307. </button>
  308. </template>
  309. </el-dialog>
  310. <el-dialog
  311. title="请选择"
  312. :visible.sync="dialogServerData"
  313. :custom-class="dialogServerSize"
  314. append-to-body
  315. >
  316. <template v-if="dialogServerData">
  317. <grid-table
  318. v-if="isImage"
  319. :schema="assetSchema"
  320. >
  321. <grid-table-item>
  322. <template v-slot="item">
  323. <div class="c-card u-pointer">
  324. <div
  325. class="c-card__content"
  326. @dblclick="onChoosenAsset(item)"
  327. >
  328. <div
  329. class="o-image"
  330. :style="{ 'background-image': `url('${item.thumbnailUrl}')` }"
  331. >
  332. <span class="c-card__name u-ellipsis">{{ item.originalName }}</span>
  333. </div>
  334. </div>
  335. </div>
  336. </template>
  337. </grid-table-item>
  338. </grid-table>
  339. <schema-table
  340. v-else
  341. :schema="assetSchema"
  342. @row-dblclick="onChoosenAsset"
  343. />
  344. </template>
  345. </el-dialog>
  346. <preview-dialog ref="previewDialog" />
  347. </div>
  348. </template>
  349. <script>
  350. import Draggable from 'vuedraggable'
  351. import domToImage from 'dom-to-image'
  352. import { updateProgram } from '@/api/program'
  353. import {
  354. getAssets,
  355. getThumbnailUrl
  356. } from '@/api/asset'
  357. import {
  358. State,
  359. AssetType
  360. } from '@/constant'
  361. import {
  362. widgets,
  363. normalize,
  364. getOptions,
  365. getIcon,
  366. copy,
  367. fix,
  368. getDuration,
  369. toJSON
  370. } from './core/utils'
  371. import base from './core/base'
  372. import Widget from './core/widget/Widget.vue'
  373. import ContentMenu from './core/components/ContentMenu.vue'
  374. import DynamicItem from './core/components/DynamicItem.vue'
  375. export default {
  376. name: 'BigScreenDesigner',
  377. components: {
  378. Widget,
  379. Draggable,
  380. ContentMenu,
  381. DynamicItem
  382. },
  383. mixins: [base],
  384. data () {
  385. return {
  386. loading: false,
  387. snapping: false,
  388. padding: 10,
  389. tabIndex: 0,
  390. optionIndex: 0,
  391. widgetConfigs: widgets,
  392. grid: false,
  393. visibleContentMenu: false,
  394. styleObj: null,
  395. widgetAttr: null,
  396. dialogVisibleAssets: false,
  397. sources: null,
  398. dragging: false,
  399. dragScale: false,
  400. dialogServerData: false,
  401. assetSchema: {
  402. list: this.getAssets,
  403. transform: this.transformAsset,
  404. cols: [
  405. { prop: 'file', label: '缩略图', type: 'asset' },
  406. { prop: 'originalName', label: '名称' },
  407. {
  408. type: 'invoke', render: [
  409. { label: '查看', on: this.onViewAsset }
  410. ]
  411. }
  412. ]
  413. }
  414. }
  415. },
  416. computed: {
  417. hasWidgets () {
  418. return this.widgets.length > 0
  419. },
  420. dynamicOptions () {
  421. if (this.widget) {
  422. return getOptions(this.widget.type)
  423. }
  424. return []
  425. },
  426. layers () {
  427. const layers = []
  428. for (let i = this.widgets.length - 1; i >= 0; i--) {
  429. const { id, type, layerName } = this.widgets[i]
  430. layers.push({
  431. id: id,
  432. icon: getIcon(type),
  433. name: layerName
  434. })
  435. }
  436. return layers
  437. },
  438. isImage () {
  439. return this.widgetAttr?.type === 'data' && this.widgetAttr.options?.type === AssetType.IMAGE
  440. },
  441. selectedWidgetId () {
  442. return this.widget.id
  443. },
  444. wrapperStyles () {
  445. return this.node ? {
  446. width: `${this.node.width * this.scale / 100}px`,
  447. height: `${this.node.height * this.scale / 100}px`
  448. } : null
  449. },
  450. dialogServerSize () {
  451. return `c-dialog ${this.isImage ? 'large' : 'medium'}`
  452. }
  453. },
  454. watch: {
  455. selectedWidgetIndex (val, old) {
  456. if (old > -1) {
  457. const id = this.widgets[old]?.id
  458. id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(false)
  459. }
  460. if (val > -1) {
  461. const id = this.widgets[val]?.id
  462. id && this.$refs.widgets[this.$refs.widgets.findIndex(w => w.node.id === id)]?.$refs.draggable.setActive(true)
  463. }
  464. this.optionIndex = 0
  465. }
  466. },
  467. methods: {
  468. clear () {
  469. this.$confirm(
  470. '清空后所有数据将丢失,确定清空画布?',
  471. { type: 'warning' }
  472. ).then(() => {
  473. this.tabIndex = 0
  474. this.selectedWidgetIndex = -1
  475. const { width, height } = this.node
  476. this.initCanvas({ width, height })
  477. })
  478. },
  479. widgetOnDragStart (evt) {
  480. evt.dataTransfer.setData('type', evt.target.dataset.type)
  481. this.dragging = true
  482. },
  483. widgetOnDragEnd () {
  484. this.dragging = false
  485. },
  486. widgetOnDrop (evt) {
  487. const type = evt.dataTransfer.getData('type')
  488. if (type) {
  489. let { offsetX: left, offsetY: top } = evt
  490. const node = normalize({ type })
  491. top -= node.height / 2
  492. if (top < 0) {
  493. top = 0
  494. }
  495. left -= node.width / 2
  496. if (left < 0) {
  497. left = 0
  498. }
  499. node.top = top
  500. node.left = left
  501. this.node.widgets.push(node)
  502. }
  503. },
  504. onWidgetFocus (index) {
  505. this.selectedWidgetIndex = index
  506. this.grid = true
  507. this.visibleContentMenu = false
  508. },
  509. onWidgetBlur () {
  510. this.grid = false
  511. },
  512. onWidgetMenu (evt, index) {
  513. this.selectedWidgetIndex = index
  514. this.onRightClick(evt)
  515. },
  516. onLayerClick (evt, index) {
  517. this.selectedWidgetIndex = this.widgets.length - 1 - index
  518. this.onRightClick(evt)
  519. },
  520. onScreenClick () {
  521. this.selectedWidgetIndex = -1
  522. },
  523. onRightClick (evt) {
  524. const { button, clientY: top, clientX: left } = evt
  525. if (button !== 2) {
  526. return
  527. }
  528. evt.stopPropagation()
  529. if (top || left) {
  530. this.styleObj = {
  531. display: 'block',
  532. top: `${top}px`,
  533. left: `${left}px`
  534. }
  535. }
  536. this.visibleContentMenu = true
  537. },
  538. // 删除
  539. deleteLayer () {
  540. this.widgets.splice(this.selectedWidgetIndex, 1)
  541. this.selectedWidgetIndex = -1
  542. },
  543. // 复制
  544. copyLayer () {
  545. this.widgets.splice(this.selectedWidgetIndex + 1, 0, copy(this.widget))
  546. },
  547. // 置顶
  548. topLayer () {
  549. if (this.selectedWidgetIndex < this.widgets.length - 1) {
  550. this.widgets.push(this.widgets.splice(
  551. this.selectedWidgetIndex,
  552. 1
  553. )[0])
  554. this.selectedWidgetIndex = this.widgets.length - 1
  555. }
  556. },
  557. // 置底
  558. lowLayer () {
  559. if (this.selectedWidgetIndex > 0) {
  560. this.widgets.unshift(this.widgets.splice(
  561. this.selectedWidgetIndex,
  562. 1
  563. )[0])
  564. this.selectedWidgetIndex = 0
  565. }
  566. },
  567. // 上移一层
  568. upLayer () {
  569. if (this.selectedWidgetIndex < this.widgets.length - 1) {
  570. this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
  571. this.selectedWidgetIndex + 1,
  572. 1,
  573. this.widgets[this.selectedWidgetIndex]
  574. )[0]
  575. this.selectedWidgetIndex += 1
  576. }
  577. },
  578. // 下移一层
  579. downLayer () {
  580. if (this.selectedWidgetIndex > 0) {
  581. this.widgets[this.selectedWidgetIndex] = this.widgets.splice(
  582. this.selectedWidgetIndex - 1,
  583. 1,
  584. this.widgets[this.selectedWidgetIndex]
  585. )[0]
  586. this.selectedWidgetIndex -= 1
  587. }
  588. },
  589. onEditAssets (attr) {
  590. this.widgetAttr = attr
  591. if (attr.options && attr.options.solo) {
  592. this.onAddAsset()
  593. } else {
  594. this.sources = this.widget[attr.key].map(
  595. this.isImage
  596. ? asset => {
  597. return {
  598. ...asset,
  599. thumbnailUrl: getThumbnailUrl(asset.keyName)
  600. }
  601. }
  602. : asset => {
  603. return {
  604. ...asset,
  605. thumbnailUrl: asset.thumbnail && getThumbnailUrl(asset.thumbnail)
  606. }
  607. }
  608. )
  609. this.dialogVisibleAssets = true
  610. }
  611. },
  612. onCloseAssetsDialog () {
  613. this.widgetAttr = null
  614. this.dialogVisibleAssets = false
  615. },
  616. changeAttr (value) {
  617. const { key, options } = this.widgetAttr
  618. this.widget[key] = value
  619. if (options) {
  620. const { callback } = options
  621. callback && callback.call(this.widget, this.node)
  622. }
  623. },
  624. onSaveAssets () {
  625. this.changeAttr(this.sources.map(({ name, keyName, thumbnail, duration, size, md5 }) => {
  626. const source = { name, keyName, size, md5 }
  627. if (!this.isImage) {
  628. if (thumbnail) {
  629. source.thumbnail = thumbnail
  630. }
  631. source.duration = duration
  632. }
  633. return source
  634. }))
  635. this.onCloseAssetsDialog()
  636. },
  637. onAddAsset () {
  638. this.dialogServerData = true
  639. },
  640. getAssets (params) {
  641. return getAssets({
  642. type: this.widgetAttr.options.type,
  643. status: State.RESOLVED,
  644. ...params
  645. })
  646. },
  647. transformAsset (asset) {
  648. if (asset.type === AssetType.IMAGE) {
  649. return {
  650. ...asset,
  651. thumbnailUrl: getThumbnailUrl(asset.thumbnail)
  652. }
  653. }
  654. return {
  655. ...asset,
  656. file: { thumbnail: asset.thumbnail }
  657. }
  658. },
  659. onView (keyName) {
  660. this.onViewAsset({ type: this.widgetAttr.options.type, keyName })
  661. },
  662. onViewAsset ({ type, keyName }) {
  663. this.$refs.previewDialog.show({ type, url: keyName })
  664. },
  665. onChoosenAsset ({ originalName, keyName, thumbnail, thumbnailUrl, duration, size, md5 }) {
  666. const source = {
  667. name: originalName,
  668. keyName,
  669. size,
  670. md5
  671. }
  672. if (this.dialogVisibleAssets) {
  673. if (this.isImage) {
  674. source.thumbnailUrl = thumbnailUrl
  675. } else {
  676. if (thumbnail) {
  677. source.thumbnail = thumbnail
  678. source.thumbnailUrl = getThumbnailUrl(thumbnail)
  679. }
  680. source.duration = duration
  681. }
  682. this.sources.unshift(source)
  683. } else {
  684. this.changeAttr([source])
  685. }
  686. this.dialogServerData = false
  687. },
  688. onDelAsset (index) {
  689. this.$confirm(
  690. '确定移除该数据?',
  691. { type: 'warning' }
  692. ).then(() => {
  693. this.sources.splice(index, 1)
  694. })
  695. },
  696. onSourceDragStart () {
  697. this.dragging = true
  698. },
  699. onSourceDragEnd () {
  700. this.dragging = false
  701. },
  702. toScale () {
  703. this.dragScale = true
  704. },
  705. hideScale () {
  706. this.dragScale = false
  707. },
  708. scaleDown () {
  709. this.scale = Math.max(this.scale - 10, this.minScale)
  710. },
  711. scaleUp () {
  712. this.scale = Math.min(this.scale + 10, this.maxScale)
  713. },
  714. onSave () {
  715. this.check().then(() => {
  716. this.selectedWidgetIndex = -1
  717. this.loading = true
  718. this.$nextTick(() => {
  719. this._save().finally(() => {
  720. this.loading = false
  721. })
  722. })
  723. })
  724. },
  725. check () {
  726. const warning = fix(this.node)
  727. if (warning) {
  728. return this.$confirm(
  729. `${warning},确定保存?`,
  730. { type: 'warning' }
  731. )
  732. }
  733. return Promise.resolve()
  734. },
  735. async _save () {
  736. try {
  737. const base64 = await this.snap()
  738. const result = await updateProgram({
  739. id: this.program.id,
  740. duration: getDuration(this.node),
  741. itemJsonStr: JSON.stringify(toJSON(this.node)),
  742. base64
  743. })
  744. if (result) {
  745. window.opener?.postMessage({
  746. id: this.program.id,
  747. base64
  748. })
  749. this.$message({
  750. type: 'success',
  751. message: '保存成功'
  752. })
  753. } else {
  754. this.$message({
  755. type: 'warning',
  756. message: '保存失败'
  757. })
  758. }
  759. } catch (e) {
  760. console.warn(e)
  761. this.$message({
  762. type: 'warning',
  763. message: '保存失败'
  764. })
  765. }
  766. },
  767. snap () {
  768. this.snapping = true
  769. return domToImage.toJpeg(this.$refs.canvas, {
  770. filter (node) {
  771. const { tagName } = node
  772. if (tagName === 'CANVAS') {
  773. return /^data:.+;base64,.+/.test(node.toDataURL())
  774. }
  775. return tagName !== 'VIDEO' && tagName !== 'IFRAME'
  776. },
  777. width: this.node.width * this.scale / 100 | 0,
  778. height: this.node.height * this.scale / 100 | 0,
  779. quality: 0.1
  780. }).finally(() => {
  781. this.snapping = false
  782. })
  783. }
  784. }
  785. }
  786. </script>
  787. <style lang="scss" scoped>
  788. $drak: #242a30;
  789. .c-designer {
  790. display: flex;
  791. height: 100%;
  792. &__side {
  793. flex: none;
  794. width: 180px;
  795. min-height: 0;
  796. &.large {
  797. width: 240px;
  798. }
  799. }
  800. &__main {
  801. flex: 1 0 0;
  802. display: flex;
  803. flex-direction: column;
  804. position: relative;
  805. min-width: 0;
  806. }
  807. &__content {
  808. flex: 1 0 0;
  809. position: relative;
  810. padding: 10px;
  811. overflow: auto;
  812. background: url("~@/assets/dot.png") repeat;
  813. }
  814. &__tool {
  815. flex: 0 0 40px;
  816. background-color: $drak;
  817. }
  818. &__tip {
  819. padding: 0 16px;
  820. }
  821. &__name {
  822. max-width: 400px;
  823. }
  824. &__name,
  825. &__ratio {
  826. color: $gray--dark;
  827. font-size: 16px;
  828. line-height: 1;
  829. }
  830. &__btn {
  831. display: inline-flex;
  832. justify-content: center;
  833. align-items: center;
  834. width: 55px;
  835. height: 100%;
  836. color: $gray;
  837. cursor: pointer;
  838. &:hover {
  839. background-color: #191d22;
  840. }
  841. &:active {
  842. background-color: darken(#191d22, 5%);
  843. }
  844. &.iconlajitong {
  845. font-size: 20px;
  846. }
  847. }
  848. &__wrapper {
  849. min-width: 100%;
  850. min-height: 100%;
  851. }
  852. &__canvas {
  853. position: relative;
  854. background-color: #000;
  855. overflow: visible;
  856. &::before {
  857. content: "";
  858. position: absolute;
  859. top: 0;
  860. left: 0;
  861. right: 0;
  862. bottom: 0;
  863. background-color: currentColor;
  864. z-index: -1;
  865. }
  866. }
  867. &__background {
  868. position: absolute;
  869. top: 0;
  870. left: 0;
  871. right: 0;
  872. bottom: 0;
  873. z-index: -1;
  874. }
  875. &__grid {
  876. position: absolute;
  877. top: 0;
  878. left: 0;
  879. right: 0;
  880. bottom: 0;
  881. background-size: 30px 30px, 30px 30px;
  882. background-image: linear-gradient(hsla(0, 0%, 100%, 0.1) 1px, transparent 0),
  883. linear-gradient(90deg, hsla(0, 0%, 100%, 0.1) 1px, transparent 0);
  884. }
  885. }
  886. .c-side {
  887. display: flex;
  888. flex-direction: column;
  889. background-color: $drak;
  890. &__tool {
  891. flex: none;
  892. display: flex;
  893. }
  894. &__item {
  895. flex: 1 0 0;
  896. color: #909399;
  897. font-size: 14px;
  898. text-align: center;
  899. line-height: 40px;
  900. background-color: #242f3b;
  901. user-select: none;
  902. cursor: pointer;
  903. &.active {
  904. color: #409eff;
  905. background-color: #31455d;
  906. }
  907. }
  908. &__content {
  909. flex: 1 0 0;
  910. min-height: 0;
  911. padding: 15px;
  912. overflow-y: auto;
  913. &::-webkit-scrollbar {
  914. width: 4px;
  915. height: 4px;
  916. }
  917. &::-webkit-scrollbar-track-piece {
  918. background-color: #29405c;
  919. }
  920. &::-webkit-scrollbar-track {
  921. box-shadow: 1px 1px 5px rgba(116, 148, 170, 0.5) inset;
  922. }
  923. &::-webkit-scrollbar-thumb {
  924. min-height: 20px;
  925. background-clip: content-box;
  926. box-shadow: 0 0 0 5px rgba(116, 148, 170, 0.5) inset;
  927. }
  928. }
  929. }
  930. .c-widget {
  931. display: flex;
  932. align-items: center;
  933. position: relative;
  934. width: 100%;
  935. height: 48px;
  936. margin-bottom: 1px;
  937. padding: 0 6px;
  938. color: #bfcbd9;
  939. font-size: 12px;
  940. user-select: none;
  941. cursor: pointer;
  942. &.active {
  943. background-color: #31455d;
  944. }
  945. &__icon {
  946. flex: none;
  947. display: inline-block;
  948. margin-right: 10px;
  949. width: 52px;
  950. height: 30px;
  951. color: #409eff;
  952. font-size: 16px;
  953. line-height: 30px;
  954. text-align: center;
  955. border: 1px solid #3a4659;
  956. background-color: #282a30;
  957. }
  958. &__text {
  959. flex: 1 1 auto;
  960. min-width: 0;
  961. }
  962. }
  963. .c-grid.dragging .c-card .c-card__icon {
  964. display: none;
  965. }
  966. .c-card {
  967. display: inline-block;
  968. position: relative;
  969. background-color: rgba(0, 0, 0, 0.8);
  970. &:hover &__icon {
  971. display: block;
  972. padding: 6px;
  973. color: #fff;
  974. border-radius: 50%;
  975. background-color: rgba(0, 0, 0, 0.6);
  976. cursor: pointer;
  977. }
  978. &.sortable-chosen &__icon {
  979. display: none;
  980. }
  981. &__content {
  982. position: relative;
  983. padding-top: 60%;
  984. }
  985. &__icon {
  986. display: none;
  987. position: absolute;
  988. top: 0;
  989. right: 0;
  990. z-index: 9;
  991. }
  992. &__name {
  993. position: absolute;
  994. left: 0;
  995. right: 0;
  996. bottom: 0;
  997. padding: 4px 6px;
  998. color: $gray;
  999. font-size: 14px;
  1000. text-align: center;
  1001. background-color: rgba(0, 0, 0, 0.6);
  1002. }
  1003. &__text {
  1004. position: absolute;
  1005. top: 50%;
  1006. left: 8px;
  1007. right: 8px;
  1008. color: #fff;
  1009. font-size: 14px;
  1010. text-align: center;
  1011. transform: translateY(-50%);
  1012. }
  1013. }
  1014. .o-image {
  1015. position: absolute;
  1016. top: 0;
  1017. left: 0;
  1018. width: 100%;
  1019. height: 100%;
  1020. background-position: center;
  1021. background-size: contain;
  1022. background-repeat: no-repeat;
  1023. }
  1024. .o-scale-slider {
  1025. justify-content: flex-start;
  1026. min-width: 48px;
  1027. width: auto;
  1028. padding: 0 $spacing;
  1029. font-size: 20px;
  1030. &__wrapper {
  1031. width: 0;
  1032. transition: width 0.1s;
  1033. overflow: hidden;
  1034. &.expand {
  1035. width: 184px;
  1036. }
  1037. }
  1038. &__slider {
  1039. width: 100px;
  1040. margin: 0 16px;
  1041. }
  1042. .el-icon-zoom-in + .svg-icon {
  1043. margin-left: 16px;
  1044. }
  1045. .svg-icon {
  1046. font-size: 16px;
  1047. }
  1048. }
  1049. </style>