Commit 5a81643a658f33c2804e10e799f04a88dd130492

Authored by xp.Huang
2 parents b6587d76 ea305212

Merge branch 'ft' into 'main_dev'

chore: 升级go-view版本至v1.2.9

See merge request yunteng/thingskit-view!131
Showing 44 changed files with 847 additions and 385 deletions
... ... @@ -32,6 +32,6 @@ module.exports = {
32 32 'vue/no-unused-vars': 'off',
33 33 'vue/multi-word-component-names': 'off',
34 34 'vue/valid-template-root': 'off',
35   - 'vue/no-mutating-props': 'off',
  35 + 'vue/no-mutating-props': 'off'
36 36 }
37 37 }
... ...
... ... @@ -4,3 +4,4 @@ dist
4 4 dist-ssr
5 5 *.local
6 6 .vscode
  7 +.idea
... ...
  1 +import TKUpload from './index.vue';
  2 +
  3 +export { TKUpload };
... ...
  1 +<template>
  2 + <n-upload
  3 + v-model:file-list="fileList"
  4 + :show-file-list="true"
  5 + :customRequest="customRequest"
  6 + :onBeforeUpload="beforeUploadHandle"
  7 + :onRemove="remove"
  8 + >
  9 + <n-upload-dragger>
  10 + <img v-if="uploadImageUrl" class="upload-show" :src="uploadImageUrl" alt="上传的图片" />
  11 + <div class="upload-img" v-show="!uploadImageUrl">
  12 + <img src="@/assets/images/canvas/noImage.png" />
  13 + <n-text class="upload-desc" depth="3">
  14 + 上传文件需小于 {{ uploadSizeFormat.size }}M ,格式为 {{ uploadSizeFormat.format }} 的文件
  15 + </n-text>
  16 + </div>
  17 + </n-upload-dragger>
  18 + </n-upload>
  19 +</template>
  20 +
  21 +<script lang="ts" setup name="TKUpload">
  22 +import { ref, PropType, nextTick } from 'vue'
  23 +import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
  24 +import { fetchRouteParamsLocation } from '@/utils'
  25 +import { uploadFile } from '@/api/external/contentSave/content'
  26 +import { FileTypeEnum } from '@/enums/external/fileTypeEnum'
  27 +
  28 +interface uploadSizeFormatIF {
  29 + size: number
  30 + format: string
  31 +}
  32 +
  33 +const props = defineProps({
  34 + uploadImageUrl: {
  35 + type: String as PropType<string>,
  36 + default: ''
  37 + },
  38 + uploadSizeFormat: {
  39 + type: Object as PropType<uploadSizeFormatIF>,
  40 + default: () => ({
  41 + size: 5,
  42 + format: 'png/jpg/jpeg/gif'
  43 + })
  44 + }
  45 +})
  46 +
  47 +const emit = defineEmits(['sendFile', 'removeFile'])
  48 +
  49 +const fileList = ref<UploadFileInfo[]>()
  50 +
  51 +// 自定义上传操作
  52 +const customRequest = (options: UploadCustomRequestOptions) => {
  53 + const { file } = options
  54 + nextTick(async () => {
  55 + if (file.file) {
  56 + const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_upload.png`, {
  57 + type: file.file.type
  58 + })
  59 + let uploadParams = new FormData()
  60 + uploadParams.append('file', newNameFile)
  61 + const uploadRes = await uploadFile(uploadParams)
  62 + if (!uploadRes) return
  63 + emit('sendFile', uploadRes?.fileStaticUri)
  64 + window['$message'].success('上传文件成功!')
  65 + } else {
  66 + window['$message'].error('上传文件失败,请稍后重试!')
  67 + }
  68 + })
  69 +}
  70 +
  71 +// 文件上传前置处理
  72 +const beforeUploadHandle = (file: UploadFileInfo) => {
  73 + fileList.value = []
  74 + const type = file.file?.type
  75 + const size = file.file?.size as number
  76 + const typeSuffix = type?.split('/')?.at(-1)?.toUpperCase() as keyof typeof FileTypeEnum
  77 + if (size > 1024 * 1024 * props.uploadSizeFormat.size) {
  78 + window['$message'].warning(`文件超出 ${props.uploadSizeFormat.size}M限制,请重新上传!`)
  79 + return false
  80 + }
  81 + if (!FileTypeEnum[typeSuffix]) {
  82 + window['$message'].warning('文件格式不符合,请重新上传!')
  83 + return false
  84 + }
  85 + return true
  86 +}
  87 +
  88 +//单个点击删除
  89 +const remove = () => {
  90 + fileList.value = []
  91 + emit('removeFile', true)
  92 +}
  93 +</script>
  94 +
  95 +<style lang="scss" scoped>
  96 +$uploadHeight: 193px;
  97 +@include deep() {
  98 + .n-card__content {
  99 + padding: 0;
  100 + overflow: hidden;
  101 + }
  102 + .n-upload-dragger {
  103 + padding: 5px;
  104 + }
  105 +}
  106 +.upload-show {
  107 + width: -webkit-fill-available;
  108 + height: $uploadHeight;
  109 + border-radius: 5px;
  110 +}
  111 +.upload-img {
  112 + display: flex;
  113 + flex-direction: column;
  114 + align-items: center;
  115 + img {
  116 + height: 150px;
  117 + }
  118 + .upload-desc {
  119 + padding: 10px 0;
  120 + }
  121 +}
  122 +</style>
... ...
  1 +// 文件上传时的格式映射
  2 +export enum FileTypeEnum {
  3 + // 文档
  4 + TXT = 'text/plain',
  5 + JSON = 'application/json',
  6 + // 图片
  7 + PNG = 'image/png',
  8 + JPEG = 'image/jpeg',
  9 + JPG = 'image/jpg',
  10 + GIF = 'image/gif',
  11 +}
... ...
... ... @@ -11,7 +11,10 @@ export const option = {
11 11 dataset: dataJson,
12 12 mapRegion: {
13 13 adcode: 'china',
14   - showHainanIsLands: true
  14 + showHainanIsLands: true,
  15 + enter: false,
  16 + backSize: 20,
  17 + backColor: '#ffffff'
15 18 },
16 19 tooltip: {
17 20 show: true,
... ... @@ -103,19 +106,19 @@ export const option = {
103 106 borderColor: 'rgba(147, 235, 248, 0.8)',
104 107 textStyle: {
105 108 color: '#FFFFFF',
106   - fontSize: 12,
  109 + fontSize: 12
107 110 }
108 111 },
109 112 label: {
110 113 show: false,
111 114 color: '#FFFFFF',
112   - fontSize: 12,
  115 + fontSize: 12
113 116 },
114 117 emphasis: {
115 118 disabled: false,
116 119 label: {
117 120 color: '#FFFFFF',
118   - fontSize: 12,
  121 + fontSize: 12
119 122 },
120 123 itemStyle: {
121 124 areaColor: '#389BB7',
... ... @@ -148,6 +151,26 @@ export const option = {
148 151 shadowOffsetY: 2,
149 152 shadowBlur: 10
150 153 }
  154 + },
  155 + {
  156 + type: 'lines',
  157 + zlevel: 2,
  158 + effect: {
  159 + show: true,
  160 + period: 4, //箭头指向速度,值越小速度越快
  161 + trailLength: 0.4, //特效尾迹长度[0,1]值越大,尾迹越长重
  162 + symbol: 'arrow', //箭头图标
  163 + symbolSize: 7 //图标大小
  164 + },
  165 + lineStyle: {
  166 + normal: {
  167 + color: '#4fb6d2',
  168 + width: 1, //线条宽度
  169 + opacity: 0.1, //尾迹线条透明度
  170 + curveness: 0.3 //尾迹线条曲直度
  171 + }
  172 + },
  173 + data: []
151 174 }
152 175 ]
153 176 }
... ...
... ... @@ -69,11 +69,7 @@
69 69 </n-space>
70 70 </SettingItem>
71 71 <SettingItem name="字体颜色">
72   - <n-color-picker
73   - size="small"
74   - :modes="['hex']"
75   - v-model:value="seriesList[1].label.color"
76   - ></n-color-picker>
  72 + <n-color-picker size="small" :modes="['hex']" v-model:value="seriesList[1].label.color"></n-color-picker>
77 73 </SettingItem>
78 74 <SettingItem name="字体大小">
79 75 <n-input-number
... ... @@ -129,7 +125,7 @@
129 125 ></n-color-picker>
130 126 </SettingItem>
131 127 </SettingItemBox>
132   -
  128 +
133 129 <SettingItemBox name="悬浮弹窗">
134 130 <SettingItem name="显示">
135 131 <n-space>
... ... @@ -180,6 +176,22 @@
180 176 <SettingItem>
181 177 <n-checkbox v-model:checked="mapRegion.showHainanIsLands" size="small">显示南海群岛</n-checkbox>
182 178 </SettingItem>
  179 + <SettingItem v-if="seriesList[2]">
  180 + <n-checkbox v-model:checked="mapRegion.enter" size="small">点击进入下级</n-checkbox>
  181 + </SettingItem>
  182 + </SettingItemBox>
  183 + <SettingItemBox name="返回图标" v-if="mapRegion.enter">
  184 + <SettingItem name="颜色">
  185 + <n-color-picker size="small" :modes="['hex']" v-model:value="mapRegion.backColor"></n-color-picker>
  186 + </SettingItem>
  187 + <SettingItem name="大小">
  188 + <n-input-number
  189 + v-model:value="mapRegion.backSize"
  190 + :min="1"
  191 + size="small"
  192 + placeholder="请输入字体大小"
  193 + ></n-input-number>
  194 + </SettingItem>
183 195 </SettingItemBox>
184 196 </CollapseItem>
185 197 <CollapseItem name="标记" :expanded="true">
... ... @@ -191,7 +203,7 @@
191 203 <n-color-picker size="small" :modes="['hex']" v-model:value="seriesList[0].itemStyle.color"></n-color-picker>
192 204 </SettingItem>
193 205 </SettingItemBox>
194   -
  206 +
195 207 <SettingItemBox name="文本">
196 208 <SettingItem name="显示">
197 209 <n-space>
... ... @@ -223,6 +235,47 @@
223 235 </SettingItem>
224 236 </SettingItemBox>
225 237 </CollapseItem>
  238 +
  239 + <CollapseItem v-if="seriesList[2]" name="飞线" :expanded="true">
  240 + <SettingItemBox name="箭头">
  241 + <SettingItem name="速度">
  242 + <n-tooltip trigger="hover">
  243 + <template #trigger>
  244 + <n-input-number v-model:value="seriesList[2].effect.period" size="small" :min="0"></n-input-number>
  245 + </template>
  246 + 值越小速度越快
  247 + </n-tooltip>
  248 + </SettingItem>
  249 + <SettingItem name="尾迹">
  250 + <n-tooltip trigger="hover">
  251 + <template #trigger>
  252 + <n-input-number
  253 + v-model:value="seriesList[2].effect.trailLength"
  254 + size="small"
  255 + :min="0"
  256 + :max="1"
  257 + ></n-input-number>
  258 + </template>
  259 + 特效尾迹长度[0,1]值越大,尾迹越长重
  260 + </n-tooltip>
  261 + </SettingItem>
  262 + <SettingItem name="大小">
  263 + <n-input-number v-model:value="seriesList[2].effect.symbolSize" size="small" :min="0"></n-input-number>
  264 + </SettingItem>
  265 + </SettingItemBox>
  266 + <SettingItemBox name="配置">
  267 + <SettingItem name="颜色">
  268 + <n-color-picker
  269 + size="small"
  270 + :modes="['hex']"
  271 + v-model:value="seriesList[2].lineStyle.normal.color"
  272 + ></n-color-picker>
  273 + </SettingItem>
  274 + <SettingItem name="宽度">
  275 + <n-input-number v-model:value="seriesList[2].lineStyle.normal.width" size="small" :min="1"></n-input-number>
  276 + </SettingItem>
  277 + </SettingItemBox>
  278 + </CollapseItem>
226 279 </template>
227 280
228 281 <script setup lang="ts">
... ...
... ... @@ -21,6 +21,32 @@
21 21 "value": [126.642464, 45.756967, 101]
22 22 }
23 23 ],
  24 + "line": [
  25 + {
  26 + "coords": [
  27 + [113.665412, 34.757975],
  28 + [116.405285, 39.904989]
  29 + ]
  30 + },
  31 + {
  32 + "coords": [
  33 + [101.778916, 36.623178],
  34 + [116.405285, 39.904989]
  35 + ]
  36 + },
  37 + {
  38 + "coords": [
  39 + [106.278179, 38.46637],
  40 + [116.405285, 39.904989]
  41 + ]
  42 + },
  43 + {
  44 + "coords": [
  45 + [126.642464, 45.756967],
  46 + [116.405285, 39.904989]
  47 + ]
  48 + }
  49 + ],
24 50 "map": [
25 51 {
26 52 "name": "北京市",
... ...
1 1 <template>
2   - <v-chart ref="vChartRef" :init-options="initOptions" :theme="themeColor" :option="option.value" :manual-update="isPreview()" autoresize>
3   - </v-chart>
  2 + <div>
  3 + <div class="back-icon" v-if="(enter && levelHistory.length !== 0) || (enter && !isPreview())" @click="backLevel">
  4 + <n-icon :color="backColor" :size="backSize * 1.1">
  5 + <ArrowBackIcon />
  6 + </n-icon>
  7 + <span
  8 + :style="{
  9 + 'font-weight': 200,
  10 + color: backColor,
  11 + 'font-size': `${backSize}px`
  12 + }"
  13 + >
  14 + 返回上级
  15 + </span>
  16 + </div>
  17 + <v-chart
  18 + ref="vChartRef"
  19 + :init-options="initOptions"
  20 + :theme="themeColor"
  21 + :option="option.value"
  22 + :manual-update="isPreview()"
  23 + autoresize
  24 + @click="chartPEvents"
  25 + >
  26 + </v-chart>
  27 + </div>
4 28 </template>
5 29
6 30 <script setup lang="ts">
7   -import { PropType, reactive, watch, ref, nextTick } from 'vue'
  31 +import { PropType, reactive, watch, ref, nextTick, toRefs } from 'vue'
8 32 import config, { includes } from './config'
9 33 import VChart from 'vue-echarts'
  34 +import { icon } from '@/plugins'
10 35 import { useCanvasInitOptions } from '@/hooks/useCanvasInitOptions.hook'
11 36 import { use, registerMap } from 'echarts/core'
12 37 import { EffectScatterChart, MapChart } from 'echarts/charts'
... ... @@ -16,6 +41,7 @@ import { mergeTheme, setOption } from '@/packages/public/chart'
16 41 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
17 42 import { isPreview } from '@/utils'
18 43 import mapJsonWithoutHainanIsLands from './mapWithoutHainanIsLands.json'
  44 +import mapChinaJson from './mapGeojson/china.json'
19 45 import { DatasetComponent, GridComponent, TooltipComponent, GeoComponent, VisualMapComponent } from 'echarts/components'
20 46
21 47 const props = defineProps({
... ... @@ -33,6 +59,10 @@ const props = defineProps({
33 59 }
34 60 })
35 61
  62 +const { ArrowBackIcon } = icon.ionicons5
  63 +let levelHistory: any = ref([])
  64 +
  65 +const { backColor, backSize, enter } = toRefs(props.chartConfig.option.mapRegion)
36 66 const initOptions = useCanvasInitOptions(props.chartConfig.option, props.themeSetting)
37 67
38 68 use([
... ... @@ -67,7 +97,7 @@ registerMap(`${props.chartConfig.option.mapRegion.adcode}`, { geoJSON: {} as any
67 97 // 进行更换初始化地图 如果为china 单独处理
68 98 const registerMapInitAsync = async () => {
69 99 await nextTick()
70   - const adCode = `${props.chartConfig.option.mapRegion.adcode}`;
  100 + const adCode = `${props.chartConfig.option.mapRegion.adcode}`
71 101 if (adCode !== 'china') {
72 102 await getGeojson(adCode)
73 103 } else {
... ... @@ -87,7 +117,16 @@ const vEchartsSetOption = () => {
87 117 const dataSetHandle = async (dataset: any) => {
88 118 props.chartConfig.option.series.forEach((item: any) => {
89 119 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point
90   - else if (item.type === 'map' && dataset.map) item.data = dataset.map
  120 + else if (item.type === 'lines' && dataset.line) {
  121 + item.data = dataset.line.map((it: any) => {
  122 + return {
  123 + ...it,
  124 + lineStyle: {
  125 + color: props.chartConfig.option.series[2].lineStyle.normal.color
  126 + }
  127 + }
  128 + })
  129 + } else if (item.type === 'map' && dataset.map) item.data = dataset.map
91 130 })
92 131 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces
93 132
... ... @@ -101,6 +140,45 @@ const hainanLandsHandle = async (newData: boolean) => {
101 140 registerMap('china', { geoJSON: mapJsonWithoutHainanIsLands as any, specialAreas: {} })
102 141 }
103 142 }
  143 +
  144 +// 点击区域
  145 +const chartPEvents = (e: any) => {
  146 + if (e.seriesType !== 'map') return
  147 + if (!props.chartConfig.option.mapRegion.enter) {
  148 + return
  149 + }
  150 + mapChinaJson.features.forEach(item => {
  151 + var pattern = new RegExp(e.name)
  152 + if (pattern.test(item.properties.name)) {
  153 + let code = String(item.properties.adcode)
  154 + levelHistory.value.push(code)
  155 + checkOrMap(code)
  156 + }
  157 + })
  158 +}
  159 +
  160 +// 返回上一级
  161 +const backLevel = () => {
  162 + levelHistory.value = []
  163 + if (levelHistory.value.length > 1) {
  164 + levelHistory.value.pop()
  165 + const code = levelHistory[levelHistory.value.length - 1]
  166 + checkOrMap(code)
  167 + } else {
  168 + checkOrMap('china')
  169 + }
  170 +}
  171 +
  172 +// 切换地图
  173 +const checkOrMap = async (newData: string) => {
  174 + await getGeojson(newData)
  175 + props.chartConfig.option.geo.map = newData
  176 + props.chartConfig.option.series.forEach((item: any) => {
  177 + if (item.type === 'map') item.map = newData
  178 + })
  179 + vEchartsSetOption()
  180 +}
  181 +
104 182 //监听 dataset 数据发生变化
105 183 watch(
106 184 () => props.chartConfig.option.dataset,
... ... @@ -113,33 +191,42 @@ watch(
113 191 }
114 192 )
115 193
116   -//监听是否显示南海群岛
117   -watch(
118   - () => props.chartConfig.option.mapRegion.showHainanIsLands,
119   - async newData => {
120   - try {
121   - await hainanLandsHandle(newData)
122   - vEchartsSetOption()
123   - } catch (error) {
124   - console.log(error)
  194 +// 监听线的颜色
  195 +if (props.chartConfig.option.series[2] && !isPreview()) {
  196 + watch(
  197 + () => props.chartConfig.option.series[2].lineStyle.normal.color,
  198 + () => {
  199 + dataSetHandle(props.chartConfig.option.dataset)
  200 + },
  201 + {
  202 + deep: false
125 203 }
126   - },
127   - {
128   - deep: false
129   - }
130   -)
  204 + )
  205 +}
131 206
  207 +//监听是否显示南海群岛
  208 +if (!isPreview()) {
  209 + watch(
  210 + () => props.chartConfig.option.mapRegion.showHainanIsLands,
  211 + async newData => {
  212 + try {
  213 + await hainanLandsHandle(newData)
  214 + vEchartsSetOption()
  215 + } catch (error) {
  216 + console.log(error)
  217 + }
  218 + },
  219 + {
  220 + deep: false
  221 + }
  222 + )
  223 +}
132 224 //监听地图展示区域发生变化
133 225 watch(
134 226 () => `${props.chartConfig.option.mapRegion.adcode}`,
135   - async newData => {
  227 + newData => {
136 228 try {
137   - await getGeojson(newData)
138   - props.chartConfig.option.geo.map = newData
139   - props.chartConfig.option.series.forEach((item: any) => {
140   - if (item.type === 'map') item.map = newData
141   - })
142   - vEchartsSetOption()
  229 + checkOrMap(newData)
143 230 } catch (error) {
144 231 console.log(error)
145 232 }
... ... @@ -154,3 +241,16 @@ useChartDataFetch(props.chartConfig, useChartEditStore, (newData: any) => {
154 241 dataSetHandle(newData)
155 242 })
156 243 </script>
  244 +
  245 +<style scope lang="scss">
  246 +.back-icon {
  247 + z-index: 50;
  248 + cursor: pointer;
  249 + position: absolute;
  250 + display: flex;
  251 + align-items: center;
  252 + top: 0;
  253 + left: 0;
  254 + gap: 2px;
  255 +}
  256 +</style>
... ...
... ... @@ -9,7 +9,8 @@ export const includes = []
9 9 // 关系图布局
10 10 export const GraphLayout = [
11 11 { label: '无', value: 'none' },
12   - { label: '环形', value: 'circular' }
  12 + { label: '环形', value: 'circular' },
  13 + { label: '力引导', value: 'force' }
13 14 ]
14 15
15 16 // 标签开关
... ... @@ -24,7 +25,13 @@ export const LabelPosition = [
24 25 { label: '右侧', value: 'right' },
25 26 { label: '顶部', value: 'top' },
26 27 { label: '底部', value: 'bottom' },
27   - { label: '内部', value: 'inside' },
  28 + { label: '内部', value: 'inside' }
  29 +]
  30 +
  31 +// 图-迭代动画
  32 +export const LayoutAnimation = [
  33 + { label: '开启', value: 1 },
  34 + { label: '关闭', value: 0 }
28 35 ]
29 36
30 37 export const option = {
... ... @@ -33,11 +40,11 @@ export const option = {
33 40 legend:{
34 41 show:true,
35 42 textStyle:{
36   - color:"#eee",
37   - fontSize: 14 ,
  43 + color: '#eee',
  44 + fontSize: 14
38 45 },
39 46 data: dataJson.categories.map(function (a) {
40   - return a.name;
  47 + return a.name
41 48 })
42 49 },
43 50 series: [
... ... @@ -47,7 +54,7 @@ export const option = {
47 54 data: dataJson.nodes,
48 55 links: dataJson.links,
49 56 categories: dataJson.categories,
50   - label: { // 标签
  57 + label: {
51 58 show: 1,
52 59 position: 'right',
53 60 formatter: '{b}'
... ... @@ -58,10 +65,17 @@ export const option = {
58 65 lineStyle: {
59 66 color: 'source', // 线条颜色
60 67 curveness: 0.2 // 线条卷曲程度
  68 + },
  69 + force: {
  70 + repulsion: 100,
  71 + gravity: 0.1,
  72 + edgeLength: 30,
  73 + layoutAnimation: 1,
  74 + friction: 0.6
61 75 }
62 76 }
63 77 ]
64   - };
  78 +}
65 79
66 80 export default class Config extends PublicConfigClass implements CreateComponentType {
67 81 public key = GraphConfig.key
... ...
... ... @@ -35,7 +35,58 @@
35 35 ></n-color-picker>
36 36 </SettingItem>
37 37 <SettingItem name="文本">
38   - <n-input-number v-model:value="optionData.legend.textStyle.fontSize" :min="0" :step="1" size="small" placeholder="文字大小">
  38 + <n-input-number
  39 + v-model:value="optionData.legend.textStyle.fontSize"
  40 + :min="0"
  41 + :step="1"
  42 + size="small"
  43 + placeholder="文字大小"
  44 + >
  45 + </n-input-number>
  46 + </SettingItem>
  47 + </SettingItemBox>
  48 + <SettingItemBox name="力引导" v-if="optionData.series[0].force && graphConfig.layout == 'force'">
  49 + <SettingItem name="斥力因子" v-if="optionData.series[0].force.repulsion">
  50 + <n-input-number
  51 + v-model:value="optionData.series[0].force.repulsion"
  52 + :min="0"
  53 + :step="1"
  54 + size="small"
  55 + placeholder="斥力因子大小"
  56 + >
  57 + </n-input-number>
  58 + </SettingItem>
  59 + <SettingItem name="引力因子" v-if="optionData.series[0].force.gravity">
  60 + <n-input-number
  61 + v-model:value="optionData.series[0].force.gravity"
  62 + :min="0"
  63 + :step="0.1"
  64 + size="small"
  65 + placeholder="引力因子"
  66 + >
  67 + </n-input-number>
  68 + </SettingItem>
  69 + <SettingItem name="节点距离">
  70 + <n-input-number
  71 + v-model:value="optionData.series[0].force.edgeLength"
  72 + :min="0"
  73 + :step="1"
  74 + size="small"
  75 + placeholder="节点距离"
  76 + >
  77 + </n-input-number>
  78 + </SettingItem>
  79 + <SettingItem name="迭代动画">
  80 + <n-select v-model:value="graphConfig.force.layoutAnimation" :options="LayoutAnimation" size="small" />
  81 + </SettingItem>
  82 + <SettingItem name="节点速度">
  83 + <n-input-number
  84 + v-model:value="optionData.series[0].force.friction"
  85 + :min="0"
  86 + :step="0.1"
  87 + size="small"
  88 + placeholder="节点速度"
  89 + >
39 90 </n-input-number>
40 91 </SettingItem>
41 92 </SettingItemBox>
... ... @@ -46,7 +97,7 @@
46 97 <script setup lang="ts">
47 98 import { PropType, computed } from 'vue'
48 99 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
49   -import { option, GraphLayout, LabelSwitch, LabelPosition } from './config'
  100 +import { option, GraphLayout, LabelSwitch, LabelPosition, LayoutAnimation } from './config'
50 101 import { GlobalThemeJsonType } from '@/settings/chartThemes/index'
51 102
52 103 const props = defineProps({
... ... @@ -56,7 +107,7 @@ const props = defineProps({
56 107 }
57 108 })
58 109
59   -const graphConfig = computed<typeof option.series[0]>(() => {
  110 +const graphConfig = computed<(typeof option.series)[0]>(() => {
60 111 return props.optionData.series[0]
61 112 })
62 113 </script>
... ...
1 1 <template>
2   - <v-chart ref="vChartRef" :init-options="initOptions" :theme="themeColor" :option="option" :manual-update="isPreview()" autoresize></v-chart>
  2 + <v-chart
  3 + ref="vChartRef"
  4 + :init-options="initOptions"
  5 + :theme="themeColor"
  6 + :option="option"
  7 + :manual-update="isPreview()"
  8 + autoresize
  9 + ></v-chart>
3 10 </template>
4 11
5 12 <script setup lang="ts">
... ...
... ... @@ -8,4 +8,14 @@ import { DialConfig } from './Dial/index'
8 8 import { SankeyConfig } from './Sankey/index'
9 9 import { GraphConfig } from './Graph/index'
10 10
11   -export default [ProcessConfig, RadarConfig, FunnelConfig, HeatmapConfig, WaterPoloConfig, TreeMapConfig, GraphConfig, SankeyConfig, DialConfig]
  11 +export default [
  12 + ProcessConfig,
  13 + RadarConfig,
  14 + FunnelConfig,
  15 + HeatmapConfig,
  16 + WaterPoloConfig,
  17 + TreeMapConfig,
  18 + GraphConfig,
  19 + SankeyConfig,
  20 + DialConfig
  21 +]
... ...
... ... @@ -33,8 +33,6 @@ const option = shallowReactive({
33 33 dataset: ''
34 34 })
35 35
36   -
37   -
38 36 const getStyle = (radius: number) => {
39 37 return {
40 38 borderRadius: `${radius}px`,
... ... @@ -78,7 +76,6 @@ watch(
78 76 return
79 77 }
80 78 option.dataset = newData
81   -
82 79 },
83 80 {
84 81 immediate: true
... ...
... ... @@ -161,6 +161,26 @@ export const option = {
161 161 shadowOffsetY: 2,
162 162 shadowBlur: 10
163 163 }
  164 + },
  165 + {
  166 + type: 'lines',
  167 + zlevel: 2,
  168 + effect: {
  169 + show: true,
  170 + period: 4, //箭头指向速度,值越小速度越快
  171 + trailLength: 0.4, //特效尾迹长度[0,1]值越大,尾迹越长重
  172 + symbol: 'arrow', //箭头图标
  173 + symbolSize: 7 //图标大小
  174 + },
  175 + lineStyle: {
  176 + normal: {
  177 + color: '#4fb6d2',
  178 + width: 1, //线条宽度
  179 + opacity: 0.1, //尾迹线条透明度
  180 + curveness: 0.3 //尾迹线条曲直度
  181 + }
  182 + },
  183 + data: []
164 184 }
165 185 ]
166 186 }
... ...
... ... @@ -246,6 +246,46 @@
246 246 </SettingItem>
247 247 </SettingItemBox>
248 248 </CollapseItem>
  249 + <CollapseItem v-if="seriesList[2]" name="飞线" :expanded="true">
  250 + <SettingItemBox name="箭头">
  251 + <SettingItem name="速度">
  252 + <n-tooltip trigger="hover">
  253 + <template #trigger>
  254 + <n-input-number v-model:value="seriesList[2].effect.period" size="small" :min="0"></n-input-number>
  255 + </template>
  256 + 值越小速度越快
  257 + </n-tooltip>
  258 + </SettingItem>
  259 + <SettingItem name="尾迹">
  260 + <n-tooltip trigger="hover">
  261 + <template #trigger>
  262 + <n-input-number
  263 + v-model:value="seriesList[2].effect.trailLength"
  264 + size="small"
  265 + :min="0"
  266 + :max="1"
  267 + ></n-input-number>
  268 + </template>
  269 + 特效尾迹长度[0,1]值越大,尾迹越长重
  270 + </n-tooltip>
  271 + </SettingItem>
  272 + <SettingItem name="大小">
  273 + <n-input-number v-model:value="seriesList[2].effect.symbolSize" size="small" :min="0"></n-input-number>
  274 + </SettingItem>
  275 + </SettingItemBox>
  276 + <SettingItemBox name="配置">
  277 + <SettingItem name="颜色">
  278 + <n-color-picker
  279 + size="small"
  280 + :modes="['hex']"
  281 + v-model:value="seriesList[2].lineStyle.normal.color"
  282 + ></n-color-picker>
  283 + </SettingItem>
  284 + <SettingItem name="宽度">
  285 + <n-input-number v-model:value="seriesList[2].lineStyle.normal.width" size="small" :min="1"></n-input-number>
  286 + </SettingItem>
  287 + </SettingItemBox>
  288 + </CollapseItem>
249 289 </template>
250 290
251 291 <script setup lang="ts">
... ...
... ... @@ -26,6 +26,36 @@
26 26 "value": [126.642464, 45.756967, 101]
27 27 }
28 28 ],
  29 + "line": [
  30 + {
  31 + "adcode": 410000,
  32 + "coords": [
  33 + [113.665412, 34.757975],
  34 + [116.405285, 39.904989]
  35 + ]
  36 + },
  37 + {
  38 + "adcode": 630000,
  39 + "coords": [
  40 + [101.778916, 36.623178],
  41 + [116.405285, 39.904989]
  42 + ]
  43 + },
  44 + {
  45 + "adcode": 640000,
  46 + "coords": [
  47 + [106.278179, 38.46637],
  48 + [116.405285, 39.904989]
  49 + ]
  50 + },
  51 + {
  52 + "adcode": 230000,
  53 + "coords": [
  54 + [126.642464, 45.756967],
  55 + [116.405285, 39.904989]
  56 + ]
  57 + }
  58 + ],
29 59 "map": [
30 60 {
31 61 "name": "北京市",
... ...
... ... @@ -202,9 +202,19 @@ const vEchartsSetOption = async () => {
202 202 const dataSetHandle = async (dataset: any) => {
203 203 props.chartConfig.option.series.forEach((item: any) => {
204 204 if (item.type === 'effectScatter' && dataset.point) item.data = dataset.point
205   - else if (item.type === 'map' && dataset.map) item.data = dataset.map
  205 + else if (item.type === 'lines' && dataset.line) {
  206 + item.data = dataset.line.map((it: any) => {
  207 + return {
  208 + ...it,
  209 + lineStyle: {
  210 + color: props.chartConfig.option.series[2].lineStyle.normal.color
  211 + }
  212 + }
  213 + })
  214 + } else if (item.type === 'map' && dataset.map) item.data = dataset.map
206 215 })
207 216 if (dataset.pieces) props.chartConfig.option.visualMap.pieces = dataset.pieces
  217 +
208 218 isPreview() && vEchartsSetOption()
209 219 }
210 220
... ... @@ -245,14 +255,16 @@ watch(
245 255 }
246 256 )
247 257
248   -//处理数据标点
  258 +//处理数据标点包括飞线
249 259 const handleDataPoint = (newData: any) => {
250 260 if (newData === 'china') {
251 261 dataSetHandle(dataJson)
252 262 } else {
253 263 const filterPoint = dataJson.point.filter((item: any) => item.adcode === newData)
  264 + // const filterLine = dataJson.line.filter((item: any) => item.adcode === newData)
254 265 dataSetHandle({
255   - point: filterPoint
  266 + point: filterPoint,
  267 + line: [] //由于点击单个地图模块,所以去除飞线
256 268 })
257 269 }
258 270 }
... ... @@ -310,7 +322,7 @@ const handleVChartClick = async (params: any) => {
310 322 const level = item.properties.level.toUpperCase()
311 323 const adcode = item.properties.adcode
312 324 if (level === 'DISTRICT') return
313   - if(String(adcode).startsWith('15') && level===areaEnum.CITY) return
  325 + if (String(adcode).startsWith('15') && level === areaEnum.CITY) return
314 326 props.chartConfig.option.mapRegion.adcode = adcode
315 327 saveLevelStr.level = level
316 328 handleDataPoint(adcode)
... ...
... ... @@ -2,24 +2,11 @@
2 2 <collapse-item name="属性" :expanded="true">
3 3 <setting-item-box name="上传图片" :alone="true">
4 4 <setting-item>
5   - <n-card class="upload-box">
6   - <n-upload
7   - :show-file-list="false"
8   - v-model:file-list="uploadFileListRef"
9   - :customRequest="customRequest"
10   - :onBeforeUpload="beforeUploadHandle"
11   - >
12   - <n-upload-dragger>
13   - <img v-if="optionData.dataset" class="upload-show" :src="optionData.dataset" alt="背景" />
14   - <div class="upload-img" v-show="!optionData.dataset">
15   - <img src="@/assets/images/canvas/noImage.png" />
16   - <n-text class="upload-desc" depth="3">
17   - 图片需小于 {{ backgroundImageSize }}M ,格式为 png/jpg/gif 的文件
18   - </n-text>
19   - </div>
20   - </n-upload-dragger>
21   - </n-upload>
22   - </n-card>
  5 + <TKUpload
  6 + :uploadImageUrl="props.optionData.dataset"
  7 + @sendFile="handleSendFile"
  8 + @removeFile="handleRemoveFile"
  9 + />
23 10 </setting-item>
24 11 </setting-item-box>
25 12 <setting-item-box name="样式">
... ... @@ -39,14 +26,10 @@
39 26 </template>
40 27
41 28 <script setup lang="ts">
42   -import { PropType, ref, nextTick } from 'vue'
  29 +import { PropType } from 'vue'
43 30 import { option } from './config'
44 31 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
45   -import { FileTypeEnum } from '@/enums/fileTypeEnum'
46   -import { uploadFile } from '@/api/external/contentSave/content'
47   -import { UploadCustomRequestOptions } from 'naive-ui'
48   -import { backgroundImageSize } from '@/settings/designSetting'
49   -import { fetchRouteParamsLocation } from '@/utils'
  32 +import { TKUpload } from '@/components/external/Common/TKUpload'
50 33
51 34 const props = defineProps({
52 35 optionData: {
... ... @@ -55,48 +38,12 @@ const props = defineProps({
55 38 }
56 39 })
57 40
58   -const uploadFileListRef = ref()
59   -
60   -// 上传图片前置处理
61   -// eslint-disable-next-line @typescript-eslint/ban-ts-comment
62   -//@ts-ignore
63   -const beforeUploadHandle = async ({ file }) => {
64   - uploadFileListRef.value = []
65   - const type = file.file.type
66   - const size = file.file.size
67   -
68   - if (size > 1024 * 1024 * backgroundImageSize) {
69   - window['$message'].warning(`图片超出 ${backgroundImageSize}M 限制,请重新上传!`)
70   - return false
71   - }
72   - if (type !== FileTypeEnum.PNG && type !== FileTypeEnum.JPEG && type !== FileTypeEnum.GIF) {
73   - window['$message'].warning('文件格式不符合,请重新上传!')
74   - return false
75   - }
76   - return true
  41 +const handleSendFile = (file: string) => {
  42 + if (!file) return
  43 + props.optionData.dataset = file
77 44 }
78 45
79   -// 自定义上传操作
80   -const customRequest = (options: UploadCustomRequestOptions) => {
81   - const { file } = options
82   - nextTick(async () => {
83   - if (file.file) {
84   - // 修改名称
85   - const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_background.png`, {
86   - type: file.file.type
87   - })
88   - let uploadParams = new FormData()
89   - uploadParams.append('file', newNameFile)
90   - const uploadRes = await uploadFile(uploadParams)
91   - if (uploadRes) {
92   - props.optionData.dataset = uploadRes?.fileStaticUri
93   - window['$message'].success('添加图片成功!')
94   - }
95   - } else {
96   - window['$message'].error('添加图片失败,请稍后重试!')
97   - }
98   - })
99   -}
  46 +const handleRemoveFile = (status: boolean) => (status ? (props.optionData.dataset = '') : null)
