node版本 18

This commit is contained in:
13713575202 2025-05-20 11:51:08 +08:00
parent 575ec062cb
commit 7b9c9e70a3
62 changed files with 27739 additions and 1771 deletions

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
View 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
View 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']
}
}

View File

@ -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",

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
// Zhvalue
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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">仅允许导入xlsxlsx格式文件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>

View 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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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,
};
}

View 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,
};
};

View 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 };
}

View 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 };
}

View 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;

View File

@ -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>

View File

@ -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);
}
// legendDataseries
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>

View File

@ -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);
}
// legendDataseries
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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

View File

@ -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==