node版本 18
This commit is contained in:
parent
575ec062cb
commit
7b9c9e70a3
1074
package-lock.json
generated
1074
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
sub-operation-service/auto-imports.d.ts
vendored
Normal file
75
sub-operation-service/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
54
sub-operation-service/components.d.ts
vendored
Normal file
54
sub-operation-service/components.d.ts
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CenterMap: typeof import('./src/components/centerMap.vue')['default']
|
||||
CodeDialog: typeof import('./src/components/code-dialog/index.vue')['default']
|
||||
copy: typeof import('./src/components/custom-scroll-title copy/index.vue')['default']
|
||||
CostomImg: typeof import('./src/components/costomImg.vue')['default']
|
||||
CustomBack: typeof import('./src/components/customBack.vue')['default']
|
||||
CustomCarouselPicture: typeof import('./src/components/custom-carousel-picture/index.vue')['default']
|
||||
CustomEchartBar: typeof import('./src/components/custom-echart-bar/index.vue')['default']
|
||||
CustomEchartBubble: typeof import('./src/components/custom-echart-bubble/index.vue')['default']
|
||||
CustomEchartColumnLine: typeof import('./src/components/custom-echart-column-line/index.vue')['default']
|
||||
CustomEchartHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/index.vue')['default']
|
||||
CustomEchartLine: typeof import('./src/components/custom-echart-line/index.vue')['default']
|
||||
CustomEchartLineLine: typeof import('./src/components/custom-echart-line-line/index.vue')['default']
|
||||
CustomEchartMaps: typeof import('./src/components/custom-echart-maps/index.vue')['default']
|
||||
CustomEchartMixin: typeof import('./src/components/custom-echart-mixin/index.vue')['default']
|
||||
CustomEchartPictorialBar: typeof import('./src/components/custom-echart-pictorial-bar/index.vue')['default']
|
||||
CustomEchartPie: typeof import('./src/components/custom-echart-pie/index.vue')['default']
|
||||
CustomEchartPie3d: typeof import('./src/components/custom-echart-pie-3d/index.vue')['default']
|
||||
CustomEchartPieGauge: typeof import('./src/components/custom-echart-pie-gauge/index.vue')['default']
|
||||
CustomEchartRadar: typeof import('./src/components/custom-echart-radar/index.vue')['default']
|
||||
CustomEchartScatterBlister: typeof import('./src/components/custom-echart-scatter-blister/index.vue')['default']
|
||||
CustomEchartTriangle: typeof import('./src/components/custom-echart-triangle/index.vue')['default']
|
||||
CustomEchartWaterDroplet: typeof import('./src/components/custom-echart-water-droplet/index.vue')['default']
|
||||
CustomEchartWordCloud: typeof import('./src/components/custom-echart-word-cloud/index.vue')['default']
|
||||
CustomIframe: typeof import('./src/components/custom-iframe/index.vue')['default']
|
||||
CustomImportExcel: typeof import('./src/components/custom-import-excel/index.vue')['default']
|
||||
CustomProgress: typeof import('./src/components/customProgress.vue')['default']
|
||||
CustomRankList: typeof import('./src/components/custom-rank-list/index.vue')['default']
|
||||
CustomRichEditor: typeof import('./src/components/custom-rich-editor/index.vue')['default']
|
||||
CustomScrollBoard: typeof import('./src/components/custom-scroll-board/index.vue')['default']
|
||||
CustomScrollTitle: typeof import('./src/components/custom-scroll-title/index.vue')['default']
|
||||
'CustomScrollTitle copy': typeof import('./src/components/custom-scroll-title copy/index.vue')['default']
|
||||
CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default']
|
||||
CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default']
|
||||
IndexBak: typeof import('./src/components/page-menu/index-bak.vue')['default']
|
||||
NewHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/new-hyaline-cake.vue')['default']
|
||||
PageLayout: typeof import('./src/components/page-layout/index.vue')['default']
|
||||
PageMenu: typeof import('./src/components/page-menu/index.vue')['default']
|
||||
PagePagination: typeof import('./src/components/page-pagination/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SubTop: typeof import('./src/components/subTop.vue')['default']
|
||||
UpFile: typeof import('./src/components/custom-rich-editor/upFile.js')['default']
|
||||
UpImg: typeof import('./src/components/upImg.vue')['default']
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "sub-operation-service",
|
||||
"name": "digital-agriculture-screen",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
@ -7,7 +7,6 @@
|
||||
"dev": "vite --mode development",
|
||||
"build": "vite build --mode production",
|
||||
"test": "vite build --mode test",
|
||||
"pre": "vite build --mode pre",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write 'src/**/*.{vue,ts,tsx,js,jsx,css,less,scss,json,md}'",
|
||||
"eslint": "npx eslint --init",
|
||||
@ -20,10 +19,15 @@
|
||||
"@smallwei/avue": "^3.6.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.6.5",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"echarts-liquidfill": "^3.1.0",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"@vuemap/vue-amap": "^2.0",
|
||||
"@vuemap/vue-amap-loca": "^2.0",
|
||||
"element-plus": "^2.7.2",
|
||||
"hls.js": "^1.6.2",
|
||||
"js-base64": "^3.7.6",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
@ -32,9 +36,11 @@
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"splitpanes": "^4.0.3",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue-cesium": "^3.2.9",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-scroll-seamless": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.7",
|
||||
|
18532
sub-operation-service/src/components/530926geo.json
Normal file
18532
sub-operation-service/src/components/530926geo.json
Normal file
File diff suppressed because it is too large
Load Diff
279
sub-operation-service/src/components/centerMap.vue
Normal file
279
sub-operation-service/src/components/centerMap.vue
Normal file
@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="map-center-warp">
|
||||
<!-- <img :src="getAssetsFile('images/vsualized/gmmap.png')" class="map-img" /> -->
|
||||
<div class="map-pos">
|
||||
<custom-echart-maps height="100%" width="100%" :option="chartsData.option" :geo="geoData" :name="mapName" @click="mapClick" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="isShow"
|
||||
:title="currentMap.name + dialogTitle"
|
||||
:width="dialogWidth"
|
||||
:style="{ 'background-image': 'url(' + getAssetsFile(bgImageVal) + ')' }"
|
||||
:show-close="false"
|
||||
:before-close="handleClose"
|
||||
custom-class="map-info-dialog"
|
||||
>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<slot name="dialogContent"></slot>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import geoJsonData from '../components/530926geo.json'; // 根据实际情况调整路径
|
||||
const route = useRoute();
|
||||
const props = defineProps({
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: '首页',
|
||||
},
|
||||
markerData: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
bgImage: {
|
||||
type: String,
|
||||
default: 'images/vsualized/mapopup.png',
|
||||
},
|
||||
dialogWidth: {
|
||||
type: Number,
|
||||
default: 360,
|
||||
},
|
||||
});
|
||||
var iconUrl = getAssetsFile('images/vsualized/gmmap2.png').href;
|
||||
let bgImageVal = computed(() => {
|
||||
return props.bgImage;
|
||||
});
|
||||
const isShow = ref(false);
|
||||
let geoData = geoJsonData;
|
||||
let mapName = ref('ZJ' + route.name);
|
||||
let mapConfig = reactive({
|
||||
map: mapName.value,
|
||||
zoom: 1,
|
||||
viewControl: {
|
||||
distance: 115,
|
||||
alpha: 60,
|
||||
beta: 0,
|
||||
minBeta: -360,
|
||||
maxBeta: 720,
|
||||
// 限制视角,使不能旋转缩放平移
|
||||
// rotateSensitivity: 0,
|
||||
// zoomSensitivity: 0,
|
||||
// panSensitivity: 0,
|
||||
},
|
||||
// itemStyle: {
|
||||
// // 三维地理坐标系组件 中三维图形的视觉属性,包括颜色,透明度,描边等。
|
||||
// color: 'rgba(75,255,180,0.2)', // 地图板块的颜色
|
||||
// opacity: 1, // 图形的不透明度 [ default: 1 ]
|
||||
// borderWidth: 1.5, // (地图板块间的分隔线)图形描边的宽度。加上描边后可以更清晰的区分每个区域 [ default: 0 ]
|
||||
// borderColor: '#4bffb4', // 图形描边的颜色。[ default: #333 ]
|
||||
// },
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderColor: '#4bffb4', // 设置镇边界的颜色
|
||||
borderWidth: 1, // 设置镇边界的宽度
|
||||
areaColor: 'rgba(75,255,180,0.2)', // 设置背景透明,只显示边界
|
||||
},
|
||||
emphasis: {
|
||||
borderColor: '#4bffb4',
|
||||
borderWidth: 2,
|
||||
areaColor: 'rgba(75,255,180,0.5)', // 设置背景透明,只显示边界
|
||||
label: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
// 选中样式:ml-citation{ref="2,7" data="citationList"}
|
||||
itemStyle: {
|
||||
areaColor: 'rgba(75,255,180,0.6)',
|
||||
borderColor: '#4bffb4',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
|
||||
label: {
|
||||
show: true,
|
||||
distance: 0,
|
||||
color: '#fff',
|
||||
padding: [6, 4, 2, 4],
|
||||
borderRadius: 4,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff', // 地图初始化区域字体颜色
|
||||
borderWidth: 0,
|
||||
borderColor: '#000',
|
||||
},
|
||||
},
|
||||
// emphasis: {
|
||||
// //高亮状态的效果
|
||||
// label: {
|
||||
// show: true,
|
||||
// color: '#fff',
|
||||
// },
|
||||
// itemStyle: {
|
||||
// color: 'rgba(75,255,180,0.3)', // 地图板块的颜色
|
||||
// },
|
||||
// },
|
||||
});
|
||||
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
title: {
|
||||
text: '',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (params) {
|
||||
if (params.seriesType === 'effectScatter') {
|
||||
return `${params.name}: (${params.value[0]}, ${params.value[1]})`;
|
||||
}
|
||||
return params.name;
|
||||
},
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
top: 'center',
|
||||
feature: {
|
||||
// dataView: { readOnly: false },
|
||||
// restore: {},
|
||||
// saveAsImage: {},
|
||||
},
|
||||
},
|
||||
geo: {
|
||||
roam: true,
|
||||
left: '30px',
|
||||
show: false,
|
||||
zlevel: -1, // 必须设置,
|
||||
...mapConfig,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
roam: true,
|
||||
type: 'map',
|
||||
left: '30px',
|
||||
...mapConfig,
|
||||
},
|
||||
{
|
||||
name: '闪烁散点',
|
||||
type: 'effectScatter', // 使用 effectScatter 类型
|
||||
coordinateSystem: 'geo',
|
||||
data: props.markerData,
|
||||
symbolSize: function (val) {
|
||||
return val[2] ? val[2] / 10 : 10; // 如果没有数值,默认大小
|
||||
},
|
||||
label: {
|
||||
formatter: '{b}',
|
||||
position: 'right',
|
||||
show: false,
|
||||
},
|
||||
rippleEffect: {
|
||||
period: 4, // 波纹动画周期
|
||||
scale: 3, // 波纹缩放比例
|
||||
brushType: 'stroke', // 波纹绘制方式:'stroke' 或 'fill'
|
||||
},
|
||||
hoverAnimation: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let currentMap = reactive({});
|
||||
|
||||
const mapClick = (data) => {
|
||||
if (props.markerData.length && props.markerData.length > 0) {
|
||||
if (data.seriesType == 'effectScatter') {
|
||||
isShow.value = true;
|
||||
currentMap = data;
|
||||
emit('mapclick', currentMap);
|
||||
}
|
||||
} else {
|
||||
isShow.value = true;
|
||||
currentMap = data;
|
||||
emit('mapclick', currentMap);
|
||||
}
|
||||
};
|
||||
const handleClose = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const emit = defineEmits(['mapclick']);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::v-deep() {
|
||||
.el-dialog {
|
||||
background: url(iconUrl) no-repeat left top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 8px !important;
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
background-size: 100% 100%;
|
||||
padding: 16px;
|
||||
margin-top: 15%;
|
||||
}
|
||||
.el-dialog__header {
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
padding-left: 48px;
|
||||
.el-dialog__title,
|
||||
i {
|
||||
color: #fff !important;
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
top: 8px !important;
|
||||
}
|
||||
}
|
||||
.map-dialog-my-header {
|
||||
margin-top: 4px;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
h4 {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.map-center-warp {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
.map-img {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
object-fit: contain;
|
||||
transform: translateX(-50%);
|
||||
max-width: 1000px;
|
||||
max-height: 1000px;
|
||||
}
|
||||
.map-pos {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
54
sub-operation-service/src/components/code-dialog/index.vue
Normal file
54
sub-operation-service/src/components/code-dialog/index.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="state.visible"
|
||||
draggable
|
||||
title="溯源码"
|
||||
width="40%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="code-panel">
|
||||
<div class="code-panel-picture">
|
||||
<el-image style="width: 200px; height: 200px" :src="row.orCodeUrl" fit="cover" lazy />
|
||||
</div>
|
||||
<el-button type="primary" @click="downloadFile(row.orCodeUrl, `${row.productName}-溯源码.png`, 'image')"> 下载溯源码</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup name="code-dialog">
|
||||
import { reactive } from 'vue';
|
||||
import { downloadFile } from '@/utils';
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['on-close']);
|
||||
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
state.visible = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
state.visible = true;
|
||||
},
|
||||
hide: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.code {
|
||||
&-panel {
|
||||
padding-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="carousel" style="width: 500px">
|
||||
<el-carousel
|
||||
v-if="type === 'image'"
|
||||
ref="carouselRef"
|
||||
:interval="option.interval"
|
||||
:arrow="option.arrow"
|
||||
:indicator-position="option.indicatorPosition"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in data" :key="index">
|
||||
<img :src="item.image" style="width: 100%; height: auto" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<div v-if="type === 'video'" class="carousel-video">
|
||||
<!-- <img :src="state.videoPicture" class="carousel-video-picture" /> -->
|
||||
<video ref="videoRef" controls class="carousel-video-video" width="100%" height="100%" @ended="pauseVideo">
|
||||
<source :src="state.videoUrl" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<!-- <span class="carousel-video-btn" @click="handlePlay">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<div class="carousel-container">
|
||||
<span class="carousel-arrow carousel-arrow-left" @click="handleLeftClick">
|
||||
<el-icon><ArrowLeftBold /></el-icon>
|
||||
</span>
|
||||
<el-scrollbar ref="scrollbarRef" class="carousel-list">
|
||||
<div
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:class="`carousel-list-item ${state.current === index ? 'active' : ''}`"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<el-image style="width: 100px; height: 100px" :src="item.image ?? item" fit="cover" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<span class="carousel-arrow carousel-arrow-right" @click="handleRightClick">
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="custom-carousel-picture">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
type: { type: String, default: 'image' },
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
height: '',
|
||||
initialIndex: 0,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
interval: 3000,
|
||||
arrow: 'never',
|
||||
indicatorPosition: 'none',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['']);
|
||||
|
||||
const carouselRef = ref(null);
|
||||
const scrollbarRef = ref(null);
|
||||
const videoRef = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
current: 0,
|
||||
isReady: false,
|
||||
videoPicture: '',
|
||||
videoUrl: '',
|
||||
});
|
||||
|
||||
const handleChange = (cur, last) => {
|
||||
state.current = cur;
|
||||
};
|
||||
|
||||
const handleLeftClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index + 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft - 120);
|
||||
};
|
||||
|
||||
const handleRightClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index - 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft + 120);
|
||||
};
|
||||
|
||||
const playVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (index) => {
|
||||
const { type, data } = props;
|
||||
switch (type) {
|
||||
case 'image': {
|
||||
carouselRef.value.setActiveItem(index);
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
const url = data[index];
|
||||
state.videoUrl = url;
|
||||
playVideo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
playVideo();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.carousel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
&-list {
|
||||
flex: 3;
|
||||
:deep(.el-scrollbar__bar) {
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.el-scrollbar__view) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
&-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
&-arrow {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
.el-icon {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&-video {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
&-picture {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
&-video {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
}
|
||||
&-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -25px;
|
||||
margin-left: -25px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
background: rgb(0 0 0 / 20%);
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
115
sub-operation-service/src/components/custom-echart-bar/index.vue
Normal file
115
sub-operation-service/src/components/custom-echart-bar/index.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
export default {
|
||||
name: 'CustomEchartBar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
isSeries: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
const barStyle = props.option?.barStyle ?? {};
|
||||
let obj = { name: type, type: props.type, ...barStyle };
|
||||
let data = [];
|
||||
xAxisData.forEach((x) => {
|
||||
let dataArr = props.chartData.filter((item) => type === item.type && item.name == x);
|
||||
if (dataArr && dataArr.length > 0) {
|
||||
data.push(dataArr[0].value);
|
||||
} else {
|
||||
data.push(null);
|
||||
}
|
||||
});
|
||||
obj['data'] = data;
|
||||
if (props.option?.color) {
|
||||
obj.color = props.option?.color[index];
|
||||
}
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = props.isSeries && option.series.length > 0 ? option.series : seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartBubble',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: function (params) {
|
||||
console.log(params);
|
||||
var str = params.marker + '' + params.data.name + '</br>' + '交易额:' + params.data.value + '万元</br>';
|
||||
return str;
|
||||
},
|
||||
},
|
||||
animationDurationUpdate: function (idx) {
|
||||
// 越往后的数据延迟越大
|
||||
return idx * 100;
|
||||
},
|
||||
animationEasingUpdate: 'bounceIn',
|
||||
color: ['#fff', '#fff', '#fff'],
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
force: {
|
||||
repulsion: 80,
|
||||
edgeLength: 20,
|
||||
},
|
||||
roam: true,
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
data: props.chartData,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
console.info(' option.series[0]', props.chartData);
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
const xData = ref([]);
|
||||
const yDataColumn = ref([]);
|
||||
const yDataLine = ref([]);
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
() => {
|
||||
console.info('props.chartData变化', props.chartData);
|
||||
props.chartData && initCharts();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function hexToRGBA(hex, alpha = 1) {
|
||||
let hexCode = hex.replace('#', '');
|
||||
if (hexCode.length === 3) {
|
||||
hexCode = hexCode
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('');
|
||||
}
|
||||
const r = parseInt(hexCode.slice(0, 2), 16);
|
||||
const g = parseInt(hexCode.slice(2, 4), 16);
|
||||
const b = parseInt(hexCode.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function setAreaStyle(color) {
|
||||
return {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRGBA(color, 1),
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRGBA(color, 0.2),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
xData.value = props.chartData.map((item) => item.name);
|
||||
yDataColumn.value = props.chartData.map((item) => item.value1);
|
||||
yDataLine.value = props.chartData.map((item) => item.value);
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = yDataColumn.value;
|
||||
option.series[1].data = yDataLine.value;
|
||||
option.xAxis.data = xData.value;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 260px"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
export default {
|
||||
name: 'CustomEchartHyalineCake',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
name: '项目一',
|
||||
value: 60,
|
||||
},
|
||||
{
|
||||
name: '项目二',
|
||||
value: 44,
|
||||
},
|
||||
{
|
||||
name: '项目三',
|
||||
value: 32,
|
||||
},
|
||||
],
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
k: 1,
|
||||
opacity: '0,6',
|
||||
itemGap: 0.2,
|
||||
itemHeight: 120,
|
||||
autoItemHeight: 0,
|
||||
legendSuffix: '',
|
||||
}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
isSeries: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = ref({});
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
option.value = Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.value = getPie3D(props.chartData, props.option.opacity);
|
||||
setOptions(option.value);
|
||||
}
|
||||
|
||||
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
|
||||
const midRatio = (startRatio + endRatio) / 2;
|
||||
const startRadian = startRatio * Math.PI * 2;
|
||||
const endRadian = endRatio * Math.PI * 2;
|
||||
const midRadian = midRatio * Math.PI * 2;
|
||||
if (startRatio === 0 && endRatio === 1) {
|
||||
isSelected = false;
|
||||
}
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
const offsetX = Math.cos(midRadian) * props.option.itemGap;
|
||||
const offsetY = Math.sin(midRadian) * props.option.itemGap;
|
||||
const hoverRate = 1;
|
||||
return {
|
||||
u: {
|
||||
min: -Math.PI,
|
||||
max: Math.PI * 3,
|
||||
step: Math.PI / 32,
|
||||
},
|
||||
v: {
|
||||
min: 0,
|
||||
max: Math.PI * 2,
|
||||
step: Math.PI / 20,
|
||||
},
|
||||
|
||||
x(u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
y(u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
z(u, v) {
|
||||
if (u < -Math.PI * 0.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
if (u > Math.PI * 2.5) {
|
||||
return Math.sin(u) * h * 0.1;
|
||||
}
|
||||
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
|
||||
},
|
||||
};
|
||||
}
|
||||
// 生成模拟 3D 饼图的配置项
|
||||
function getPie3D(pieData) {
|
||||
const series = [];
|
||||
// 总和
|
||||
let sumValue = 0;
|
||||
let startValue = 0;
|
||||
let endValue = 0;
|
||||
const legendData = [];
|
||||
const k = props.option.k ?? 1;
|
||||
for (let i = 0; i < pieData.length; i += 1) {
|
||||
sumValue += pieData[i].value;
|
||||
|
||||
const seriesItem = {
|
||||
name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
|
||||
type: 'surface',
|
||||
parametric: true,
|
||||
wireframe: {
|
||||
show: false,
|
||||
},
|
||||
pieData: pieData[i],
|
||||
|
||||
itemStyle: {
|
||||
// color: colors[i], // 自定义颜色
|
||||
opacity: props.option.opacity,
|
||||
borderRadius: 300,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 0,
|
||||
},
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k,
|
||||
},
|
||||
};
|
||||
if (typeof pieData[i].itemStyle !== 'undefined') {
|
||||
const { itemStyle } = pieData[i];
|
||||
typeof pieData[i].itemStyle.color !== 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
|
||||
typeof pieData[i].itemStyle.opacity !== 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null;
|
||||
seriesItem.itemStyle = itemStyle;
|
||||
}
|
||||
series.push(seriesItem);
|
||||
}
|
||||
for (let i = 0; i < series.length; i += 1) {
|
||||
endValue = startValue + series[i].pieData.value;
|
||||
series[i].pieData.startRatio = startValue / sumValue;
|
||||
series[i].pieData.endRatio = endValue / sumValue;
|
||||
series[i].parametricEquation = getParametricEquation(
|
||||
series[i].pieData.startRatio,
|
||||
series[i].pieData.endRatio,
|
||||
false,
|
||||
false,
|
||||
k,
|
||||
props.option.autoItemHeight > 0 ? props.option.autoItemHeight * series[i].pieData.value : props.option.itemHeight
|
||||
);
|
||||
startValue = endValue;
|
||||
legendData.push(series[i].name);
|
||||
}
|
||||
const option = Object.assign(
|
||||
{
|
||||
tooltip: {
|
||||
// tooltip样式调整添加这个类名
|
||||
className: 'custom-tooltip-container', // 自定义父容器类名
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderColor: '#35d0c0',
|
||||
color: '#fff',
|
||||
position: function (point, params, dom, rect, size) {
|
||||
var x = point[0];
|
||||
var y = point[1];
|
||||
var viewWidth = size.viewSize[0];
|
||||
var viewHeight = size.viewSize[1];
|
||||
var boxWidth = size.contentSize[0];
|
||||
var boxHeight = size.contentSize[1];
|
||||
// 判断 tooltip 位置,调整其位置使其不会超出图表边界
|
||||
if (x + boxWidth > viewWidth) {
|
||||
x = x - boxWidth;
|
||||
}
|
||||
if (y + boxHeight > viewHeight) {
|
||||
y = y - boxHeight;
|
||||
}
|
||||
// 保证 tooltip 始终在图表内部
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
|
||||
return [x, y];
|
||||
},
|
||||
formatter: (params) => {
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
return `
|
||||
<span style="color:#FFF">
|
||||
${params.seriesName}<br/>
|
||||
<span style="display:inline-block;
|
||||
margin-right:5px;
|
||||
border-radius:10px;
|
||||
width:10px;
|
||||
height:10px;
|
||||
background-color:${params.color};"></span>
|
||||
${option.series[params.seriesIndex].pieData.value}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
xAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
yAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
zAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 5,
|
||||
top: '0',
|
||||
left: '-20%',
|
||||
viewControl: {
|
||||
//3d效果可以放大、旋转等,请自己去查看官方配置
|
||||
alpha: 60, //角度(这个很重要 调节角度的)
|
||||
distance: 240, //调整视角到主体的距离,类似调整zoom(这是整体大小)
|
||||
rotateSensitivity: 10, //设置旋转灵敏度,为0无法旋转
|
||||
zoomSensitivity: 10, //设置缩放灵敏度,为0无法缩放
|
||||
panSensitivity: 10, //设置平移灵敏度,0无法平移
|
||||
autoRotate: true, //自动旋转
|
||||
autoRotateAfterStill: 2, //在鼠标静止操作后恢复自动旋转的时间间隔,在开启 autoRotate 后有效
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
right: '25%',
|
||||
top: '25%',
|
||||
orient: 'vertical',
|
||||
icon: 'circle',
|
||||
itemHeight: 12,
|
||||
itemWidth: 12,
|
||||
itemGap: 10,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
},
|
||||
formatter: (name) => {
|
||||
if (props.chartData.length) {
|
||||
const item = props.chartData.filter((item) => item.name === name)[0];
|
||||
return `${name}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
series,
|
||||
},
|
||||
props.option
|
||||
);
|
||||
return option;
|
||||
}
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ width: width, height: height }"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
defineOptions({ name: 'NewHyalineCake' });
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
// 默认示例数据
|
||||
{ name: '项目一', value: 60 },
|
||||
{ name: '项目二', value: 44 },
|
||||
{ name: '项目三', value: 32 },
|
||||
],
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// 控制内外径关系的系数,1 表示无内径(实心饼),值越小内径越大
|
||||
k: 1,
|
||||
// 扇形边缘向外偏移距离比例(用于选中效果),单位视图坐标,可调
|
||||
itemGap: 0.2,
|
||||
// 扇形高度(影响z轴拉伸程度)
|
||||
itemHeight: 120,
|
||||
// 自动计算扇形高度时使用的系数(>0时 itemHeight 失效,使用 autoItemHeight * value )
|
||||
autoItemHeight: 0,
|
||||
// 透明度设置
|
||||
opacity: 0.6,
|
||||
// 图例后缀
|
||||
legendSuffix: '',
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
// 声明组件触发的事件
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// 绑定到DOM的容器引用
|
||||
const chartRef = ref(null);
|
||||
// 使用 useEcharts 钩子初始化 ECharts 实例,并获取控制方法
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
|
||||
// 存储当前的 ECharts 配置项
|
||||
const chartOption = ref({});
|
||||
|
||||
// 参数方程函数:生成每个扇形曲面的参数方程,用于 series-surface.parametricEquation
|
||||
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
|
||||
// 中心弧度用于计算偏移方向
|
||||
const midRatio = (startRatio + endRatio) / 2;
|
||||
const startRadian = startRatio * Math.PI * 2;
|
||||
const endRadian = endRatio * Math.PI * 2;
|
||||
const midRadian = midRatio * Math.PI * 2;
|
||||
|
||||
// 如果整个饼只剩一个扇形,则不实现选中效果
|
||||
if (startRatio === 0 && endRatio === 1) {
|
||||
isSelected = false;
|
||||
}
|
||||
// k 取默认值 1/3(环的厚度比例),如果传入 k 则使用传入值
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
|
||||
// 计算选中效果的偏移量(基于扇形中心角度)
|
||||
const offsetX = isSelected ? Math.cos(midRadian) * props.option.itemGap : 0;
|
||||
const offsetY = isSelected ? Math.sin(midRadian) * props.option.itemGap : 0;
|
||||
// 计算高亮时的放大比例(未高亮时为1)
|
||||
const hoverRate = isHovered ? 1.05 : 1;
|
||||
|
||||
// 返回 parametric 曲面方程
|
||||
return {
|
||||
u: {
|
||||
// u 参数控制饼的周向角度:从 -π 到 3π,可以完整绘制一圈
|
||||
min: -Math.PI,
|
||||
max: Math.PI * 3,
|
||||
step: Math.PI / 32,
|
||||
},
|
||||
v: {
|
||||
// v 从 0 - 2π 参数控制扇形的径向方向(厚度方向)
|
||||
min: 0,
|
||||
max: Math.PI * 2,
|
||||
step: Math.PI / 20,
|
||||
},
|
||||
x(u, v) {
|
||||
// 如果在起始弧度之前,保持扇形起点的半径
|
||||
if (u < startRadian) {
|
||||
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
// 如果在结束弧度之后,保持扇形终点的半径
|
||||
if (u > endRadian) {
|
||||
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
// 扇形中间部分正常绘制
|
||||
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
y(u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
z(u, v) {
|
||||
// 在 u < -π/2 时,防止出现不必要的翻转,直接使用基本正弦
|
||||
if (u < -Math.PI * 0.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
// 在 u > 2.5π 时,处理扇形尾部厚度
|
||||
if (u > Math.PI * 2.5) {
|
||||
return Math.sin(u) * h * 0.1;
|
||||
}
|
||||
// 正常情况下,z 根据 v 参数控制上下表面的高度差
|
||||
// 当前图形的高度是Z根据h(每个value的值决定的)
|
||||
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 生成整个 3D 饼图的配置项
|
||||
function getPie3D(pieData) {
|
||||
const series = [];
|
||||
let sumValue = 0;
|
||||
// 计算总值
|
||||
pieData.forEach((item) => {
|
||||
sumValue += item.value;
|
||||
});
|
||||
|
||||
// k 为外径厚度系数
|
||||
const k = props.option.k ?? 1;
|
||||
// 构建每个扇形的 series 数据
|
||||
pieData.forEach((dataItem, idx) => {
|
||||
const seriesItem = {
|
||||
name: dataItem.name ?? `series${idx}`,
|
||||
type: 'surface',
|
||||
parametric: true,
|
||||
wireframe: { show: false },
|
||||
pieData: dataItem,
|
||||
itemStyle: {
|
||||
opacity: props.option.opacity,
|
||||
borderRadius: 300,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 0,
|
||||
},
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k,
|
||||
},
|
||||
};
|
||||
// 合并自定义样式(如果有)
|
||||
if (dataItem.itemStyle) {
|
||||
const customStyle = {};
|
||||
if (dataItem.itemStyle.color !== undefined) {
|
||||
customStyle.color = dataItem.itemStyle.color;
|
||||
}
|
||||
if (dataItem.itemStyle.opacity !== undefined) {
|
||||
customStyle.opacity = dataItem.itemStyle.opacity;
|
||||
}
|
||||
seriesItem.itemStyle = { ...seriesItem.itemStyle, ...customStyle };
|
||||
}
|
||||
series.push(seriesItem);
|
||||
});
|
||||
|
||||
// 计算每个扇形的 startRatio/endRatio 和参数方程
|
||||
let startValue = 0;
|
||||
series.forEach((serie) => {
|
||||
const endValue = startValue + serie.pieData.value;
|
||||
const startRatio = startValue / sumValue;
|
||||
const endRatio = endValue / sumValue;
|
||||
serie.pieData.startRatio = startRatio;
|
||||
serie.pieData.endRatio = endRatio;
|
||||
// 初始均为未选中未高亮
|
||||
serie.parametricEquation = getParametricEquation(
|
||||
startRatio,
|
||||
endRatio,
|
||||
false,
|
||||
true,
|
||||
k,
|
||||
// 扇形高度:根据配置自动计算或使用固定高度
|
||||
props.option.autoItemHeight > 0 ? props.option.autoItemHeight * serie.pieData.value : props.option.itemHeight
|
||||
);
|
||||
startValue = endValue;
|
||||
});
|
||||
|
||||
// 添加一个透明圆环,用于实现 hover 在扇形外面时取消高亮
|
||||
series.push({
|
||||
name: 'mouseoutSeries',
|
||||
type: 'surface',
|
||||
parametric: true,
|
||||
wireframe: { show: false },
|
||||
itemStyle: { opacity: 0 },
|
||||
parametricEquation: {
|
||||
u: { min: 0, max: Math.PI * 2, step: Math.PI / 20 },
|
||||
v: { min: 0, max: Math.PI, step: Math.PI / 20 },
|
||||
x(u, v) {
|
||||
return Math.sin(v) * Math.sin(u) + Math.sin(u);
|
||||
},
|
||||
y(u, v) {
|
||||
return Math.sin(v) * Math.cos(u) + Math.cos(u);
|
||||
},
|
||||
z(u, v) {
|
||||
return Math.cos(v) > 0 ? 0.1 : -0.1;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 基础配置:坐标轴、视角、图例、提示框等
|
||||
const option = Object.assign(
|
||||
{
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(18, 55, 85, 0.8)',
|
||||
borderColor: '#35d0c0',
|
||||
color: '#fff',
|
||||
position: (point, params, dom, rect, size) => {
|
||||
// 动态调整 tooltip 位置,避免溢出
|
||||
let x = point[0],
|
||||
y = point[1];
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [boxW, boxH] = size.contentSize;
|
||||
if (x + boxW > viewW) x -= boxW;
|
||||
if (y + boxH > viewH) y -= boxH;
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
return [x, y];
|
||||
},
|
||||
formatter: (params) => {
|
||||
// 只对非透明环(实际扇形)显示信息
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
return `
|
||||
<span style="color:#FFF">
|
||||
${params.seriesName}<br/>
|
||||
<span style="
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
border-radius:10px;
|
||||
width:10px;
|
||||
height:10px;
|
||||
background-color:${params.color};"></span>
|
||||
${chartOption.value.series[params.seriesIndex].pieData.value}
|
||||
</span>`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
xAxis3D: { min: -1, max: 1 },
|
||||
yAxis3D: { min: -1, max: 1 },
|
||||
zAxis3D: { min: -1, max: 1 },
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 5,
|
||||
top: '0',
|
||||
left: '-20%',
|
||||
viewControl: {
|
||||
// 3D 视角设置
|
||||
alpha: 60, // 俯仰角度
|
||||
distance: 240, // 视角距离
|
||||
rotateSensitivity: 10,
|
||||
zoomSensitivity: 10,
|
||||
panSensitivity: 10,
|
||||
autoRotate: true,
|
||||
autoRotateAfterStill: 2,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
selectedMode: false,
|
||||
right: '5%',
|
||||
top: '25%',
|
||||
orient: 'vertical',
|
||||
icon: 'circle',
|
||||
itemHeight: 12,
|
||||
itemWidth: 12,
|
||||
itemGap: 10,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
},
|
||||
// 图例标签显示名称和数值
|
||||
formatter: (name) => {
|
||||
const item = props.chartData.find((d) => d.name === name);
|
||||
return item ? ` ${name} ${item.value}${props.option.legendSuffix || ''}` : name;
|
||||
},
|
||||
},
|
||||
series,
|
||||
},
|
||||
// 将外部配置项覆盖基础配置
|
||||
props.option
|
||||
);
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
// 生成基础的3D饼图配置
|
||||
const baseOption = getPie3D(props.chartData);
|
||||
// 合并用户配置,确保不修改原始对象
|
||||
const finalOption = Object.assign({}, baseOption, cloneDeep(props.option || {}));
|
||||
chartOption.value = finalOption;
|
||||
// 设置图表配置
|
||||
setOptions(chartOption.value);
|
||||
// 等待 DOM + ECharts 初始化完成
|
||||
nextTick(() => {
|
||||
const chart = getInstance();
|
||||
if (!chart) {
|
||||
console.warn('ECharts 实例未初始化,事件绑定失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免重复绑定事件
|
||||
chart.off('click');
|
||||
chart.off('mouseover');
|
||||
chart.off('globalout');
|
||||
|
||||
chart.on('click', handleClick);
|
||||
chart.on('mouseover', handleMouseover);
|
||||
chart.on('globalout', handleGlobalout);
|
||||
});
|
||||
}
|
||||
function handleClick(params) {
|
||||
// 如果点击的是透明辅助环,则忽略
|
||||
if (params.seriesName === 'mouseoutSeries') return;
|
||||
|
||||
const optionVal = chartOption.value;
|
||||
const series = optionVal.series;
|
||||
const idx = params.seriesIndex;
|
||||
|
||||
// 新的选中状态(取反)
|
||||
const isSelected = !series[idx].pieStatus.selected;
|
||||
const isHovered = series[idx].pieStatus.hovered;
|
||||
const k = series[idx].pieStatus.k;
|
||||
const startRatio = series[idx].pieData.startRatio;
|
||||
const endRatio = series[idx].pieData.endRatio;
|
||||
const h = series[idx].pieData.value;
|
||||
|
||||
// 如果之前有其他扇形被选中,取消其选中状态
|
||||
if (selectedIndex !== null && selectedIndex !== idx) {
|
||||
const prev = series[selectedIndex];
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
false,
|
||||
false,
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
prev.pieStatus.selected = false;
|
||||
selectedIndex = null;
|
||||
}
|
||||
|
||||
// 设置当前扇形的选中/取消选中状态
|
||||
series[idx].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h);
|
||||
series[idx].pieStatus.selected = isSelected;
|
||||
|
||||
// 更新记录选中的系列索引
|
||||
selectedIndex = isSelected ? idx : null;
|
||||
|
||||
// 更新图表配置
|
||||
setOptions(optionVal);
|
||||
// 触发组件 click 事件供父组件使用
|
||||
emit('click', params);
|
||||
}
|
||||
// 在函数顶部声明(确保作用域覆盖所有需要的地方)
|
||||
window.debugZValues = {
|
||||
current: null,
|
||||
history: [],
|
||||
};
|
||||
function handleMouseover(params) {
|
||||
if (params.seriesName === 'mouseoutSeries') return;
|
||||
|
||||
const chart = getInstance();
|
||||
const optionVal = chart.getOption(); // 改用实时获取最新配置
|
||||
const series = optionVal.series;
|
||||
const idx = params.seriesIndex;
|
||||
const hh = series[idx].parametricEquation.z();
|
||||
window.debugZValues.current = hh;
|
||||
window.debugZValues.history.push({
|
||||
event: 'mouseover',
|
||||
series: series[idx].name,
|
||||
zValue: hh,
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
console.log('当前Z值:', hh, '历史记录:', window.debugZValues.history);
|
||||
// 打印移入前的完整状态(调试用)
|
||||
console.log(
|
||||
'[移入] 当前所有扇形状态',
|
||||
series.map((s) => ({
|
||||
name: s.name,
|
||||
hovered: s.pieStatus?.hovered,
|
||||
selected: s.pieStatus?.selected,
|
||||
height: s.parametricEquation?.z(Math.PI, Math.PI),
|
||||
}))
|
||||
);
|
||||
|
||||
// 取消之前的高亮(确保只修改状态,不破坏参数方程)
|
||||
if (hoveredIndex !== null && hoveredIndex !== idx) {
|
||||
const prev = series[hoveredIndex];
|
||||
prev.pieStatus.hovered = false; // 仅修改状态
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
// 重新生成正确方程
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
prev.pieStatus.selected,
|
||||
false, // isHovered=false
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
}
|
||||
|
||||
// 设置新高亮
|
||||
const current = series[idx];
|
||||
current.pieStatus.hovered = true;
|
||||
current.parametricEquation = getParametricEquation(
|
||||
current.pieData.startRatio,
|
||||
current.pieData.endRatio,
|
||||
current.pieStatus.selected,
|
||||
true, // isHovered=true
|
||||
current.pieStatus.k,
|
||||
current.pieData.value
|
||||
);
|
||||
|
||||
hoveredIndex = idx;
|
||||
chart.setOption({ series }); // 仅更新series部分
|
||||
}
|
||||
function handleGlobalout() {
|
||||
if (hoveredIndex !== null) {
|
||||
const chart = getInstance();
|
||||
const optionVal = chart.getOption();
|
||||
const series = optionVal.series;
|
||||
const prev = series[hoveredIndex];
|
||||
|
||||
// 打印修复前的错误状态(调试用)
|
||||
console.warn('[修复前] 异常状态', {
|
||||
name: prev.name,
|
||||
z: prev.parametricEquation.z(Math.PI, Math.PI),
|
||||
equation: prev.parametricEquation,
|
||||
});
|
||||
|
||||
// 重置状态并重新生成方程
|
||||
prev.pieStatus.hovered = false;
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
prev.pieStatus.selected,
|
||||
false, // isHovered=false
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
|
||||
hoveredIndex = null;
|
||||
chart.setOption({ series }, { replaceMerge: 'series' }); // 强制替换series
|
||||
}
|
||||
}
|
||||
// 记录当前选中和高亮的系列索引
|
||||
let selectedIndex = null;
|
||||
let hoveredIndex = null;
|
||||
|
||||
// 组件挂载后绑定事件
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
// 监听数据或配置变更,重新初始化图表
|
||||
watch(
|
||||
[() => props.chartData, () => props.option],
|
||||
() => {
|
||||
if (props.chartData && props.chartData.length) {
|
||||
initChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可根据需要自定义图表容器样式 */
|
||||
</style>
|
@ -0,0 +1,78 @@
|
||||
<!-- <template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template> -->
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 260px"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLineLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const optionVal = reactive({});
|
||||
|
||||
watchEffect(() => {
|
||||
props.option && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(optionVal, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(props.option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function hexToRGBA(hex, alpha = 1) {
|
||||
let hexCode = hex.replace('#', '');
|
||||
if (hexCode.length === 3) {
|
||||
hexCode = hexCode
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('');
|
||||
}
|
||||
const r = parseInt(hexCode.slice(0, 2), 16);
|
||||
const g = parseInt(hexCode.slice(2, 4), 16);
|
||||
const b = parseInt(hexCode.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function setAreaStyle(color) {
|
||||
return {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRGBA(color, 1),
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRGBA(color, 0.2),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
let obj = {
|
||||
name: type,
|
||||
type: props.type,
|
||||
smooth: true,
|
||||
};
|
||||
if (props.option?.color) {
|
||||
obj.areaStyle = setAreaStyle(props.option?.color[index]);
|
||||
}
|
||||
const findItem = props.chartData.find((item) => item.type == type);
|
||||
if (findItem && findItem.color) {
|
||||
obj.color = findItem.color;
|
||||
obj.areaStyle = setAreaStyle(findItem.color[index]);
|
||||
}
|
||||
let data = [];
|
||||
xAxisData.forEach((x) => {
|
||||
let dataArr = props.chartData.filter((item) => type === item.type && item.name == x);
|
||||
if (dataArr && dataArr.length > 0) {
|
||||
data.push(dataArr[0].value);
|
||||
} else {
|
||||
data.push(null);
|
||||
}
|
||||
});
|
||||
obj['data'] = data;
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
// getInstance()?.off('click', onClick);
|
||||
// getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div ref="chartMap" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect, nextTick } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartMaps',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
geo: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartMap = ref(null);
|
||||
const { setOptions, getInstance, resize, regMap, startAutoPlay, onMapClick } = useEcharts(chartMap);
|
||||
const option = reactive({});
|
||||
|
||||
watchEffect(() => {
|
||||
regMap(props.name, props.geo);
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
|
||||
setOptions(option);
|
||||
|
||||
onMapClick((data) => {
|
||||
emit('click', data);
|
||||
});
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
}
|
||||
|
||||
return { chartMap };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 260px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartMixin',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'bar',
|
||||
type: 'bar',
|
||||
data: [],
|
||||
itemStyle: {
|
||||
barWidth: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
console.log(props.option);
|
||||
if (props.option.series) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
} else {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
const barStyle = props.option?.barStyle ?? {};
|
||||
let obj = { name: type, ...barStyle };
|
||||
let chartArr = props.chartData.filter((item) => type === item.type);
|
||||
obj['data'] = chartArr.map((item) => item.value);
|
||||
obj['type'] = chartArr[0].seriesType;
|
||||
obj['stack'] = chartArr[0].stack;
|
||||
obj['itemStyle'] = chartArr[0].itemStyle;
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
}
|
||||
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartPictorialBar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '6%',
|
||||
top: '11%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
formatter: '{b}',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pictorialBar',
|
||||
barCategoryGap: '40%',
|
||||
barWidth: '100%',
|
||||
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#000001', // 起始颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#0175b6', // 结束颜色
|
||||
},
|
||||
],
|
||||
global: false, // 默认为 false
|
||||
},
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'top',
|
||||
formatter: '{c}',
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPie3d',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, onMapClick } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.option,
|
||||
() => {
|
||||
initCharts();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
onMapClick((data) => {
|
||||
emit('click', data);
|
||||
});
|
||||
// getInstance()?.off('click', onClick);
|
||||
// getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPieGauge',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartPie',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: '{b} ({c})',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '72%',
|
||||
center: ['50%', '55%'],
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} \n ({d}%)',
|
||||
color: '#B1B9D3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartRadar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'radar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
legend: {},
|
||||
radar: {},
|
||||
tooltip: {},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let indicator = Array.from(
|
||||
new Set(
|
||||
props.chartData.map((item) => {
|
||||
let { name, max } = item;
|
||||
return { name, max };
|
||||
})
|
||||
)
|
||||
);
|
||||
let data = [];
|
||||
typeArr.forEach((type) => {
|
||||
const radarStyle = props.option?.radarStyle ?? {};
|
||||
let obj = { name: type, ...radarStyle };
|
||||
let chartArr = props.chartData.filter((item) => type === item.type);
|
||||
obj['value'] = chartArr.map((item) => item.value);
|
||||
data.push(obj);
|
||||
});
|
||||
option.radar.indicator = indicator;
|
||||
option.series[0]['data'] = data;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartScatterBlister',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartTriangle',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
backgroundColor: 'rgba(18, 55, 85, 0.8);',
|
||||
borderColor: '#35d0c0',
|
||||
formatter: (data) => {
|
||||
const params = data.data;
|
||||
let str = `<div class="custom-echarts-tips">
|
||||
<span>${params.name}</span><br/>
|
||||
<span>${params.reaVal}%</span>
|
||||
</div>`;
|
||||
return str;
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
zlevel: 1,
|
||||
name: '漏斗图',
|
||||
type: 'funnel',
|
||||
top: '11%',
|
||||
left: 'center',
|
||||
width: '25%',
|
||||
sort: 'ascending',
|
||||
gap: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
width: '200px',
|
||||
align: 'right',
|
||||
formatter: function (params) {
|
||||
if (!params.data.reaVal) return '';
|
||||
let arr = [`{a|${params.data.name}}`, `{b| ${params.data.reaVal}%}`];
|
||||
return arr.join('\n');
|
||||
},
|
||||
rich: {
|
||||
a: { color: '#fff', fontSize: '16px' },
|
||||
b: { color: '#05FCC6', fontSize: '16px', marginTop: '10px' },
|
||||
},
|
||||
verticalAlign: 'middle',
|
||||
padding: [5, 6], // 增加标签内边距
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 10,
|
||||
length2: 50,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: '#ffffff',
|
||||
opacity: 1,
|
||||
type: 'solid',
|
||||
},
|
||||
},
|
||||
// 添加第二段折线
|
||||
// markLine: {
|
||||
// symbol: 'none', // 隐藏端点
|
||||
// lineStyle: {
|
||||
// type: 'solid',
|
||||
// color: '#fff',
|
||||
// width: 1,
|
||||
// },
|
||||
// data: [
|
||||
// // 从默认 labelLine 的终点到自定义位置
|
||||
// [
|
||||
// {
|
||||
// coord: [50, 50], // 第一段线的终点(需动态计算)
|
||||
// name: 'Label1',
|
||||
// },
|
||||
// {
|
||||
// coord: [80, 50], // 第二段线的终点
|
||||
// name: 'Label1',
|
||||
// },
|
||||
// ],
|
||||
// ],
|
||||
// },
|
||||
itemStyle: {
|
||||
show: false,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: 20,
|
||||
},
|
||||
},
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 260px"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartWaterDroplet',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: '{b} ({c})',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '72%',
|
||||
center: ['50%', '55%'],
|
||||
data: [],
|
||||
labelLine: { show: true },
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b} \n ({d}%)',
|
||||
color: '#B1B9D3',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
setOptions(props.option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartWordCloud',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series = props.chartData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
39
sub-operation-service/src/components/custom-iframe/index.vue
Normal file
39
sub-operation-service/src/components/custom-iframe/index.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<iframe v-if="state.url" :src="state.url" frameborder="0" width="100%" height="100%" @load="onLoad"></iframe>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-iframe">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { isExternal } from '@/utils/validate';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['on-load']);
|
||||
|
||||
const state = reactive({
|
||||
url: '',
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.url,
|
||||
(val) => {
|
||||
if (isExternal(val)) {
|
||||
state.url = val;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const onLoad = () => {
|
||||
state.loaded = true;
|
||||
emit('on-load', state.loaded);
|
||||
};
|
||||
</script>
|
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="state.visible"
|
||||
draggable
|
||||
append-to-body
|
||||
:title="title"
|
||||
width="50%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="import-tips">
|
||||
<p>{{ tips }}</p>
|
||||
<el-button v-if="templateUrl" type="primary" icon="download" text @click="emit('on-download', templateUrl)">下载模板</el-button>
|
||||
</div>
|
||||
<el-upload ref="uploadRef" drag action="#" :show-file-list="true" accept=".xlsx,.xls" :limit="1" :http-request="onUploadExcel">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件放在此处或单击上传</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅允许导入xls、xlsx格式文件,excel文件大小小于500kb</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="onConfirm"> 确定导入</el-button>
|
||||
<el-button @click="onClose"> 取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup name="custom-import-excel">
|
||||
import { reactive, ref, shallowRef } from 'vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '文件导入',
|
||||
},
|
||||
tips: {
|
||||
type: String,
|
||||
default: '提示:导入前请先下载模板填写信息,然后再导入!',
|
||||
},
|
||||
templateUrl: {
|
||||
type: [String, URL],
|
||||
default: '',
|
||||
},
|
||||
// options: {
|
||||
// type: Object,
|
||||
// default: () => {
|
||||
// return {
|
||||
// tips: '提示:导入前请先下载模板填写信息,然后再导入!',
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
});
|
||||
const emit = defineEmits(['on-confirm', 'on-close', 'on-download']);
|
||||
|
||||
const uploadRef = ref(null);
|
||||
const formDate = shallowRef(null);
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const onUploadExcel = ({ file }) => {
|
||||
if (isEmpty(file.name)) return;
|
||||
formDate.value = new FormData();
|
||||
formDate.value.append('file', file);
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
emit('on-confirm', formDate.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
uploadRef?.value && uploadRef.value.clearFiles();
|
||||
state.visible = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
formDate.value = null;
|
||||
state.visible = true;
|
||||
},
|
||||
hide: () => {
|
||||
onClose();
|
||||
},
|
||||
clear: () => {
|
||||
uploadRef?.value && uploadRef.value.clearFiles();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.import {
|
||||
&-tips {
|
||||
@include flex-row;
|
||||
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #979797;
|
||||
p {
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
269
sub-operation-service/src/components/custom-rank-list/index.vue
Normal file
269
sub-operation-service/src/components/custom-rank-list/index.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div :class="`custom-rank-list rank-${type}`" :style="`color: ${textColor}`">
|
||||
<div
|
||||
v-for="(item, i) in status.rows"
|
||||
:key="item.toString() + item.scroll"
|
||||
:class="`row-item row-item-${item.ranking}`"
|
||||
:style="`height: ${status.heights[i]}px;`"
|
||||
>
|
||||
<div class="ranking-info">
|
||||
<div class="rank" :style="`color: ${color};font-size: ${indexFontSize}px`">{{ indexPrefix }}{{ item.ranking }}</div>
|
||||
<div class="info-name" :style="`font-size: ${leftFontSize}px`" v-html="item.name" />
|
||||
<div v-if="type === 'column'" class="ranking-value" :style="`color: ${textColor};font-size: ${rightFontSize}px`">
|
||||
{{ status.mergedConfig.valueFormatter ? status.mergedConfig.valueFormatter(item) : item.value }}
|
||||
{{ unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ranking-column" :style="`border-color: ${borderColor ?? 'none'}`">
|
||||
<div class="inside-column" :style="`width: ${item.percent}%;background: ${color};borderRadius: ${borderRadius};`">
|
||||
<div class="shine" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type === 'row'" class="ranking-value" :style="`color: ${textColor};font-size: ${rightFontSize}px`">
|
||||
{{ status.mergedConfig.valueFormatter ? status.mergedConfig.valueFormatter(item) : item.value }}
|
||||
{{ unit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-rank-list">
|
||||
import { onUnmounted, reactive, toRefs, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
chartConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const { w, h } = toRefs(props.chartConfig.attr);
|
||||
const { type, rowNum, unit, height, color, textColor, borderColor, borderRadius, indexPrefix, indexFontSize, leftFontSize, rightFontSize } = toRefs(
|
||||
props.chartConfig.option
|
||||
);
|
||||
|
||||
const status = reactive({
|
||||
mergedConfig: props.chartConfig.option,
|
||||
rowsData: [],
|
||||
rows: [
|
||||
{
|
||||
scroll: 0,
|
||||
ranking: 1,
|
||||
name: '',
|
||||
value: '',
|
||||
percent: 0,
|
||||
},
|
||||
],
|
||||
heights: [0],
|
||||
animationIndex: 0,
|
||||
animationHandler: 0,
|
||||
updater: 0,
|
||||
avgHeight: 0,
|
||||
});
|
||||
|
||||
const calcRowsData = () => {
|
||||
let { dataset, rowNum, sort } = status.mergedConfig;
|
||||
// @ts-ignore
|
||||
sort &&
|
||||
dataset.sort(({ value: a }, { value: b }) => {
|
||||
if (a > b) return -1;
|
||||
if (a < b) return 1;
|
||||
if (a === b) return 0;
|
||||
});
|
||||
// @ts-ignore
|
||||
const value = dataset.map(({ value }) => value);
|
||||
const min = Math.min(...value) || 0;
|
||||
// abs of min
|
||||
const minAbs = Math.abs(min);
|
||||
const max = Math.max(...value) || 0;
|
||||
// abs of max
|
||||
const maxAbs = Math.abs(max);
|
||||
const total = max + minAbs;
|
||||
dataset = dataset.map((row, i) => ({
|
||||
...row,
|
||||
ranking: i + 1,
|
||||
percent: ((row.value + minAbs) / total) * 100,
|
||||
}));
|
||||
const rowLength = dataset.length;
|
||||
if (rowLength > rowNum && rowLength < 2 * rowNum) {
|
||||
dataset = [...dataset, ...dataset];
|
||||
}
|
||||
dataset = dataset.map((d, i) => ({ ...d, scroll: i }));
|
||||
status.rowsData = dataset;
|
||||
status.rows = dataset;
|
||||
};
|
||||
|
||||
const calcHeights = (onresize = false) => {
|
||||
const { rowNum, dataset } = status.mergedConfig;
|
||||
const avgHeight = h.value / rowNum;
|
||||
status.avgHeight = avgHeight;
|
||||
|
||||
if (!onresize) status.heights = new Array(dataset.length).fill(avgHeight);
|
||||
};
|
||||
|
||||
const animation = async (start = false) => {
|
||||
let { avgHeight, animationIndex, mergedConfig, rowsData, updater } = status;
|
||||
const { isAnimation, waitTime, carousel, rowNum } = mergedConfig;
|
||||
const rowLength = rowsData.length;
|
||||
if (rowNum >= rowLength) return;
|
||||
if (start) {
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
if (updater !== status.updater) return;
|
||||
}
|
||||
const animationNum = carousel === 'single' ? 1 : rowNum;
|
||||
let rows = rowsData.slice(animationIndex);
|
||||
rows.push(...rowsData.slice(0, animationIndex));
|
||||
status.rows = rows.slice(0, rowNum + 1);
|
||||
status.heights = new Array(rowLength).fill(avgHeight);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
if (!isAnimation) return;
|
||||
if (updater !== status.updater) return;
|
||||
status.heights.splice(0, animationNum, ...new Array(animationNum).fill(0));
|
||||
animationIndex += animationNum;
|
||||
const back = animationIndex - rowLength;
|
||||
if (back >= 0) animationIndex = back;
|
||||
|
||||
status.animationIndex = animationIndex;
|
||||
status.animationHandler = setTimeout(animation, waitTime * 1000 - 300);
|
||||
};
|
||||
|
||||
const stopAnimation = () => {
|
||||
status.updater = (status.updater + 1) % 999999;
|
||||
if (!status.animationHandler) return;
|
||||
clearTimeout(status.animationHandler);
|
||||
};
|
||||
|
||||
const onRestart = async () => {
|
||||
try {
|
||||
if (!status.mergedConfig) return;
|
||||
let { dataset, rowNum, sort } = status.mergedConfig;
|
||||
stopAnimation();
|
||||
calcRowsData();
|
||||
let flag = true;
|
||||
if (dataset.length <= rowNum) {
|
||||
flag = false;
|
||||
}
|
||||
calcHeights(flag);
|
||||
animation(flag);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onRestart();
|
||||
|
||||
watch(
|
||||
() => w.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => h.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => rowNum.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
// 数据更新(配置时触发)
|
||||
watch(
|
||||
() => props.chartConfig.option.dataset,
|
||||
() => {
|
||||
onRestart();
|
||||
},
|
||||
{
|
||||
deep: false,
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-rank-list {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: v-bind('h+"px"');
|
||||
.row-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
flex-direction: v-bind('type');
|
||||
}
|
||||
.ranking-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
flex: none;
|
||||
.rank {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.info-name {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.ranking-column {
|
||||
flex: 1;
|
||||
.inside-column {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2px;
|
||||
height: v-bind('height+"px"');
|
||||
border-radius: 1px;
|
||||
}
|
||||
.shine {
|
||||
position: absolute;
|
||||
top: v-bind('(height/2)+"px"');
|
||||
left: 0%;
|
||||
width: 50px;
|
||||
height: 2px;
|
||||
background: radial-gradient(rgb(40 248 255) 5%, transparent 80%);
|
||||
transform: translateX(-100%);
|
||||
// animation: shine 3s ease-in-out infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
&.rank-column {
|
||||
.ranking-info {
|
||||
width: 100%;
|
||||
}
|
||||
.ranking-column {
|
||||
margin-top: 5px;
|
||||
border-bottom: 2px solid v-bind('borderColor');
|
||||
}
|
||||
}
|
||||
|
||||
&.rank-row {
|
||||
.row-item {
|
||||
align-items: center;
|
||||
}
|
||||
.ranking-info {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.ranking-value {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
80% {
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,173 @@
|
||||
<!--
|
||||
* @Description:
|
||||
* @Author: zenghua.wang
|
||||
* @Date: 2024-03-24 11:04:52
|
||||
* @LastEditors: zenghua.wang “1048523306@qq.com”
|
||||
* @LastEditTime: 2025-01-20 09:34:23
|
||||
-->
|
||||
<template>
|
||||
<section class="rich-editor">
|
||||
<Toolbar v-show="toolbarShow" class="rich-editor-toolbar" :editor="refEditor" :default-config="options.toolbarConfig" :mode="mode" />
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
class="rich-editor-toolbar"
|
||||
:style="styleEditor"
|
||||
:default-config="options.editorConfig"
|
||||
:mode="mode"
|
||||
@on-created="handleCreated"
|
||||
@on-change="handleChange"
|
||||
@on-destroyed="handleDestroyed"
|
||||
@on-focus="handleFocus"
|
||||
@on-blur="handleBlur"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
|
||||
import { shallowRef, ref, computed, nextTick, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
// import { CommonUpload, UploadImageFromEditor } from '@/apis/common';
|
||||
import { imageUpload } from './upFile';
|
||||
|
||||
const { VITE_APP_OSS_URL } = import.meta.env;
|
||||
|
||||
export default {
|
||||
name: 'CustomRichEditor',
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default', //'default' 或 'simple'
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
toolbarConfig: {},
|
||||
editorConfig: {
|
||||
placeholder: '请输入内容...',
|
||||
readOnly: false,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: '',
|
||||
base64LimitSize: 10 * 1024, //10kb
|
||||
maxFileSize: 10 * 1024 * 1024, //10M
|
||||
maxNumberOfFiles: 10,
|
||||
allowedFileTypes: ['image/*'],
|
||||
customUpload: imageUpload,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
toolbarShow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['focus', 'blur', 'change', 'update:value'],
|
||||
setup(props, cxt) {
|
||||
const refEditor = shallowRef();
|
||||
const valueHtml = ref('');
|
||||
|
||||
const styleEditor = computed(() => {
|
||||
return {
|
||||
height: props.options.contentHeight || '300px',
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleCreated = (editor) => {
|
||||
refEditor.value = editor;
|
||||
props.readOnly ? editor.disable() : editor.enable();
|
||||
};
|
||||
/**
|
||||
* 组件内容变化
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleChange = (editor) => {
|
||||
cxt.emit('change', editor);
|
||||
cxt.emit('update:value', valueHtml.value);
|
||||
};
|
||||
/**
|
||||
* 组件销毁
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleDestroyed = (editor) => {
|
||||
valueHtml.value = '';
|
||||
};
|
||||
/**
|
||||
* 光标处于编辑区
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleFocus = (editor) => {
|
||||
cxt.emit('focus', editor);
|
||||
};
|
||||
/**
|
||||
* 光标离开编辑区
|
||||
* @param {*} editor
|
||||
*/
|
||||
const handleBlur = (editor) => {
|
||||
cxt.emit('blur', editor);
|
||||
};
|
||||
/**
|
||||
* 挂载
|
||||
*/
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props?.value) {
|
||||
valueHtml.value = props.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 组件销毁时,也及时销毁编辑器
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
if (!refEditor?.value) return;
|
||||
refEditor.value.destroy();
|
||||
});
|
||||
|
||||
return {
|
||||
refEditor,
|
||||
valueHtml,
|
||||
styleEditor,
|
||||
handleCreated,
|
||||
handleChange,
|
||||
handleDestroyed,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.rich-editor {
|
||||
z-index: 9999;
|
||||
border: 1px solid $color-border;
|
||||
&-toolbar {
|
||||
border-bottom: 1px solid $color-border;
|
||||
:deep(.w-e-bar-divider) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
&-editor {
|
||||
overflow-y: hidden;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,10 @@
|
||||
import { CommonUpload } from '../../apis/common';
|
||||
|
||||
export async function imageUpload(file, insertFn) {
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
let res = await CommonUpload(formData);
|
||||
if (res.code == 200) {
|
||||
insertFn(res.data.url, file.name, res.data.key);
|
||||
}
|
||||
}
|
@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="dv-scroll-board">
|
||||
<div v-if="status.header.length && status.mergedConfig" class="header" :style="`background: ${status.mergedConfig.headerBGC};`">
|
||||
<div
|
||||
v-for="(headerItem, i) in status.header"
|
||||
:key="`${headerItem}${i}`"
|
||||
class="header-item"
|
||||
:style="`
|
||||
height: ${status.mergedConfig.headerHeight}px;
|
||||
line-height: ${status.mergedConfig.headerHeight}px;
|
||||
width: ${status.widths[i]}px;
|
||||
`"
|
||||
:align="status.aligns[i]"
|
||||
v-html="headerItem"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-if="status.mergedConfig" class="rows" :style="`height: ${h - (status.header.length ? status.mergedConfig.headerHeight : 0)}px;`">
|
||||
<div
|
||||
v-for="(row, ri) in status.rows"
|
||||
:key="`${row.toString()}${row.scroll}`"
|
||||
class="row-item"
|
||||
:style="`
|
||||
height: ${status.heights[ri]}px;
|
||||
line-height: ${status.heights[ri]}px;
|
||||
background: ${status.mergedConfig[row.rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC']};
|
||||
`"
|
||||
>
|
||||
<div
|
||||
v-for="(ceil, ci) in row.ceils"
|
||||
:key="`${ceil}${ri}${ci}`"
|
||||
class="ceil"
|
||||
:style="`width: ${status.widths[ci]}px;`"
|
||||
:align="status.aligns[ci]"
|
||||
v-html="ceil"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="custom-scroll-board">
|
||||
import { onUnmounted, reactive, toRefs, watch, onMounted } from 'vue';
|
||||
import merge from 'lodash/merge';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
const props = defineProps({
|
||||
chartConfig: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { w, h } = toRefs(props.chartConfig.attr);
|
||||
|
||||
const status = reactive({
|
||||
defaultConfig: {
|
||||
/**
|
||||
* @description Board header
|
||||
* @type {Array<String>}
|
||||
* @default header = []
|
||||
* @example header = ['column1', 'column2', 'column3']
|
||||
*/
|
||||
header: [],
|
||||
/**
|
||||
* @description Board dataset
|
||||
* @type {Array<Array>}
|
||||
* @default dataset = []
|
||||
*/
|
||||
dataset: [],
|
||||
/**
|
||||
* @description Row num
|
||||
* @type {Number}
|
||||
* @default rowNum = 5
|
||||
*/
|
||||
rowNum: 5,
|
||||
/**
|
||||
* @description Header background color
|
||||
* @type {String}
|
||||
* @default headerBGC = '#00BAFF'
|
||||
*/
|
||||
headerBGC: '#00BAFF',
|
||||
/**
|
||||
* @description Odd row background color
|
||||
* @type {String}
|
||||
* @default oddRowBGC = '#003B51'
|
||||
*/
|
||||
oddRowBGC: '#003B51',
|
||||
/**
|
||||
* @description Even row background color
|
||||
* @type {String}
|
||||
* @default evenRowBGC = '#003B51'
|
||||
*/
|
||||
evenRowBGC: '#0A2732',
|
||||
/**
|
||||
* @description Scroll wait time
|
||||
* @type {Number}
|
||||
* @default waitTime = 2
|
||||
*/
|
||||
waitTime: 2,
|
||||
/**
|
||||
* @description Header height
|
||||
* @type {Number}
|
||||
* @default headerHeight = 35
|
||||
*/
|
||||
headerHeight: 35,
|
||||
/**
|
||||
* @description Column width
|
||||
* @type {Array<Number>}
|
||||
* @default columnWidth = []
|
||||
*/
|
||||
columnWidth: [],
|
||||
/**
|
||||
* @description Column align
|
||||
* @type {Array<String>}
|
||||
* @default align = []
|
||||
* @example align = ['left', 'center', 'right']
|
||||
*/
|
||||
align: [],
|
||||
/**
|
||||
* @description Show index
|
||||
* @type {Boolean}
|
||||
* @default index = false
|
||||
*/
|
||||
index: false,
|
||||
/**
|
||||
* @description index Header
|
||||
* @type {String}
|
||||
* @default indexHeader = '#'
|
||||
*/
|
||||
indexHeader: '#',
|
||||
/**
|
||||
* @description Carousel type
|
||||
* @type {String}
|
||||
* @default carousel = 'single'
|
||||
* @example carousel = 'single' | 'page'
|
||||
*/
|
||||
carousel: 'single',
|
||||
/**
|
||||
* @description Pause scroll when mouse hovered
|
||||
* @type {Boolean}
|
||||
* @default hoverPause = true
|
||||
* @example hoverPause = true | false
|
||||
*/
|
||||
hoverPause: true,
|
||||
},
|
||||
mergedConfig: props.chartConfig.option,
|
||||
header: [],
|
||||
rowsData: [],
|
||||
rows: [
|
||||
{
|
||||
ceils: [],
|
||||
rowIndex: 0,
|
||||
scroll: 0,
|
||||
},
|
||||
],
|
||||
widths: [],
|
||||
heights: [0],
|
||||
avgHeight: 0,
|
||||
aligns: [],
|
||||
animationIndex: 0,
|
||||
animationHandler: 0,
|
||||
updater: 0,
|
||||
needCalc: false,
|
||||
});
|
||||
|
||||
const calcData = () => {
|
||||
mergeConfig();
|
||||
calcHeaderData();
|
||||
calcRowsData();
|
||||
calcWidths();
|
||||
calcHeights();
|
||||
calcAligns();
|
||||
animation(true);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcData();
|
||||
});
|
||||
|
||||
const mergeConfig = () => {
|
||||
status.mergedConfig = merge(cloneDeep(status.defaultConfig), props.chartConfig.option);
|
||||
};
|
||||
|
||||
const calcHeaderData = () => {
|
||||
let { header, index, indexHeader } = status.mergedConfig;
|
||||
if (!header.length) {
|
||||
status.header = [];
|
||||
return;
|
||||
}
|
||||
header = [...header];
|
||||
if (index) header.unshift(indexHeader);
|
||||
status.header = header;
|
||||
};
|
||||
|
||||
const calcRowsData = () => {
|
||||
let { dataset, index, headerBGC, rowNum } = status.mergedConfig;
|
||||
if (index) {
|
||||
dataset = dataset.map((row, i) => {
|
||||
row = [...row];
|
||||
const indexTag = `<span class="index" style="background-color: ${headerBGC};border-radius: 3px;padding: 0px 3px;">${i + 1}</span>`;
|
||||
row.unshift(indexTag);
|
||||
return row;
|
||||
});
|
||||
}
|
||||
dataset = dataset.map((ceils, i) => ({ ceils, rowIndex: i }));
|
||||
const rowLength = dataset.length;
|
||||
if (rowLength > rowNum && rowLength < 2 * rowNum) {
|
||||
dataset = [...dataset, ...dataset];
|
||||
}
|
||||
dataset = dataset.map((d, i) => ({ ...d, scroll: i }));
|
||||
|
||||
status.rowsData = dataset;
|
||||
status.rows = dataset;
|
||||
};
|
||||
|
||||
const calcWidths = () => {
|
||||
const { mergedConfig, rowsData } = status;
|
||||
const { columnWidth, header } = mergedConfig;
|
||||
const usedWidth = columnWidth.reduce((all, ws) => all + ws, 0);
|
||||
let columnNum = 0;
|
||||
if (rowsData[0]) {
|
||||
columnNum = rowsData[0].ceils.length;
|
||||
} else if (header.length) {
|
||||
columnNum = header.length;
|
||||
}
|
||||
const avgWidth = (w.value - usedWidth) / (columnNum - columnWidth.length);
|
||||
const widths = new Array(columnNum).fill(avgWidth);
|
||||
status.widths = merge(widths, columnWidth);
|
||||
};
|
||||
|
||||
const calcHeights = (onresize = false) => {
|
||||
const { mergedConfig, header } = status;
|
||||
const { headerHeight, rowNum, dataset } = mergedConfig;
|
||||
let allHeight = h.value;
|
||||
if (header.length) allHeight -= headerHeight;
|
||||
const avgHeight = allHeight / rowNum;
|
||||
status.avgHeight = avgHeight;
|
||||
if (!onresize) status.heights = new Array(dataset.length).fill(avgHeight);
|
||||
};
|
||||
|
||||
const calcAligns = () => {
|
||||
const { header, mergedConfig } = status;
|
||||
|
||||
const columnNum = header.length;
|
||||
|
||||
let aligns = new Array(columnNum).fill('left');
|
||||
|
||||
const { align } = mergedConfig;
|
||||
|
||||
status.aligns = merge(aligns, align);
|
||||
};
|
||||
|
||||
const animation = async (start = false) => {
|
||||
const { needCalc } = status;
|
||||
|
||||
if (needCalc) {
|
||||
calcRowsData();
|
||||
calcHeights();
|
||||
status.needCalc = false;
|
||||
}
|
||||
let { avgHeight, animationIndex, mergedConfig, rowsData, updater } = status;
|
||||
const { waitTime, carousel, rowNum } = mergedConfig;
|
||||
|
||||
const rowLength = rowsData.length;
|
||||
if (rowNum >= rowLength) return;
|
||||
if (start) {
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
|
||||
if (updater !== status.updater) return;
|
||||
}
|
||||
const animationNum = carousel === 'single' ? 1 : rowNum;
|
||||
let rows = rowsData.slice(animationIndex);
|
||||
rows.push(...rowsData.slice(0, animationIndex));
|
||||
status.rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1);
|
||||
status.heights = new Array(rowLength).fill(avgHeight);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
if (updater !== status.updater) return;
|
||||
status.heights.splice(0, animationNum, ...new Array(animationNum).fill(0));
|
||||
animationIndex += animationNum;
|
||||
const back = animationIndex - rowLength;
|
||||
if (back >= 0) animationIndex = back;
|
||||
status.animationIndex = animationIndex;
|
||||
status.animationHandler = setTimeout(animation, waitTime * 1000 - 300);
|
||||
};
|
||||
|
||||
const stopAnimation = () => {
|
||||
status.updater = (status.updater + 1) % 999999;
|
||||
if (!status.animationHandler) return;
|
||||
clearTimeout(status.animationHandler);
|
||||
};
|
||||
|
||||
const onRestart = async () => {
|
||||
try {
|
||||
if (!status.mergedConfig) return;
|
||||
stopAnimation();
|
||||
calcData();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => w.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => h.value,
|
||||
() => {
|
||||
onRestart();
|
||||
}
|
||||
);
|
||||
|
||||
// 数据更新
|
||||
watch(
|
||||
() => props.chartConfig.option,
|
||||
() => {
|
||||
onRestart();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAnimation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dv-scroll-board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #ffffff;
|
||||
.text {
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 15px;
|
||||
.header-item {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
.rows {
|
||||
overflow: hidden;
|
||||
.row-item {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<section
|
||||
class="header_title"
|
||||
:style="{
|
||||
'--titleContentW': titleContentW + 'px',
|
||||
'--itemW': itemW + 'px',
|
||||
'--gap': gap + 'px',
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="props.titles.length > 6" icon="el-icon-arrow-left" class="left_btn" @click="handleTitleBtn(1)">
|
||||
<ArrowLeftBold />
|
||||
</el-icon>
|
||||
<el-icon v-if="props.titles.length > 6" icon="el-icon-arrow-right" class="right_btn" @click="handleTitleBtn(-1)"><ArrowRightBold /></el-icon>
|
||||
<section class="left_titles_container">
|
||||
<section v-if="route.name != 'home'" class="title_content" :style="{ left: `-${position}px` }">
|
||||
<section
|
||||
v-for="(item, i) in leftTitles"
|
||||
:key="`left_title_${i}`"
|
||||
:class="['title_item', activeTitle == item.value ? 'active' : '']"
|
||||
@click="handleTitleClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section class="sys_name">{{ route.name != 'home' ? '政务云数字农业智慧大屏' : '数字农业智慧大脑' }}</section>
|
||||
<section class="right_titles_container">
|
||||
<section v-if="route.name != 'home'" class="title_content" :style="{ left: `${right ? right + 'px' : '-' + position + 'px'}` }">
|
||||
<section
|
||||
v-for="(item, i) in rightTitles"
|
||||
:key="`right_title_${i}`"
|
||||
:class="['title_item', activeTitle == item.value ? 'active' : '']"
|
||||
@click="handleTitleClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
onMounted(() => {
|
||||
handleWidth();
|
||||
activeTitle.value = '/v2/' + router.currentRoute.value.name;
|
||||
});
|
||||
// const emit = defineEmits(['changeTitle']);
|
||||
const props = defineProps({
|
||||
titles: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [
|
||||
{ label: '首页', value: '/new-digital-agriculture-screen/v2/home' },
|
||||
{ label: '土地资源', value: '/new-digital-agriculture-screen/v2/land' },
|
||||
{ label: '投入品监管', value: '/new-digital-agriculture-screen/v2/inputs' },
|
||||
{ label: '产出品管理', value: '/new-digital-agriculture-screen/v2/entities' },
|
||||
// { label: '智慧种植监测', value: 'plant' },
|
||||
// { label: '智慧养殖监测', value: 'breed' },
|
||||
{ label: '生产经营主体', value: '/new-digital-agriculture-screen/v2/business' },
|
||||
{ label: '农产品溯源', value: '/new-digital-agriculture-screen/v2/trace' },
|
||||
// { label: '产业预警决策', value: 'early' },
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
const titleContentW = ref('0');
|
||||
const itemW = ref('1');
|
||||
const gap = ref(1);
|
||||
const leftNum = ref(0);
|
||||
const position = ref(0);
|
||||
const right = ref(null);
|
||||
const activeTitle = ref('1');
|
||||
const leftTitles = ref([]);
|
||||
const rightTitles = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.titles,
|
||||
(val) => {
|
||||
if (val && val.length) {
|
||||
activeTitle.value = val[0].value;
|
||||
let l = val.length;
|
||||
if (l > 6) {
|
||||
leftTitles.value = val.slice(0, l - 3);
|
||||
rightTitles.value = val.slice(3);
|
||||
} else {
|
||||
leftTitles.value = val.slice(0, 3);
|
||||
if (l > 3) {
|
||||
rightTitles.value = val.slice(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
function handleWidth() {
|
||||
let ld = document.querySelector('.left_titles_container');
|
||||
if (ld) {
|
||||
let w = ld.clientWidth;
|
||||
itemW.value = (w - 2 * gap.value) / 3;
|
||||
titleContentW.value = itemW.value * leftTitles.value.length + leftTitles.value.length * gap.value;
|
||||
if (props.titles.length > 3 && props.titles.length < 6) {
|
||||
let l = 3 - (props.titles.length - 3);
|
||||
right.value = l * itemW.value + (l - 1) * gap.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleTitleBtn(t = -1) {
|
||||
if (props.titles.length > 6) {
|
||||
if (leftNum.value > -1) leftNum.value = leftNum.value + t;
|
||||
if (leftNum.value < 0) leftNum.value = 0;
|
||||
if (leftNum.value > leftTitles.value.length - 3) leftNum.value = leftTitles.value.length - 3;
|
||||
position.value = leftNum.value * itemW.value + leftNum.value * gap.value;
|
||||
}
|
||||
}
|
||||
function handleTitleClick(val) {
|
||||
activeTitle.value = val;
|
||||
// emit('changeTitle', val);
|
||||
router.push({ path: val });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header_title {
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
background: url('../../assets/images/basic/headerBG.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.sys_name {
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
font-family: 'JinBuTi';
|
||||
}
|
||||
.left_btn,
|
||||
.right_btn {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
padding: 6px 6px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: rgb(6, 155, 118, 0.5);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.left_btn {
|
||||
left: 14px;
|
||||
}
|
||||
.right_btn {
|
||||
right: 14px;
|
||||
}
|
||||
.left_titles_container,
|
||||
.right_titles_container {
|
||||
position: relative;
|
||||
width: 31%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
line-height: 90px;
|
||||
.title_content {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: var(--titleContentW);
|
||||
height: 56px;
|
||||
transition: all 0.4s ease;
|
||||
font-size: 24px;
|
||||
// font-weight: bold;
|
||||
.active {
|
||||
color: #fff;
|
||||
opacity: 1 !important;
|
||||
text-shadow: 0px 4px 10px #fff;
|
||||
}
|
||||
.title_item {
|
||||
margin-right: var(--gap);
|
||||
display: inline-block;
|
||||
min-width: var(--itemW);
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
text-align: center;
|
||||
color: #f5fffe;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left_titles_container {
|
||||
.title_item {
|
||||
background: url('../../assets/images/basic/leftTitleBG.png') no-repeat;
|
||||
}
|
||||
}
|
||||
.right_titles_container {
|
||||
.title_item {
|
||||
background: url('../../assets/images/basic/rightTitleBG.png') no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<section
|
||||
class="header_title"
|
||||
:style="{
|
||||
'--titleContentW': titleContentW + 'px',
|
||||
'--itemW': itemW + 'px',
|
||||
'--gap': gap + 'px',
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="props.titles.length > 6" icon="el-icon-arrow-left" class="left_btn" @click="handleTitleBtn(1)">
|
||||
<ArrowLeftBold />
|
||||
</el-icon>
|
||||
<el-icon v-if="props.titles.length > 6" icon="el-icon-arrow-right" class="right_btn" @click="handleTitleBtn(-1)"><ArrowRightBold /></el-icon>
|
||||
<!-- <section class="left_titles_container">
|
||||
<section v-if="route.name != 'home'" class="title_content" :style="{ left: `-${position}px` }">
|
||||
<section
|
||||
v-for="(item, i) in leftTitles"
|
||||
:key="`left_title_${i}`"
|
||||
:class="['title_item', activeTitle == item.value ? 'active' : '']"
|
||||
@click="handleTitleClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</section>
|
||||
</section>
|
||||
</section> -->
|
||||
<section class="sys_name">{{ route.name != 'home' ? '产业运营平台综合看板' : '数字农业智慧大脑' }}</section>
|
||||
<!-- <section class="right_titles_container">
|
||||
<section v-if="route.name != 'home'" class="title_content" :style="{ left: `${right ? right + 'px' : '-' + position + 'px'}` }">
|
||||
<section
|
||||
v-for="(item, i) in rightTitles"
|
||||
:key="`right_title_${i}`"
|
||||
:class="['title_item', activeTitle == item.value ? 'active' : '']"
|
||||
@click="handleTitleClick(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</section>
|
||||
</section>
|
||||
</section> -->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
onMounted(() => {
|
||||
handleWidth();
|
||||
activeTitle.value = '/v2/' + router.currentRoute.value.name;
|
||||
});
|
||||
// const emit = defineEmits(['changeTitle']);
|
||||
const props = defineProps({
|
||||
titles: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [
|
||||
{ label: '首页', value: '/new-digital-agriculture-screen/v2/home' },
|
||||
{ label: '土地资源', value: '/new-digital-agriculture-screen/v2/land' },
|
||||
{ label: '投入品监管', value: '/new-digital-agriculture-screen/v2/inputs' },
|
||||
{ label: '产出品管理', value: '/new-digital-agriculture-screen/v2/entities' },
|
||||
// { label: '智慧种植监测', value: 'plant' },
|
||||
// { label: '智慧养殖监测', value: 'breed' },
|
||||
{ label: '生产经营主体', value: '/new-digital-agriculture-screen/v2/business' },
|
||||
{ label: '农产品溯源', value: '/new-digital-agriculture-screen/v2/trace' },
|
||||
// { label: '产业预警决策', value: 'early' },
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
const titleContentW = ref('0');
|
||||
const itemW = ref('1');
|
||||
const gap = ref(1);
|
||||
const leftNum = ref(0);
|
||||
const position = ref(0);
|
||||
const right = ref(null);
|
||||
const activeTitle = ref('1');
|
||||
const leftTitles = ref([]);
|
||||
const rightTitles = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.titles,
|
||||
(val) => {
|
||||
if (val && val.length) {
|
||||
activeTitle.value = val[0].value;
|
||||
let l = val.length;
|
||||
if (l > 6) {
|
||||
leftTitles.value = val.slice(0, l - 3);
|
||||
rightTitles.value = val.slice(3);
|
||||
} else {
|
||||
leftTitles.value = val.slice(0, 3);
|
||||
if (l > 3) {
|
||||
rightTitles.value = val.slice(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
function handleWidth() {
|
||||
let ld = document.querySelector('.left_titles_container');
|
||||
if (ld) {
|
||||
let w = ld.clientWidth;
|
||||
itemW.value = (w - 2 * gap.value) / 3;
|
||||
titleContentW.value = itemW.value * leftTitles.value.length + leftTitles.value.length * gap.value;
|
||||
if (props.titles.length > 3 && props.titles.length < 6) {
|
||||
let l = 3 - (props.titles.length - 3);
|
||||
right.value = l * itemW.value + (l - 1) * gap.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleTitleBtn(t = -1) {
|
||||
if (props.titles.length > 6) {
|
||||
if (leftNum.value > -1) leftNum.value = leftNum.value + t;
|
||||
if (leftNum.value < 0) leftNum.value = 0;
|
||||
if (leftNum.value > leftTitles.value.length - 3) leftNum.value = leftTitles.value.length - 3;
|
||||
position.value = leftNum.value * itemW.value + leftNum.value * gap.value;
|
||||
}
|
||||
}
|
||||
function handleTitleClick(val) {
|
||||
router.push({ path: '/sub-operation-service/breed' });
|
||||
// router.push({ path: '/sub-operation-service/403' });
|
||||
console.log(activeTitle.value);
|
||||
// activeTitle.value = val;
|
||||
// // emit('changeTitle', val);
|
||||
// router.push({ path: val });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header_title {
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
background: url('../../assets/images/basic/headerBG.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.sys_name {
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
font-family: 'JinBuTi';
|
||||
}
|
||||
.left_btn,
|
||||
.right_btn {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
padding: 6px 6px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: rgb(6, 155, 118, 0.5);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.left_btn {
|
||||
left: 14px;
|
||||
}
|
||||
.right_btn {
|
||||
right: 14px;
|
||||
}
|
||||
.left_titles_container,
|
||||
.right_titles_container {
|
||||
position: relative;
|
||||
width: 31%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
line-height: 90px;
|
||||
.title_content {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
width: var(--titleContentW);
|
||||
height: 56px;
|
||||
transition: all 0.4s ease;
|
||||
font-size: 24px;
|
||||
// font-weight: bold;
|
||||
.active {
|
||||
color: #fff;
|
||||
opacity: 1 !important;
|
||||
text-shadow: 0px 4px 10px #fff;
|
||||
}
|
||||
.title_item {
|
||||
margin-right: var(--gap);
|
||||
display: inline-block;
|
||||
min-width: var(--itemW);
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
text-align: center;
|
||||
color: #f5fffe;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left_titles_container {
|
||||
.title_item {
|
||||
background: url('../../assets/images/basic/leftTitleBG.png') no-repeat;
|
||||
}
|
||||
}
|
||||
.right_titles_container {
|
||||
.title_item {
|
||||
background: url('../../assets/images/basic/rightTitleBG.png') no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<el-dropdown popper-class="custom-table-operate">
|
||||
<el-icon class="custom-table-operate__more">
|
||||
<template v-if="show">
|
||||
<More />
|
||||
</template>
|
||||
</el-icon>
|
||||
<template v-if="show" #dropdown>
|
||||
<el-dropdown-menu v-if="!isEmpty(actions)">
|
||||
<template v-for="item in actions" :key="item.name">
|
||||
<el-dropdown-item v-if="onPermission(item)" @click="item.event(data)">
|
||||
<el-button :type="item.type ?? 'primary'" :icon="formatterIcon(item)" :size="item.size" :disabled="item.disabled" text>
|
||||
{{ formatterName(item) }}
|
||||
</el-button>
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<script setup name="custom-table-operate">
|
||||
import { isEmpty } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
actions: { type: Array, default: () => [] },
|
||||
data: { type: Object, default: () => {} },
|
||||
show: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const onPermission = (row) => {
|
||||
if (row.auth === undefined) return true;
|
||||
return typeof row.auth === 'function' ? row.auth(props.data) : row.auth;
|
||||
};
|
||||
|
||||
const formatterName = (row) => {
|
||||
return typeof row.name === 'function' ? row.name(props.data) : row.name;
|
||||
};
|
||||
|
||||
const formatterIcon = (row) => {
|
||||
return typeof row.icon === 'function' ? row.icon(props.data) : row.icon;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-table-operate {
|
||||
&__more {
|
||||
padding: 20px 5px;
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-button {
|
||||
&,
|
||||
&:hover &.is-text:hover,
|
||||
&.is-text:not(.is-disabled):hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
115
sub-operation-service/src/components/custom-table-tree/index.vue
Normal file
115
sub-operation-service/src/components/custom-table-tree/index.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div :class="`custom-table-tree ${shadow ? 'custom-table-tree__shadow' : ''}`">
|
||||
<div v-if="title" class="title">{{ title }}</div>
|
||||
<div class="panel">
|
||||
<el-input v-if="filter" v-model="state.keyword" clearable placeholder="请输入关键字筛选" class="panel-filter" />
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="state.list"
|
||||
:node-key="option.nodeKey"
|
||||
:show-checkbox="option.showCheckbox"
|
||||
:default-expanded-keys="option.defaultExpandedKeys"
|
||||
:default-checked-keys="option.defaultCheckedKeys"
|
||||
:default-expand-all="option.defaultExpandAll"
|
||||
:props="option.props ?? option.defaultProps"
|
||||
:filter-node-method="filterNodeMethod"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data: rows }">
|
||||
<slot :data="rows"></slot>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="custom-table-tree">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
shadow: { type: Boolean, default: false },
|
||||
filter: { type: Boolean, default: false },
|
||||
data: { type: Array, default: () => [] },
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
nodeKey: 'id',
|
||||
showCheckbox: false,
|
||||
props: {},
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
},
|
||||
defaultExpandedKeys: [],
|
||||
defaultCheckedKeys: [],
|
||||
defaultExpandAll: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['node-click']);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const state = reactive({
|
||||
keyword: '',
|
||||
list: [],
|
||||
});
|
||||
const label = props.option.props?.label ?? 'label';
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
state.list = val;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => state.keyword,
|
||||
(val) => {
|
||||
treeRef.value.filter(val);
|
||||
}
|
||||
);
|
||||
|
||||
const filterNodeMethod = (value, data) => {
|
||||
if (!value) return true;
|
||||
return data[label].includes(value);
|
||||
};
|
||||
|
||||
const handleNodeClick = (data, node) => {
|
||||
emit('node-click', data, node);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-table-tree {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
@include flex-column;
|
||||
&__shadow {
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
|
||||
}
|
||||
.title {
|
||||
padding: 0 20px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
background: var(--el-color-primary);
|
||||
line-height: 36px;
|
||||
}
|
||||
.panel {
|
||||
padding: 16px 10px 10px;
|
||||
&-filter {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.el-tree {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
89
sub-operation-service/src/components/customBack.vue
Normal file
89
sub-operation-service/src/components/customBack.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="custom-back-warp">
|
||||
<subTop
|
||||
:title="topTitle"
|
||||
:postion="topPostion"
|
||||
:is-down="isDown"
|
||||
:down-title="downTitle"
|
||||
:label-field="labelField"
|
||||
:value-field="valueField"
|
||||
:down-width="downWidth"
|
||||
:options="options"
|
||||
@command="handeleCommand"
|
||||
></subTop>
|
||||
<div class="custom-back-content">
|
||||
<slot name="back"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import subTop from '../components/subTop.vue';
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useApp } from '@/hooks';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
topTitle: {
|
||||
type: String,
|
||||
default: '统计',
|
||||
},
|
||||
topPostion: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [
|
||||
{ label: '耿马镇', value: '42611' },
|
||||
{ label: '勐撒镇', value: '9259' },
|
||||
{ label: '勐永镇', value: '17787' },
|
||||
{ label: '孟定镇', value: '42610' },
|
||||
{ label: '勐简乡', value: '17788' },
|
||||
{ label: '贺派乡', value: '40161' },
|
||||
{ label: '四排山乡', value: '40163' },
|
||||
{ label: '大兴乡', value: '40159' },
|
||||
];
|
||||
},
|
||||
},
|
||||
isDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downTitle: {
|
||||
type: String,
|
||||
default: '全县',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'label',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
downWidth: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['command']);
|
||||
const handeleCommand = (data) => {
|
||||
emit('command', data);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-back-warp {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.custom-back-content {
|
||||
height: calc(100% - 38px);
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
67
sub-operation-service/src/components/customProgress.vue
Normal file
67
sub-operation-service/src/components/customProgress.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div ref="progress" class="custom-progress-val" :style="{ background: inactiveBg }">
|
||||
<div class="progress-warp" :style="{ height: height }">
|
||||
<div class="progress" :style="{ height: height, width: pWidth + 'px', background: activateBg }"></div>
|
||||
</div>
|
||||
{{ width }}
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, reactive, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '10px',
|
||||
},
|
||||
inactiveBg: {
|
||||
type: String,
|
||||
default: 'transparent',
|
||||
},
|
||||
activateBg: {
|
||||
type: String,
|
||||
default: 'linear-gradient(90deg, #45bfe9 0%, #01589c 100%)',
|
||||
},
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
let progress = ref(null);
|
||||
let maxwidth = computed(() => {
|
||||
return progress.value && progress.value.clientWidth;
|
||||
});
|
||||
let pWidth = computed(() => {
|
||||
let num = 0;
|
||||
num = Number(((maxwidth.value * props.percent) / 100).toFixed(0));
|
||||
return num;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-progress-val {
|
||||
width: calc(100%);
|
||||
border-radius: 6px;
|
||||
.progress-warp {
|
||||
width: 100%;
|
||||
.progress {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
animation: expandWidth 1s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes expandWidth {
|
||||
from {
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
width: maxwidth + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
186
sub-operation-service/src/components/subTop.vue
Normal file
186
sub-operation-service/src/components/subTop.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="title-top-warp">
|
||||
<div
|
||||
:style="{
|
||||
'background-image': 'url(' + getAssetsFile('images/vsualized/home/titlebg.png') + ')',
|
||||
transform: pos == 'right' ? 'rotateY(0deg)' : '',
|
||||
}"
|
||||
class="title-top-bg"
|
||||
></div>
|
||||
<span class="title-top-content" :style="{ 'text-align': props.left }">{{ topTitle || '--' }}</span>
|
||||
<div v-if="isDown" class="down-list" :style="{ width: downWidth }">
|
||||
<el-dropdown :hide-on-click="true" trigger="click" @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
{{ currentVal && currentVal[labelField] ? currentVal[labelField] : downTitle }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="down-menu">
|
||||
<el-dropdown-item v-for="(n, index) in optionsList" :key="n[valueField]" :command="n[valueField]">{{ n[labelField] }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useApp } from '@/hooks';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '统计分析',
|
||||
},
|
||||
postion: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [
|
||||
{ label: '耿马镇', value: '42611' },
|
||||
{ label: '勐撒镇', value: '9259' },
|
||||
{ label: '勐永镇', value: '17787' },
|
||||
{ label: '孟定镇', value: '42610' },
|
||||
{ label: '勐简乡', value: '17788' },
|
||||
{ label: '贺派乡', value: '40161' },
|
||||
{ label: '四排山乡', value: '40163' },
|
||||
{ label: '大兴乡', value: '40159' },
|
||||
];
|
||||
},
|
||||
},
|
||||
isDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
downTitle: {
|
||||
type: String,
|
||||
default: '全县',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'label',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
downWidth: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['command']);
|
||||
let topTitle = ref('');
|
||||
let pos = ref('');
|
||||
|
||||
let optionsList = reactive(props.options);
|
||||
let currentVal = reactive(null);
|
||||
|
||||
watch(
|
||||
() => (props.title, props.postion, props.options),
|
||||
() => {
|
||||
topTitle.value = props.title;
|
||||
pos.value = props.postion;
|
||||
optionsList = props.options;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const handleCommand = (data) => {
|
||||
let index = optionsList.findIndex((m) => {
|
||||
return m[props.valueField] == data;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
currentVal = optionsList[index];
|
||||
}
|
||||
// console.info('handleCommand', currentVal);
|
||||
emit('command', currentVal);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.down-menu {
|
||||
background: transparent;
|
||||
}
|
||||
.el-dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
color: $color-custom-main !important;
|
||||
i {
|
||||
color: $color-custom-main !important;
|
||||
}
|
||||
}
|
||||
.el-dropdown__popper {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
.el-popper__arrow::before {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
.el-dropdown-menu__item:hover,
|
||||
.el-dropdown-menu__item:active {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.title-top-warp {
|
||||
position: relative;
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
.title-top-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.title-top-content {
|
||||
line-height: 38px;
|
||||
font-size: 20px;
|
||||
// font-weight: bold;
|
||||
display: inline-block;
|
||||
transform: skewX(-13deg);
|
||||
background: linear-gradient(to bottom, '#ff7e5f', '#548fff');
|
||||
-webkit-background-clip: text;
|
||||
color: #fff;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: -2px 0 10px #add8f1;
|
||||
width: 100%;
|
||||
padding: 0 36px 0 72px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.down-list {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-dropdown {
|
||||
border: 1px solid $color-custom-main;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
84
sub-operation-service/src/hooks/useBreakpoint.js
Normal file
84
sub-operation-service/src/hooks/useBreakpoint.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { useEventListener } from './useEventListener';
|
||||
|
||||
let globalScreenRef = 0;
|
||||
let globalWidthRef = 0;
|
||||
let globalRealWidthRef = 0;
|
||||
|
||||
const screenMap = new Map();
|
||||
screenMap.set('XS', 480);
|
||||
screenMap.set('SM', 576);
|
||||
screenMap.set('MD', 768);
|
||||
screenMap.set('LG', 992);
|
||||
screenMap.set('XL', 1200);
|
||||
screenMap.set('XXL', 1600);
|
||||
|
||||
export function useBreakpoint() {
|
||||
return {
|
||||
screenRef: computed(() => unref(globalScreenRef)),
|
||||
widthRef: globalWidthRef,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
||||
|
||||
// Just call it once
|
||||
export function createBreakpointListen(fn) {
|
||||
const screenRef = ref('XL');
|
||||
const realWidthRef = ref(window.innerWidth);
|
||||
|
||||
function getWindowWidth() {
|
||||
const width = document.body.clientWidth;
|
||||
const xs = screenMap.get('XS');
|
||||
const sm = screenMap.get('SM');
|
||||
const md = screenMap.get('MD');
|
||||
const lg = screenMap.get('LG');
|
||||
const xl = screenMap.get('XL');
|
||||
const xxl = screenMap.set('XXL', 1600);
|
||||
if (width < xs) {
|
||||
screenRef.value = xs;
|
||||
} else if (width < sm) {
|
||||
screenRef.value = sm;
|
||||
} else if (width < md) {
|
||||
screenRef.value = md;
|
||||
} else if (width < lg) {
|
||||
screenRef.value = lg;
|
||||
} else if (width < xl) {
|
||||
screenRef.value = xl;
|
||||
} else {
|
||||
screenRef.value = xxl;
|
||||
}
|
||||
realWidthRef.value = width;
|
||||
}
|
||||
|
||||
useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
|
||||
listener: () => {
|
||||
getWindowWidth();
|
||||
resizeFn();
|
||||
},
|
||||
// wait: 100,
|
||||
});
|
||||
|
||||
getWindowWidth();
|
||||
globalScreenRef = computed(() => unref(screenRef));
|
||||
globalWidthRef = computed(() => screenMap.get(unref(screenRef)));
|
||||
globalRealWidthRef = computed(() => unref(realWidthRef));
|
||||
|
||||
function resizeFn() {
|
||||
fn?.({
|
||||
screen: globalScreenRef,
|
||||
width: globalWidthRef,
|
||||
realWidth: globalRealWidthRef,
|
||||
screenMap,
|
||||
});
|
||||
}
|
||||
|
||||
resizeFn();
|
||||
return {
|
||||
screenRef: globalScreenRef,
|
||||
widthRef: globalWidthRef,
|
||||
realWidthRef: globalRealWidthRef,
|
||||
};
|
||||
}
|
269
sub-operation-service/src/hooks/useEcharts.js
Normal file
269
sub-operation-service/src/hooks/useEcharts.js
Normal file
@ -0,0 +1,269 @@
|
||||
import { unref, nextTick, watch, computed, ref, markRaw } from 'vue';
|
||||
import { useDebounceFn, tryOnUnmounted } from '@vueuse/core';
|
||||
import { useTimeoutFn } from './useTimeout';
|
||||
import { useEventListener } from './useEventListener';
|
||||
import { useBreakpoint } from './useBreakpoint';
|
||||
import echarts from '../utils/echarts';
|
||||
|
||||
export const useEcharts = (elRef, theme = 'default') => {
|
||||
// 新增轮播相关状态
|
||||
const autoPlayTimer = ref(null); // 定时器句柄
|
||||
const currentIndex = ref(-1); // 当前高亮的数据索引
|
||||
const dataLength = ref(0); // 当前系列的数据总长度
|
||||
|
||||
let mapClickHandler = null;
|
||||
|
||||
// 新增方法 - 启动轮播
|
||||
const startAutoPlay = (options = {}) => {
|
||||
const {
|
||||
interval = 2000, // 轮播间隔(ms)
|
||||
seriesIndex = 0, // 默认操作第一个系列
|
||||
showTooltip = false, // 是否显示提示框,默认是false,外部配置了,无论是否true都会显示
|
||||
showMarkPoint = true, // 是否显示默认的动态标记点
|
||||
} = options;
|
||||
|
||||
stopAutoPlay(); // 先停止已有轮播
|
||||
|
||||
// 获取当前系列的数据长度
|
||||
const seriesData = unref(getOptions).series?.[seriesIndex]?.data;
|
||||
const xAxisData = unref(getOptions).xAxis?.data || [];
|
||||
// 获取当前 option 中的 yAxis 类型来判断是否倒置
|
||||
const axisType = unref(getOptions).yAxis?.type;
|
||||
const isCategoryY = axisType === 'category';
|
||||
|
||||
dataLength.value = seriesData?.length || 0;
|
||||
if (dataLength.value === 0) return;
|
||||
|
||||
autoPlayTimer.value = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % dataLength.value;
|
||||
// 更新MarkPoint点信息
|
||||
if (showMarkPoint) {
|
||||
updateMarkPoint(currentIndex.value, xAxisData, seriesData, isCategoryY);
|
||||
}
|
||||
|
||||
// 重置之前的高亮
|
||||
chartInstance?.dispatchAction({
|
||||
type: 'downplay',
|
||||
seriesIndex: seriesIndex,
|
||||
});
|
||||
// 高亮当前项
|
||||
chartInstance?.dispatchAction({
|
||||
type: 'highlight',
|
||||
seriesIndex: seriesIndex,
|
||||
dataIndex: currentIndex.value,
|
||||
});
|
||||
// 显示 tooltip(可选)
|
||||
if (showTooltip) {
|
||||
chartInstance?.dispatchAction({
|
||||
type: 'showTip',
|
||||
seriesIndex: seriesIndex,
|
||||
dataIndex: currentIndex.value,
|
||||
});
|
||||
}
|
||||
}, interval);
|
||||
};
|
||||
|
||||
function updateMarkPoint(index, xAxis, seriesData, isCategoryY) {
|
||||
const x = isCategoryY ? seriesData[index] : xAxis[index];
|
||||
const y = isCategoryY ? xAxis[index] : seriesData[index];
|
||||
const updatedSeries = chartInstance.getOption().series;
|
||||
|
||||
if (updatedSeries[0].markPoint && Array.isArray(updatedSeries[0].markPoint.data) && updatedSeries[0].markPoint.data.length > 1) {
|
||||
// 已初始化:只改坐标
|
||||
updatedSeries[0].markPoint.data.forEach((el) => {
|
||||
el.coord = [x, y];
|
||||
});
|
||||
} else {
|
||||
// 未初始化或数据不对:重建
|
||||
updatedSeries[0].markPoint = {
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
itemStyle: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ coord: [x, y], symbolSize: 16, itemStyle: { color: '#ffffff' }, z: 12 },
|
||||
{ coord: [x, y], symbolSize: 24, itemStyle: { color: 'rgba(1, 238, 255, 0.5)' }, z: 11 },
|
||||
{ coord: [x, y], symbolSize: 40, itemStyle: { color: 'rgba(1, 238, 255, 0.3)' }, z: 10 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
chartInstance.setOption({
|
||||
series: updatedSeries,
|
||||
});
|
||||
}
|
||||
|
||||
// 新增方法 - 停止轮播
|
||||
const stopAutoPlay = () => {
|
||||
if (autoPlayTimer.value) {
|
||||
clearInterval(autoPlayTimer.value);
|
||||
autoPlayTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const getDarkMode = computed(() => {
|
||||
return theme === 'default' ? 'dark' : theme;
|
||||
});
|
||||
|
||||
let chartInstance = null;
|
||||
let resizeFn = resize;
|
||||
const cacheOptions = ref({});
|
||||
let removeResizeFn = null;
|
||||
|
||||
resizeFn = useDebounceFn(resize, 200);
|
||||
|
||||
const getOptions = computed(() => {
|
||||
if (getDarkMode.value !== 'dark') {
|
||||
return cacheOptions.value;
|
||||
}
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
...cacheOptions.value,
|
||||
};
|
||||
});
|
||||
|
||||
function initCharts(t = theme) {
|
||||
const el = unref(elRef);
|
||||
if (!el || !unref(el)) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
if (el.offsetWidth === 0 || el.offsetHeight === 0) {
|
||||
// console.warn('图表容器不可见,延迟初始化');
|
||||
useTimeoutFn(() => initCharts(t), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
chartInstance = markRaw(echarts.init(el, t));
|
||||
|
||||
const { removeEvent } = useEventListener({
|
||||
el: window,
|
||||
name: 'resize',
|
||||
listener: resizeFn,
|
||||
});
|
||||
removeResizeFn = removeEvent;
|
||||
const { widthRef } = useBreakpoint();
|
||||
if (unref(widthRef) <= 768 || el.offsetHeight === 0) {
|
||||
useTimeoutFn(() => {
|
||||
resizeFn();
|
||||
}, 30);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMapClick(params) {
|
||||
console.info('handleMapClick', params);
|
||||
|
||||
// 执行注册的回调函数
|
||||
if (typeof mapClickHandler === 'function') {
|
||||
console.info('mapClickHandler', params);
|
||||
mapClickHandler(params);
|
||||
}
|
||||
}
|
||||
|
||||
function onMapClick(handler) {
|
||||
mapClickHandler = handler;
|
||||
|
||||
// 返回解绑方法
|
||||
return () => {
|
||||
mapClickHandler = null;
|
||||
};
|
||||
}
|
||||
|
||||
function setOptions(options = {}, clear = true) {
|
||||
const mergedOptions = {
|
||||
animation: true,
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicOut',
|
||||
...unref(options),
|
||||
animationThreshold: 2000, // 数据量超过2000自动关闭动画
|
||||
animationDelayUpdate: (idx) => idx * 50, // 数据项延迟
|
||||
};
|
||||
cacheOptions.value = mergedOptions;
|
||||
if (unref(elRef)?.offsetHeight === 0) {
|
||||
useTimeoutFn(() => {
|
||||
setOptions(unref(getOptions));
|
||||
}, 30);
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
useTimeoutFn(() => {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value ?? 'default');
|
||||
|
||||
if (!chartInstance) return;
|
||||
}
|
||||
clear && chartInstance?.clear();
|
||||
chartInstance?.setOption(unref(getOptions));
|
||||
// 立即绑定事件
|
||||
chartInstance.off('click');
|
||||
chartInstance.on('click', handleMapClick);
|
||||
}, 30);
|
||||
});
|
||||
}
|
||||
|
||||
function resize() {
|
||||
chartInstance?.resize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册地图数据
|
||||
* @param {string} mapName - 地图名称
|
||||
* @param {object} geoJSON - GeoJSON 数据
|
||||
*/
|
||||
function regMap(mapName, geoJSON) {
|
||||
if (!mapName || !geoJSON) {
|
||||
console.warn('地图名称或 GeoJSON 数据无效');
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('getMap', echarts.getMap(mapName));
|
||||
if (!echarts.getMap(mapName)) {
|
||||
echarts.registerMap(mapName, geoJSON, { override: true });
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => getDarkMode.value,
|
||||
(theme) => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
initCharts(theme);
|
||||
setOptions(cacheOptions.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
stopAutoPlay(); // 清理定时器
|
||||
if (!chartInstance) return;
|
||||
if (chartInstance) {
|
||||
chartInstance.off('click', handleMapClick);
|
||||
}
|
||||
removeResizeFn();
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
|
||||
function getInstance() {
|
||||
if (!chartInstance) {
|
||||
initCharts(getDarkMode.value ?? 'default');
|
||||
}
|
||||
return chartInstance;
|
||||
}
|
||||
|
||||
return {
|
||||
setOptions,
|
||||
resize,
|
||||
echarts,
|
||||
getInstance: () => chartInstance,
|
||||
regMap,
|
||||
startAutoPlay, // 暴露轮播方法
|
||||
stopAutoPlay,
|
||||
onMapClick,
|
||||
};
|
||||
};
|
38
sub-operation-service/src/hooks/useEventListener.js
Normal file
38
sub-operation-service/src/hooks/useEventListener.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { ref, watch, unref } from 'vue';
|
||||
import { useThrottleFn, useDebounceFn } from '@vueuse/core';
|
||||
|
||||
export function useEventListener({ el = window, name, listener, options, autoRemove = true, isDebounce = true, wait = 80 }) {
|
||||
let remove;
|
||||
const isAddRef = ref(false);
|
||||
|
||||
if (el) {
|
||||
const element = ref(el);
|
||||
|
||||
const handler = isDebounce ? useDebounceFn(listener, wait) : useThrottleFn(listener, wait);
|
||||
const realHandler = wait ? handler : listener;
|
||||
const removeEventListener = (e) => {
|
||||
isAddRef.value = true;
|
||||
e.removeEventListener(name, realHandler, options);
|
||||
};
|
||||
const addEventListener = (e) => e.addEventListener(name, realHandler, options);
|
||||
|
||||
const removeWatch = watch(
|
||||
element,
|
||||
(v, _ov, cleanUp) => {
|
||||
if (v) {
|
||||
!unref(isAddRef) && addEventListener(v);
|
||||
cleanUp(() => {
|
||||
autoRemove && removeEventListener(v);
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
remove = () => {
|
||||
removeEventListener(element.value);
|
||||
removeWatch();
|
||||
};
|
||||
}
|
||||
return { removeEvent: remove };
|
||||
}
|
44
sub-operation-service/src/hooks/useTimeout.js
Normal file
44
sub-operation-service/src/hooks/useTimeout.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { tryOnUnmounted } from '@vueuse/core';
|
||||
|
||||
export function useTimeoutFn(handle, wait, native = false) {
|
||||
if (typeof handle !== 'function') {
|
||||
throw new Error('handle is not Function!');
|
||||
}
|
||||
|
||||
const { readyRef, stop, start } = useTimeoutRef(wait);
|
||||
if (native) {
|
||||
handle();
|
||||
} else {
|
||||
watch(
|
||||
readyRef,
|
||||
(maturity) => {
|
||||
maturity && handle();
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
}
|
||||
return { readyRef, stop, start };
|
||||
}
|
||||
|
||||
export function useTimeoutRef(wait) {
|
||||
const readyRef = ref(false);
|
||||
|
||||
let timer;
|
||||
function stop() {
|
||||
readyRef.value = false;
|
||||
timer && window.clearTimeout(timer);
|
||||
}
|
||||
function start() {
|
||||
stop();
|
||||
timer = setTimeout(() => {
|
||||
readyRef.value = true;
|
||||
}, wait);
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
tryOnUnmounted(stop);
|
||||
|
||||
return { readyRef, stop, start };
|
||||
}
|
75
sub-operation-service/src/utils/echarts.js
Normal file
75
sub-operation-service/src/utils/echarts.js
Normal file
@ -0,0 +1,75 @@
|
||||
import * as echarts from 'echarts/core';
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
LinesChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
PictorialBarChart,
|
||||
RadarChart,
|
||||
GraphChart,
|
||||
GaugeChart,
|
||||
ScatterChart,
|
||||
EffectScatterChart,
|
||||
} from 'echarts/charts';
|
||||
import 'echarts-gl';
|
||||
import 'echarts-liquidfill';
|
||||
import 'echarts-wordcloud';
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
LegendComponent,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
GeoComponent,
|
||||
MarkPointComponent,
|
||||
MarkLineComponent,
|
||||
MarkAreaComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
echarts.use([
|
||||
LegendComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
PolarComponent,
|
||||
AriaComponent,
|
||||
ParallelComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
LinesChart,
|
||||
PieChart,
|
||||
MapChart,
|
||||
RadarChart,
|
||||
CanvasRenderer,
|
||||
PictorialBarChart,
|
||||
RadarComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
TimelineComponent,
|
||||
CalendarComponent,
|
||||
GraphicComponent,
|
||||
GraphChart,
|
||||
GaugeChart,
|
||||
ScatterChart,
|
||||
EffectScatterChart,
|
||||
GeoComponent,
|
||||
MarkPointComponent,
|
||||
MarkLineComponent,
|
||||
MarkAreaComponent,
|
||||
]);
|
||||
|
||||
export default echarts;
|
@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="inventory-charts">
|
||||
<custom-echart-line-line :chart-data="chartsData.valData" height="100%" :option="chartsData.option" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
const legendData = reactive(['分拣', '包装']);
|
||||
let dataItem = reactive([100, 90, 200, 250, 240, 275, 120, 300, 320, 270, 290, 120]);
|
||||
let colors = reactive({
|
||||
分拣: '#3685fe',
|
||||
包装: '#41b879',
|
||||
});
|
||||
let colorBg = reactive({
|
||||
分拣: [
|
||||
{ offset: 0, color: 'rgba(54,161,255,0.6)' },
|
||||
{ offset: 1, color: 'rgba(25,104,255,0)' },
|
||||
],
|
||||
包装: [
|
||||
{ offset: 0, color: 'rgba(0,255,0,0.6)' },
|
||||
{ offset: 1, color: 'rgba(25,104,255,0)' },
|
||||
],
|
||||
});
|
||||
|
||||
const currentMonth = ref(new Date().getMonth() + 1);
|
||||
const yAxisData = computed(() => {
|
||||
let list = [];
|
||||
for (let i = 1; i < 13; i++) {
|
||||
let mouth = i < 10 ? i : i;
|
||||
list.push(mouth + '月');
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
let seriesItem = reactive({
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
symbol: 'none',
|
||||
smooth: true,
|
||||
});
|
||||
|
||||
let seriesData = computed(() => {
|
||||
let list = [];
|
||||
if (legendData.length && legendData.length > 0) {
|
||||
legendData.forEach((m) => {
|
||||
let val = {
|
||||
...seriesItem,
|
||||
name: m,
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: colorBg[m],
|
||||
global: false, // 缺省为 false
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: colors[m],
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
},
|
||||
data: dataItem,
|
||||
};
|
||||
|
||||
list.push(val);
|
||||
});
|
||||
}
|
||||
return list;
|
||||
});
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '15%',
|
||||
bottom: '1%',
|
||||
top: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
},
|
||||
confine: true, // 超出范围
|
||||
backgroundColor: 'rgba(17,95,182,0.5)', //设置背景颜色
|
||||
formatter: function (item) {
|
||||
let params = [...item];
|
||||
var res = params[0].name + '<br/>';
|
||||
for (var i = 0, l = params.length; i < l; i++) {
|
||||
res += params[i].value !== '-' ? params[i].marker + params[i].seriesName + ' : ' + params[i].value + ' <br/>' : '';
|
||||
}
|
||||
return res;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
data: Array.from(legendData),
|
||||
right: '0', // 距离左侧10%的位置
|
||||
top: 'middle', // 垂直居中
|
||||
orient: 'vertical', // 图例垂直排列
|
||||
itemWidth: 15, // 图例标记的宽度
|
||||
itemHeight: 8, // 图例标记的高度
|
||||
textStyle: {
|
||||
fontSize: 10, // 图例文字的字体大小
|
||||
color: '#fff', // 图例文字的颜色
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#94A7BD', //轴线和单位颜色
|
||||
},
|
||||
},
|
||||
data: yAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: ' ',
|
||||
nameTextStyle: {
|
||||
fontSize: 14,
|
||||
color: '#94A7BD',
|
||||
padding: [0, 0, 0, -45],
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#94A7BD', //轴线和单位颜色
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#182D46',
|
||||
type: [2, 3],
|
||||
dashOffset: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: seriesData.value,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.inventory-charts {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="benefit-charts">
|
||||
<custom-echart-mixin :chart-data="handelData" :option="chartsData.option" height="100%" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
let itemStyle = reactive({
|
||||
itemStyle: { borderRadius: [8, 8, 0, 0] },
|
||||
});
|
||||
|
||||
let legendList = reactive(['成本', '收入', '繁殖率', '配种成功率']);
|
||||
|
||||
var { data, optionConfig } = {
|
||||
data: [
|
||||
{ name: '已使用', value: 15, itemStyle: { color: '#0096f9' }, startRatio: 0, endRatio: 0.15 },
|
||||
{ name: '未使用', value: 25, itemStyle: { color: '#00e8ce' }, startRatio: 0.15, endRatio: 0.4 },
|
||||
],
|
||||
optionConfig: {},
|
||||
};
|
||||
const getPie3D = (pieData, internalDiameterRatio) => {
|
||||
let series = [];
|
||||
let sumValue = 0;
|
||||
let startValue = 0;
|
||||
let endValue = 0;
|
||||
let legendData = [];
|
||||
let k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3;
|
||||
|
||||
// 为每一个饼图数据,生成一个 series-surface 配置
|
||||
for (let i = 0; i < pieData.length; i++) {
|
||||
sumValue += pieData[i].value;
|
||||
|
||||
let seriesItem = {
|
||||
name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
|
||||
type: 'surface',
|
||||
|
||||
parametric: true,
|
||||
wireframe: {
|
||||
show: false,
|
||||
},
|
||||
pieData: pieData[i],
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k: k,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
// normal: {
|
||||
// position: "inner",
|
||||
// formatter: (params) => {
|
||||
// return params;
|
||||
// },
|
||||
// },
|
||||
},
|
||||
itemStyle: {
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof pieData[i].itemStyle != 'undefined') {
|
||||
let itemStyle = {};
|
||||
|
||||
typeof pieData[i].itemStyle.color != 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
|
||||
typeof pieData[i].itemStyle.opacity != 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null;
|
||||
|
||||
seriesItem.itemStyle = itemStyle;
|
||||
}
|
||||
series.push(seriesItem);
|
||||
}
|
||||
|
||||
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
|
||||
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
endValue = startValue + series[i].pieData.value;
|
||||
// console.log(series[i]);
|
||||
series[i].pieData.startRatio = startValue / sumValue;
|
||||
series[i].pieData.endRatio = endValue / sumValue;
|
||||
series[i].parametricEquation = getParametricEquation(
|
||||
series[i].pieData.startRatio,
|
||||
series[i].pieData.endRatio,
|
||||
false,
|
||||
false,
|
||||
k,
|
||||
series[i].pieData.value
|
||||
);
|
||||
|
||||
startValue = endValue;
|
||||
|
||||
legendData.push(series[i].name);
|
||||
}
|
||||
|
||||
// 准备待返回的配置项,把准备好的 legendData、series 传入。
|
||||
let option = {
|
||||
tooltip: {
|
||||
// backgroundColor: '#053A8D',
|
||||
formatter: (params) => {
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
return `${
|
||||
params.seriesName
|
||||
}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${
|
||||
params.color
|
||||
};"></span>${option.series[params.seriesIndex].pieData.value}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal', // 设置为水平排列
|
||||
left: 'right', // 水平居中
|
||||
bottom: 70,
|
||||
data: legendData,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
icon: 'roundRect',
|
||||
formatter: function (name) {
|
||||
let item = data.filter((item) => item.name == name)[0];
|
||||
return `${item.name}`;
|
||||
},
|
||||
// top: '30%', //居右显示
|
||||
},
|
||||
xAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
yAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
zAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 4,
|
||||
top: '-12%',
|
||||
left: '-2%',
|
||||
// environment: "#021041",
|
||||
viewControl: {
|
||||
distance: 6000,
|
||||
alpha: 60,
|
||||
beta: 10,
|
||||
},
|
||||
},
|
||||
series: series,
|
||||
};
|
||||
return option;
|
||||
};
|
||||
// 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
|
||||
const getParametricEquation = (startRatio, endRatio, isSelected, isHovered, k, height) => {
|
||||
// 计算
|
||||
let midRatio = (startRatio + endRatio) / 2;
|
||||
|
||||
let startRadian = startRatio * Math.PI * 2;
|
||||
let endRadian = endRatio * Math.PI * 2;
|
||||
let midRadian = midRatio * Math.PI * 2;
|
||||
|
||||
// 如果只有一个扇形,则不实现选中效果。
|
||||
if (startRatio === 0 && endRatio === 1) {
|
||||
isSelected = false;
|
||||
}
|
||||
|
||||
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
|
||||
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
|
||||
let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
|
||||
let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
|
||||
|
||||
// 计算高亮效果的放大比例(未高亮,则比例为 1)
|
||||
let hoverRate = isHovered ? 1.05 : 1;
|
||||
|
||||
// 返回曲面参数方程
|
||||
return {
|
||||
u: {
|
||||
min: -Math.PI,
|
||||
max: Math.PI * 3,
|
||||
step: Math.PI / 32,
|
||||
},
|
||||
|
||||
v: {
|
||||
min: 0,
|
||||
max: Math.PI * 2,
|
||||
step: Math.PI / 20,
|
||||
},
|
||||
|
||||
x: function (u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
|
||||
y: function (u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
|
||||
z: function (u, v) {
|
||||
if (u < -Math.PI * 0.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
if (u > Math.PI * 2.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
return Math.sin(v) > 0 ? 1 * height : -1;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
// color: ['#3685fe', '#8dcbe9', '#ffd500', '#631f9f'],
|
||||
// title: {
|
||||
// text: ' ',
|
||||
// textStyle: {
|
||||
// color: '#333',
|
||||
// },
|
||||
// },
|
||||
// legend: {
|
||||
// show: true,
|
||||
// data: legendList,
|
||||
// left: '0', // 距离左侧10%的位置
|
||||
// top: '0', // 垂直居中
|
||||
// itemWidth: 15, // 图例标记的宽度
|
||||
// itemHeight: 8, // 图例标记的高度
|
||||
// textStyle: {
|
||||
// fontSize: 10, // 图例文字的字体大小
|
||||
// color: '#fff', // 图例文字的颜色
|
||||
// },
|
||||
// },
|
||||
// barStyle: {
|
||||
// barWidth: 10,
|
||||
// },
|
||||
// dataZoom: [
|
||||
// // {
|
||||
// // type: 'slider', // 滑动条型数据区域缩放组件
|
||||
// // startValue: 0, // 数据窗口起始值的索引
|
||||
// // endValue: 2, // 数据窗口结束值的索引
|
||||
// // },
|
||||
// // {
|
||||
// // type: 'inside', // 支持鼠标滚轮和触控板缩放和平移
|
||||
// // startValue: 0,
|
||||
// // endValue: 2,
|
||||
// // },
|
||||
// ],
|
||||
// yAxis: [
|
||||
// {
|
||||
// type: 'value',
|
||||
// name: ' ',
|
||||
// axisLabel: {
|
||||
// formatter: '{value}',
|
||||
// },
|
||||
// splitLine: {
|
||||
// show: true, // 显示分割线
|
||||
// lineStyle: {
|
||||
// type: 'dashed', // 设置为虚线
|
||||
// width: 0.5, // 分割线宽度
|
||||
// },
|
||||
// },
|
||||
// itemStyle: { fontSize: 8 },
|
||||
// },
|
||||
// ],
|
||||
// grid: {
|
||||
// x: '10%',
|
||||
// x2: '10%',
|
||||
// y: '20%',
|
||||
// y2: '20%',
|
||||
// },
|
||||
},
|
||||
valData: [],
|
||||
});
|
||||
|
||||
chartsData.option = getPie3D(data, 0);
|
||||
|
||||
const randomVal = (num) => {
|
||||
let list = [];
|
||||
for (let i = 0; i < legendList.length; i++) {
|
||||
let addNum = [10, 8, 2, 5];
|
||||
let val = {
|
||||
name: num + '月',
|
||||
value: Number(Math.random() * 100 + addNum[i]).toFixed(2),
|
||||
seriesType: i < legendList.length - 2 ? 'bar' : 'line',
|
||||
type: legendList[i],
|
||||
stack: num + '月',
|
||||
};
|
||||
if (val.seriesType == 'line') {
|
||||
val.smooth = 30;
|
||||
val.symbol = 'none';
|
||||
}
|
||||
let lastVal = {
|
||||
...val,
|
||||
...itemStyle,
|
||||
};
|
||||
list[i] = i < legendList.length - 3 ? val : lastVal;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
let handelData = computed(() => {
|
||||
let list = [];
|
||||
let maxMouth = 12;
|
||||
for (let i = 0; i < maxMouth; i++) {
|
||||
let val = randomVal(i + 1);
|
||||
list = [...list, ...val];
|
||||
}
|
||||
|
||||
list.map((m, indexm) => {
|
||||
return { ...m, value: Number(Number(m.value) + Math.random() + indexm).toFixed(0) };
|
||||
});
|
||||
// console.info('handelData', list);
|
||||
return list;
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.benefit-charts {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<div class="benefit-charts">
|
||||
<custom-echart-mixin :chart-data="handelData" :option="chartsData.option" height="100%" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
let itemStyle = reactive({
|
||||
itemStyle: { borderRadius: [8, 8, 0, 0] },
|
||||
});
|
||||
|
||||
let legendList = reactive(['成本', '收入', '繁殖率', '配种成功率']);
|
||||
|
||||
var { data, optionConfig } = {
|
||||
data: [
|
||||
{ name: '蔬菜', value: 15, itemStyle: { color: '#938df6' }, startRatio: 0, endRatio: 0.15 },
|
||||
{ name: '水果', value: 25, itemStyle: { color: '#0ce4d1' }, startRatio: 0.15, endRatio: 0.4 },
|
||||
{ name: '肉类', value: 5, itemStyle: { color: '#2196f3' }, startRatio: 0.15, endRatio: 0.4 },
|
||||
],
|
||||
optionConfig: {},
|
||||
};
|
||||
const getPie3D = (pieData, internalDiameterRatio) => {
|
||||
let series = [];
|
||||
let sumValue = 0;
|
||||
let startValue = 0;
|
||||
let endValue = 0;
|
||||
let legendData = [];
|
||||
let k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3;
|
||||
|
||||
// 为每一个饼图数据,生成一个 series-surface 配置
|
||||
for (let i = 0; i < pieData.length; i++) {
|
||||
sumValue += pieData[i].value;
|
||||
|
||||
let seriesItem = {
|
||||
name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
|
||||
type: 'surface',
|
||||
|
||||
parametric: true,
|
||||
wireframe: {
|
||||
show: false,
|
||||
},
|
||||
pieData: pieData[i],
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k: k,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
// normal: {
|
||||
// position: "inner",
|
||||
// formatter: (params) => {
|
||||
// return params;
|
||||
// },
|
||||
// },
|
||||
},
|
||||
itemStyle: {
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof pieData[i].itemStyle != 'undefined') {
|
||||
let itemStyle = {};
|
||||
|
||||
typeof pieData[i].itemStyle.color != 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
|
||||
typeof pieData[i].itemStyle.opacity != 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null;
|
||||
|
||||
seriesItem.itemStyle = itemStyle;
|
||||
}
|
||||
series.push(seriesItem);
|
||||
}
|
||||
|
||||
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
|
||||
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
endValue = startValue + series[i].pieData.value;
|
||||
// console.log(series[i]);
|
||||
series[i].pieData.startRatio = startValue / sumValue;
|
||||
series[i].pieData.endRatio = endValue / sumValue;
|
||||
series[i].parametricEquation = getParametricEquation(
|
||||
series[i].pieData.startRatio,
|
||||
series[i].pieData.endRatio,
|
||||
false,
|
||||
false,
|
||||
k,
|
||||
series[i].pieData.value
|
||||
);
|
||||
|
||||
startValue = endValue;
|
||||
|
||||
legendData.push(series[i].name);
|
||||
}
|
||||
|
||||
// 准备待返回的配置项,把准备好的 legendData、series 传入。
|
||||
let option = {
|
||||
tooltip: {
|
||||
backgroundColor: '#053A8D',
|
||||
formatter: (params) => {
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
return `${
|
||||
params.seriesName
|
||||
}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${
|
||||
params.color
|
||||
};"></span>${option.series[params.seriesIndex].pieData.value}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal', // 设置为水平排列
|
||||
left: 'left', // 水平居中
|
||||
bottom: 70,
|
||||
data: legendData,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
icon: 'roundRect',
|
||||
formatter: function (name) {
|
||||
let item = data.filter((item) => item.name == name)[0];
|
||||
return `${item.name}`;
|
||||
},
|
||||
// top: '30%', //居右显示
|
||||
},
|
||||
xAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
yAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
zAxis3D: {
|
||||
min: -1.3,
|
||||
max: 1.3,
|
||||
},
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 4,
|
||||
top: '-12%',
|
||||
left: '-2%',
|
||||
// environment: "#021041",
|
||||
viewControl: {
|
||||
distance: 6000,
|
||||
alpha: 60,
|
||||
beta: 10,
|
||||
},
|
||||
},
|
||||
series: series,
|
||||
};
|
||||
return option;
|
||||
};
|
||||
// 生成扇形的曲面参数方程,用于 series-surface.parametricEquation
|
||||
const getParametricEquation = (startRatio, endRatio, isSelected, isHovered, k, height) => {
|
||||
// 计算
|
||||
let midRatio = (startRatio + endRatio) / 2;
|
||||
|
||||
let startRadian = startRatio * Math.PI * 2;
|
||||
let endRadian = endRatio * Math.PI * 2;
|
||||
let midRadian = midRatio * Math.PI * 2;
|
||||
|
||||
// 如果只有一个扇形,则不实现选中效果。
|
||||
if (startRatio === 0 && endRatio === 1) {
|
||||
isSelected = false;
|
||||
}
|
||||
|
||||
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
|
||||
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
|
||||
let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
|
||||
let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
|
||||
|
||||
// 计算高亮效果的放大比例(未高亮,则比例为 1)
|
||||
let hoverRate = isHovered ? 1.05 : 1;
|
||||
|
||||
// 返回曲面参数方程
|
||||
return {
|
||||
u: {
|
||||
min: -Math.PI,
|
||||
max: Math.PI * 3,
|
||||
step: Math.PI / 32,
|
||||
},
|
||||
|
||||
v: {
|
||||
min: 0,
|
||||
max: Math.PI * 2,
|
||||
step: Math.PI / 20,
|
||||
},
|
||||
|
||||
x: function (u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
|
||||
y: function (u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
|
||||
z: function (u, v) {
|
||||
if (u < -Math.PI * 0.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
if (u > Math.PI * 2.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
return Math.sin(v) > 0 ? 1 * height : -1;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
// color: ['#3685fe', '#8dcbe9', '#ffd500', '#631f9f'],
|
||||
// title: {
|
||||
// text: ' ',
|
||||
// textStyle: {
|
||||
// color: '#333',
|
||||
// },
|
||||
// },
|
||||
// legend: {
|
||||
// show: true,
|
||||
// data: legendList,
|
||||
// left: '0', // 距离左侧10%的位置
|
||||
// top: '0', // 垂直居中
|
||||
// itemWidth: 15, // 图例标记的宽度
|
||||
// itemHeight: 8, // 图例标记的高度
|
||||
// textStyle: {
|
||||
// fontSize: 10, // 图例文字的字体大小
|
||||
// color: '#fff', // 图例文字的颜色
|
||||
// },
|
||||
// },
|
||||
// barStyle: {
|
||||
// barWidth: 10,
|
||||
// },
|
||||
// dataZoom: [
|
||||
// // {
|
||||
// // type: 'slider', // 滑动条型数据区域缩放组件
|
||||
// // startValue: 0, // 数据窗口起始值的索引
|
||||
// // endValue: 2, // 数据窗口结束值的索引
|
||||
// // },
|
||||
// // {
|
||||
// // type: 'inside', // 支持鼠标滚轮和触控板缩放和平移
|
||||
// // startValue: 0,
|
||||
// // endValue: 2,
|
||||
// // },
|
||||
// ],
|
||||
// yAxis: [
|
||||
// {
|
||||
// type: 'value',
|
||||
// name: ' ',
|
||||
// axisLabel: {
|
||||
// formatter: '{value}',
|
||||
// },
|
||||
// splitLine: {
|
||||
// show: true, // 显示分割线
|
||||
// lineStyle: {
|
||||
// type: 'dashed', // 设置为虚线
|
||||
// width: 0.5, // 分割线宽度
|
||||
// },
|
||||
// },
|
||||
// itemStyle: { fontSize: 8 },
|
||||
// },
|
||||
// ],
|
||||
// grid: {
|
||||
// x: '10%',
|
||||
// x2: '10%',
|
||||
// y: '20%',
|
||||
// y2: '20%',
|
||||
// },
|
||||
},
|
||||
valData: [],
|
||||
});
|
||||
|
||||
chartsData.option = getPie3D(data, 0);
|
||||
|
||||
const randomVal = (num) => {
|
||||
let list = [];
|
||||
for (let i = 0; i < legendList.length; i++) {
|
||||
let addNum = [10, 8, 2, 5];
|
||||
let val = {
|
||||
name: num + '月',
|
||||
value: Number(Math.random() * 100 + addNum[i]).toFixed(2),
|
||||
seriesType: i < legendList.length - 2 ? 'bar' : 'line',
|
||||
type: legendList[i],
|
||||
stack: num + '月',
|
||||
};
|
||||
if (val.seriesType == 'line') {
|
||||
val.smooth = 30;
|
||||
val.symbol = 'none';
|
||||
}
|
||||
let lastVal = {
|
||||
...val,
|
||||
...itemStyle,
|
||||
};
|
||||
list[i] = i < legendList.length - 3 ? val : lastVal;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
let handelData = computed(() => {
|
||||
let list = [];
|
||||
let maxMouth = 12;
|
||||
for (let i = 0; i < maxMouth; i++) {
|
||||
let val = randomVal(i + 1);
|
||||
list = [...list, ...val];
|
||||
}
|
||||
|
||||
list.map((m, indexm) => {
|
||||
return { ...m, value: Number(Number(m.value) + Math.random() + indexm).toFixed(0) };
|
||||
});
|
||||
// console.info('handelData', list);
|
||||
return list;
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.benefit-charts {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="business">
|
||||
<div class="business-left">
|
||||
<custom-echart-water-droplet width="100%" height="100%" :option="state.option" />
|
||||
<div class="business-title">证件齐全率</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="business-right">
|
||||
<div class="business-title">提交申请数量</div>
|
||||
<ul class="business-info">
|
||||
<li class="success">
|
||||
<span>253家</span>
|
||||
</li>
|
||||
<!-- <li class="warning">
|
||||
<b>临期</b>
|
||||
<span>5家</span>
|
||||
</li>
|
||||
<li class="danger">
|
||||
<b>已过期</b>
|
||||
<span>0家</span>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
<div class="business-right">
|
||||
<div class="business-title">审核通过数量</div>
|
||||
<ul class="business-info">
|
||||
<li class="success">
|
||||
<span>253家</span>
|
||||
</li>
|
||||
<!-- <li class="warning">
|
||||
<b>临期</b>
|
||||
<span>5家</span>
|
||||
</li>
|
||||
<li class="danger">
|
||||
<b>已过期</b>
|
||||
<span>0家</span>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
option: {
|
||||
backgroundColor: 'transparent', //背景色
|
||||
series: [
|
||||
{
|
||||
name: '',
|
||||
type: 'liquidFill',
|
||||
radius: '80%',
|
||||
center: ['50%', '50%'],
|
||||
backgroundStyle: {
|
||||
color: 'transparent',
|
||||
},
|
||||
data: [],
|
||||
amplitude: 12, //水波振幅
|
||||
label: {
|
||||
position: ['50%', '45%'],
|
||||
// formatter: 0.3998 * 100 + '%', //显示文本,
|
||||
textStyle: {
|
||||
fontSize: '20px', //文本字号,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
borderDistance: 2,
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: {
|
||||
type: 'linear',
|
||||
x: 1,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 0,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(71, 202, 219, 0.5)',
|
||||
},
|
||||
{
|
||||
offset: 0.6,
|
||||
color: 'rgba(45, 209, 185, 0.5)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(13, 204, 163, 0.5)',
|
||||
},
|
||||
],
|
||||
globalCoord: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear', // 线性渐变
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(13, 204, 163, 0.6)' },
|
||||
{ offset: 1, color: 'rgba(71, 202, 219, 1)' },
|
||||
],
|
||||
global: false, // 默认为 false
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
if (!isEmpty(val)) {
|
||||
state.option.series[0].data = [0, val.percent];
|
||||
state.option.series[0].label.formatter = val.percent * 100 + '%';
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.business-right {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.business {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include flex-row();
|
||||
|
||||
&-title {
|
||||
width: 160px;
|
||||
margin: 0 auto;
|
||||
height: 32px;
|
||||
line-height: 26px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 0px 10px 0px #01eeff;
|
||||
background: url('@/assets/images/business/bg_title.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
text-shadow:
|
||||
0 0 10px #01eeff,
|
||||
0 0 20px #01eeff,
|
||||
0 0 30px #01eeff,
|
||||
0 0 40px #01eeff;
|
||||
}
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
@include flex-column();
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-info {
|
||||
width: 100%;
|
||||
@include flex-column();
|
||||
text-align: center;
|
||||
|
||||
li {
|
||||
@include flex-column();
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
margin-top: 16px;
|
||||
|
||||
b {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
margin-top: 0 !important;
|
||||
span {
|
||||
color: #02fd94;
|
||||
}
|
||||
b {
|
||||
&::before {
|
||||
background-color: #02fd94;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
span {
|
||||
color: #fef906;
|
||||
}
|
||||
b {
|
||||
&::before {
|
||||
background-color: #fef906;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.danger {
|
||||
span {
|
||||
color: #fc0003;
|
||||
}
|
||||
b {
|
||||
&::before {
|
||||
background-color: #fc0003;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<centerMap
|
||||
:dialog-title="'经营主体'"
|
||||
:marker-data="markerData"
|
||||
:bg-image="'images/vsualized/mapopup1.png'"
|
||||
:dialog-width="240"
|
||||
@mapclick="doMapclick"
|
||||
>
|
||||
<template #header>
|
||||
<div class="buiness-map-pop-header">
|
||||
<div class="title">{{ currentRegion && currentRegion.name ? currentRegion.name : '经营主体' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #dialogContent>
|
||||
<div class="buiness-map-pop-content">
|
||||
<div class="addr">
|
||||
<el-icon :size="'18px'" :color="'#fff'">
|
||||
<LocationFilled />
|
||||
</el-icon>
|
||||
{{ testInfo.addr || ' --' }}
|
||||
</div>
|
||||
<div class="tips-warp">
|
||||
<div class="label">负责人:</div>
|
||||
<div class="val">{{ testInfo.user || '--' }}</div>
|
||||
</div>
|
||||
<div class="tips-warp">
|
||||
<div class="label">联系方式:</div>
|
||||
<div class="val">{{ testInfo.tel || '--' }}</div>
|
||||
</div>
|
||||
<div class="tips-warp">
|
||||
<div class="label">注册资本:</div>
|
||||
<div class="val">{{ testInfo.capital || '--' }}</div>
|
||||
</div>
|
||||
<div class="tips-warp">
|
||||
<div class="label">成立时间:</div>
|
||||
<div class="val">{{ testInfo.time || '--' }}</div>
|
||||
</div>
|
||||
<div class="tips-warp">
|
||||
<div class="label">信用等级:</div>
|
||||
<div class="val">{{ testInfo.credit || '--' }}</div>
|
||||
</div>
|
||||
<div class="img-list">
|
||||
<div class="img-item">
|
||||
<el-image
|
||||
style="width: 80px; height: 60px; cursor: pointer"
|
||||
:preview-src-list="
|
||||
testInfo.imglist.map((m) => {
|
||||
return getAssetsFile(m);
|
||||
})
|
||||
"
|
||||
:src="getAssetsFile(testInfo.imglist[0])"
|
||||
fit="cover"
|
||||
lazy
|
||||
/>
|
||||
<div class="img-name">营业执照</div>
|
||||
</div>
|
||||
<div class="img-item">
|
||||
<el-image
|
||||
style="width: 80px; height: 60px; cursor: pointer"
|
||||
:preview-src-list="
|
||||
testInfo.imglist.map((m) => {
|
||||
return getAssetsFile(m);
|
||||
})
|
||||
"
|
||||
:src="getAssetsFile(testInfo.imglist[1])"
|
||||
fit="cover"
|
||||
lazy
|
||||
/>
|
||||
<div class="img-name">经营许可证</div>
|
||||
</div>
|
||||
<!-- <div class="img-item">
|
||||
<el-image
|
||||
style="width: 80px; height: 60px; cursor: pointer"
|
||||
:preview-src-list="
|
||||
testInfo.imglist.map((m) => {
|
||||
return getAssetsFile(m);
|
||||
})
|
||||
"
|
||||
:src="getAssetsFile(testInfo.imglist[2])"
|
||||
fit="cover"
|
||||
lazy
|
||||
/>
|
||||
<div class="img-name">其他证件</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</centerMap>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
|
||||
const list = reactive([
|
||||
{ title: '年总产值', value: 3.49, color: '#01FEFD', unit: '亿元' },
|
||||
{ title: '年人均收入', value: 6.98, color: '#FEF906', unit: '万元' },
|
||||
]);
|
||||
let testInfo = reactive({});
|
||||
let currentRegion = ref(null);
|
||||
const doMapclick = (data) => {
|
||||
currentRegion.value = data;
|
||||
if (data.name == '永星食品加工厂') {
|
||||
testInfo = {
|
||||
addr: '云南省临沧市耿马傣族佤族自治县孟定镇101号',
|
||||
user: '张强',
|
||||
tel: '15331683325',
|
||||
capital: '500万',
|
||||
time: '2018年12月1日',
|
||||
credit: 'AA',
|
||||
imglist: ['images/business/b1-1.png', 'images/business/b1-2.png', 'images/business/b1-3.png'],
|
||||
};
|
||||
}
|
||||
if (data.name == '欣欣种源企业') {
|
||||
testInfo = {
|
||||
addr: '云南省临沧市耿马傣族佤族自治耿马镇102号',
|
||||
user: '李欣',
|
||||
tel: '13713575206',
|
||||
capital: '600万',
|
||||
time: '2020年10月1日',
|
||||
credit: 'AA',
|
||||
imglist: ['images/business/b2-1.png', 'images/business/b1-2.png', 'images/business/b1-2.png'],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let markerData = reactive([
|
||||
// 示例数据点,实际应用中应替换为真实的数据
|
||||
{
|
||||
name: '永星食品加工厂',
|
||||
value: [99.081993, 23.524045, 150], // 经度, 纬度, 数值
|
||||
symbol: 'image://' + getAssetsFile('images/vsualized/marker.png'),
|
||||
itemStyle: {
|
||||
color: '#4bffb4', //
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '欣欣种源企业',
|
||||
value: [99.402267, 23.538889, 150], // 经度, 纬度, 数值
|
||||
symbol: 'image://' + getAssetsFile('images/vsualized/marker.png'),
|
||||
itemStyle: {
|
||||
color: '#4bffb4', // 勐永镇的颜色
|
||||
},
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.buiness-map-pop-header {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
.title,
|
||||
.value {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: $color-white;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
}
|
||||
.value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.buiness-map-pop-content {
|
||||
width: 100%;
|
||||
.addr {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
.tips-warp {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
margin: 6px 0;
|
||||
.label {
|
||||
color: #fef906;
|
||||
}
|
||||
.val {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.img-list {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
gap: 10;
|
||||
flex-wrap: wrap;
|
||||
.img-item {
|
||||
width: calc((100% - 10px) / 2);
|
||||
margin-bottom: 8px;
|
||||
.el-image {
|
||||
background: transparent !important;
|
||||
}
|
||||
.img-name {
|
||||
color: #fff;
|
||||
padding: 6px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="demo device-charts" style="height: 90%">
|
||||
<div class="list-item-header item-warp" :style="{ flex: listKeys.length }">
|
||||
<template v-for="(h, indexh) in listKeys" :key="indexh">
|
||||
<div class="item-td" :style="{ width: 'calc(100% / ' + listKeys.length + ')' }">{{ listKeysHeader[h] }}</div>
|
||||
</template>
|
||||
</div>
|
||||
<vue3ScrollSeamless class="scroll-wrap" :class-options="classOptions" :data-list="datalist">
|
||||
<div v-for="(item, index) in datalist" :key="index" class="list-item">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-boday item-warp" :style="{ flex: listKeys.length }">
|
||||
<template v-for="(b, indexb) in listKeys" :key="indexb">
|
||||
<div class="item-td" :class="item.status == 1 ? 'td-title' : 'td-warn'" :style="{ width: 'calc(100% / ' + listKeys.length + ')' }">
|
||||
<span v-if="b == 'num'">
|
||||
{{ item[b] }}
|
||||
</span>
|
||||
<span v-else-if="b == 'duration'" class="duration">
|
||||
<span class="val">{{ item[b] + 'h' }}</span>
|
||||
<div class="pro">
|
||||
<customProgress height="5px" :percent="item.percent" inactive-bg="#081931"></customProgress>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ item[b] == 0 ? '待机' : '运行' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</vue3ScrollSeamless>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, reactive } from 'vue';
|
||||
import { vue3ScrollSeamless } from 'vue3-scroll-seamless';
|
||||
import customProgress from '@/components/customProgress.vue';
|
||||
const props = defineProps({
|
||||
// items: {
|
||||
// type: Array,
|
||||
// default: () => [],
|
||||
// },
|
||||
});
|
||||
|
||||
let list = reactive([
|
||||
{ num: '投喂机', duration: '3.7', status: 1 },
|
||||
{ num: '喂水机', duration: '10.0', status: 1 },
|
||||
{ num: '投喂机', duration: '6.4', status: 1 },
|
||||
{ num: '喂水机', duration: '3.9', status: 1 },
|
||||
{ num: '投喂机', duration: '3.6', status: 0 },
|
||||
{ num: '喂水机', duration: '4.5', status: 1 },
|
||||
{ num: '投喂机', duration: '5.6', status: 1 },
|
||||
]);
|
||||
|
||||
const listKeys = reactive(['num', 'status', 'duration']);
|
||||
const listKeysHeader = reactive({
|
||||
num: '设备编号',
|
||||
status: '设备状态',
|
||||
duration: '设备今日运行时长',
|
||||
});
|
||||
|
||||
let datalist = computed(() => {
|
||||
return list.map((m) => {
|
||||
return {
|
||||
...m,
|
||||
percent: Number((Number(parseInt(m.duration) / max.value) * 100).toFixed(0)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let max = computed(() => {
|
||||
let valueList = new Set(list.map((item) => item.duration));
|
||||
let sortValue = [...valueList].sort((a, b) => b - a) || [];
|
||||
// console.info('valueList', sortValue);
|
||||
return sortValue.length ? sortValue[0] : 0;
|
||||
});
|
||||
|
||||
const classOptions = {
|
||||
singleHeight: 48,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.device-charts {
|
||||
margin-top: 8px;
|
||||
.scroll-wrap {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
margin: 4px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item-header {
|
||||
background: #144482;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
.item-td {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
}
|
||||
.list-item-boday {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
.item-td {
|
||||
padding: 4px 6px;
|
||||
&.td-title {
|
||||
color: #6beff9 !important;
|
||||
}
|
||||
|
||||
&.td-warn {
|
||||
color: red !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-warp {
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
.item-td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
.duration {
|
||||
width: 100%;
|
||||
.val,
|
||||
.pro {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.val {
|
||||
width: 50px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.pro {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
}
|
||||
.list-item {
|
||||
// border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
line-height: 18px;
|
||||
|
||||
.list-item-content {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
.demo {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// margin-top: 10px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<customEchartHyalineCake :chart-data="dataList" height="100%" :option="option" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
/* --------------- 左中饼图 --------------- */
|
||||
// #region
|
||||
const dataList = ref([
|
||||
{
|
||||
name: '小麦',
|
||||
value: 60.8,
|
||||
money: 100,
|
||||
},
|
||||
{
|
||||
name: '荞麦',
|
||||
value: 44.4,
|
||||
money: 88,
|
||||
},
|
||||
{
|
||||
name: '贡菜',
|
||||
value: 24.3,
|
||||
money: 92,
|
||||
},
|
||||
{
|
||||
name: '油菜',
|
||||
value: 32.7,
|
||||
money: 56,
|
||||
},
|
||||
{
|
||||
name: '其他',
|
||||
value: 32.9,
|
||||
money: 18,
|
||||
},
|
||||
]);
|
||||
|
||||
const option = reactive({
|
||||
k: 0.3,
|
||||
opacity: 1,
|
||||
itemGap: 0,
|
||||
autoItemHeight: 2,
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 4, //厚度
|
||||
top: '0', //距离顶部的距离
|
||||
left: '-20%',
|
||||
viewControl: {
|
||||
//3d效果可以放大、旋转等,请自己去查看官方配置
|
||||
alpha: 30, //角度(这个很重要 调节角度的)
|
||||
distance: 200, //调整视角到主体的距离,类似调整zoom(这是整体大小)
|
||||
rotateSensitivity: 10, //设置旋转灵敏度,为0无法旋转
|
||||
zoomSensitivity: 10, //设置缩放灵敏度,为0无法缩放
|
||||
panSensitivity: 10, //设置平移灵敏度,0无法平移
|
||||
autoRotate: true, //自动旋转
|
||||
autoRotateAfterStill: 2, //在鼠标静止操作后恢复自动旋转的时间间隔,在开启 autoRotate 后有效
|
||||
},
|
||||
},
|
||||
// series: [
|
||||
// // 透明的 2D 饼图(仅用于生成标签折线)
|
||||
// {
|
||||
// type: 'pie',
|
||||
// radius: ['30%', '70%'],
|
||||
// center: ['40%', '50%'],
|
||||
// startAngle: -40, // 调整角度与 3D 图形对齐
|
||||
// clockwise: false,
|
||||
// label: {
|
||||
// show: true,
|
||||
// position: 'outside',
|
||||
// formatter: (params) => {
|
||||
// console.log(params);
|
||||
// return `{a|${params.data.name}}\n{b|${params.data.money}万元}`;
|
||||
// },
|
||||
// rich: {
|
||||
// a: { color: '#ffffff' },
|
||||
// b: { color: '#79F5AF' },
|
||||
// },
|
||||
// },
|
||||
// labelLine: {
|
||||
// show: true,
|
||||
// length: 10,
|
||||
// length2: 15,
|
||||
// lineStyle: {
|
||||
// color: '#ffffff',
|
||||
// width: 1,
|
||||
// },
|
||||
// },
|
||||
// data: dataList,
|
||||
// itemStyle: {
|
||||
// opacity: 1, // 隐藏扇区,仅保留标签和折线
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
});
|
||||
// #endregion
|
||||
|
||||
/* --------------- methods --------------- */
|
||||
// #region
|
||||
|
||||
// #endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="plant-environment-warp">
|
||||
<div class="data-item-row">
|
||||
<div
|
||||
v-for="(n, index) in datalist"
|
||||
:key="index"
|
||||
:style="{
|
||||
'background-image': 'url(' + getAssetsFile('images/plant/bg3.png') + ')',
|
||||
width: 'calc((100% - 30px) /' + datalist.length + ')',
|
||||
}"
|
||||
class="data-item"
|
||||
>
|
||||
<div class="data-warp">
|
||||
<div class="data-pos">
|
||||
<div class="data-pos-center">
|
||||
<div class="pos-center">
|
||||
<span class="label">{{ n.label }}</span>
|
||||
<div class="value">
|
||||
<span>{{ n.value }}</span>
|
||||
<span class="unit">{{ n.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-bg">
|
||||
<img :src="getAssetsFile('images/plant/bg6.png')" />
|
||||
<img :src="getAssetsFile('images/plant/' + n.icon)" class="img-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useApp } from '@/hooks';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '统计分析',
|
||||
},
|
||||
postion: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
});
|
||||
|
||||
let topTitle = ref('');
|
||||
let pos = ref('');
|
||||
|
||||
const datalist = reactive([
|
||||
{ label: '空气温度', value: 28.6, unit: '℃', icon: 'icon4.png' },
|
||||
{ label: '空气湿度', value: 30, unit: '%', icon: 'icon3.png' },
|
||||
{ label: '光照强度', value: 1000, unit: 'lux', icon: 'icon1.png' },
|
||||
{ label: '降水量', value: 100, unit: 'mm', icon: 'icon2.png' },
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
if (datalist.length) {
|
||||
datalist.forEach((m, index) => {
|
||||
let num = 0;
|
||||
switch (index) {
|
||||
case 0:
|
||||
num = 20;
|
||||
break;
|
||||
case 1:
|
||||
num = 30;
|
||||
break;
|
||||
case 2:
|
||||
num = 1000;
|
||||
break;
|
||||
case 3:
|
||||
num = 100;
|
||||
break;
|
||||
default:
|
||||
num = 10;
|
||||
break;
|
||||
}
|
||||
|
||||
m.value = (Math.random() + num).toFixed(2);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => (props.title, props.postion),
|
||||
() => {
|
||||
topTitle.value = props.title;
|
||||
pos.value = props.postion;
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.plant-environment-warp {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.data-item-row {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.data-item {
|
||||
height: 100%;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
}
|
||||
.data-warp {
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
.small-bg,
|
||||
.data-pos {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
.data-pos-center {
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.pos-center {
|
||||
}
|
||||
}
|
||||
}
|
||||
.small-bg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
position: relative;
|
||||
margin-top: 10%;
|
||||
.img-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 38%;
|
||||
height: 38%;
|
||||
}
|
||||
}
|
||||
.data-pos {
|
||||
width: calc(100% - 54px);
|
||||
.label,
|
||||
.value {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.label {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
color: #6beff9;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.unit {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="growth-indexes-charts">
|
||||
<custom-echart-mixin :chart-data="handelData" :option="chartsData.option" height="100%" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
let itemStyle = reactive({
|
||||
itemStyle: { borderRadius: [8, 8, 0, 0] },
|
||||
});
|
||||
|
||||
let legendList = reactive(['猪', '牛', '羊', '鸡', '其他']);
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
title: {
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '3%',
|
||||
top: '10%',
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
data: ['货款', '投保'],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: '15',
|
||||
},
|
||||
},
|
||||
calculable: true,
|
||||
xAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
boundaryGap: [0, 0.01],
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
name: '',
|
||||
data: ['种植', '生产加工', '仓储', '其他'],
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
fontSize: 15,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '货款',
|
||||
type: 'bar',
|
||||
barWidth: '10px',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#35d0c0',
|
||||
barBorderRadius: 10,
|
||||
},
|
||||
},
|
||||
data: [100, 150, 580, 850, 500, 3000],
|
||||
},
|
||||
{
|
||||
name: '投保',
|
||||
type: 'bar',
|
||||
barWidth: '10px',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: '#fef906',
|
||||
barBorderRadius: 10,
|
||||
},
|
||||
},
|
||||
data: [200, 300, 650, 880, 300, 3500],
|
||||
},
|
||||
],
|
||||
// color: ['#3685fe', '#41b879', '#ffd500', '#e57373'],
|
||||
// title: {
|
||||
// text: ' ',
|
||||
// textStyle: {
|
||||
// color: '#333',
|
||||
// },
|
||||
// },
|
||||
// legend: {
|
||||
// show: true,
|
||||
// data: legendList,
|
||||
// left: '0', // 距离左侧10%的位置
|
||||
// top: '0', // 垂直居中
|
||||
// itemWidth: 15, // 图例标记的宽度
|
||||
// itemHeight: 8, // 图例标记的高度
|
||||
// textStyle: {
|
||||
// fontSize: 10, // 图例文字的字体大小
|
||||
// color: '#fff', // 图例文字的颜色
|
||||
// },
|
||||
// },
|
||||
// barStyle: {
|
||||
// barWidth: 10,
|
||||
// },
|
||||
// dataZoom: [
|
||||
// // {
|
||||
// // type: 'slider', // 滑动条型数据区域缩放组件
|
||||
// // startValue: 0, // 数据窗口起始值的索引
|
||||
// // endValue: 2, // 数据窗口结束值的索引
|
||||
// // },
|
||||
// // {
|
||||
// // type: 'inside', // 支持鼠标滚轮和触控板缩放和平移
|
||||
// // startValue: 0,
|
||||
// // endValue: 2,
|
||||
// // },
|
||||
// ],
|
||||
// yAxis: [
|
||||
// {
|
||||
// type: 'value',
|
||||
// name: ' ',
|
||||
// axisLabel: {
|
||||
// formatter: '{value}',
|
||||
// },
|
||||
// splitLine: {
|
||||
// show: true, // 显示分割线
|
||||
// lineStyle: {
|
||||
// type: 'dashed', // 设置为虚线
|
||||
// width: 0.5, // 分割线宽度
|
||||
// },
|
||||
// },
|
||||
|
||||
// itemStyle: { fontSize: 8 },
|
||||
// },
|
||||
// ],
|
||||
// grid: {
|
||||
// x: '10%',
|
||||
// x2: '10%',
|
||||
// y: '20%',
|
||||
// y2: '20%',
|
||||
// },
|
||||
},
|
||||
valData: [],
|
||||
});
|
||||
|
||||
const randomVal = (num) => {
|
||||
let list = [];
|
||||
for (let i = 0; i < legendList.length; i++) {
|
||||
let addNum = [10, 8, 2, 5];
|
||||
let val = {
|
||||
name: num + '月',
|
||||
value: Number(Math.random() * 100 + addNum[i]).toFixed(2),
|
||||
seriesType: 'bar',
|
||||
type: legendList[i],
|
||||
stack: num + '月',
|
||||
};
|
||||
let lastVal = {
|
||||
...val,
|
||||
...itemStyle,
|
||||
};
|
||||
list[i] = i < legendList.length - 1 ? val : lastVal;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
let handelData = computed(() => {
|
||||
let list = [];
|
||||
let maxMouth = 12;
|
||||
for (let i = 0; i < maxMouth; i++) {
|
||||
let val = randomVal(i + 1);
|
||||
list = [...list, ...val];
|
||||
}
|
||||
|
||||
list.map((m, indexm) => {
|
||||
return { ...m, value: Number(Number(m.value) + Math.random() + indexm).toFixed(0) };
|
||||
});
|
||||
// console.info('handelData', list);
|
||||
return list;
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.growth-indexes-charts {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="demo health-status-charts" style="height: 90%">
|
||||
<div class="list-item-header item-warp" :style="{ flex: listKeys.length }">
|
||||
<template v-for="(h, indexh) in listKeys" :key="indexh">
|
||||
<div class="item-td" :style="{ width: 'calc(100% / ' + listKeys.length + ')' }">{{ listKeysHeader[h] }}</div>
|
||||
</template>
|
||||
</div>
|
||||
<vue3ScrollSeamless class="scroll-wrap" :class-options="classOptions" :data-list="list">
|
||||
<div v-for="(item, index) in list" :key="index" class="list-item">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-boday item-warp" :style="{ flex: listKeys.length }">
|
||||
<template v-for="(b, indexb) in listKeys" :key="indexb">
|
||||
<div class="item-td" :class="item.status == 1 ? 'td-title' : 'td-warn'" :style="{ width: 'calc(100% / ' + listKeys.length + ')' }">
|
||||
<span v-if="b != 'status'">
|
||||
{{ item[b] }}
|
||||
</span>
|
||||
<el-icon v-else>
|
||||
<Bell></Bell>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</vue3ScrollSeamless>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, reactive } from 'vue';
|
||||
import { vue3ScrollSeamless } from 'vue3-scroll-seamless';
|
||||
const props = defineProps({
|
||||
// items: {
|
||||
// type: Array,
|
||||
// default: () => [],
|
||||
// },
|
||||
});
|
||||
|
||||
let list = reactive([
|
||||
{ diseaseType: '瘟疫', incidenceRate: '23%', coverage: '84%', status: 1 },
|
||||
{ diseaseType: '蓝耳病', incidenceRate: '19%', coverage: '88%', status: 1 },
|
||||
{ diseaseType: '口蹄疫', incidenceRate: '45%', coverage: '68%', status: 1 },
|
||||
{ diseaseType: '链球病菌', incidenceRate: '35%', coverage: '88%', status: 1 },
|
||||
{ diseaseType: '炎症', incidenceRate: '8%', coverage: '98%', status: 1 },
|
||||
{ diseaseType: '寄生虫', incidenceRate: '11%', coverage: '99%', status: 1 },
|
||||
]);
|
||||
|
||||
const listKeys = reactive(['diseaseType', 'incidenceRate', 'coverage', 'status']);
|
||||
const listKeysHeader = reactive({
|
||||
diseaseType: '疾病种类',
|
||||
incidenceRate: '发病率',
|
||||
coverage: '疫苗接种率',
|
||||
status: '预警',
|
||||
});
|
||||
|
||||
const classOptions = {
|
||||
singleHeight: 48,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.health-status-charts {
|
||||
margin-top: 8px;
|
||||
.scroll-wrap {
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
margin: 4px auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item-header {
|
||||
background: #144482;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
.item-td {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
}
|
||||
.list-item-boday {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
.item-td {
|
||||
padding: 4px 6px;
|
||||
&.td-title {
|
||||
color: #6beff9 !important;
|
||||
}
|
||||
|
||||
&.td-warn {
|
||||
color: red !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-warp {
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
.item-td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.list-item {
|
||||
// border-bottom: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
line-height: 18px;
|
||||
|
||||
.list-item-content {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
.demo {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// margin-top: 10px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="irrigation-charts">
|
||||
<div class="charts-content">
|
||||
<div class="water-droplet-bg" :style="{ 'background-image': 'url(' + getAssetsFile('images/plant/bg5.png') + ')' }">
|
||||
<div class="water-droplet">
|
||||
<custom-echart-water-droplet height="100%" :option="option" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="water-droplet-warp">
|
||||
<template v-for="(n, index) in itemlist" :key="index">
|
||||
<div class="water-droplet-item" :style="{ height: 'calc((100% - 20px) / ' + itemlist.length + ')' }">
|
||||
<div class="title" :style="{ 'background-image': 'url(' + getAssetsFile(n.bg1) + ')' }">
|
||||
<div class="title-val" :style="{ color: n.color }">{{ n.title }}</div>
|
||||
</div>
|
||||
<div class="tips" :style="{ 'background-image': 'url(' + getAssetsFile(n.bg2) + ')' }">
|
||||
<span class="tips-val">{{ n.tips }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
let percent = ref(0.6);
|
||||
let itemlist = reactive([
|
||||
{ title: '智能喂水', bg1: 'images/plant/bg8.png', bg2: 'images/plant/bg7.png', tips: '去喂水', color: '#4a90e2ff' },
|
||||
{ title: '智能投喂', bg1: 'images/plant/bg9.png', bg2: 'images/plant/bg7.png', tips: '去投喂', color: '#50e3c2ff' },
|
||||
]);
|
||||
const option = reactive({
|
||||
backgroundColor: 'transparent', //背景色
|
||||
series: [
|
||||
{
|
||||
name: '预估量',
|
||||
type: 'liquidFill',
|
||||
radius: '80%',
|
||||
center: ['50%', '50%'],
|
||||
backgroundStyle: {
|
||||
color: 'transparent',
|
||||
},
|
||||
data: [percent.value, percent.value],
|
||||
amplitude: 12, //水波振幅
|
||||
label: {
|
||||
//标签设置
|
||||
position: ['50%', '45%'],
|
||||
formatter: percent.value * 100 + '%', //显示文本,
|
||||
textStyle: {
|
||||
fontSize: '20px', //文本字号,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
borderDistance: 3,
|
||||
itemStyle: {
|
||||
borderWidth: 1,
|
||||
borderColor: {
|
||||
type: 'linear',
|
||||
x: 1,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 0,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
{
|
||||
offset: 0.6,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
],
|
||||
globalCoord: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear', // 线性渐变
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#45bfe9' },
|
||||
{ offset: 1, color: '#01589c' },
|
||||
],
|
||||
global: false, // 默认为 false
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.irrigation-charts {
|
||||
height: 100%;
|
||||
.charts-content {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.water-droplet-bg {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center bottom;
|
||||
position: relative;
|
||||
.water-droplet {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 10%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.water-droplet-warp {
|
||||
width: 60%;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
flex-direction: column;
|
||||
.water-droplet-item {
|
||||
width: 100%;
|
||||
}
|
||||
.title,
|
||||
.tips {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
vertical-align: top;
|
||||
.tips-val {
|
||||
display: inline-flex;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transform: skewX(-13deg) translateY(-50%);
|
||||
background: linear-gradient(to bottom, '#ff7e5f', '#548fff');
|
||||
-webkit-background-clip: text;
|
||||
color: #fff;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: -6px 0 0 1px #add8f1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-position: left bottom;
|
||||
background-repeat: no-repeat;
|
||||
.title-val {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="monitoring-screen-warp">
|
||||
<div class="monitoring-screen-content" :style="{ 'background-image': 'url(' + getAssetsFile('images/plant/bg2.png') + ')' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.monitoring-screen-warp {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.monitoring-screen-content {
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px 0;
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="notice-bar-warp" :style="{ height: height }">
|
||||
<div class="notice-bar-pos">
|
||||
<div class="notice-icon">
|
||||
<img :src="getAssetsFile('images/plant/icon5.png')" />
|
||||
</div>
|
||||
<div class="notice-bar" :style="{ 'line-height': height }">
|
||||
<div class="scrolling-text">
|
||||
<span>{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '这是一条滚动通知消息,请注意查看!',
|
||||
},
|
||||
height: {
|
||||
type: String || Number,
|
||||
default: '40px',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notice-bar-warp {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.notice-bar-pos {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
}
|
||||
.notice-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
img {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
margin: 10% 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.notice-bar {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.scrolling-text {
|
||||
white-space: nowrap;
|
||||
animation: scroll-left 10s linear infinite;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@keyframes scroll-left {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
</style>
|
116
sub-operation-service/src/views/dashboard/components/plantgs.vue
Normal file
116
sub-operation-service/src/views/dashboard/components/plantgs.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="plant-gs-warp">
|
||||
<div ref="viewwarp" class="plant-gs-content" :style="{ 'background-image': 'url(' + getAssetsFile('images/plant/bg1.png') + ')' }">
|
||||
<vc-config-provider :cesium-path="vcConfig.cesiumPath" :access-token="vcConfig.accessToken">
|
||||
<vc-viewer ref="viewerRef" @ready="onViewerReady">
|
||||
<vc-layer-imagery :minimum-level="12" :maximum-level="18" :contrast="1.8" :saturation="1" :alpha="1">
|
||||
<vc-imagery-provider-amap :key="'c843a50db7157faf295c6fa37c48719f'"></vc-imagery-provider-amap>
|
||||
</vc-layer-imagery>
|
||||
|
||||
<vc-overlay-html ref="html" :position="pos" :show="show">
|
||||
<div class="vc-name">耿马镇</div>
|
||||
</vc-overlay-html>
|
||||
|
||||
<vc-entity description="Hello VueCesium">
|
||||
<vc-graphics-rectangle
|
||||
ref="rectangle2"
|
||||
:coordinates="hierarchy4"
|
||||
:material="[59, 199, 231, 80]"
|
||||
:rotation="0.9"
|
||||
:extruded-height="1000"
|
||||
:height="0"
|
||||
:outline="true"
|
||||
:outline-width="0.1"
|
||||
:clamp-to-ground="false"
|
||||
:outline-color="[59, 199, 231, 150]"
|
||||
></vc-graphics-rectangle>
|
||||
</vc-entity>
|
||||
</vc-viewer>
|
||||
</vc-config-provider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from 'vue';
|
||||
import {
|
||||
VcViewer,
|
||||
VcConfigProvider,
|
||||
VcImageryProviderAmap,
|
||||
VcLayerImagery,
|
||||
VcGraphicsPlane,
|
||||
VcEntity,
|
||||
VcGraphicsPolygon,
|
||||
VcGraphicsRectangle,
|
||||
VcOverlayHtml,
|
||||
} from 'vue-cesium';
|
||||
import 'vue-cesium/dist/index.css';
|
||||
|
||||
const vcConfig = reactive({
|
||||
cesiumPath: 'https://cdn.bootcdn.net/ajax/libs/cesium/1.80.0/Cesium.js',
|
||||
accessToken:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiNjJjZTUxYi1lOTQ3LTQ3YjctOGI3ZS02ZGUzY2E4YWFkNDkiLCJpZCI6Mjg4MjAxLCJpYXQiOjE3NDMwNTY0MzN9.rjHQiqf7Y8bccaqsapqveULVAUH6M1QkeFp-AKG-frA',
|
||||
});
|
||||
const viewerRef = ref(null);
|
||||
const viewwarp = ref(null);
|
||||
const rectangle2 = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
Promise.all([rectangle2.value.creatingPromise]).then((instances) => {
|
||||
// console.info('aa', instances[0].viewer.entities);
|
||||
instances[0].viewer.zoomTo(instances[0].viewer.entities);
|
||||
});
|
||||
});
|
||||
const hierarchy4 = reactive([99.302267, 23.438899, 99.312267, 23.448889]);
|
||||
const pos = reactive([99.302267, 23.438899]);
|
||||
|
||||
const onViewerReady = (readyObj) => {
|
||||
console.info('readyObj', readyObj.Cesium); // Cesium namespace object
|
||||
console.log(readyObj.viewer); // instanceof Cesium.Viewer
|
||||
|
||||
const { Cesium, viewer } = readyObj;
|
||||
|
||||
// 设置相机视角
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(99.516667, 23.640556, 4000), // 目标中心点
|
||||
orientation: {
|
||||
ading: Cesium.Math.toRadians(6), //
|
||||
pitch: Cesium.Math.toRadians(-25), //
|
||||
roll: Cesium.Math.toRadians(6), //
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.plant-gs-warp {
|
||||
height: 100%;
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.plant-gs-content {
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.vc-name {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
::v-deep() {
|
||||
.vc-viewer {
|
||||
height: calc(100% - 30px) !important;
|
||||
width: calc(100% - 20px) !important;
|
||||
margin: 20px 10px 10px 10px !important;
|
||||
}
|
||||
canvas {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
.cesium-viewer-bottom {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="water-intake-charts">
|
||||
<custom-echart-mixin :chart-data="handelData" :option="chartsData.option" height="100%" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
let itemStyle = reactive({
|
||||
itemStyle: { borderRadius: [8, 8, 0, 0] },
|
||||
});
|
||||
|
||||
let legendList = reactive(['饮水量', '饲料量']);
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
color: ['#3685fe', '#41b879', '#ffd500', '#e57373'],
|
||||
title: {
|
||||
text: ' ',
|
||||
textStyle: {
|
||||
color: '#333',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
data: legendList,
|
||||
left: '0', // 距离左侧10%的位置
|
||||
top: '0', // 垂直居中
|
||||
itemWidth: 15, // 图例标记的宽度
|
||||
itemHeight: 8, // 图例标记的高度
|
||||
textStyle: {
|
||||
fontSize: 10, // 图例文字的字体大小
|
||||
color: '#fff', // 图例文字的颜色
|
||||
},
|
||||
},
|
||||
barStyle: {
|
||||
barWidth: 10,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider', // 滑动条型数据区域缩放组件
|
||||
startValue: 0, // 数据窗口起始值的索引
|
||||
endValue: 5, // 数据窗口结束值的索引
|
||||
},
|
||||
{
|
||||
type: 'inside', // 支持鼠标滚轮和触控板缩放和平移
|
||||
startValue: 0,
|
||||
endValue: 5,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: ' ',
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
},
|
||||
splitLine: {
|
||||
show: true, // 显示分割线
|
||||
lineStyle: {
|
||||
type: 'dashed', // 设置为虚线
|
||||
width: 0.5, // 分割线宽度
|
||||
},
|
||||
},
|
||||
|
||||
itemStyle: { fontSize: 8 },
|
||||
},
|
||||
],
|
||||
grid: {
|
||||
x: '10%',
|
||||
x2: '15%',
|
||||
y: '20%',
|
||||
y2: '20%',
|
||||
},
|
||||
},
|
||||
valData: [],
|
||||
});
|
||||
|
||||
const randomVal = (num) => {
|
||||
let list = [];
|
||||
for (let i = 0; i < legendList.length; i++) {
|
||||
let addNum = [10, 8, 2, 5];
|
||||
let val = {
|
||||
name: num + '月',
|
||||
value: Number(Math.random() * 100 + addNum[i]).toFixed(2),
|
||||
seriesType: 'bar',
|
||||
type: legendList[i],
|
||||
...itemStyle,
|
||||
};
|
||||
list[i] = val;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
let handelData = computed(() => {
|
||||
let list = [];
|
||||
let maxMouth = 12;
|
||||
for (let i = 0; i < maxMouth; i++) {
|
||||
let val = randomVal(i + 1);
|
||||
list = [...list, ...val];
|
||||
}
|
||||
|
||||
list.map((m) => {
|
||||
return { ...m, value: Number(m.value + Math.random() + 10).toFixed(2) };
|
||||
});
|
||||
// console.info('handelData', list);
|
||||
return list;
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.water-intake-charts {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,19 +1,166 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="data-home-index">
|
||||
<el-row style="width: 100%; height: 100%">
|
||||
<el-col :span="6" class="left-charts">
|
||||
<div class="left-charts-item">
|
||||
<customBack top-title="智慧种植数据统计" :top-postion="'left'">
|
||||
<template #back>
|
||||
<entitieslist></entitieslist>
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
<div class="left-charts-item">
|
||||
<customBack top-title="电商数据统计" :top-postion="'left'">
|
||||
<template #back>
|
||||
<waterIntakeCharts></waterIntakeCharts>
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
<div class="left-charts-item">
|
||||
<customBack top-title="涉农金融数据统计" :top-postion="'left'">
|
||||
<template #back>
|
||||
<growthIndexesCharts></growthIndexesCharts>
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<businessMap></businessMap>
|
||||
</el-col>
|
||||
<!-- <el-col :span="12" style="height: 100%">
|
||||
<el-row style="height: 67%">
|
||||
<el-col :span="24" class="center-top" style="height: 100%">
|
||||
<div class="notice">
|
||||
<noticeBar :height="'40px'"></noticeBar>
|
||||
</div>
|
||||
<div class="top">
|
||||
<environment></environment>
|
||||
</div>
|
||||
<div class="map-gis">
|
||||
<plantgs></plantgs>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row style="height: 33%" :gutter="30">
|
||||
<el-col :span="12" style="height: 100%">
|
||||
<customBack top-title="健康状况指标" :top-postion="'left'">
|
||||
<template #back>
|
||||
<healthStatusCharts></healthStatusCharts>
|
||||
</template>
|
||||
</customBack>
|
||||
</el-col>
|
||||
<el-col :span="12" style="height: 100%">
|
||||
<customBack top-title="智慧投喂控制" :top-postion="'right'">
|
||||
<template #back>
|
||||
<irrigationCharts></irrigationCharts>
|
||||
</template>
|
||||
</customBack>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col> -->
|
||||
<el-col :span="6" class="right-charts">
|
||||
<div class="right-charts-item">
|
||||
<customBack top-title="分拣包装数据统计" :top-postion="'right'">
|
||||
<template #back>
|
||||
<InventoryCharts></InventoryCharts>
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
<div class="right-charts-item">
|
||||
<customBack top-title="仓储物流数据统计" :top-postion="'right'">
|
||||
<template #back>
|
||||
<div style="display: flex; justify-content: space-evenly; align-items: center">
|
||||
<div style="width: 200px; height: 200px">
|
||||
<benefitCharts></benefitCharts>
|
||||
</div>
|
||||
<div style="width: 200px; height: 200px">
|
||||
<benefitChartsl></benefitChartsl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
<div class="right-charts-item">
|
||||
<customBack top-title="公共品牌数据统计" :top-postion="'right'">
|
||||
<template #back>
|
||||
<businessFour :data="four" />
|
||||
</template>
|
||||
</customBack>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import InventoryCharts from './components/InventoryCharts.vue';
|
||||
import waterIntakeCharts from './components/waterIntakeCharts.vue';
|
||||
import growthIndexesCharts from './components/growthIndexesCharts.vue';
|
||||
import businessFour from './components/businessFour.vue';
|
||||
import plantgs from './components/plantgs.vue';
|
||||
import noticeBar from './components/noticeBar.vue';
|
||||
import irrigationCharts from './components/irrigationCharts.vue';
|
||||
// import healthStatusCharts from './components/healthStatusCharts.vue';
|
||||
import monitoringScreen from './components/monitoringScreen.vue';
|
||||
import benefitCharts from './components/benefitCharts.vue';
|
||||
import benefitChartsl from './components/benefitChartsl.vue';
|
||||
import { reactive } from 'vue';
|
||||
import entitieslist from './components/entitieslist.vue';
|
||||
import businessMap from './components/businessMap.vue';
|
||||
// import deviceCharts from './components/deviceCharts.vue';
|
||||
|
||||
/* --------------- data --------------- */
|
||||
// #region
|
||||
|
||||
// #endregion
|
||||
|
||||
/* --------------- methods --------------- */
|
||||
// #region
|
||||
|
||||
// #endregion
|
||||
let four = reactive({
|
||||
percent: 0.3998,
|
||||
success: 253,
|
||||
warning: 5,
|
||||
danger: 0,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.data-home-index {
|
||||
background-color: black;
|
||||
height: 70vh;
|
||||
margin: 0 10%;
|
||||
.left-charts {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-charts-item {
|
||||
width: 100%;
|
||||
height: calc((100% - 30px) / 3);
|
||||
}
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
.right-charts {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.right-charts-item {
|
||||
width: 100%;
|
||||
height: calc((100% - 30px) / 3);
|
||||
}
|
||||
|
||||
.center-top {
|
||||
padding: 16px;
|
||||
.notice,
|
||||
.top,
|
||||
.map-gis {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.notice {
|
||||
height: 40px;
|
||||
}
|
||||
.top {
|
||||
height: 80px;
|
||||
}
|
||||
.map-gis {
|
||||
height: calc(100% - 140px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -794,7 +794,7 @@ safe-regex-test@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
|
||||
semver@^5.5.0, "semver@2 || 3 || 4 || 5":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz"
|
||||
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
|
||||
|
Loading…
x
Reference in New Issue
Block a user