冲突合并
4
.gitignore
vendored
@ -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
|
||||
|
@ -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',
|
||||
|
@ -43,6 +43,7 @@ declare module 'vue' {
|
||||
CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default']
|
||||
CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default']
|
||||
NewHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/new-hyaline-cake.vue')['default']
|
||||
NewPie: typeof import('./src/components/custom-echart-hyaline-cake/new-pie.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SubTop: typeof import('./src/components/subTop.vue')['default']
|
||||
|
@ -9,16 +9,13 @@ import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
defineOptions({ name: 'NewHyalineCake' });
|
||||
|
||||
// 记录当前选中和高亮的系列索引
|
||||
let selectedIndex = null;
|
||||
let hoveredIndex = null;
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
// 默认示例数据
|
||||
{ name: '项目一', value: 60 },
|
||||
{ name: '项目二', value: 44 },
|
||||
{ name: '项目三', value: 32 },
|
||||
@ -27,8 +24,8 @@ const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
k: 1, // 控制内外径关系的系数,1 表示无内径(实心饼),值越小内径越大
|
||||
itemGap: 0.1, // 扇形边缘向外偏移距离比例(用于选中效果),扇形的间距,单位视图坐标,可调
|
||||
k: 1, // 控制内外径关系的系数,1 表示无内径,值越小内径越大
|
||||
itemGap: 0.1, // 扇形的间距
|
||||
itemHeight: 120, // 扇形高度(影响z轴拉伸程度)
|
||||
autoItemHeight: 0, // 自动计算扇形高度时使用的系数(>0时 itemHeight 失效,使用 autoItemHeight * value )
|
||||
opacity: 0.6, // 透明度设置
|
||||
@ -46,17 +43,79 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// 声明组件触发的事件
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// 绑定到DOM的容器引用
|
||||
const chartRef = ref(null);
|
||||
// 使用 useEcharts 钩子初始化 ECharts 实例,并获取控制方法
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
|
||||
// 存储当前的 ECharts 配置项
|
||||
const chartOption = ref({});
|
||||
// pie2d 默认配置
|
||||
const defaultPie2dOption = {
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
show: false,
|
||||
position: 'inside',
|
||||
formatter: '{c}',
|
||||
color: '#fff',
|
||||
distance: 0,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
smooth: false,
|
||||
length: props.option.itemGap * 500,
|
||||
length2: props.option.itemGap * 800,
|
||||
lineStyle: { color: '#fff', width: 1 },
|
||||
},
|
||||
};
|
||||
// 合并父组件传入的 pie2dConfig
|
||||
const pie2dOption = {
|
||||
...defaultPie2dOption,
|
||||
...(props.option.pie2dConfig || {}),
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
// 生成基础的3D饼图配置(也就是默认的配置)
|
||||
const baseOption = getPie3D(props.chartData, props.option.k);
|
||||
// 合并用户配置,确保不修改原始对象
|
||||
const finalOption = merge({}, baseOption, cloneDeep(props.option || {}));
|
||||
chartOption.value = finalOption;
|
||||
|
||||
// 设置图表配置
|
||||
setOptions(chartOption.value);
|
||||
// 等待 DOM + ECharts 初始化完成
|
||||
nextTick(() => {
|
||||
const chart = getInstance();
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免重复绑定事件
|
||||
// chart.off('click');
|
||||
chart.off('mouseover');
|
||||
chart.off('globalout');
|
||||
|
||||
// chart.on('click', handleClick);
|
||||
chart.on('mouseover', handleMouseover);
|
||||
chart.on('globalout', handleGlobalout);
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载后绑定事件
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
// 监听数据或配置变更,重新初始化图表
|
||||
watch(
|
||||
[() => props.chartData, () => props.option],
|
||||
() => {
|
||||
if (props.chartData && props.chartData.length) {
|
||||
initChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
/**
|
||||
* 获取 parametric 曲面方程
|
||||
* @param {Number} startRatio 起点弧度
|
||||
@ -69,8 +128,7 @@ const chartOption = ref({});
|
||||
* @return {Object} parametric 曲面方程
|
||||
*/
|
||||
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h, offsetZ = 0) {
|
||||
console.log('getParametricEquation params :>> ', startRatio, endRatio, isSelected, isHovered, k, h, offsetZ);
|
||||
// 中心弧度用于计算偏移方向
|
||||
// console.log('getParametricEquation params :>> ', startRatio, endRatio, isSelected, isHovered, k, h, offsetZ);
|
||||
const midRatio = (startRatio + endRatio) / 2;
|
||||
const startRadian = startRatio * Math.PI * 2;
|
||||
const endRadian = endRatio * Math.PI * 2;
|
||||
@ -89,7 +147,6 @@ function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h
|
||||
// 计算高亮时的放大比例(未高亮时为1)
|
||||
const hoverRate = isHovered ? 1.05 : 1;
|
||||
|
||||
// 返回 parametric 曲面方程
|
||||
return {
|
||||
u: {
|
||||
// u 参数控制饼的周向角度:从 -π 到 3π,可以完整绘制一圈
|
||||
@ -155,10 +212,6 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
|
||||
const k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3;
|
||||
|
||||
// 计算总值
|
||||
// pieData.forEach((item) => {
|
||||
// sumValue += item.value;
|
||||
// });
|
||||
// 构建每个扇形的 series 数据
|
||||
for (let i = 0; i < pieData.length; i += 1) {
|
||||
sumValue += pieData[i].value;
|
||||
@ -169,13 +222,8 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
parametric: true,
|
||||
wireframe: { show: false },
|
||||
pieData: pieData[i],
|
||||
// itemStyle: {
|
||||
// opacity: props.option.opacity,
|
||||
// borderRadius: 300,
|
||||
// borderColor: '#fff',
|
||||
// }
|
||||
pieStatus: {
|
||||
selected: true,
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k,
|
||||
},
|
||||
@ -189,9 +237,10 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
}
|
||||
series.push(seriesItem);
|
||||
}
|
||||
|
||||
// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
|
||||
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
|
||||
console.log(series);
|
||||
// console.log(series);
|
||||
for (let i = 0; i < series.length; i += 1) {
|
||||
endValue = startValue + series[i].pieData.value;
|
||||
|
||||
@ -212,6 +261,34 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
|
||||
legendData.push(series[i].name);
|
||||
}
|
||||
// 3. 构造透明的 2D pie,用于 legend/label
|
||||
const outerPercent = 50;
|
||||
const innerPercent =
|
||||
internalDiameterRatio != null ? Math.round(internalDiameterRatio * outerPercent) : Math.round(((1 - k) / (1 + k)) * outerPercent);
|
||||
|
||||
const pie2dData = [];
|
||||
for (let i = 0; i < pieData.length; i++) {
|
||||
pie2dData.push({
|
||||
name: pieData[i].name,
|
||||
value: pieData[i].value,
|
||||
// itemStyle: { opacity: 1 },
|
||||
});
|
||||
}
|
||||
series.push({
|
||||
name: 'pie2d',
|
||||
type: 'pie',
|
||||
data: pie2dData,
|
||||
radius: [`${innerPercent}%`, `${outerPercent}%`],
|
||||
center: pie2dOption.center,
|
||||
startAngle: props.option.startAngle || 90,
|
||||
clockwise: false,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
},
|
||||
avoidLabelOverlap: false,
|
||||
label: pie2dOption.label,
|
||||
labelLine: pie2dOption.labelLine,
|
||||
});
|
||||
// 基础配置:坐标轴、视角、图例、提示框等
|
||||
// 准备待返回的配置项,把准备好的 legendData、series 传入。
|
||||
const option = {
|
||||
@ -240,12 +317,12 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
// animation: false,
|
||||
tooltip: {
|
||||
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}`;
|
||||
// 只针对 3D surface 系列
|
||||
if (params.seriesType === 'surface') {
|
||||
// params.seriesIndex 是扇区的索引
|
||||
const serie = chartOption.value.series[params.seriesIndex];
|
||||
const realValue = serie.pieData.value;
|
||||
return `${params.seriesName}:${realValue}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
@ -276,23 +353,6 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
autoRotate: true,
|
||||
distance: 150,
|
||||
},
|
||||
// 后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。
|
||||
postEffect: {
|
||||
// 配置这项会出现锯齿,请自己去查看官方配置有办法解决
|
||||
enable: false,
|
||||
bloom: {
|
||||
enable: true,
|
||||
bloomIntensity: 0.1,
|
||||
},
|
||||
SSAO: {
|
||||
enable: true,
|
||||
quality: 'medium',
|
||||
radius: 2,
|
||||
},
|
||||
// temporalSuperSampling: {
|
||||
// enable: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
series,
|
||||
};
|
||||
@ -301,7 +361,7 @@ function getPie3D(pieData, internalDiameterRatio) {
|
||||
|
||||
// 监听 mouseover,近似实现高亮(放大)效果
|
||||
function handleMouseover(params) {
|
||||
console.log('mouseover');
|
||||
// console.log('mouseover');
|
||||
const idx = params.seriesIndex;
|
||||
const series = chartOption.value.series;
|
||||
|
||||
@ -345,7 +405,7 @@ function updateSeriesHover(index, toHover) {
|
||||
const k = typeof status.k === 'number' ? status.k : 1 / 3;
|
||||
// 计算 newHeight:如果 hover,则加 5,否则按原 height
|
||||
const baseHeight = props.option.autoItemHeight > 0 ? props.option.autoItemHeight : props.option.itemHeight;
|
||||
const newHeight = toHover ? baseHeight + 5 : baseHeight;
|
||||
const newHeight = toHover ? baseHeight + 80 : baseHeight;
|
||||
// 更新 parametricEquation
|
||||
item.parametricEquation = getParametricEquation(start, end, isSelected, toHover, k, newHeight);
|
||||
// 更新状态
|
||||
@ -355,34 +415,6 @@ function updateSeriesHover(index, toHover) {
|
||||
};
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
// 生成基础的3D饼图配置(也就是默认的配置)
|
||||
const baseOption = getPie3D(props.chartData, props.option.k);
|
||||
// 合并用户配置,确保不修改原始对象
|
||||
// const finalOption = Object.assign({}, baseOption, cloneDeep(props.option || {}));
|
||||
const finalOption = merge({}, 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;
|
||||
@ -418,35 +450,10 @@ function handleClick(params) {
|
||||
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: [],
|
||||
};
|
||||
|
||||
// 组件挂载后绑定事件
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
// 监听数据或配置变更,重新初始化图表
|
||||
watch(
|
||||
[() => props.chartData, () => props.option],
|
||||
() => {
|
||||
if (props.chartData && props.chartData.length) {
|
||||
initChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="chart-container" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 内外半径百分比,比如 ['30%', '50%']
|
||||
radius: {
|
||||
type: Array,
|
||||
default: () => ['30%', '50%'],
|
||||
},
|
||||
// 初始俯视角度
|
||||
alpha: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
},
|
||||
// 初始水平旋转角
|
||||
beta: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 是否自动旋转
|
||||
autoRotate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '300px',
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = ref(null);
|
||||
const { setOptions } = useEcharts(chartRef);
|
||||
|
||||
function buildOption(data) {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}:{c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 20,
|
||||
top: 'center',
|
||||
textStyle: { color: '#fff' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie3D',
|
||||
name: '3D 饼图',
|
||||
data,
|
||||
radius: props.radius,
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{c}',
|
||||
},
|
||||
shading: 'realistic', // 光照效果
|
||||
itemStyle: {
|
||||
opacity: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
emphasis: {
|
||||
label: { fontSize: 16, color: '#fff' },
|
||||
itemStyle: { opacity: 1 },
|
||||
},
|
||||
viewControl: {
|
||||
alpha: props.alpha, // 俯仰角
|
||||
beta: props.beta, // 水平旋转角
|
||||
autoRotate: props.autoRotate,
|
||||
distance: 120,
|
||||
zoomSensitivity: 0,
|
||||
panSensitivity: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let option = buildOption(props.chartData);
|
||||
|
||||
onMounted(() => {
|
||||
setOptions(option);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
(v) => {
|
||||
option = buildOption(v);
|
||||
setOptions(option);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: var(--chart-height);
|
||||
}
|
||||
/* 传递 height 到局部变量 */
|
||||
:root {
|
||||
--chart-height: 300px;
|
||||
}
|
||||
</style>
|
@ -42,20 +42,49 @@ const state = reactive({
|
||||
icon: 'rect',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#fff', // 根据你的主题调整颜色
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
display: 'float',
|
||||
top: '120px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
formatter: (name) => name,
|
||||
},
|
||||
pie2dConfig: {
|
||||
center: ['50%', '40%'],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: (params) => {
|
||||
const total = 221.8 + 70.01; // 或动态计算
|
||||
const percent = ((params.value / total) * 100).toFixed(0);
|
||||
return `{name|${params.name}}\n{value|${params.value} 万吨\n(${percent}%)}`;
|
||||
},
|
||||
rich: {
|
||||
name: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#ffffff',
|
||||
lineHeight: 20,
|
||||
align: 'center',
|
||||
},
|
||||
value: {
|
||||
fontSize: 16,
|
||||
color: '#79F5AF',
|
||||
lineHeight: 18,
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
color: '#fff',
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 40,
|
||||
length2: 40,
|
||||
lineStyle: { color: '#fff', width: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
data: [
|
||||
{ value: 76, name: '产业运营平台' },
|
||||
{ value: 24, name: '其它', floatZ: 1 },
|
||||
{ value: 221.8, name: '产业运营平台' },
|
||||
{ value: 70.01, name: '其它', floatZ: 1 },
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
@ -43,6 +43,7 @@ const chartConfig = ref({
|
||||
['圆茄种苗', '0.3元/棵', '0.4元/棵'],
|
||||
['高氮复合肥', '1850元/吨', '1980元/吨'],
|
||||
['硫酸钾', '1250元/吨', '1380元/吨'],
|
||||
['高氮复合肥', '1850元/吨', '1980元/吨'],
|
||||
['西红柿种苗', '0.3元/棵', '0.4元/棵'],
|
||||
],
|
||||
// 样式配置
|
||||
|
@ -4,7 +4,7 @@
|
||||
<section class="_circle">{{ _circleNum }}</section>
|
||||
<section class="_text">{{ allNum }}万亩</section>
|
||||
</section>
|
||||
<section class="_right _scroll">
|
||||
<section ref="scrollRef" class="_right _scroll">
|
||||
<section
|
||||
v-for="(item, i) in list"
|
||||
:key="`right_${i}`"
|
||||
@ -23,23 +23,26 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { ref, watch, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const list = ref([]);
|
||||
const ac = ref(0);
|
||||
const allNum = ref(0);
|
||||
const currNum = ref(0);
|
||||
const scrollRef = ref(null);
|
||||
let timer = null;
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
allNum.value = 0;
|
||||
list.value = props.data.map((v, i) => {
|
||||
allNum.value += v.value;
|
||||
return {
|
||||
@ -49,29 +52,74 @@ watch(
|
||||
};
|
||||
});
|
||||
list.value.sort((a, b) => b.value - a.value);
|
||||
|
||||
if (list.value.length) {
|
||||
ac.value = list.value[0].id;
|
||||
currNum.value = list.value[0].value;
|
||||
scrollToActive();
|
||||
startAutoScroll();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function randomColor() {
|
||||
let obj = {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
obj[key] = Math.floor(Math.random() * 256);
|
||||
});
|
||||
return `rgb(${obj.r},${obj.g},${obj.b})`;
|
||||
return `rgb(${rand255()},${rand255()},${rand255()})`;
|
||||
}
|
||||
function rand255() {
|
||||
return Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
function handleAc(val) {
|
||||
ac.value = val;
|
||||
currNum.value = list.value.find((v) => v.id == val).value ?? 0;
|
||||
const target = list.value.find((v) => v.id == val);
|
||||
currNum.value = target?.value ?? 0;
|
||||
scrollToActive();
|
||||
}
|
||||
|
||||
function scrollToActive() {
|
||||
nextTick(() => {
|
||||
const container = scrollRef.value;
|
||||
const activeEl = container?.querySelector('.list_item.active');
|
||||
if (container && activeEl) {
|
||||
const containerTop = container.scrollTop;
|
||||
const containerHeight = container.clientHeight;
|
||||
const elTop = activeEl.offsetTop;
|
||||
const elHeight = activeEl.offsetHeight;
|
||||
if (elTop < containerTop || elTop + elHeight > containerTop + containerHeight) {
|
||||
container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const _circleNum = computed(() => {
|
||||
let s = ((currNum.value / allNum.value) * 100).toFixed(1);
|
||||
return s + '%';
|
||||
if (!allNum.value) return '0%';
|
||||
return ((currNum.value / allNum.value) * 100).toFixed(1) + '%';
|
||||
});
|
||||
|
||||
function startAutoScroll() {
|
||||
stopAutoScroll();
|
||||
let index = list.value.findIndex((item) => item.id === ac.value);
|
||||
timer = setInterval(() => {
|
||||
index = (index + 1) % list.value.length;
|
||||
handleAc(list.value[index].id);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopAutoScroll() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAutoScroll();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoScroll();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -156,5 +204,8 @@ const _circleNum = computed(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
._right::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,7 +17,7 @@ const state = reactive({
|
||||
option: {
|
||||
k: 0,
|
||||
opacity: 1,
|
||||
itemGap: 0.2,
|
||||
itemGap: 0.1,
|
||||
legendSuffix: '万亩',
|
||||
itemHeight: 200,
|
||||
grid3D: {
|
||||
|
@ -1,344 +0,0 @@
|
||||
let selectedIndex = '';
|
||||
let hoveredIndex = '';
|
||||
option = getPie3D(
|
||||
[
|
||||
{
|
||||
name: 'cc',
|
||||
value: 47,
|
||||
itemStyle: {
|
||||
color: '#f77b66',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'aa',
|
||||
value: 44,
|
||||
itemStyle: {
|
||||
color: '#3edce0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'bb',
|
||||
value: 32,
|
||||
itemStyle: {
|
||||
color: '#f94e76',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ee',
|
||||
value: 16,
|
||||
itemStyle: {
|
||||
color: '#018ef1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dd',
|
||||
value: 23,
|
||||
itemStyle: {
|
||||
color: '#9e60f9',
|
||||
},
|
||||
},
|
||||
],
|
||||
0.59
|
||||
);
|
||||
// 生成扇形的曲面参数方程
|
||||
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) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
isSelected = false;
|
||||
}
|
||||
|
||||
// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
|
||||
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
|
||||
const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
|
||||
const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
|
||||
|
||||
// 计算高亮效果的放大比例(未高亮,则比例为 1)
|
||||
const 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(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;
|
||||
}
|
||||
// 当前图形的高度是Z根据h(每个value的值决定的)
|
||||
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
|
||||
},
|
||||
};
|
||||
}
|
||||
// 生成模拟 3D 饼图的配置项
|
||||
function getPie3D(pieData, internalDiameterRatio) {
|
||||
const series = [];
|
||||
// 总和
|
||||
let sumValue = 0;
|
||||
let startValue = 0;
|
||||
let endValue = 0;
|
||||
const legendData = [];
|
||||
const k =
|
||||
typeof internalDiameterRatio !== 'undefined'
|
||||
? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
|
||||
: 1 / 3;
|
||||
|
||||
// 为每一个饼图数据,生成一个 series-surface 配置
|
||||
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],
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k,
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof pieData[i].itemStyle !== 'undefined') {
|
||||
const { itemStyle } = pieData[i];
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
typeof pieData[i].itemStyle.color !== 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
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,也就是实现每一个扇形。
|
||||
console.log(series);
|
||||
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,
|
||||
// 我这里做了一个处理,使除了第一个之外的值都是10
|
||||
series[i].pieData.value === series[0].pieData.value ? 35 : 10
|
||||
);
|
||||
|
||||
startValue = endValue;
|
||||
|
||||
legendData.push(series[i].name);
|
||||
}
|
||||
|
||||
// 准备待返回的配置项,把准备好的 legendData、series 传入。
|
||||
const option = {
|
||||
// animation: false,
|
||||
tooltip: {
|
||||
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}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
xAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
yAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
zAxis3D: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
},
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 5,
|
||||
top: '-20%',
|
||||
viewControl: {
|
||||
// 3d效果可以放大、旋转等,请自己去查看官方配置
|
||||
alpha: 35,
|
||||
// beta: 30,
|
||||
rotateSensitivity: 1,
|
||||
zoomSensitivity: 0,
|
||||
panSensitivity: 0,
|
||||
autoRotate: true,
|
||||
distance: 150,
|
||||
},
|
||||
// 后处理特效可以为画面添加高光、景深、环境光遮蔽(SSAO)、调色等效果。可以让整个画面更富有质感。
|
||||
postEffect: {
|
||||
// 配置这项会出现锯齿,请自己去查看官方配置有办法解决
|
||||
enable: false,
|
||||
bloom: {
|
||||
enable: true,
|
||||
bloomIntensity: 0.1,
|
||||
},
|
||||
SSAO: {
|
||||
enable: true,
|
||||
quality: 'medium',
|
||||
radius: 2,
|
||||
},
|
||||
// temporalSuperSampling: {
|
||||
// enable: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
series,
|
||||
};
|
||||
return option;
|
||||
}
|
||||
// 修正取消高亮失败的 bug
|
||||
// 监听 mouseover,近似实现高亮(放大)效果
|
||||
myChart.on('mouseover', function (params) {
|
||||
// 准备重新渲染扇形所需的参数
|
||||
let isSelected;
|
||||
let isHovered;
|
||||
let startRatio;
|
||||
let endRatio;
|
||||
let k;
|
||||
let i;
|
||||
|
||||
// 如果触发 mouseover 的扇形当前已高亮,则不做操作
|
||||
if (hoveredIndex === params.seriesIndex) {
|
||||
return;
|
||||
|
||||
// 否则进行高亮及必要的取消高亮操作
|
||||
} else {
|
||||
// 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
|
||||
if (hoveredIndex !== '') {
|
||||
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
|
||||
isSelected = option.series[hoveredIndex].pieStatus.selected;
|
||||
isHovered = false;
|
||||
startRatio = option.series[hoveredIndex].pieData.startRatio;
|
||||
endRatio = option.series[hoveredIndex].pieData.endRatio;
|
||||
k = option.series[hoveredIndex].pieStatus.k;
|
||||
i = option.series[hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10;
|
||||
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
|
||||
option.series[hoveredIndex].parametricEquation = getParametricEquation(
|
||||
startRatio,
|
||||
endRatio,
|
||||
isSelected,
|
||||
isHovered,
|
||||
k,
|
||||
i
|
||||
);
|
||||
option.series[hoveredIndex].pieStatus.hovered = isHovered;
|
||||
|
||||
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
|
||||
hoveredIndex = '';
|
||||
}
|
||||
|
||||
// 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
|
||||
isSelected = option.series[params.seriesIndex].pieStatus.selected;
|
||||
isHovered = true;
|
||||
startRatio = option.series[params.seriesIndex].pieData.startRatio;
|
||||
endRatio = option.series[params.seriesIndex].pieData.endRatio;
|
||||
k = option.series[params.seriesIndex].pieStatus.k;
|
||||
|
||||
// 对当前点击的扇形,执行高亮操作(对 option 更新)
|
||||
option.series[params.seriesIndex].parametricEquation = getParametricEquation(
|
||||
startRatio,
|
||||
endRatio,
|
||||
isSelected,
|
||||
isHovered,
|
||||
k,
|
||||
option.series[params.seriesIndex].pieData.value + 5
|
||||
);
|
||||
option.series[params.seriesIndex].pieStatus.hovered = isHovered;
|
||||
|
||||
// 记录上次高亮的扇形对应的系列号 seriesIndex
|
||||
hoveredIndex = params.seriesIndex;
|
||||
}
|
||||
|
||||
// 使用更新后的 option,渲染图表
|
||||
myChart.setOption(option);
|
||||
}
|
||||
});
|
||||
|
||||
// 修正取消高亮失败的 bug
|
||||
myChart.on('globalout', function () {
|
||||
if (hoveredIndex !== '') {
|
||||
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
|
||||
isSelected = option.series[hoveredIndex].pieStatus.selected;
|
||||
isHovered = false;
|
||||
k = option.series[hoveredIndex].pieStatus.k;
|
||||
startRatio = option.series[hoveredIndex].pieData.startRatio;
|
||||
endRatio = option.series[hoveredIndex].pieData.endRatio;
|
||||
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
|
||||
i = option.series[hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10;
|
||||
option.series[hoveredIndex].parametricEquation = getParametricEquation(
|
||||
startRatio,
|
||||
endRatio,
|
||||
isSelected,
|
||||
isHovered,
|
||||
k,
|
||||
i
|
||||
);
|
||||
option.series[hoveredIndex].pieStatus.hovered = isHovered;
|
||||
|
||||
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
|
||||
hoveredIndex = '';
|
||||
}
|
||||
|
||||
// 使用更新后的 option,渲染图表
|
||||
myChart.setOption(option);
|
||||
});
|
1074
package-lock.json
generated
@ -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
@ -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
@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CenterMap: typeof import('./src/components/centerMap.vue')['default']
|
||||
CodeDialog: typeof import('./src/components/code-dialog/index.vue')['default']
|
||||
copy: typeof import('./src/components/custom-scroll-title copy/index.vue')['default']
|
||||
CostomImg: typeof import('./src/components/costomImg.vue')['default']
|
||||
CustomBack: typeof import('./src/components/customBack.vue')['default']
|
||||
CustomCarouselPicture: typeof import('./src/components/custom-carousel-picture/index.vue')['default']
|
||||
CustomEchartBar: typeof import('./src/components/custom-echart-bar/index.vue')['default']
|
||||
CustomEchartBubble: typeof import('./src/components/custom-echart-bubble/index.vue')['default']
|
||||
CustomEchartColumnLine: typeof import('./src/components/custom-echart-column-line/index.vue')['default']
|
||||
CustomEchartHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/index.vue')['default']
|
||||
CustomEchartLine: typeof import('./src/components/custom-echart-line/index.vue')['default']
|
||||
CustomEchartLineLine: typeof import('./src/components/custom-echart-line-line/index.vue')['default']
|
||||
CustomEchartMaps: typeof import('./src/components/custom-echart-maps/index.vue')['default']
|
||||
CustomEchartMixin: typeof import('./src/components/custom-echart-mixin/index.vue')['default']
|
||||
CustomEchartPictorialBar: typeof import('./src/components/custom-echart-pictorial-bar/index.vue')['default']
|
||||
CustomEchartPie: typeof import('./src/components/custom-echart-pie/index.vue')['default']
|
||||
CustomEchartPie3d: typeof import('./src/components/custom-echart-pie-3d/index.vue')['default']
|
||||
CustomEchartPieGauge: typeof import('./src/components/custom-echart-pie-gauge/index.vue')['default']
|
||||
CustomEchartRadar: typeof import('./src/components/custom-echart-radar/index.vue')['default']
|
||||
CustomEchartScatterBlister: typeof import('./src/components/custom-echart-scatter-blister/index.vue')['default']
|
||||
CustomEchartTriangle: typeof import('./src/components/custom-echart-triangle/index.vue')['default']
|
||||
CustomEchartWaterDroplet: typeof import('./src/components/custom-echart-water-droplet/index.vue')['default']
|
||||
CustomEchartWordCloud: typeof import('./src/components/custom-echart-word-cloud/index.vue')['default']
|
||||
CustomIframe: typeof import('./src/components/custom-iframe/index.vue')['default']
|
||||
CustomImportExcel: typeof import('./src/components/custom-import-excel/index.vue')['default']
|
||||
CustomProgress: typeof import('./src/components/customProgress.vue')['default']
|
||||
CustomRankList: typeof import('./src/components/custom-rank-list/index.vue')['default']
|
||||
CustomRichEditor: typeof import('./src/components/custom-rich-editor/index.vue')['default']
|
||||
CustomScrollBoard: typeof import('./src/components/custom-scroll-board/index.vue')['default']
|
||||
CustomScrollTitle: typeof import('./src/components/custom-scroll-title/index.vue')['default']
|
||||
'CustomScrollTitle copy': typeof import('./src/components/custom-scroll-title copy/index.vue')['default']
|
||||
CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default']
|
||||
CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default']
|
||||
IndexBak: typeof import('./src/components/page-menu/index-bak.vue')['default']
|
||||
NewHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/new-hyaline-cake.vue')['default']
|
||||
PageLayout: typeof import('./src/components/page-layout/index.vue')['default']
|
||||
PageMenu: typeof import('./src/components/page-menu/index.vue')['default']
|
||||
PagePagination: typeof import('./src/components/page-pagination/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SubTop: typeof import('./src/components/subTop.vue')['default']
|
||||
UpFile: typeof import('./src/components/custom-rich-editor/upFile.js')['default']
|
||||
UpImg: typeof import('./src/components/upImg.vue')['default']
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "sub-operation-service",
|
||||
"name": "digital-agriculture-screen",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
@ -7,7 +7,6 @@
|
||||
"dev": "vite --mode development",
|
||||
"build": "vite build --mode production",
|
||||
"test": "vite build --mode test",
|
||||
"pre": "vite build --mode pre",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write 'src/**/*.{vue,ts,tsx,js,jsx,css,less,scss,json,md}'",
|
||||
"eslint": "npx eslint --init",
|
||||
@ -18,10 +17,16 @@
|
||||
"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",
|
||||
@ -32,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",
|
||||
|
19
sub-operation-service/src/apis/agricultural.js
Normal 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,
|
||||
});
|
||||
}
|
BIN
sub-operation-service/src/assets/images/basic/containerBG.png
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
sub-operation-service/src/assets/images/basic/containerBotBG.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
sub-operation-service/src/assets/images/basic/footerBG.png
Normal file
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 10 KiB |
BIN
sub-operation-service/src/assets/images/basic/headerBG.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
sub-operation-service/src/assets/images/basic/leftArrowIcon.png
Normal file
After Width: | Height: | Size: 517 B |
BIN
sub-operation-service/src/assets/images/basic/leftTitleBG.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
sub-operation-service/src/assets/images/basic/rightArrowIcon.png
Normal file
After Width: | Height: | Size: 517 B |
BIN
sub-operation-service/src/assets/images/basic/rightTitleBG.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
sub-operation-service/src/assets/images/basic/tagBG-small.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
sub-operation-service/src/assets/images/basic/tagBG.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (1).png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (2).png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (3).png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (4).png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (5).png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
sub-operation-service/src/assets/images/brand/1 (6).png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
sub-operation-service/src/assets/images/brand/2 (1).png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
sub-operation-service/src/assets/images/brand/2 (2).png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
sub-operation-service/src/assets/images/brand/2 (3).png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
sub-operation-service/src/assets/images/brand/p (1).png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
sub-operation-service/src/assets/images/brand/p (3).png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
sub-operation-service/src/assets/images/brand/sqzs.png
Normal file
After Width: | Height: | Size: 388 KiB |
BIN
sub-operation-service/src/assets/images/brand/生产管理控制.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
sub-operation-service/src/assets/images/brand/用户信息.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
sub-operation-service/src/assets/images/brand/组 1532.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
sub-operation-service/src/assets/images/brand/组 1533.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
sub-operation-service/src/assets/images/brand/识别二维码.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/danger.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/normal.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/分光器.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/悬浮物.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/水质溶解氧.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/水质电导率.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/浊度.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/温度.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
sub-operation-service/src/assets/images/smartFarm/酸碱度.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
sub-operation-service/src/assets/images/userCenter/menu1-1.png
Normal file
After Width: | Height: | Size: 600 B |
BIN
sub-operation-service/src/assets/images/userCenter/menu1.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
sub-operation-service/src/assets/images/userCenter/menu2-1.png
Normal file
After Width: | Height: | Size: 669 B |
BIN
sub-operation-service/src/assets/images/userCenter/menu2.png
Normal file
After Width: | Height: | Size: 804 B |
BIN
sub-operation-service/src/assets/images/userCenter/menu3-1.png
Normal file
After Width: | Height: | Size: 964 B |
BIN
sub-operation-service/src/assets/images/userCenter/menu3.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
sub-operation-service/src/assets/images/vsualized/bottombj.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/gmmap.png
Normal file
After Width: | Height: | Size: 170 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 8.2 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home/area.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 2.2 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home/nav.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 7.5 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home1.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home2.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home3.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/home4.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/homeb.png
Normal file
After Width: | Height: | Size: 812 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/hraderbg.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/mapopup.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/mapopup1.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/marker.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
sub-operation-service/src/assets/images/vsualized/screenbg.png
Normal file
After Width: | Height: | Size: 285 KiB |
18532
sub-operation-service/src/components/530926geo.json
Normal file
279
sub-operation-service/src/components/centerMap.vue
Normal file
@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="map-center-warp">
|
||||
<!-- <img :src="getAssetsFile('images/vsualized/gmmap.png')" class="map-img" /> -->
|
||||
<div class="map-pos">
|
||||
<custom-echart-maps height="100%" width="100%" :option="chartsData.option" :geo="geoData" :name="mapName" @click="mapClick" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="isShow"
|
||||
:title="currentMap.name + dialogTitle"
|
||||
:width="dialogWidth"
|
||||
:style="{ 'background-image': 'url(' + getAssetsFile(bgImageVal) + ')' }"
|
||||
:show-close="false"
|
||||
:before-close="handleClose"
|
||||
custom-class="map-info-dialog"
|
||||
>
|
||||
<template #header="{ close, titleId, titleClass }">
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<slot name="dialogContent"></slot>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { isEmpty, getAssetsFile } from '@/utils';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import geoJsonData from '../components/530926geo.json'; // 根据实际情况调整路径
|
||||
const route = useRoute();
|
||||
const props = defineProps({
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: '首页',
|
||||
},
|
||||
markerData: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
bgImage: {
|
||||
type: String,
|
||||
default: 'images/vsualized/mapopup.png',
|
||||
},
|
||||
dialogWidth: {
|
||||
type: Number,
|
||||
default: 360,
|
||||
},
|
||||
});
|
||||
var iconUrl = getAssetsFile('images/vsualized/gmmap2.png').href;
|
||||
let bgImageVal = computed(() => {
|
||||
return props.bgImage;
|
||||
});
|
||||
const isShow = ref(false);
|
||||
let geoData = geoJsonData;
|
||||
let mapName = ref('ZJ' + route.name);
|
||||
let mapConfig = reactive({
|
||||
map: mapName.value,
|
||||
zoom: 1,
|
||||
viewControl: {
|
||||
distance: 115,
|
||||
alpha: 60,
|
||||
beta: 0,
|
||||
minBeta: -360,
|
||||
maxBeta: 720,
|
||||
// 限制视角,使不能旋转缩放平移
|
||||
// rotateSensitivity: 0,
|
||||
// zoomSensitivity: 0,
|
||||
// panSensitivity: 0,
|
||||
},
|
||||
// itemStyle: {
|
||||
// // 三维地理坐标系组件 中三维图形的视觉属性,包括颜色,透明度,描边等。
|
||||
// color: 'rgba(75,255,180,0.2)', // 地图板块的颜色
|
||||
// opacity: 1, // 图形的不透明度 [ default: 1 ]
|
||||
// borderWidth: 1.5, // (地图板块间的分隔线)图形描边的宽度。加上描边后可以更清晰的区分每个区域 [ default: 0 ]
|
||||
// borderColor: '#4bffb4', // 图形描边的颜色。[ default: #333 ]
|
||||
// },
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderColor: '#4bffb4', // 设置镇边界的颜色
|
||||
borderWidth: 1, // 设置镇边界的宽度
|
||||
areaColor: 'rgba(75,255,180,0.2)', // 设置背景透明,只显示边界
|
||||
},
|
||||
emphasis: {
|
||||
borderColor: '#4bffb4',
|
||||
borderWidth: 2,
|
||||
areaColor: 'rgba(75,255,180,0.5)', // 设置背景透明,只显示边界
|
||||
label: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
// 选中样式:ml-citation{ref="2,7" data="citationList"}
|
||||
itemStyle: {
|
||||
areaColor: 'rgba(75,255,180,0.6)',
|
||||
borderColor: '#4bffb4',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
|
||||
label: {
|
||||
show: true,
|
||||
distance: 0,
|
||||
color: '#fff',
|
||||
padding: [6, 4, 2, 4],
|
||||
borderRadius: 4,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff', // 地图初始化区域字体颜色
|
||||
borderWidth: 0,
|
||||
borderColor: '#000',
|
||||
},
|
||||
},
|
||||
// emphasis: {
|
||||
// //高亮状态的效果
|
||||
// label: {
|
||||
// show: true,
|
||||
// color: '#fff',
|
||||
// },
|
||||
// itemStyle: {
|
||||
// color: 'rgba(75,255,180,0.3)', // 地图板块的颜色
|
||||
// },
|
||||
// },
|
||||
});
|
||||
|
||||
const chartsData = reactive({
|
||||
option: {
|
||||
title: {
|
||||
text: '',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (params) {
|
||||
if (params.seriesType === 'effectScatter') {
|
||||
return `${params.name}: (${params.value[0]}, ${params.value[1]})`;
|
||||
}
|
||||
return params.name;
|
||||
},
|
||||
},
|
||||
toolbox: {
|
||||
show: false,
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
top: 'center',
|
||||
feature: {
|
||||
// dataView: { readOnly: false },
|
||||
// restore: {},
|
||||
// saveAsImage: {},
|
||||
},
|
||||
},
|
||||
geo: {
|
||||
roam: true,
|
||||
left: '30px',
|
||||
show: false,
|
||||
zlevel: -1, // 必须设置,
|
||||
...mapConfig,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
roam: true,
|
||||
type: 'map',
|
||||
left: '30px',
|
||||
...mapConfig,
|
||||
},
|
||||
{
|
||||
name: '闪烁散点',
|
||||
type: 'effectScatter', // 使用 effectScatter 类型
|
||||
coordinateSystem: 'geo',
|
||||
data: props.markerData,
|
||||
symbolSize: function (val) {
|
||||
return val[2] ? val[2] / 10 : 10; // 如果没有数值,默认大小
|
||||
},
|
||||
label: {
|
||||
formatter: '{b}',
|
||||
position: 'right',
|
||||
show: false,
|
||||
},
|
||||
rippleEffect: {
|
||||
period: 4, // 波纹动画周期
|
||||
scale: 3, // 波纹缩放比例
|
||||
brushType: 'stroke', // 波纹绘制方式:'stroke' 或 'fill'
|
||||
},
|
||||
hoverAnimation: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let currentMap = reactive({});
|
||||
|
||||
const mapClick = (data) => {
|
||||
if (props.markerData.length && props.markerData.length > 0) {
|
||||
if (data.seriesType == 'effectScatter') {
|
||||
isShow.value = true;
|
||||
currentMap = data;
|
||||
emit('mapclick', currentMap);
|
||||
}
|
||||
} else {
|
||||
isShow.value = true;
|
||||
currentMap = data;
|
||||
emit('mapclick', currentMap);
|
||||
}
|
||||
};
|
||||
const handleClose = () => {
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const emit = defineEmits(['mapclick']);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::v-deep() {
|
||||
.el-dialog {
|
||||
background: url(iconUrl) no-repeat left top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 8px !important;
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
background-size: 100% 100%;
|
||||
padding: 16px;
|
||||
margin-top: 15%;
|
||||
}
|
||||
.el-dialog__header {
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
padding-left: 48px;
|
||||
.el-dialog__title,
|
||||
i {
|
||||
color: #fff !important;
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
top: 8px !important;
|
||||
}
|
||||
}
|
||||
.map-dialog-my-header {
|
||||
margin-top: 4px;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
h4 {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.map-center-warp {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 90%;
|
||||
.map-img {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
object-fit: contain;
|
||||
transform: translateX(-50%);
|
||||
max-width: 1000px;
|
||||
max-height: 1000px;
|
||||
}
|
||||
.map-pos {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
54
sub-operation-service/src/components/code-dialog/index.vue
Normal file
@ -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>
|
@ -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;
|
||||
|
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="carousel" style="width: 500px">
|
||||
<el-carousel
|
||||
v-if="type === 'image'"
|
||||
ref="carouselRef"
|
||||
:interval="option.interval"
|
||||
:arrow="option.arrow"
|
||||
:indicator-position="option.indicatorPosition"
|
||||
@change="handleChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in data" :key="index">
|
||||
<img :src="item.image" style="width: 100%; height: auto" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<div v-if="type === 'video'" class="carousel-video">
|
||||
<!-- <img :src="state.videoPicture" class="carousel-video-picture" /> -->
|
||||
<video ref="videoRef" controls class="carousel-video-video" width="100%" height="100%" @ended="pauseVideo">
|
||||
<source :src="state.videoUrl" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<!-- <span class="carousel-video-btn" @click="handlePlay">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<div class="carousel-container">
|
||||
<span class="carousel-arrow carousel-arrow-left" @click="handleLeftClick">
|
||||
<el-icon><ArrowLeftBold /></el-icon>
|
||||
</span>
|
||||
<el-scrollbar ref="scrollbarRef" class="carousel-list">
|
||||
<div
|
||||
v-for="(item, index) in data"
|
||||
:key="index"
|
||||
:class="`carousel-list-item ${state.current === index ? 'active' : ''}`"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<el-image style="width: 100px; height: 100px" :src="item.image ?? item" fit="cover" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<span class="carousel-arrow carousel-arrow-right" @click="handleRightClick">
|
||||
<el-icon><ArrowRightBold /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="custom-carousel-picture">
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
type: { type: String, default: 'image' },
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
height: '',
|
||||
initialIndex: 0,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
interval: 3000,
|
||||
arrow: 'never',
|
||||
indicatorPosition: 'none',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['']);
|
||||
|
||||
const carouselRef = ref(null);
|
||||
const scrollbarRef = ref(null);
|
||||
const videoRef = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
current: 0,
|
||||
isReady: false,
|
||||
videoPicture: '',
|
||||
videoUrl: '',
|
||||
});
|
||||
|
||||
const handleChange = (cur, last) => {
|
||||
state.current = cur;
|
||||
};
|
||||
|
||||
const handleLeftClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index + 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft - 120);
|
||||
};
|
||||
|
||||
const handleRightClick = () => {
|
||||
// const index = carouselRef.value.activeIndex;
|
||||
// carouselRef.value.setActiveItem(index - 1);
|
||||
scrollbarRef.value.setScrollLeft(scrollbarRef.value.wrapRef.scrollLeft + 120);
|
||||
};
|
||||
|
||||
const playVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.play();
|
||||
}
|
||||
};
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (videoRef.value) {
|
||||
videoRef.value.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (index) => {
|
||||
const { type, data } = props;
|
||||
switch (type) {
|
||||
case 'image': {
|
||||
carouselRef.value.setActiveItem(index);
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
const url = data[index];
|
||||
state.videoUrl = url;
|
||||
playVideo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
playVideo();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.carousel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
&-list {
|
||||
flex: 3;
|
||||
:deep(.el-scrollbar__bar) {
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.el-scrollbar__view) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
&-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
&-arrow {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
.el-icon {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&-video {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
&-picture {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
&-video {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
}
|
||||
&-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -25px;
|
||||
margin-left: -25px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
background: rgb(0 0 0 / 20%);
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
.el-icon {
|
||||
font-size: 32px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
115
sub-operation-service/src/components/custom-echart-bar/index.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
export default {
|
||||
name: 'CustomEchartBar',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'bar',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
isSeries: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
let typeArr = Array.from(new Set(props.chartData.map((item) => item.type)));
|
||||
let xAxisData = Array.from(new Set(props.chartData.map((item) => item.name)));
|
||||
let seriesData = [];
|
||||
typeArr.forEach((type, index) => {
|
||||
const barStyle = props.option?.barStyle ?? {};
|
||||
let obj = { name: type, type: props.type, ...barStyle };
|
||||
let data = [];
|
||||
xAxisData.forEach((x) => {
|
||||
let dataArr = props.chartData.filter((item) => type === item.type && item.name == x);
|
||||
if (dataArr && dataArr.length > 0) {
|
||||
data.push(dataArr[0].value);
|
||||
} else {
|
||||
data.push(null);
|
||||
}
|
||||
});
|
||||
obj['data'] = data;
|
||||
if (props.option?.color) {
|
||||
obj.color = props.option?.color[index];
|
||||
}
|
||||
seriesData.push(obj);
|
||||
});
|
||||
option.series = props.isSeries && option.series.length > 0 ? option.series : seriesData;
|
||||
option.xAxis.data = xAxisData;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watch, watchEffect } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'customEchartBubble',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, resize } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
formatter: function (params) {
|
||||
console.log(params);
|
||||
var str = params.marker + '' + params.data.name + '</br>' + '交易额:' + params.data.value + '万元</br>';
|
||||
return str;
|
||||
},
|
||||
},
|
||||
animationDurationUpdate: function (idx) {
|
||||
// 越往后的数据延迟越大
|
||||
return idx * 100;
|
||||
},
|
||||
animationEasingUpdate: 'bounceIn',
|
||||
color: ['#fff', '#fff', '#fff'],
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
force: {
|
||||
repulsion: 80,
|
||||
edgeLength: 20,
|
||||
},
|
||||
roam: true,
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
data: props.chartData,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.size,
|
||||
() => {
|
||||
console.info(' option.series[0]', props.chartData);
|
||||
resize();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function initCharts() {
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = props.chartData;
|
||||
setOptions(option);
|
||||
resize();
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ height, width }"></div>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, reactive, watchEffect, watch } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
export default {
|
||||
name: 'CustomEchartLine',
|
||||
props: {
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 78px)',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null);
|
||||
const { setOptions, getInstance, startAutoPlay } = useEcharts(chartRef);
|
||||
const option = reactive({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 30,
|
||||
},
|
||||
grid: {
|
||||
top: 60,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [],
|
||||
});
|
||||
const xData = ref([]);
|
||||
const yDataColumn = ref([]);
|
||||
const yDataLine = ref([]);
|
||||
|
||||
watchEffect(() => {
|
||||
props.chartData && initCharts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
() => {
|
||||
console.info('props.chartData变化', props.chartData);
|
||||
props.chartData && initCharts();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function hexToRGBA(hex, alpha = 1) {
|
||||
let hexCode = hex.replace('#', '');
|
||||
if (hexCode.length === 3) {
|
||||
hexCode = hexCode
|
||||
.split('')
|
||||
.map((char) => char + char)
|
||||
.join('');
|
||||
}
|
||||
const r = parseInt(hexCode.slice(0, 2), 16);
|
||||
const g = parseInt(hexCode.slice(2, 4), 16);
|
||||
const b = parseInt(hexCode.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function setAreaStyle(color) {
|
||||
return {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRGBA(color, 1),
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRGBA(color, 0.2),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
xData.value = props.chartData.map((item) => item.name);
|
||||
yDataColumn.value = props.chartData.map((item) => item.value1);
|
||||
yDataLine.value = props.chartData.map((item) => item.value);
|
||||
if (props.option) {
|
||||
Object.assign(option, cloneDeep(props.option));
|
||||
}
|
||||
option.series[0].data = yDataColumn.value;
|
||||
option.series[1].data = yDataLine.value;
|
||||
option.xAxis.data = xData.value;
|
||||
setOptions(option);
|
||||
startAutoPlay({
|
||||
interval: 2000,
|
||||
seriesIndex: 0,
|
||||
showTooltip: true,
|
||||
});
|
||||
getInstance()?.off('click', onClick);
|
||||
getInstance()?.on('click', onClick);
|
||||
}
|
||||
|
||||
function onClick(params) {
|
||||
emit('click', params);
|
||||
}
|
||||
|
||||
return { chartRef };
|
||||
},
|
||||
};
|
||||
</script>
|
@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 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>
|
@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ width: width, height: height }"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, nextTick } from 'vue';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useEcharts } from '@/hooks/useEcharts';
|
||||
|
||||
defineOptions({ name: 'NewHyalineCake' });
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
// 默认示例数据
|
||||
{ name: '项目一', value: 60 },
|
||||
{ name: '项目二', value: 44 },
|
||||
{ name: '项目三', value: 32 },
|
||||
],
|
||||
},
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
// 控制内外径关系的系数,1 表示无内径(实心饼),值越小内径越大
|
||||
k: 1,
|
||||
// 扇形边缘向外偏移距离比例(用于选中效果),单位视图坐标,可调
|
||||
itemGap: 0.2,
|
||||
// 扇形高度(影响z轴拉伸程度)
|
||||
itemHeight: 120,
|
||||
// 自动计算扇形高度时使用的系数(>0时 itemHeight 失效,使用 autoItemHeight * value )
|
||||
autoItemHeight: 0,
|
||||
// 透明度设置
|
||||
opacity: 0.6,
|
||||
// 图例后缀
|
||||
legendSuffix: '',
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
// 声明组件触发的事件
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// 绑定到DOM的容器引用
|
||||
const chartRef = ref(null);
|
||||
// 使用 useEcharts 钩子初始化 ECharts 实例,并获取控制方法
|
||||
const { setOptions, getInstance } = useEcharts(chartRef);
|
||||
|
||||
// 存储当前的 ECharts 配置项
|
||||
const chartOption = ref({});
|
||||
|
||||
// 参数方程函数:生成每个扇形曲面的参数方程,用于 series-surface.parametricEquation
|
||||
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
|
||||
// 中心弧度用于计算偏移方向
|
||||
const midRatio = (startRatio + endRatio) / 2;
|
||||
const startRadian = startRatio * Math.PI * 2;
|
||||
const endRadian = endRatio * Math.PI * 2;
|
||||
const midRadian = midRatio * Math.PI * 2;
|
||||
|
||||
// 如果整个饼只剩一个扇形,则不实现选中效果
|
||||
if (startRatio === 0 && endRatio === 1) {
|
||||
isSelected = false;
|
||||
}
|
||||
// k 取默认值 1/3(环的厚度比例),如果传入 k 则使用传入值
|
||||
k = typeof k !== 'undefined' ? k : 1 / 3;
|
||||
|
||||
// 计算选中效果的偏移量(基于扇形中心角度)
|
||||
const offsetX = isSelected ? Math.cos(midRadian) * props.option.itemGap : 0;
|
||||
const offsetY = isSelected ? Math.sin(midRadian) * props.option.itemGap : 0;
|
||||
// 计算高亮时的放大比例(未高亮时为1)
|
||||
const hoverRate = isHovered ? 1.05 : 1;
|
||||
|
||||
// 返回 parametric 曲面方程
|
||||
return {
|
||||
u: {
|
||||
// u 参数控制饼的周向角度:从 -π 到 3π,可以完整绘制一圈
|
||||
min: -Math.PI,
|
||||
max: Math.PI * 3,
|
||||
step: Math.PI / 32,
|
||||
},
|
||||
v: {
|
||||
// v 从 0 - 2π 参数控制扇形的径向方向(厚度方向)
|
||||
min: 0,
|
||||
max: Math.PI * 2,
|
||||
step: Math.PI / 20,
|
||||
},
|
||||
x(u, v) {
|
||||
// 如果在起始弧度之前,保持扇形起点的半径
|
||||
if (u < startRadian) {
|
||||
return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
// 如果在结束弧度之后,保持扇形终点的半径
|
||||
if (u > endRadian) {
|
||||
return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
// 扇形中间部分正常绘制
|
||||
return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
y(u, v) {
|
||||
if (u < startRadian) {
|
||||
return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
if (u > endRadian) {
|
||||
return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
}
|
||||
return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
|
||||
},
|
||||
z(u, v) {
|
||||
// 在 u < -π/2 时,防止出现不必要的翻转,直接使用基本正弦
|
||||
if (u < -Math.PI * 0.5) {
|
||||
return Math.sin(u);
|
||||
}
|
||||
// 在 u > 2.5π 时,处理扇形尾部厚度
|
||||
if (u > Math.PI * 2.5) {
|
||||
return Math.sin(u) * h * 0.1;
|
||||
}
|
||||
// 正常情况下,z 根据 v 参数控制上下表面的高度差
|
||||
// 当前图形的高度是Z根据h(每个value的值决定的)
|
||||
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 生成整个 3D 饼图的配置项
|
||||
function getPie3D(pieData) {
|
||||
const series = [];
|
||||
let sumValue = 0;
|
||||
// 计算总值
|
||||
pieData.forEach((item) => {
|
||||
sumValue += item.value;
|
||||
});
|
||||
|
||||
// k 为外径厚度系数
|
||||
const k = props.option.k ?? 1;
|
||||
// 构建每个扇形的 series 数据
|
||||
pieData.forEach((dataItem, idx) => {
|
||||
const seriesItem = {
|
||||
name: dataItem.name ?? `series${idx}`,
|
||||
type: 'surface',
|
||||
parametric: true,
|
||||
wireframe: { show: false },
|
||||
pieData: dataItem,
|
||||
itemStyle: {
|
||||
opacity: props.option.opacity,
|
||||
borderRadius: 300,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 0,
|
||||
},
|
||||
pieStatus: {
|
||||
selected: false,
|
||||
hovered: false,
|
||||
k,
|
||||
},
|
||||
};
|
||||
// 合并自定义样式(如果有)
|
||||
if (dataItem.itemStyle) {
|
||||
const customStyle = {};
|
||||
if (dataItem.itemStyle.color !== undefined) {
|
||||
customStyle.color = dataItem.itemStyle.color;
|
||||
}
|
||||
if (dataItem.itemStyle.opacity !== undefined) {
|
||||
customStyle.opacity = dataItem.itemStyle.opacity;
|
||||
}
|
||||
seriesItem.itemStyle = { ...seriesItem.itemStyle, ...customStyle };
|
||||
}
|
||||
series.push(seriesItem);
|
||||
});
|
||||
|
||||
// 计算每个扇形的 startRatio/endRatio 和参数方程
|
||||
let startValue = 0;
|
||||
series.forEach((serie) => {
|
||||
const endValue = startValue + serie.pieData.value;
|
||||
const startRatio = startValue / sumValue;
|
||||
const endRatio = endValue / sumValue;
|
||||
serie.pieData.startRatio = startRatio;
|
||||
serie.pieData.endRatio = endRatio;
|
||||
// 初始均为未选中未高亮
|
||||
serie.parametricEquation = getParametricEquation(
|
||||
startRatio,
|
||||
endRatio,
|
||||
false,
|
||||
true,
|
||||
k,
|
||||
// 扇形高度:根据配置自动计算或使用固定高度
|
||||
props.option.autoItemHeight > 0 ? props.option.autoItemHeight * serie.pieData.value : props.option.itemHeight
|
||||
);
|
||||
startValue = endValue;
|
||||
});
|
||||
|
||||
// 添加一个透明圆环,用于实现 hover 在扇形外面时取消高亮
|
||||
series.push({
|
||||
name: 'mouseoutSeries',
|
||||
type: 'surface',
|
||||
parametric: true,
|
||||
wireframe: { show: false },
|
||||
itemStyle: { opacity: 0 },
|
||||
parametricEquation: {
|
||||
u: { min: 0, max: Math.PI * 2, step: Math.PI / 20 },
|
||||
v: { min: 0, max: Math.PI, step: Math.PI / 20 },
|
||||
x(u, v) {
|
||||
return Math.sin(v) * Math.sin(u) + Math.sin(u);
|
||||
},
|
||||
y(u, v) {
|
||||
return Math.sin(v) * Math.cos(u) + Math.cos(u);
|
||||
},
|
||||
z(u, v) {
|
||||
return Math.cos(v) > 0 ? 0.1 : -0.1;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 基础配置:坐标轴、视角、图例、提示框等
|
||||
const option = Object.assign(
|
||||
{
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(18, 55, 85, 0.8)',
|
||||
borderColor: '#35d0c0',
|
||||
color: '#fff',
|
||||
position: (point, params, dom, rect, size) => {
|
||||
// 动态调整 tooltip 位置,避免溢出
|
||||
let x = point[0],
|
||||
y = point[1];
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [boxW, boxH] = size.contentSize;
|
||||
if (x + boxW > viewW) x -= boxW;
|
||||
if (y + boxH > viewH) y -= boxH;
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
return [x, y];
|
||||
},
|
||||
formatter: (params) => {
|
||||
// 只对非透明环(实际扇形)显示信息
|
||||
if (params.seriesName !== 'mouseoutSeries') {
|
||||
return `
|
||||
<span style="color:#FFF">
|
||||
${params.seriesName}<br/>
|
||||
<span style="
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
border-radius:10px;
|
||||
width:10px;
|
||||
height:10px;
|
||||
background-color:${params.color};"></span>
|
||||
${chartOption.value.series[params.seriesIndex].pieData.value}
|
||||
</span>`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
xAxis3D: { min: -1, max: 1 },
|
||||
yAxis3D: { min: -1, max: 1 },
|
||||
zAxis3D: { min: -1, max: 1 },
|
||||
grid3D: {
|
||||
show: false,
|
||||
boxHeight: 5,
|
||||
top: '0',
|
||||
left: '-20%',
|
||||
viewControl: {
|
||||
// 3D 视角设置
|
||||
alpha: 60, // 俯仰角度
|
||||
distance: 240, // 视角距离
|
||||
rotateSensitivity: 10,
|
||||
zoomSensitivity: 10,
|
||||
panSensitivity: 10,
|
||||
autoRotate: true,
|
||||
autoRotateAfterStill: 2,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
selectedMode: false,
|
||||
right: '5%',
|
||||
top: '25%',
|
||||
orient: 'vertical',
|
||||
icon: 'circle',
|
||||
itemHeight: 12,
|
||||
itemWidth: 12,
|
||||
itemGap: 10,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
},
|
||||
// 图例标签显示名称和数值
|
||||
formatter: (name) => {
|
||||
const item = props.chartData.find((d) => d.name === name);
|
||||
return item ? ` ${name} ${item.value}${props.option.legendSuffix || ''}` : name;
|
||||
},
|
||||
},
|
||||
series,
|
||||
},
|
||||
// 将外部配置项覆盖基础配置
|
||||
props.option
|
||||
);
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
// 生成基础的3D饼图配置
|
||||
const baseOption = getPie3D(props.chartData);
|
||||
// 合并用户配置,确保不修改原始对象
|
||||
const finalOption = Object.assign({}, baseOption, cloneDeep(props.option || {}));
|
||||
chartOption.value = finalOption;
|
||||
// 设置图表配置
|
||||
setOptions(chartOption.value);
|
||||
// 等待 DOM + ECharts 初始化完成
|
||||
nextTick(() => {
|
||||
const chart = getInstance();
|
||||
if (!chart) {
|
||||
console.warn('ECharts 实例未初始化,事件绑定失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免重复绑定事件
|
||||
chart.off('click');
|
||||
chart.off('mouseover');
|
||||
chart.off('globalout');
|
||||
|
||||
chart.on('click', handleClick);
|
||||
chart.on('mouseover', handleMouseover);
|
||||
chart.on('globalout', handleGlobalout);
|
||||
});
|
||||
}
|
||||
function handleClick(params) {
|
||||
// 如果点击的是透明辅助环,则忽略
|
||||
if (params.seriesName === 'mouseoutSeries') return;
|
||||
|
||||
const optionVal = chartOption.value;
|
||||
const series = optionVal.series;
|
||||
const idx = params.seriesIndex;
|
||||
|
||||
// 新的选中状态(取反)
|
||||
const isSelected = !series[idx].pieStatus.selected;
|
||||
const isHovered = series[idx].pieStatus.hovered;
|
||||
const k = series[idx].pieStatus.k;
|
||||
const startRatio = series[idx].pieData.startRatio;
|
||||
const endRatio = series[idx].pieData.endRatio;
|
||||
const h = series[idx].pieData.value;
|
||||
|
||||
// 如果之前有其他扇形被选中,取消其选中状态
|
||||
if (selectedIndex !== null && selectedIndex !== idx) {
|
||||
const prev = series[selectedIndex];
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
false,
|
||||
false,
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
prev.pieStatus.selected = false;
|
||||
selectedIndex = null;
|
||||
}
|
||||
|
||||
// 设置当前扇形的选中/取消选中状态
|
||||
series[idx].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h);
|
||||
series[idx].pieStatus.selected = isSelected;
|
||||
|
||||
// 更新记录选中的系列索引
|
||||
selectedIndex = isSelected ? idx : null;
|
||||
|
||||
// 更新图表配置
|
||||
setOptions(optionVal);
|
||||
// 触发组件 click 事件供父组件使用
|
||||
emit('click', params);
|
||||
}
|
||||
// 在函数顶部声明(确保作用域覆盖所有需要的地方)
|
||||
window.debugZValues = {
|
||||
current: null,
|
||||
history: [],
|
||||
};
|
||||
function handleMouseover(params) {
|
||||
if (params.seriesName === 'mouseoutSeries') return;
|
||||
|
||||
const chart = getInstance();
|
||||
const optionVal = chart.getOption(); // 改用实时获取最新配置
|
||||
const series = optionVal.series;
|
||||
const idx = params.seriesIndex;
|
||||
const hh = series[idx].parametricEquation.z();
|
||||
window.debugZValues.current = hh;
|
||||
window.debugZValues.history.push({
|
||||
event: 'mouseover',
|
||||
series: series[idx].name,
|
||||
zValue: hh,
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
console.log('当前Z值:', hh, '历史记录:', window.debugZValues.history);
|
||||
// 打印移入前的完整状态(调试用)
|
||||
console.log(
|
||||
'[移入] 当前所有扇形状态',
|
||||
series.map((s) => ({
|
||||
name: s.name,
|
||||
hovered: s.pieStatus?.hovered,
|
||||
selected: s.pieStatus?.selected,
|
||||
height: s.parametricEquation?.z(Math.PI, Math.PI),
|
||||
}))
|
||||
);
|
||||
|
||||
// 取消之前的高亮(确保只修改状态,不破坏参数方程)
|
||||
if (hoveredIndex !== null && hoveredIndex !== idx) {
|
||||
const prev = series[hoveredIndex];
|
||||
prev.pieStatus.hovered = false; // 仅修改状态
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
// 重新生成正确方程
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
prev.pieStatus.selected,
|
||||
false, // isHovered=false
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
}
|
||||
|
||||
// 设置新高亮
|
||||
const current = series[idx];
|
||||
current.pieStatus.hovered = true;
|
||||
current.parametricEquation = getParametricEquation(
|
||||
current.pieData.startRatio,
|
||||
current.pieData.endRatio,
|
||||
current.pieStatus.selected,
|
||||
true, // isHovered=true
|
||||
current.pieStatus.k,
|
||||
current.pieData.value
|
||||
);
|
||||
|
||||
hoveredIndex = idx;
|
||||
chart.setOption({ series }); // 仅更新series部分
|
||||
}
|
||||
function handleGlobalout() {
|
||||
if (hoveredIndex !== null) {
|
||||
const chart = getInstance();
|
||||
const optionVal = chart.getOption();
|
||||
const series = optionVal.series;
|
||||
const prev = series[hoveredIndex];
|
||||
|
||||
// 打印修复前的错误状态(调试用)
|
||||
console.warn('[修复前] 异常状态', {
|
||||
name: prev.name,
|
||||
z: prev.parametricEquation.z(Math.PI, Math.PI),
|
||||
equation: prev.parametricEquation,
|
||||
});
|
||||
|
||||
// 重置状态并重新生成方程
|
||||
prev.pieStatus.hovered = false;
|
||||
prev.parametricEquation = getParametricEquation(
|
||||
prev.pieData.startRatio,
|
||||
prev.pieData.endRatio,
|
||||
prev.pieStatus.selected,
|
||||
false, // isHovered=false
|
||||
prev.pieStatus.k,
|
||||
prev.pieData.value
|
||||
);
|
||||
|
||||
hoveredIndex = null;
|
||||
chart.setOption({ series }, { replaceMerge: 'series' }); // 强制替换series
|
||||
}
|
||||
}
|
||||
// 记录当前选中和高亮的系列索引
|
||||
let selectedIndex = null;
|
||||
let hoveredIndex = null;
|
||||
|
||||
// 组件挂载后绑定事件
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
// 监听数据或配置变更,重新初始化图表
|
||||
watch(
|
||||
[() => props.chartData, () => props.option],
|
||||
() => {
|
||||
if (props.chartData && props.chartData.length) {
|
||||
initChart();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可根据需要自定义图表容器样式 */
|
||||
</style>
|