Преглед на файлове

feat:新增图片导出功能

奔跑的面条 преди 3 години
родител
ревизия
0d482ad9c4

+ 1 - 0
package.json

@@ -15,6 +15,7 @@
     "crypto-ts": "^1.0.2",
     "echarts-liquidfill": "3",
     "highlight.js": "^11.5.0",
+    "html2canvas": "^1.4.1",
     "naive-ui": "^2.25.2",
     "pinia": "^2.0.6",
     "screenfull": "^6.0.0",

+ 33 - 0
pnpm-lock.yaml

@@ -24,6 +24,7 @@ specifiers:
   eslint-plugin-prettier: ^4.0.0
   eslint-plugin-vue: ^8.2.0
   highlight.js: ^11.5.0
+  html2canvas: ^1.4.1
   lodash: ~4.17.21
   mockjs: ^1.1.0
   naive-ui: ^2.25.2
@@ -56,6 +57,7 @@ dependencies:
   crypto-ts: r2.cnpmjs.org/crypto-ts/1.0.2
   echarts-liquidfill: r2.cnpmjs.org/echarts-liquidfill/3.1.0_echarts@5.3.0
   highlight.js: registry.npmjs.org/highlight.js/11.5.0
+  html2canvas: 1.4.1
   naive-ui: registry.npmjs.org/naive-ui/2.25.2_vue@3.2.24
   pinia: rg.cnpmjs.org/pinia/2.0.6_typescript@4.5.2+vue@3.2.24
   screenfull: rg.cnpmjs.org/screenfull/6.0.0
@@ -100,6 +102,17 @@ devDependencies:
 
 packages:
 
+  /base64-arraybuffer/1.0.2:
+    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
+    engines: {node: '>= 0.6.0'}
+    dev: false
+
+  /css-line-break/2.1.0:
+    resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
+    dependencies:
+      utrie: 1.0.2
+    dev: false
+
   /esbuild-android-arm64/0.13.15:
     resolution: {integrity: sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==}
     cpu: [arm64]
@@ -261,6 +274,14 @@ packages:
     requiresBuild: true
     dev: true
 
+  /html2canvas/1.4.1:
+    resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
+    engines: {node: '>=8.0.0'}
+    dependencies:
+      css-line-break: 2.1.0
+      text-segmentation: 1.0.3
+    dev: false
+
   /jest-diff/27.4.6:
     resolution: {integrity: sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==}
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -290,6 +311,12 @@ packages:
     resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
     dev: false
 
+  /text-segmentation/1.0.3:
+    resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
+    dependencies:
+      utrie: 1.0.2
+    dev: false
+
   /uglify-js/3.15.1:
     resolution: {integrity: sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==}
     engines: {node: '>=0.8.0'}
@@ -298,6 +325,12 @@ packages:
     dev: true
     optional: true
 
+  /utrie/1.0.2:
+    resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
+    dependencies:
+      base64-arraybuffer: 1.0.2
+    dev: false
+
   /vfonts/0.0.3:
     resolution: {integrity: sha512-nguyw8L6Un8eelg1vQ31vIU2ESxqid7EYmy8V+MDeMaHBqaRSkg3dTBToC1PR00D89UzS/SLkfYPnx0Wf23IQQ==}
     dev: false

+ 3 - 1
src/plugins/icon.ts