100 47
101 48 // 适应类型
102 49 const fitList = [
... ... @@ -122,35 +69,3 @@ const fitList = [
122 69 }
123 70 ]
124 71 </script>
125   -<style lang="scss" scoped>
126   -$uploadHeight: 193px;
127   -.upload-box {
128   - cursor: pointer;
129   - margin-bottom: 20px;
130   - @include deep() {
131   - .n-card__content {
132   - padding: 0;
133   - overflow: hidden;
134   - }
135   - .n-upload-dragger {
136   - padding: 5px;
137   - }
138   - }
139   - .upload-show {
140   - width: -webkit-fill-available;
141   - height: $uploadHeight;
142   - border-radius: 5px;
143   - }
144   - .upload-img {
145   - display: flex;
146   - flex-direction: column;
147   - align-items: center;
148   - img {
149   - height: 150px;
150   - }
151   - .upload-desc {
152   - padding: 10px 0;
153   - }
154   - }
155   -}
156   -</style>
... ...
1 1 <template>
2   - <div class="videoPlay">
  2 + <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }">
3 3 <video
4   - style="object-fit: cover"
5   - :poster="poster"
6 4 crossOrigin="anonymous"
  5 + :id="`my-player`"
7 6 ref="videoRef"
8   - class="video-js vjs-default-skin vjs-big-play-centered"
9   - controls
10   - >
11   - <source :src="path" />
12   - </video>
  7 + class="video-js my-video vjs-theme-city vjs-big-play-centered"
  8 + ></video>
