This commit is contained in:
沈鸿 2025-05-20 13:33:04 +08:00
commit d485768e8f
184 changed files with 30239 additions and 3108 deletions

4
.gitignore vendored
View File

@ -23,3 +23,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
sub-operation-service/package-lock.json
main/package-lock.json
sub-operation-service/vite.config.js
main/.eslintrc.cjs

View File

@ -39,7 +39,12 @@ module.exports = {
},
// 这里时配置规则的,自己看情况配置
rules: {
'prettier/prettier': 'error',
'prettier/prettier': [
'error',
{
endOfLine: 'auto', // 允许自动检测换行符
},
],
'no-debugger': 'off',
'no-unused-vars': 'off',
'vue/multi-word-component-names': 'off',

View File

@ -57,11 +57,11 @@ export const rightApps = [
},
{
// name: 'sub-government-screen-service',
name: 'new-digital-agriculture-screen',
name: 'new-digital-agriculture-screen/v2/land',
// entry: VITE_APP_SUB_GSS,
entry: VITE_APP_SUB_GSR,
// activeRule: '/sub-government-screen-service',
activeRule: '/new-digital-agriculture-screen',
activeRule: '/new-digital-agriculture-screen/v2/land',
title: '数据大屏',
icon: 'images/platform/icon-screen.png',
},

View File

@ -28,7 +28,7 @@ const props = defineProps({
});
const titles = ref([
{ label: '首页', value: 'home' },
// { label: '', value: 'home' },
{ label: '土地资源', value: 'land' },
{ label: '投入品', value: 'inputs' },
{ label: '生产经营主体', value: 'entities' },

View File

@ -54,7 +54,7 @@ const props = defineProps({
type: Array,
default() {
return [
{ label: '首页', value: '/new-digital-agriculture-screen/v2/home' },
// { 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' },

View File

@ -58,10 +58,10 @@ onMounted(() => {
user-select: none;
box-sizing: border-box;
width: 100%;
height: 100;
height: 100%;
background:
url('@/assets/images/basic/containerBG.png') no-repeat center 100%,
url('@/assets/images/basic/containerBotBG.png') no-repeat bottom center;
url('../assets/images/basic/containerBG.png') no-repeat center 100%,
url('../assets/images/basic/containerBotBG.png') no-repeat bottom center;
&-header {
width: 100%;
margin-bottom: 16px;

View File

@ -17,21 +17,22 @@ export const constantRoutes = [
component: () => import('@/views/error/403.vue'),
hidden: true,
},
{
path: '/new-digital-agriculture-screen',
name: 'layout',
component: Layout,
redirect: '/new-digital-agriculture-screen/v2/home',
meta: { title: '首页', icon: 'House' },
children: [
{
path: '/new-digital-agriculture-screen/v2/home',
component: () => import('@/views/home/index.vue'),
name: 'home',
meta: { title: '首页', icon: '' },
},
],
},
// {
// // 原来的首页默认配置
// path: '/new-digital-agriculture-screen',
// name: 'layout',
// component: Layout,
// redirect: '/new-digital-agriculture-screen/v2/home',
// meta: { title: '首页', icon: 'House' },
// children: [
// {
// path: '/new-digital-agriculture-screen/v2/home',
// component: () => import('@/views/home/index.vue'),
// name: 'home',
// meta: { title: '首页', icon: '' },
// },
// ],
// },
// ...demoRouters,
v2,
// {

View File

@ -4,7 +4,7 @@ export default {
path: '/new-digital-agriculture-screen',
name: 'layout',
component: Layout,
redirect: '/new-digital-agriculture-screen/v2/home',
redirect: '/new-digital-agriculture-screen/v2/land', //原home
meta: { title: '首页', icon: '' },
children: [
{

1074
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ VITE_PORT = 9526
VITE_APP_MIAN = 'daimp-front-main'
VITE_APP_MIAN_URL = 'http://localhost:9000'
VITE_APP_NAME = 'sub-operation-service'
VITE_APP_BASE_API = '/platform'
VITE_APP_BASE_API = '/apis'
VITE_APP_BASE_URL = 'http://192.168.18.99:88'
VITE_APP_UPLOAD_API = '/uploadApis'
VITE_APP_UPLOAD_URL = 'http://192.168.18.99:9300'

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",
@ -18,11 +17,18 @@
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@smallwei/avue": "^3.6.2",
"@vuemap/vue-amap": "^2.0",
"@vuemap/vue-amap-loca": "^2.0",
"@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",
"element-plus": "^2.7.2",
"hls.js": "^1.6.2",
"js-base64": "^3.7.6",
"lodash": "^4.17.21",
"moment": "^2.30.1",
@ -31,9 +37,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",

View File

@ -0,0 +1,19 @@
import request from '@/utils/axios';
//农资
//获取农资分类查询数据
export function transaction(params = {}) {
return request('goods/business/category/transactionType?type=1', {
method: 'GET',
params,
});
}
//获取农资列表数据
export function agriculturalList(params) {
return request('goods/business/category/transactionGoodInfo', {
method: 'GET',
params,
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

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

@ -79,24 +79,23 @@ const toPreview = () => {
}
.viewer-btn-warp {
position: absolute;
top: 50%;
left: 0;
display: none;
width: 100%;
text-align: center;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.viewer-btn {
background: $color-balck-mask;
display: inline-block;
padding: 4px 8px;
font-size: 14px;
border-radius: 16px;
color: $color-fff;
font-size: 14px;
background: $color-balck-mask;
cursor: pointer;
}
}
.c-custom-img-warp:hover {
.viewer-btn-warp {
display: inline-block !important;

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: 170px"></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: 170px"></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>

Some files were not shown because too many files have changed in this diff Show More