@@ -8,7 +8,8 @@ import {
   Pencil as PencilIcon,
   HammerOutline as HammerIcon,
   DesktopOutline as DesktopOutlineIcon,
-  DownloadOutline as DownloadIcon,
+  Download as DownloadIcon,
+  DownloadOutline as DownloadOutlineIcon,
   Open as OpenIcon,
   Send as SendIcon,
   InformationCircleOutline as InformationCircleIcon,
@@ -122,6 +123,7 @@ const ionicons5 = {
   DesktopOutlineIcon,
   // 下载
   DownloadIcon,
+  DownloadOutlineIcon,
   // 导出
   OpenIcon,
   // 导出

+ 8 - 17
src/store/modules/chartEditStore/chartEditStore.ts

@@ -508,16 +508,6 @@ export const useChartEditStore = defineStore({
       }
     }, 
     // ----------------
-    // * 设置页面变换时候的 Class
-    setPageSizeClass(): void {
-      const dom = this.getEditCanvas.editContentDom
-      if (dom) {
-        dom.classList.add('content-resize')
-        setTimeout(() => {
-          dom.classList.remove('content-resize')
-        }, 600)
-      }
-    },
     // * 设置页面大小
     setPageSize(scale: number): void {
       this.setPageStyle('height', `${this.editCanvasConfig.height * scale}px`)
@@ -572,15 +562,16 @@ export const useChartEditStore = defineStore({
       }
       return remove
     },
-    // * 设置缩放
-    setScale(scale: number, sys = true): void {
-      if (!this.getEditCanvas.lockScale) {
-        this.setPageSizeClass()
+    /**
+     * * 设置缩放
+     * @param scale 0~1 number 缩放比例;
+     * @param force boolean 强制缩放
+     */
+    setScale(scale: number, force = false): void {
+      if (!this.getEditCanvas.lockScale || force) {
         this.setPageSize(scale)
         this.getEditCanvas.userScale = scale
-        if (sys) {
-          this.getEditCanvas.scale = scale
-        }
+        this.getEditCanvas.scale = scale
       }
     }
   }

+ 1 - 1
src/styles/common/_dark.scss

@@ -30,6 +30,6 @@ $dark: (
   // hover 边框颜色
     hover-border-color: $--color-dark-bg-5,
   // 阴影
-    box-shadow: 0 8px 20px #5252521f
+    box-shadow: 0 8px 10px #1e1e1e1f
     
 );

+ 1 - 1
src/styles/common/_light.scss

@@ -32,5 +32,5 @@ $light: (
   // hover 边框颜色
     hover-border-color: $--color-light-bg-4,
   // 阴影
-    box-shadow: 0 8px 20px #0000001a
+    box-shadow: 0 8px 10px #00000012
 );

+ 1 - 1
src/utils/crypto.ts

@@ -1,5 +1,5 @@
 import * as CryptoJS from 'crypto-ts'
-
+// 加密
 const AES_KEY = 'mt'
 
 export const cryptoEncode = (data: string): string => {

+ 21 - 10
src/utils/file.ts

@@ -18,26 +18,37 @@ export const readFile = (file: File) => {
   })
 }
 
+/**
+ * * 通过 a 标签下载数据
+ * @param url 
+ * @param filename 
+ * @param fileSuffix 
+ */
+export const downloadByA = (url: string, filename = new Date().getDate().toString(), fileSuffix?: string) => {
+  const ele = document.createElement('a') // 创建下载链接
+  ele.download = `${filename}.${fileSuffix}` //设置下载的名称
+  ele.style.display = 'none' // 隐藏的可下载链接
+  // 字符内容转变成blob地址
+  ele.href = url
+  // 绑定点击时间
+  document.body.appendChild(ele)
+  ele.click()
+  // 然后移除
+  document.body.removeChild(ele)
+}
+
 /**
  * 下载数据
  * @param { string } content 数据内容
  * @param { ?string } filename 文件名称(默认随机字符)
  * @param { ?string } fileSuffix 文件名称(默认随机字符)
  */
-export const downloadFile = (
+export const downloadTextFile = (
   content: string,
   filename = new Date().getDate().toString(),
   fileSuffix?: string
 ) => {
-  const ele = document.createElement('a') // 创建下载链接
-  ele.download = `${filename}.${fileSuffix}` //设置下载的名称
-  ele.style.display = 'none' // 隐藏的可下载链接
   // 字符内容转变成blob地址
   const blob = new Blob([content])
-  ele.href = URL.createObjectURL(blob)
-  // 绑定点击时间
-  document.body.appendChild(ele)
-  ele.click()
-  // 然后移除
-  document.body.removeChild(ele)
+  downloadByA(URL.createObjectURL(blob), filename, fileSuffix)
 }

+ 21 - 1
src/utils/utils.ts

@@ -3,6 +3,8 @@ import { NIcon } from 'naive-ui'
 import screenfull from 'screenfull'
 import throttle from 'lodash/throttle'
 import Image_404 from '../assets/images/exception/image-404.png'
+import html2canvas from 'html2canvas'
+import { downloadByA } from './file'
 
 /**
  * * 判断是否是开发环境
@@ -146,7 +148,7 @@ export const addEventListener = <K extends keyof WindowEventMap>(
     type,
     throttle(listener, 300, {
       leading: true,
-      trailing: false
+      trailing: false,
     }),
     options
   )
@@ -163,3 +165,21 @@ export const removeEventListener = <K extends keyof WindowEventMap>(
   if (!target) return
   target.removeEventListener(type, listener)
 }
+
+/**
+ * * 截取画面为图片
+ * @param html 需要截取的 DOM
+ */
+export const canvasCut = (html: HTMLElement | null, callback?: Function) => {
+  if (!html) {
+    window['$message'].error('导出失败!')
+    if (callback) callback()
+    return
+  }
+
+  html2canvas(html).then((canvas: HTMLCanvasElement) => {
+    window['$message'].success('导出成功!')
+    downloadByA(canvas.toDataURL(), undefined, 'png')
+    if (callback) callback()
+  })
+}

+ 2 - 2
src/views/chart/ContentConfigurations/components/ChartData/hooks/useFile.hooks.ts

@@ -1,7 +1,7 @@
 import { ref, toRef, nextTick } from 'vue'
 import { UploadCustomRequestOptions } from 'naive-ui'
 import { FileTypeEnum } from '@/enums/fileTypeEnum'
-import { readFile, downloadFile } from '@/utils'
+import { readFile, downloadTextFile } from '@/utils'
 
 export const useFile = (targetData: any) => {
   const uploadFileListRef = ref()
@@ -35,7 +35,7 @@ export const useFile = (targetData: any) => {
   const download = () => {
     try {
       window['$message'].success('下载中,请耐心等待...')
-      downloadFile(JSON.stringify(targetData.value.option.dataset), undefined, 'json')
+      downloadTextFile(JSON.stringify(targetData.value.option.dataset), undefined, 'json')
     } catch (error) {
       window['$message'].error('下载失败,数据错误!')
     }

+ 0 - 4
src/views/chart/ContentEdit/components/EditRange/index.vue

@@ -54,12 +54,8 @@ const rangeStyle = computed(() => {
 <style lang="scss" scoped>
 @include go(edit-range) {
   position: relative;
-  border: 1px solid;
-  border-radius: 15px;
   transform-origin: left top;
-  @include fetch-theme('box-shadow');
   @include filter-border-color('hover-border-color');
-  @include fetch-theme-custom('border-color', 'background-color4');
   @include filter-bg-color('background-color2');
 }
 </style>

+ 7 - 9
src/views/chart/ContentEdit/index.vue

@@ -87,20 +87,18 @@ onMounted(() => {
 </script>
 
 <style lang="scss" scoped>
-@include goId(chart-edit-layout) {
+@include goId('chart-edit-layout') {
   position: relative;
   width: 100%;
   overflow: hidden;
-  @include background-image('background-point');
   @extend .go-point-bg;
-  @include goId(chart-edit-content) {
-    padding: 20px;
-    border: 1px solid rgba(0, 0, 0, 0);
+  @include background-image('background-point');
+  @include goId('chart-edit-content') {
+    border-radius: 5px;
+    margin: 15px;
+    overflow: hidden;
     @extend .go-transition;
-    &.content-resize {
-      border-radius: 15px;
-      @include hover-border-color('hover-border-color');
-    }
+    @include fetch-theme('box-shadow');
     .edit-content-chart {
       position: absolute;
     }

+ 38 - 3
src/views/chart/HeaderRightBtn/index.vue

@@ -11,14 +11,16 @@
 
 <script setup lang="ts">
 import { shallowReactive } from 'vue'
-import { renderIcon, fetchPathByName, routerTurnByPath,setSessionStorage, getLocalStorage } from '@/utils'
+import { renderIcon, fetchPathByName, routerTurnByPath, setSessionStorage, getLocalStorage } from '@/utils'
 import { PreviewEnum } from '@/enums/pageEnum'
 import { StorageEnum } from '@/enums/storageEnum'
 import { icon } from '@/plugins'
+import { canvasCut } from '@/utils'
 import { useRoute } from 'vue-router'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { EditCanvasTypeEnum } from '@/store/modules/chartEditStore/chartEditStore.d'
 
-const { BrowsersOutlineIcon, SendIcon } = icon.ionicons5
+const { BrowsersOutlineIcon, SendIcon, DownloadIcon } = icon.ionicons5
 const chartEditStore = useChartEditStore()
 
 // TODO 我也不知道为什么不能实时获取,必须初始化获取
@@ -53,6 +55,34 @@ const previewHandle = () => {
   routerTurnByPath(path, [previewId], undefined, true)
 }
 
+// 导出
+const exportHandle = () => {
+  const ruler = document.getElementById('mb-ruler')
+  const range = document.querySelector('.go-edit-range') as HTMLElement
+
+  // 隐藏边距线
+  if (!ruler || !range) {
+    window['$message'].error('导出失败!')
+    return
+  }
+  // 记录缩放比例
+  const scaleTemp = chartEditStore.getEditCanvas.scale
+  // 去除标尺Dom
+  ruler.style.display = 'none'
+  // 百分百展示页面
+  chartEditStore.setScale(1, true)
+
+  window['$message'].warning('生成截图和数据中, 请耐心等待...')
+  setTimeout(() => {
+    canvasCut(range, () => {
+      // 放开边距线
+      if (ruler) ruler.style.display = 'block'
+      // 还原页面大小
+      chartEditStore.setScale(scaleTemp, true)
+    })
+  }, 600)
+}
+
 // 发布
 const sendHandle = () => {
   window['$message'].warning('该功能暂未实现(因为压根没有后台)')
@@ -60,12 +90,17 @@ const sendHandle = () => {
 
 const btnList = shallowReactive([
   {
-    key: '',
     select: true,
     title: '预览',
     icon: renderIcon(BrowsersOutlineIcon),
     event: previewHandle
   },
+  {
+    select: true,
+    title: '下载',
+    icon: renderIcon(DownloadIcon),
+    event: exportHandle
+  },
   {
     select: true,
     title: '发布',

+ 2 - 2
src/views/project/items/components/ProjectItemsCard/index.vue

@@ -93,7 +93,7 @@ const {
   TrashIcon,
   PencilIcon,
   BrowsersOutlineIcon,
-  DownloadIcon,
+  DownloadOutlineIcon,
   HammerIcon,
   SendIcon
 } = icon.ionicons5
@@ -153,7 +153,7 @@ const selectOptions = ref([
   {
     label: renderLang('global.r_download'),
     key: 'download',
-    icon: renderIcon(DownloadIcon)
+    icon: renderIcon(DownloadOutlineIcon)
   },
   {
     type: 'divider',