13 9 </div>
14 10 </template>
15   -<script lang="ts" setup>
16   -import { nextTick, onBeforeUnmount, onMounted, ref, watch, toRefs } from 'vue'
17   -import videojs, { VideoJsPlayer } from 'video.js'
  11 +<script setup lang="ts">
  12 +import { onMounted, ref, onUnmounted, watch, unref } from 'vue'
  13 +import videojs from 'video.js'
  14 +import 'videojs-flvjs-es6'
18 15 import type { VideoJsPlayerOptions } from 'video.js'
19   -import 'video.js/dist/video-js.css'
20   -import zh from 'video.js/dist/lang/zh-CN.json'
21   -
22   -const props = withDefaults(
23   - defineProps<{
24   - path: string
25   - autoPlay?: boolean
26   - h?: number
27   - poster?: string
28   - }>(),
29   - { autoPlay: false }
30   -)
  16 +import 'video.js/dist/video-js.min.css'
  17 +import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'
  18 +import { isShareMode } from '@/views/share/hook'
  19 +import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay'
  20 +import { useFingerprint } from '@/utils/external/useFingerprint'
  21 +import { GetResult } from '@fingerprintjs/fingerprintjs'
31 22
32   -const videoRef = ref()
33   -
34   -let player: VideoJsPlayer
35   -
36   -const initPlay = async () => {
37   - videojs.addLanguage('zh-CN', zh)
38   - await nextTick()
39   - const options: VideoJsPlayerOptions = {
40   - muted: true,
41   - controls: true,
42   - autoplay: props.autoPlay,
43   - src: props?.path,
44   - poster: props?.poster,
45   - language: 'zh-CN',
46   - techOrder: ['html5'],
47   - preload: 'none'
  23 +const props = defineProps({
  24 + sourceSrc: {
  25 + type: String
  26 + },
  27 + autoplay: {
  28 + type: Boolean
  29 + },
  30 + name: {
  31 + type: String
  32 + },
  33 + avatar: {
  34 + type: String
  35 + },
  36 + w: {
  37 + type: Number,
  38 + default: 300
  39 + },
  40 + h: {
  41 + type: Number,
  42 + default: 300
48 43 }
49   - player = videojs(videoRef.value, options, () => {
50   - videojs.log('Video is reading')
51   - if (props.autoPlay) {
52   - player.play()
  44 +})
  45 +
  46 +enum VideoPlayerType {
  47 + m3u8 = 'application/x-mpegURL',
  48 + mp4 = 'video/mp4',
  49 + webm = 'video/webm',
  50 + flv = 'video/x-flv'
  51 +}
  52 +
  53 +const isRtspProtocol = (url: string) => {
  54 + const reg = /^rtsp:\/\//g
  55 + return reg.test(url)
  56 +}
  57 +
  58 +const getVideoTypeByUrl = (url = '') => {
  59 + try {
  60 + const { protocol, pathname } = new URL(url)
  61 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv
  62 + const reg = /[^.]\w*$/
  63 + const mathValue = pathname.match(reg) || []
  64 + const ext = (mathValue[0] as keyof typeof VideoPlayerType) || 'webm'
  65 + const type = VideoPlayerType[ext]
  66 + return type ? type : VideoPlayerType.webm
  67 + } catch (error) {
  68 + console.error(error)
  69 + return VideoPlayerType.webm
  70 + }
  71 +}
  72 +
  73 +// video标签
  74 +const videoRef = ref<HTMLElement | null>(null)
  75 +
  76 +// video实例对象
  77 +let videoPlayer: videojs.Player | null = null
  78 +
  79 +const fingerprintResult = ref<Nullable<GetResult>>(null)
  80 +
  81 +//options配置
  82 +const options: VideoJsPlayerOptions & Recordable = {
  83 + language: 'zh-CN', // 设置语言
  84 + controls: true, // 是否显示控制条
  85 + preload: 'auto', // 预加载
  86 + autoplay: true, // 是否自动播放
  87 + fluid: false, // 自适应宽高
  88 + poster: props?.avatar || '',
  89 + // src: getSource() || '', // 要嵌入的视频源的源 URL
  90 + sources: [],
  91 + muted: true,
  92 + userActions: {
  93 + hotkeys: true
  94 + },
  95 + techOrder: ['html5', 'flvjs'],
  96 + flvjs: {
  97 + mediaDataSource: {
  98 + isLive: true,
  99 + cors: true,
  100 + withCredentials: false,
  101 + hasAudio: false
  102 + },
  103 + config: {
  104 + autoCleanupSourceBuffer: true
53 105 }
54   - player.on('ended', () => {
55   - videojs.log('Play end')
56   - })
57   - player.on('error', () => {
58   - player.errorDisplay.close()
59   - videojs.log('Play parse error')
60   - })
61   - })
  106 + }
62 107 }
63 108
64   -onMounted(() => {
65   - initPlay()
66   -})
  109 +const { getResult } = useFingerprint()
  110 +async function getSource() {
  111 + fingerprintResult.value = await getResult()
  112 + let src = props.sourceSrc || ''
  113 + if (isRtspProtocol(props.sourceSrc!)) {
  114 + src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')
  115 + }
  116 + return [
  117 + {
  118 + type: getVideoTypeByUrl(props.sourceSrc),
  119 + src
  120 + }
  121 + ]
  122 +}
67 123
68   -//直接改变路径测试
69   -watch(
70   - () => props.path,
71   - () => {
72   - if (props.path && props.autoPlay) {
73   - try {
74   - player?.pause()
75   - player?.load()
76   - player?.src(props.path)
77   - player?.play()
78   - } catch (e) {
79   - console.log(e)
  124 +// 初始化videojs
  125 +const initVideo = async () => {
  126 + if (videoRef.value) {
  127 + // 创建 video 实例
  128 + options.sources = await getSource()
  129 + if (options.sources && options.sources.length) {
  130 + if (isRtspProtocol(props.sourceSrc || '')) {
  131 + options.flvjs = {
  132 + ...(options.flvjs || {}),
  133 + config: {
  134 + headers: {
  135 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`
  136 + }
  137 + }
  138 + }
80 139 }
  140 + videoPlayer = videojs(videoRef.value, options)
81 141 }
  142 + }
  143 +}
  144 +
  145 +watch(
  146 + () => props.sourceSrc,
  147 + async (newData: any) => {
  148 + const result = await getSource()
  149 + // props.sourceSrc = newData
  150 + videoPlayer?.src(result) as any
  151 + videoPlayer?.play()
82 152 },
83 153 {
84 154 immediate: true
85 155 }
86 156 )
87 157
88   -onBeforeUnmount(() => {
89   - player?.dispose()
  158 +onMounted(() => {
  159 + initVideo()
  160 +})
  161 +
  162 +onUnmounted(() => {
  163 + if (props.sourceSrc) {
  164 + closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!)
  165 + }
  166 + handleVideoDispose()
  167 +})
  168 +
  169 +//播放
  170 +const handleVideoPlay = () => videoPlayer?.play()
  171 +
  172 +const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
  173 +//暂停
  174 +defineExpose({
  175 + handleVideoPlay,
  176 + handleVideoDispose
90 177 })
91 178 </script>
92   -<style>
93   -.vjs-poster {
94   - background-size: cover !important;
95   -}
96   -</style>
  179 +
97 180 <style lang="scss" scoped>
98   -.videoPlay {
99   - flex: 1;
100   - height: v-bind('`${h}px`');
101   - .video-js {
102   - height: 100%;
103   - width: 100%;
  181 +.go-content-box {
  182 + display: flex;
  183 + align-items: center;
  184 + justify-content: center;
  185 +
  186 + .my-video {
  187 + width: 100% !important;
  188 + height: 100% !important;
  189 + position: relative;
104 190 }
105 191 }
106 192 </style>
... ...
... ... @@ -2,29 +2,12 @@
2 2 <CollapseItem name="播放器配置" :expanded="true">
3 3 <setting-item-box name="上传图片" :alone="true">
4 4 <setting-item>
5   - <n-card class="upload-box">
6   - <n-upload
7   - :show-file-list="false"
8   - v-model:file-list="uploadFileListRef"
9   - :customRequest="customRequest"
10   - :onBeforeUpload="beforeUploadHandle"
11   - >
12   - <n-upload-dragger>
13   - <img v-if="optionData.poster" class="upload-show" :src="optionData.poster" alt="背景" />
14   - <div class="upload-img" v-show="!optionData.poster">
15   - <img src="@/assets/images/canvas/noImage.png" />
16   - <n-text class="upload-desc" depth="3">
17   - 图片需小于 {{ backgroundImageSize }}M ,格式为 png/jpg/gif 的文件
18   - </n-text>
19   - </div>
20   - </n-upload-dragger>
21   - </n-upload>
22   - </n-card>
  5 + <TKUpload :uploadImageUrl="optionData.poster" @sendFile="handleSendFile" @removeFile="handleRemoveFile" />
23 6 </setting-item>
24 7 </setting-item-box>
25 8 <setting-item-box name="源类型" :alone="true">
26 9 <setting-item>
27   - <n-radio-group @change="handleChecked" v-model:value="optionData.sourceType" name="radiogroup">
  10 + <n-radio-group @update:value="handleChecked" v-model:value="optionData.sourceType" name="radiogroup">
28 11 <n-space>
29 12 <n-radio v-for="(item, index) in sourceTypes" :key="item.value" :value="item.value">
30 13 {{ item.label }}
... ... @@ -55,7 +38,12 @@
55 38 </setting-item-box>
56 39 <setting-item-box v-if="optionData.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true">
57 40 <setting-item>
58   - <n-select @update:value="handleSelect" v-model:value="optionData.dataset" :options="videoOptions" />
  41 + <n-select
  42 + @update:value="handleSelect"
  43 + v-model:value="optionData.dataset"
  44 + :options="videoOptions"
  45 + placeholder="请选择视频地址"
  46 + />
59 47 </setting-item>
60 48 </setting-item-box>
61 49 <setting-item-box name="自动播放">
... ... @@ -67,15 +55,21 @@
67 55 </template>
68 56
69 57 <script setup lang="ts">
70   -import { PropType, ref, nextTick, onMounted } from 'vue'
  58 +import { PropType, ref, onMounted } from 'vue'
71 59 import { option, sourceTypeEnum } from './config'
72 60 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
73   -import { FileTypeEnum } from '@/enums/fileTypeEnum'
74   -import { uploadFile } from '@/api/external/contentSave/content'
75   -import { UploadCustomRequestOptions, NTreeSelect } from 'naive-ui'
76   -import { backgroundImageSize } from '@/settings/designSetting'
77   -import { fetchRouteParamsLocation } from '@/utils'
  61 +import { NTreeSelect } from 'naive-ui'
78 62 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
  63 +import { TKUpload } from '@/components/external/Common/TKUpload'
  64 +
  65 +interface videoListIF {
  66 + name: string
  67 + accessMode: number
  68 + id: string
  69 + videoUrl: string
  70 + label: string
  71 + value: string
  72 +}
79 73
80 74 const props = defineProps({
81 75 optionData: {
... ... @@ -84,8 +78,6 @@ const props = defineProps({
84 78 }
85 79 })
86 80
87   -const uploadFileListRef = ref()
88   -
89 81 const sourceTypes = [
90 82 {
91 83 value: 'custom',
... ... @@ -99,7 +91,7 @@ const sourceTypes = [
99 91
100 92 const originationOption = ref([])
101 93
102   -const videoOptions = ref([])
  94 +const videoOptions = ref<videoListIF[]>([])
103 95
104 96 const getOriginationList = async () => {
105 97 const res = await getOrganizationList()
... ... @@ -113,7 +105,8 @@ const handleUpdateTreeValue = (value: string) => {
113 105
114 106 const getVideoLists = async (organizationId: string) => {
115 107 const res = await getVideoList({ organizationId })
116   - videoOptions.value = res?.data?.map((item: any) => ({
  108 + if (!res) return
  109 + videoOptions.value = res?.data?.map((item: videoListIF) => ({
117 110 label: item.name,
118 111 value: item.accessMode === 1 ? item.id : item.videoUrl,
119 112 id: item.id,
... ... @@ -128,16 +121,16 @@ const getVideoUrlById = async (id: string) => {
128 121 props.optionData.url = url
129 122 }
130 123
131   -const handleChecked = ({ target }: any) => {
  124 +const handleChecked = (value: string) => {
132 125 props.optionData.dataset = ''
133   - const { value } = target
134 126 if (value === sourceTypeEnum.PLATFORM) {
135 127 getOriginationList()
136 128 }
137 129 }
138 130
139   -const handleSelect = (value: string, e: any) => {
  131 +const handleSelect = (_: string, e: videoListIF) => {
140 132 const { accessMode, id } = e
  133 + //1表示,需要从服务端调取接口换取播放的地址,0则不需要
141 134 if (accessMode === 1) {
142 135 getVideoUrlById(id)
143 136 } else {
... ... @@ -154,76 +147,10 @@ onMounted(() => {
154 147 }
155 148 })
156 149
157   -// 上传图片前置处理
158   -// eslint-disable-next-line @typescript-eslint/ban-ts-comment
159   -//@ts-ignore
160   -const beforeUploadHandle = async ({ file }) => {
161   - uploadFileListRef.value = []
162   - const type = file.file.type
163   - const size = file.file.size
164   -
165   - if (size > 1024 * 1024 * backgroundImageSize) {
166   - window['$message'].warning(`图片超出 ${backgroundImageSize}M 限制,请重新上传!`)
167   - return false
168   - }
169   - if (type !== FileTypeEnum.PNG && type !== FileTypeEnum.JPEG && type !== FileTypeEnum.GIF) {
170   - window['$message'].warning('文件格式不符合,请重新上传!')
171   - return false
172   - }
173   - return true
  150 +const handleSendFile = (file: string) => {
  151 + if (!file) return
  152 + props.optionData.poster = file
174 153 }
175 154
176   -// 自定义上传操作
177   -const customRequest = (options: UploadCustomRequestOptions) => {
178   - const { file } = options
179   - nextTick(async () => {
180   - if (file.file) {
181   - // 修改名称
182   - const newNameFile = new File([file.file], `${fetchRouteParamsLocation()}_index_background.png`, {
183   - type: file.file.type
184   - })
185   - let uploadParams = new FormData()
186   - uploadParams.append('file', newNameFile)
187   - const uploadRes = await uploadFile(uploadParams)
188   - if (uploadRes) {
189   - props.optionData.poster = uploadRes?.fileStaticUri
190   - window['$message'].success('添加图片成功!')
191   - }
192   - } else {
193   - window['$message'].error('添加图片失败,请稍后重试!')
194   - }
195   - })
196   -}
  155 +const handleRemoveFile = (status: boolean) => (status ? (props.optionData.poster = '') : null)
197 156 </script>
198   -<style lang="scss" scoped>
199   -$uploadHeight: 193px;
200   -.upload-box {
201   - cursor: pointer;
202   - margin-bottom: 20px;
203   - @include deep() {
204   - .n-card__content {
205   - padding: 0;
206   - overflow: hidden;
207   - }
208   - .n-upload-dragger {
209   - padding: 5px;
210   - }
211   - }
212   - .upload-show {
213   - width: -webkit-fill-available;
214   - height: $uploadHeight;
215   - border-radius: 5px;
216   - }
217   - .upload-img {
218   - display: flex;
219   - flex-direction: column;
220   - align-items: center;
221   - img {
222   - height: 150px;
223   - }
224   - .upload-desc {
225   - padding: 10px 0;
226   - }
227   - }
228   -}
229   -</style>
... ...
1 1 <template>
2 2 <div>
3   - <VideoPlay :h="h" :path="option.dataset" :autoPlay="autoplay" :poster="option.poster" />
  3 + <VideoPlay :w="w" :h="h" :sourceSrc="option.dataset" :autoPlay="autoplay" :avatar="option.poster" />
4 4 </div>
5 5 </template>
6 6 <script setup lang="ts">
... ... @@ -16,7 +16,7 @@ const props = defineProps({
16 16 }
17 17 })
18 18
19   -const { h } = toRefs(props.chartConfig.attr)
  19 +const { w, h } = toRefs(props.chartConfig.attr)
20 20
21 21 const { autoplay, dataset, poster, url } = toRefs(props.chartConfig.option)
22 22
... ...
... ... @@ -2,7 +2,7 @@
2 2 <collapse-item name="信息" :expanded="true">
3 3 <setting-item-box name="文字" :alone="true">
4 4 <setting-item>
5   - <n-input v-model:value="optionData.dataset" size="small"></n-input>
  5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 6 </setting-item>
7 7 </setting-item-box>
8 8 </collapse-item>
... ...
... ... @@ -2,7 +2,7 @@
2 2 <collapse-item name="信息" :expanded="true">
3 3 <setting-item-box name="文字" :alone="true">
4 4 <setting-item>
5   - <n-input v-model:value="optionData.dataset" size="small"></n-input>
  5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 6 </setting-item>
7 7 </setting-item-box>
8 8 <setting-item-box name="链接" :alone="true">
... ...
1 1 <template>
2 2 <div class="go-text-box">
3 3 <div class="content">
4   - <span style="cursor: pointer" v-if="link" @click="click">{{ option.dataset }}</span>
5   - <span v-else>{{ option.dataset }}</span>
  4 + <span style="cursor: pointer;white-space: pre-wrap" v-if="link" @click="click">{{ option.dataset }}</span>
  5 + <span style="white-space: pre-wrap" v-else>{{ option.dataset }}</span>
6 6 </div>
7 7 </div>
8 8 </template>
... ...
... ... @@ -2,7 +2,7 @@
2 2 <collapse-item name="信息" :expanded="true">
3 3 <setting-item-box name="文字" :alone="true">
4 4 <setting-item>
5   - <n-input v-model:value="optionData.dataset" size="small"></n-input>
  5 + <n-input v-model:value="optionData.dataset" type="textarea" size="small"></n-input>
6 6 </setting-item>
7 7 </setting-item-box>
8 8 </collapse-item>
... ...
... ... @@ -27,6 +27,16 @@ export let packagesList: PackagesType = {
27 27 [PackagesCategoryEnum.ICONS]: IconList
28 28 }
29 29
  30 +// 组件缓存, 可以大幅度提升组件加载速度
  31 +const componentCacheMap = new Map<string, any>()
  32 +const loadConfig = (packageName: string, categoryName: string, keyName: string) => {
  33 + const key = packageName + categoryName + keyName
  34 + if (!componentCacheMap.has(key)) {
  35 + componentCacheMap.set(key, import(`./components/${packageName}/${categoryName}/${keyName}/config.ts`))
  36 + }
  37 + return componentCacheMap.get(key)
  38 +}
  39 +
30 40 /**
31 41 * * 获取目标组件配置信息
32 42 * @param targetData
... ... @@ -36,10 +46,10 @@ export const createComponent = async (targetData: ConfigType) => {
36 46 // redirectComponent 是给图片组件库和图标组件库使用的
37 47 if (redirectComponent) {
38 48 const [packageName, categoryName, keyName] = redirectComponent.split('/')
39   - const redirectChart = await import(`./components/${packageName}/${categoryName}/${keyName}/config.ts`)
  49 + const redirectChart = await loadConfig(packageName, categoryName, keyName)
40 50 return new redirectChart.default()
41 51 }
42   - const chart = await import(`./components/${targetData.package}/${category}/${key}/config.ts`)
  52 + const chart = await loadConfig(targetData.package, category, key)
43 53 return new chart.default()
44 54 }
45 55
... ... @@ -98,5 +108,4 @@ export const fetchImages = async (targetData?: ConfigType) => {
98 108 }
99 109 return ''
100 110 }
101   -
102 111 useInjectLib(packagesList as any) // THINGS_KIT 修改注册组件
... ...
... ... @@ -24,7 +24,6 @@ import cloneDeep from 'lodash/cloneDeep'
24 24 * 修改后的代码在注释之间,并标注好源代码和修改后代码,方便回溯
25 25 * 源代码 requestDataType: RequestDataTypeEnum.STATIC,
26 26 * 修改后的代码 requestDataType: RequestDataTypeEnum.Pond,
27   - * 修改后代码在//ft之间
28 27 */
29 28 // 请求基础属性
30 29 export const requestConfig: RequestConfigType = {
... ... @@ -49,7 +48,6 @@ export const requestConfig: RequestConfigType = {
49 48 Params: {}
50 49 }
51 50 }
52   -//ft之间
53 51
54 52 // 单实例类
55 53 export class PublicConfigClass implements PublicConfigType {
... ...
... ... @@ -162,17 +162,17 @@ export const useChartEditStore = defineStore({
162 162 },
163 163 getComponentList(): Array<CreateComponentType | CreateComponentGroupType> {
164 164 return this.componentList
165   - },
166   - // 获取需要存储的数据项
  165 + }
  166 + },
  167 + actions: {
  168 + // * 获取需要存储的数据项
167 169 getStorageInfo(): ChartEditStorage {
168 170 return {
169 171 [ChartEditStoreEnum.EDIT_CANVAS_CONFIG]: this.getEditCanvasConfig,
170 172 [ChartEditStoreEnum.COMPONENT_LIST]: this.getComponentList,
171 173 [ChartEditStoreEnum.REQUEST_GLOBAL_CONFIG]: this.getRequestGlobalConfig
172 174 }
173   - }
174   - },
175   - actions: {
  175 + },
176 176 // * 设置 editCanvas 数据项
177 177 setEditCanvas<T extends keyof EditCanvasType, K extends EditCanvasType[T]>(key: T, value: K) {
178 178 this.editCanvas[key] = value
... ...
... ... @@ -10,13 +10,7 @@ import { useInjectAside } from './external/useInjectAside'
10 10
11 11 // 图标
12 12 const { AirPlaneOutlineIcon, ImageIcon, BarChartIcon } = icon.ionicons5
13   -const {
14   - TableSplitIcon,
15   - RoadmapIcon,
16   - SpellCheckIcon,
17   - GraphicalDataFlowIcon,
18   -} = icon.carbon
19   -
  13 +const { TableSplitIcon, RoadmapIcon, SpellCheckIcon, GraphicalDataFlowIcon } = icon.carbon
20 14
21 15 // 图表
22 16 export type MenuOptionsType = {
... ...
... ... @@ -9,7 +9,7 @@ export const exportHandle = () => {
9 9
10 10 // 导出数据
11 11 downloadTextFile(
12   - JSONStringify(chartEditStore.getStorageInfo || []),
  12 + JSONStringify(chartEditStore.getStorageInfo() || []),
13 13 undefined,
14 14 'json'
15 15 )
... ...
... ... @@ -19,14 +19,14 @@ export const syncData = () => {
19 19 transformOrigin: 'center',
20 20 onPositiveCallback: () => {
21 21 window['$message'].success('正在同步编辑器...')
22   - dispatchEvent(new CustomEvent(SavePageEnum.CHART, { detail: chartEditStore.getStorageInfo }))
  22 + dispatchEvent(new CustomEvent(SavePageEnum.CHART, { detail: chartEditStore.getStorageInfo() }))
23 23 }
24 24 })
25 25 }
26 26
27 27 // 同步数据到预览页
28 28 export const syncDataToPreview = () => {
29   - dispatchEvent(new CustomEvent(SavePageEnum.CHART_TO_PREVIEW, { detail: chartEditStore.getStorageInfo }))
  29 + dispatchEvent(new CustomEvent(SavePageEnum.CHART_TO_PREVIEW, { detail: chartEditStore.getStorageInfo() }))
30 30 }
31 31
32 32 // 侦听器更新
... ...
... ... @@ -160,7 +160,7 @@ const editHandle = () => {
160 160
161 161 // 把内存中的数据同步到SessionStorage 便于传递给新窗口初始化数据
162 162 const updateToSession = (id: string) => {
163   - const storageInfo = chartEditStore.getStorageInfo
  163 + const storageInfo = chartEditStore.getStorageInfo()
164 164 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
165 165
166 166 if (sessionStorageInfo?.length) {
... ...
... ... @@ -9,7 +9,7 @@ export const exportHandle = () => {
9 9
10 10 // 导出数据
11 11 downloadTextFile(
12   - JSONStringify(chartEditStore.getStorageInfo || []),
  12 + JSONStringify(chartEditStore.getStorageInfo() || []),
13 13 undefined,
14 14 'json'
15 15 )
... ...
... ... @@ -32,7 +32,7 @@ const previewHandle = () => {
32 32 const { id } = routerParamsInfo.params
33 33 // id 标识
34 34 const previewId = typeof id === 'string' ? id : id[0]
35   - const storageInfo = chartEditStore.getStorageInfo
  35 + const storageInfo = chartEditStore.getStorageInfo()
36 36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
37 37
38 38 if (sessionStorageInfo?.length) {
... ...
... ... @@ -32,7 +32,7 @@ const previewHandle = () => {
32 32 const { id } = routerParamsInfo.params
33 33 // id 标识
34 34 const previewId = typeof id === 'string' ? id : id[0]
35   - const storageInfo = chartEditStore.getStorageInfo
  35 + const storageInfo = chartEditStore.getStorageInfo()
36 36 const sessionStorageInfo = getLocalStorage(StorageEnum.GO_CHART_STORAGE_LIST) || []
37 37
38 38 if (sessionStorageInfo?.length) {
... ...
... ... @@ -76,7 +76,7 @@ const handleBlur = async () => {
76 76 dataViewId,
77 77 dataViewContent: {
78 78 id: dataViewContent.id,
79   - content: JSON.stringify(chartEditStore.getStorageInfo)
  79 + content: JSON.stringify(chartEditStore.getStorageInfo())
80 80 }
81 81 } as unknown as BaseUpdateContentParams
82 82 await contentUpdateApi(saveContent)
... ...
... ... @@ -92,7 +92,7 @@ export const useSyncRemote = () => {
92 92 const saveContent = {
93 93 dataViewContent: {
94 94 id: dataViewContent.id,
95   - content: JSONStringify(chartEditStore.getStorageInfo || {})
  95 + content: JSONStringify(chartEditStore.getStorageInfo() || {})
96 96 },
97 97 dataViewName,
98 98 dataViewId
... ...
1 1 <template>
2 2 <div
3   - class="chart-item"
4   - v-for="item in groupData.groupList"
5   - :class="animationsClass(item.styles.animations)"
6   - :key="item.id"
  3 + :class="animationsClass(groupData.styles.animations)"
7 4 :style="{
8   - ...getComponentAttrStyle(item.attr, groupIndex),
9   - ...getFilterStyle(item.styles),
10   - ...getTransformStyle(item.styles),
11   - ...getStatusStyle(item.status),
12   - ...getPreviewConfigStyle(item.preview),
13   - ...getBlendModeStyle(item.styles) as any
  5 + ...getSizeStyle(groupData.attr),
  6 + ...getFilterStyle(groupData.styles),
14 7 }"
15 8 >
  9 + <div
  10 + class="chart-item"
  11 + v-for="item in groupData.groupList"
  12 + :class="animationsClass(item.styles.animations)"
  13 + :key="item.id"
  14 + :style="{
  15 + ...getComponentAttrStyle(item.attr, groupIndex),
  16 + ...getStatusStyle(item.status),
  17 + ...getPreviewConfigStyle(item.preview),
  18 + ...getBlendModeStyle(item.styles) as any
  19 + }"
  20 + >
16 21 <component
17 22 :is="item.chartConfig.chartKey"
18 23 :id="item.id"
19 24 :chartConfig="item"
20 25 :themeSetting="themeSetting"
21 26 :themeColor="themeColor"
22   - :style="{ ...getSizeStyle(item.attr) }"
23   - v-on="useLifeHandler(item)"
24   - ></component>
  27 + :style="{
  28 + ...getSizeStyle(item.attr),
  29 + ...getFilterStyle(item.styles),
  30 + ...getTransformStyle(item.styles)
  31 + }"
  32 + v-on="useLifeHandler(item)"
  33 + ></component>
  34 + </div>
25 35 </div>
26 36 </template>
27 37
... ...
... ... @@ -6,7 +6,6 @@
6 6 :key="item.id"
7 7 :style="{
8 8 ...getComponentAttrStyle(item.attr, index),
9   - ...getFilterStyle(item.styles),
10 9 ...getTransformStyle(item.styles),
11 10 ...getStatusStyle(item.status),
12 11 ...getPreviewConfigStyle(item.preview),
... ... @@ -31,7 +30,10 @@
31 30 :chartConfig="item"
32 31 :themeSetting="themeSetting"
33 32 :themeColor="themeColor"
34   - :style="{ ...getSizeStyle(item.attr) }"
  33 + :style="{
  34 + ...getSizeStyle(item.attr),
  35 + ...getFilterStyle(item.styles)
  36 + }"
35 37 v-on="useLifeHandler(item)"
36 38 ></component>
37 39 </div>
... ...
... ... @@ -33,7 +33,7 @@ import { getFilterStyle, setTitle } from '@/utils'
33 33
34 34 // THINGS_KIT 重写预览逻辑,调用接口
35 35 import { dragCanvas } from './utils'
36   -import { getEditCanvasConfigStyle,keyRecordHandle } from './utils'
  36 +import { getEditCanvasConfigStyle, keyRecordHandle } from './utils'
37 37 import { getSessionStorageInfo } from './external/usePreview'
38 38
39 39 import { useComInstall } from './hooks/useComInstall.hook'
... ... @@ -52,6 +52,7 @@ setTitle(`预览-${chartEditStore.editCanvasConfig.projectName}`)
52 52
53 53 const previewRefStyle = computed(() => {
54 54 return {
  55 + overflow: 'hidden',
55 56 ...getEditCanvasConfigStyle(chartEditStore.editCanvasConfig),
56 57 ...getFilterStyle(chartEditStore.editCanvasConfig)
57 58 }
... ...
... ... @@ -15,7 +15,7 @@ let key = ref(Date.now())
15 15
16 16 // 数据变更 -> 组件销毁重建
17 17 ;[SavePageEnum.JSON, SavePageEnum.CHART_TO_PREVIEW].forEach((saveEvent: string) => {
18   - if (!window.opener) return
  18 + if (!window.opener && !window.opener.addEventListener) return
19 19 window.opener.addEventListener(saveEvent, async (e: any) => {
20 20 const localStorageInfo: ChartEditStorageType = await getSessionStorageInfo() as unknown as ChartEditStorageType
21 21 setSessionStorage(StorageEnum.GO_CHART_STORAGE_LIST, [{ ...e.detail, id: localStorageInfo.id }])
... ...