fix: first commit
|
|
@ -0,0 +1,23 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# cockpit
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "cockpit",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0",
|
||||
"core-js": "^3.6.5",
|
||||
"dayjs": "^1.10.7",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"echarts": "^5.4.2",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-ui": "^2.15.13",
|
||||
"node-sass": "^4.0.0",
|
||||
"sass-loader": "^10.1.0",
|
||||
"three": "^0.177.0",
|
||||
"v-scale-screen": "1.0.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-axios": "^3.4.0",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.15",
|
||||
"@vue/cli-plugin-router": "~4.5.15",
|
||||
"@vue/cli-plugin-vuex": "~4.5.15",
|
||||
"@vue/cli-service": "~4.5.15",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
<style>
|
||||
body{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import axios from 'axios';
|
||||
|
||||
axios.defaults.baseURL = 'http://180.108.205.89:8011/'
|
||||
|
||||
// 请求拦截
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log(`请求地址:${axios.duefaults.baseURL}${config.url} (axios拦截器)`);
|
||||
config.headers['Authorization'] = 'Bearer ' + sessionStorage.getItem('token')
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// 响应拦截
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
console.log('↓↓↓ axios响应的数据 ↓↓↓');
|
||||
console.log(JSON.parse(JSON.stringify(response.data)));
|
||||
console.log('↑↑↑ axios响应的数据 ↑↑↑');
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
// get
|
||||
export function get() {
|
||||
return axios({
|
||||
method: 'get',
|
||||
url: '/get',
|
||||
})
|
||||
}
|
||||
|
||||
// post
|
||||
export function post(data) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
.section-left, .section-right {
|
||||
flex: 1;
|
||||
background-color: #42b98330;
|
||||
opacity: 0; /* 初始状态不可见 */
|
||||
animation-fill-mode: forwards; /* 保持动画完成时状态 */
|
||||
}
|
||||
|
||||
.section-left {
|
||||
animation: slideInFromLeft 1s ease-out forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.section-right {
|
||||
animation: slideInFromRight 1s ease-out forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
flex: 2;
|
||||
|
||||
.screen-title {
|
||||
width: 100%;
|
||||
height: 10vh;
|
||||
line-height: 10vh;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'YouSheBiaoTiHei';
|
||||
src: url('./YouSheBiaoTiHei-2.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 367 B |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
|
@ -0,0 +1,323 @@
|
|||
<template>
|
||||
<div
|
||||
ref="chartContainer"
|
||||
:style="{ width: '100%', height: chartHeight }"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'JilinCityBar',
|
||||
props: {
|
||||
chartHeight: { type: String, default: '220px' },
|
||||
interval: { type: Number, default: 2500 }, // 滚动间隔(ms)
|
||||
districtName: { type: String, default: null }, // 当前选中的区县
|
||||
districtData: { type: Object, default: () => ({}) }, // 区县数据
|
||||
dataType: { type: String, default: 'patents' } // 数据类型:patents(专利数量)、research(研究机构)、conversion(转化数量)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
timer: null,
|
||||
pageSize: 4,
|
||||
cursor: 0,
|
||||
|
||||
// 默认数据,当没有提供区县数据时使用
|
||||
defaultXData: [
|
||||
'朝阳区', '南关区', '宽城区', '二道区',
|
||||
'绿园区', '双阳区', '九台区', '农安县'
|
||||
],
|
||||
defaultValues: [220, 180, 190, 170, 185, 120, 140, 130]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 根据props计算实际要显示的数据
|
||||
xData() {
|
||||
return Object.keys(this.districtData).length > 0 ? Object.keys(this.districtData) : this.defaultXData;
|
||||
},
|
||||
values() {
|
||||
if (Object.keys(this.districtData).length === 0) {
|
||||
return this.defaultValues;
|
||||
}
|
||||
|
||||
// 根据dataType选择要显示的数据类型
|
||||
return this.xData.map(district => {
|
||||
if (this.districtData[district]) {
|
||||
return this.districtData[district][this.dataType] || 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
// 图表标题
|
||||
chartTitle() {
|
||||
switch(this.dataType) {
|
||||
case 'research':
|
||||
return '研究机构数量';
|
||||
case 'conversion':
|
||||
return '专利转化数量';
|
||||
default:
|
||||
return '专利数量';
|
||||
}
|
||||
},
|
||||
// 高亮的区县索引
|
||||
highlightIndex() {
|
||||
return this.districtName ? this.xData.indexOf(this.districtName) : -1;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
this.myChart && this.myChart.resize();
|
||||
},
|
||||
districtName() {
|
||||
this.updateChart();
|
||||
},
|
||||
dataType() {
|
||||
this.updateChart();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/* ---------- 立方体形状 ---------- */
|
||||
registerCubeShapes() {
|
||||
const dx = 0;
|
||||
const g = this.$echarts.graphic;
|
||||
|
||||
const L = g.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath(ctx, s) {
|
||||
const { x, y, xAxisPoint } = s;
|
||||
ctx.moveTo(x + dx, y)
|
||||
.lineTo(x - 9 + dx, y - 9)
|
||||
.lineTo(xAxisPoint[0] - 9 + dx, xAxisPoint[1] - 9)
|
||||
.lineTo(xAxisPoint[0] + dx, xAxisPoint[1])
|
||||
.closePath();
|
||||
}
|
||||
});
|
||||
const R = g.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath(ctx, s) {
|
||||
const { x, y, xAxisPoint } = s;
|
||||
ctx.moveTo(x + dx, y)
|
||||
.lineTo(xAxisPoint[0] + dx, xAxisPoint[1])
|
||||
.lineTo(xAxisPoint[0] + 12 + dx, xAxisPoint[1] - 6)
|
||||
.lineTo(x + 12 + dx, y - 6)
|
||||
.closePath();
|
||||
}
|
||||
});
|
||||
const T = g.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath(ctx, s) {
|
||||
const { x, y } = s;
|
||||
ctx.moveTo(x + dx, y)
|
||||
.lineTo(x + 12 + dx, y - 6)
|
||||
.lineTo(x + 3 + dx, y - 15)
|
||||
.lineTo(x - 9 + dx, y - 9)
|
||||
.closePath();
|
||||
}
|
||||
});
|
||||
g.registerShape('CubeLeft', L);
|
||||
g.registerShape('CubeRight', R);
|
||||
g.registerShape('CubeTop', T);
|
||||
},
|
||||
|
||||
/* ---------- 初始化 ---------- */
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
this.registerCubeShapes();
|
||||
this.render(); // 首次渲染
|
||||
if (this.xData.length > this.pageSize) this.startScroll();
|
||||
},
|
||||
|
||||
/* ---------- 更新图表 ---------- */
|
||||
updateChart() {
|
||||
if (this.myChart) {
|
||||
// 停止自动滚动
|
||||
clearInterval(this.timer);
|
||||
|
||||
// 如果有选中的区县,确保它在视图中
|
||||
if (this.highlightIndex >= 0) {
|
||||
// 计算滚动位置,使选中的区县在视图中
|
||||
const startIdx = Math.max(0, Math.min(this.highlightIndex, this.xData.length - this.pageSize));
|
||||
this.cursor = startIdx;
|
||||
|
||||
this.myChart.setOption({
|
||||
dataZoom: [{
|
||||
startValue: startIdx,
|
||||
endValue: startIdx + this.pageSize - 1
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
this.myChart.setOption({
|
||||
xAxis: [{
|
||||
data: this.xData
|
||||
}],
|
||||
series: [
|
||||
{
|
||||
data: this.values,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// 如果是当前选中的区县,使用高亮颜色
|
||||
if (this.highlightIndex === params.dataIndex) {
|
||||
return '#FFE777'; // 高亮黄色
|
||||
}
|
||||
return undefined; // 使用默认颜色
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: this.values
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 重新启动自动滚动
|
||||
if (this.xData.length > this.pageSize) this.startScroll();
|
||||
}
|
||||
},
|
||||
|
||||
/* ---------- 配置 & 渲染 ---------- */
|
||||
render() {
|
||||
const TEAL_LIGHT = '#86F3CE';
|
||||
const TEAL_DEEP = '#1EAF9A';
|
||||
const HIGHLIGHT_COLOR = '#FFE777'; // 高亮颜色
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
animationDurationUpdate: 600,
|
||||
animationEasingUpdate: 'cubicOut',
|
||||
|
||||
grid: { left: 30, right: 30, bottom: 20, top: 40, containLabel: true },
|
||||
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: d =>
|
||||
`${d[0].axisValue}<br/>
|
||||
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background:${d[0].color || TEAL_LIGHT}"></span>
|
||||
${this.chartTitle} <span style="padding-left:13px;">${d[0].value}</span>`
|
||||
},
|
||||
|
||||
legend: {
|
||||
data: [{ name: this.chartTitle, itemStyle: {color: TEAL_LIGHT}}],
|
||||
textStyle: {fontSize: 14, color: '#A7C4F6'},
|
||||
itemWidth: 10, itemHeight: 8, top: '1%', right: '3%'
|
||||
},
|
||||
|
||||
/* ——— 关键:dataZoom 控制窗口宽度=4 ——— */
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
zoomLock: true,
|
||||
startValue: 0,
|
||||
endValue: this.pageSize - 1
|
||||
}],
|
||||
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
data: this.xData,
|
||||
axisLine: {lineStyle: {color: '#FFFFFF80'}},
|
||||
axisTick: {show: false},
|
||||
axisLabel: {fontSize: 14, color: '#A7C4F6'}
|
||||
}],
|
||||
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
min: 0,
|
||||
splitLine: {lineStyle: {color: '#FFFFFF20', type: 'dashed'}},
|
||||
axisLine: {show: false},
|
||||
axisTick: {show: false},
|
||||
axisLabel: {fontSize: 12, color: '#A7C4F6'}
|
||||
}],
|
||||
|
||||
series: [
|
||||
{
|
||||
type: 'custom',
|
||||
name: this.chartTitle,
|
||||
renderItem: (p, api) => {
|
||||
const loc = api.coord([api.value(0), api.value(1)]);
|
||||
const axis = api.coord([api.value(0), 0]);
|
||||
|
||||
// 确定颜色 - 如果是高亮项目则使用高亮颜色
|
||||
const isHighlight = this.highlightIndex === api.dataIndex();
|
||||
const topColor = isHighlight ? HIGHLIGHT_COLOR : TEAL_LIGHT;
|
||||
const deepColor = isHighlight ? '#E5C848' : TEAL_DEEP;
|
||||
const sideColor = isHighlight ? '#B19A3A' : '#157B6E';
|
||||
|
||||
return {
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
type: 'CubeLeft',
|
||||
shape: {x: loc[0], y: loc[1], xAxisPoint: axis},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{offset: 0, color: deepColor},
|
||||
{offset: 1, color: sideColor}
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'CubeRight',
|
||||
shape: {x: loc[0], y: loc[1], xAxisPoint: axis},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{offset: 0, color: topColor},
|
||||
{offset: 1, color: deepColor}
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'CubeTop',
|
||||
shape: {x: loc[0], y: loc[1], xAxisPoint: axis},
|
||||
style: {fill: topColor}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
data: this.values,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
offset: [0, -8],
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
data: this.values,
|
||||
itemStyle: {color: 'transparent'},
|
||||
name: this.chartTitle
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
|
||||
/* ---------- 平滑滚动 ---------- */
|
||||
startScroll() {
|
||||
clearInterval(this.timer); // 先清除之前的定时器
|
||||
|
||||
const len = this.xData.length;
|
||||
this.timer = setInterval(() => {
|
||||
this.cursor = (this.cursor + 1) % len;
|
||||
this.myChart.setOption({
|
||||
dataZoom: [{
|
||||
startValue: this.cursor,
|
||||
endValue: this.cursor + this.pageSize - 1
|
||||
}]
|
||||
});
|
||||
}, this.interval);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
<template>
|
||||
<div ref="chartContainer" :style="{width: '100%', height: '200px'}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DynamicChart',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize(); // 让 ECharts 重新适应新高度
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('->>>>')
|
||||
this.initChart();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
let colors = ['#1FDEA3'];
|
||||
const backgroundData = [50, 50, 50, 50]; // 自定义背景柱高度
|
||||
|
||||
let c = 0;
|
||||
const CubeLeft = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const xAxisPoint = shape.xAxisPoint;
|
||||
const c0 = [shape.x + c, shape.y];
|
||||
const c1 = [shape.x - 9 + c, shape.y - 9];
|
||||
const c2 = [xAxisPoint[0] - 9 + c, xAxisPoint[1] - 9];
|
||||
const c3 = [xAxisPoint[0] + c, xAxisPoint[1]];
|
||||
ctx.moveTo(c0[0], c0[1])
|
||||
.lineTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
const CubeRight = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const xAxisPoint = shape.xAxisPoint;
|
||||
const c1 = [shape.x + c, shape.y];
|
||||
const c2 = [xAxisPoint[0] + c, xAxisPoint[1]];
|
||||
const c3 = [xAxisPoint[0] + 12 + c, xAxisPoint[1] - 6];
|
||||
const c4 = [shape.x + 12 + c, shape.y - 6];
|
||||
ctx.moveTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.lineTo(c4[0], c4[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
const CubeTop = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const c1 = [shape.x + c, shape.y];
|
||||
const c2 = [shape.x + 12 + c, shape.y - 6];
|
||||
const c3 = [shape.x + 3 + c, shape.y - 15];
|
||||
const c4 = [shape.x - 9 + c, shape.y - 9];
|
||||
ctx.moveTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.lineTo(c4[0], c4[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
const CubeLeft1 = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const xAxisPoint = shape.xAxisPoint;
|
||||
const c0 = [shape.x - c, shape.y];
|
||||
const c1 = [shape.x - 9 - c, shape.y - 9];
|
||||
const c2 = [xAxisPoint[0] - 9 - c, xAxisPoint[1] - 9];
|
||||
const c3 = [xAxisPoint[0] - c, xAxisPoint[1]];
|
||||
ctx.moveTo(c0[0], c0[1])
|
||||
.lineTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
const CubeRight1 = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const xAxisPoint = shape.xAxisPoint;
|
||||
const c1 = [shape.x - c, shape.y];
|
||||
const c2 = [xAxisPoint[0] - c, xAxisPoint[1]];
|
||||
const c3 = [xAxisPoint[0] + 12 - c, xAxisPoint[1] - 6];
|
||||
const c4 = [shape.x + 12 - c, shape.y - 6];
|
||||
ctx.moveTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.lineTo(c4[0], c4[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
const CubeTop1 = this.$echarts.graphic.extendShape({
|
||||
shape: { x: 0, y: 0 },
|
||||
buildPath: function (ctx, shape) {
|
||||
const c1 = [shape.x - c, shape.y];
|
||||
const c2 = [shape.x + 12 - c, shape.y - 6];
|
||||
const c3 = [shape.x + 3 - c, shape.y - 15];
|
||||
const c4 = [shape.x - 9 - c, shape.y - 9];
|
||||
ctx.moveTo(c1[0], c1[1])
|
||||
.lineTo(c2[0], c2[1])
|
||||
.lineTo(c3[0], c3[1])
|
||||
.lineTo(c4[0], c4[1])
|
||||
.closePath();
|
||||
},
|
||||
});
|
||||
|
||||
this.$echarts.graphic.registerShape('CubeLeft', CubeLeft);
|
||||
this.$echarts.graphic.registerShape('CubeRight', CubeRight);
|
||||
this.$echarts.graphic.registerShape('CubeTop', CubeTop);
|
||||
|
||||
this.$echarts.graphic.registerShape('CubeLeft1', CubeLeft1);
|
||||
this.$echarts.graphic.registerShape('CubeRight1', CubeRight1);
|
||||
this.$echarts.graphic.registerShape('CubeTop1', CubeTop1);
|
||||
let xData = ['50万以下', '100万以下', '200万以下', '200万以上']
|
||||
const planned = [50, 10, 30, 40];
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
left: 30,
|
||||
right: 30,
|
||||
bottom: 20,
|
||||
top: 40,
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// 坐标轴指示器,坐标轴触发有效(axis)
|
||||
axisPointer: {
|
||||
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
|
||||
},
|
||||
formatter: function (params) {
|
||||
let returnData = params[0].axisValue + '<br/>';
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
returnData += '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background:' + colors[i] + '"></span>';
|
||||
returnData += params[i].seriesName + '<span style="padding-left:13px;">' + params[i].value + '</span><br/>';
|
||||
if(i>0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
{
|
||||
name: '主体数量',
|
||||
itemStyle: {
|
||||
color: colors[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '品牌数量',
|
||||
itemStyle: {
|
||||
color: colors[1]
|
||||
}
|
||||
}
|
||||
],
|
||||
textStyle: {
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
fontSize: 14,
|
||||
color: 'rgba(178, 175, 173, 1)'
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 8,
|
||||
itemGap: 15,
|
||||
top: '1%',
|
||||
right: '3%'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xData,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
offset: 10,
|
||||
axisTick: {
|
||||
show: false,
|
||||
length: 9,
|
||||
alignWithLabel: true,
|
||||
lineStyle: {
|
||||
color: '#748EAB',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: '#85B6BC',
|
||||
margin: 6,
|
||||
interval: 0,
|
||||
formatter: function (value) {
|
||||
return `{down|${value}}`;
|
||||
},
|
||||
rich: {
|
||||
down: {
|
||||
padding: [5, 0, 0, 0],
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: '#85B6BC',
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#FFFFFF20',
|
||||
type: 'dashed', // 改为虚线
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
fontSize: 12,
|
||||
color: '#85B6BC'
|
||||
},
|
||||
min: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'custom',
|
||||
name: '主体数量(个)',
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
|
||||
fontSize: 16,
|
||||
color: '#fff',
|
||||
offset: [2, -25]
|
||||
}
|
||||
},
|
||||
renderItem: (params, api) => {
|
||||
const location = api.coord([api.value(0), api.value(1)]);
|
||||
return {
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
type: 'CubeLeft',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#023A8A' },
|
||||
{ offset: 1, color: '#0EC071' }
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'CubeRight',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#023A8A' },
|
||||
{ offset: 1, color: '#27E588' }
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'CubeTop',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#6EFFBA' },
|
||||
{ offset: 1, color: '#1EE783' }
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
data: planned,
|
||||
},
|
||||
{
|
||||
//柱形顶端显示x轴数据
|
||||
type: "bar",
|
||||
name:'主体数量(个)',
|
||||
label: {
|
||||
normal: {
|
||||
distance:20,
|
||||
show: true,
|
||||
position: "top",
|
||||
fontSize: 12,
|
||||
color: "#fff",
|
||||
align: "center",
|
||||
verticalAlign: "bottom",
|
||||
offset: [0, 0],
|
||||
fontWeight: "bold"
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: "transparent",
|
||||
},
|
||||
data:planned,
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
name: '背景柱',
|
||||
renderItem: (params, api) => {
|
||||
const location = api.coord([api.value(0), api.value(1)]);
|
||||
return {
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
type: 'CubeLeft',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: '#2D9DD628' // 左面半透明色
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'CubeRight',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: '##01E0B360' // 左面半透明色
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'CubeTop',
|
||||
shape: {
|
||||
api,
|
||||
xValue: api.value(0),
|
||||
yValue: api.value(1),
|
||||
x: location[0],
|
||||
y: location[1],
|
||||
xAxisPoint: api.coord([api.value(0), 0]),
|
||||
},
|
||||
style: {
|
||||
fill: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#0F795D40' },
|
||||
{ offset: 1, color: '#52FFCF40' }
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
data: backgroundData,
|
||||
z: 1 // 背景柱层级靠后
|
||||
},
|
||||
],
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
ref="chartContainer"
|
||||
:style="{width: '500px', height: '400px'}"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseLeave"
|
||||
></div>
|
||||
<button @click="move('left')">左</button>
|
||||
<button @click="move('right')">右</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DynamicChart',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
count: 11,
|
||||
categories: this.generateCategories(),
|
||||
categories2: Array.from({length: 10}, (_, i) => 10 - i - 1),
|
||||
data: Array.from({length: 10}, () => Math.round(Math.random() * 1000)),
|
||||
data2: Array.from({length: 10}, () => +(Math.random() * 10 + 5).toFixed(1)),
|
||||
isMouseDown: false,
|
||||
lastMousePosition: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.initChart();
|
||||
this.startDataUpdater();
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.dataUpdater);
|
||||
},
|
||||
methods: {
|
||||
move(direction) {
|
||||
// 停止自动更新
|
||||
clearInterval(this.dataUpdater);
|
||||
|
||||
if (direction === 'left') {
|
||||
this.data.unshift(Math.round(Math.random() * 1000));
|
||||
this.data.pop();
|
||||
|
||||
this.data2.unshift(+(Math.random() * 10 + 5).toFixed(1));
|
||||
this.data2.pop();
|
||||
|
||||
this.categories.unshift(new Date().toLocaleTimeString().replace(/^\D*/, ''));
|
||||
this.categories.pop();
|
||||
|
||||
this.categories2.unshift(this.count--);
|
||||
this.categories2.pop();
|
||||
} else if (direction === 'right') {
|
||||
this.data.push(Math.round(Math.random() * 1000));
|
||||
this.data.shift();
|
||||
|
||||
this.data2.push(+(Math.random() * 10 + 5).toFixed(1));
|
||||
this.data2.shift();
|
||||
|
||||
this.categories.push(new Date().toLocaleTimeString().replace(/^\D*/, ''));
|
||||
this.categories.shift();
|
||||
|
||||
this.categories2.push(this.count++);
|
||||
this.categories2.shift();
|
||||
}
|
||||
|
||||
this.myChart.setOption({
|
||||
xAxis: [
|
||||
{data: this.categories},
|
||||
{data: this.categories2}
|
||||
],
|
||||
series: [
|
||||
{data: this.data},
|
||||
{data: this.data2}
|
||||
]
|
||||
});
|
||||
},
|
||||
generateCategories() {
|
||||
let now = new Date();
|
||||
let res = [];
|
||||
let len = 10;
|
||||
while (len--) {
|
||||
res.unshift(now.toLocaleTimeString().replace(/^\D*/, ''));
|
||||
now = new Date(+now - 2000);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
initChart() {
|
||||
// 初始化ECharts实例
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Dynamic Data'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#283b56'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {},
|
||||
dataZoom: {
|
||||
show: false,
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
data: this.categories
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
data: this.categories2
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: 'Price',
|
||||
max: 30,
|
||||
min: 0,
|
||||
boundaryGap: [0.2, 0.2]
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
scale: true,
|
||||
name: 'Order',
|
||||
max: 1200,
|
||||
min: 0,
|
||||
boundaryGap: [0.2, 0.2]
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Dynamic Bar',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: this.data
|
||||
},
|
||||
{
|
||||
name: 'Dynamic Line',
|
||||
type: 'line',
|
||||
data: this.data2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
startDataUpdater() {
|
||||
this.dataUpdater = setInterval(() => {
|
||||
let axisData = new Date().toLocaleTimeString().replace(/^\D*/, '');
|
||||
|
||||
this.data.shift();
|
||||
this.data.push(Math.round(Math.random() * 1000));
|
||||
|
||||
this.data2.shift();
|
||||
this.data2.push(+(Math.random() * 10 + 5).toFixed(1));
|
||||
|
||||
this.categories.shift();
|
||||
this.categories.push(axisData);
|
||||
|
||||
this.categories2.shift();
|
||||
this.categories2.push(this.count++);
|
||||
|
||||
this.myChart.setOption({
|
||||
xAxis: [
|
||||
{data: this.categories},
|
||||
{data: this.categories2}
|
||||
],
|
||||
series: [
|
||||
{data: this.data},
|
||||
{data: this.data2}
|
||||
]
|
||||
});
|
||||
}, 2100);
|
||||
},
|
||||
handleMouseDown(event) {
|
||||
this.isMouseDown = true;
|
||||
this.lastMousePosition = event.clientX;
|
||||
},
|
||||
handleMouseMove(event) {
|
||||
if (!this.isMouseDown) return;
|
||||
|
||||
const currentMousePosition = event.clientX;
|
||||
const threshold = 100; // 需要拖动多少像素才触发移动
|
||||
|
||||
if (Math.abs(currentMousePosition - this.lastMousePosition) >= threshold) {
|
||||
if (currentMousePosition > this.lastMousePosition) {
|
||||
this.move('right');
|
||||
} else {
|
||||
this.move('left');
|
||||
}
|
||||
this.lastMousePosition = currentMousePosition;
|
||||
}
|
||||
},
|
||||
handleMouseUp() {
|
||||
this.isMouseDown = false;
|
||||
this.lastMousePosition = null;
|
||||
},
|
||||
handleMouseLeave() {
|
||||
this.isMouseDown = false;
|
||||
this.lastMousePosition = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<div class="card" :style="{ height: cardHeight }">
|
||||
<!-- 头部 -->
|
||||
<div :class="type === 'long' ? 'card-long-header' : 'card-header'">
|
||||
<div class="header-container">
|
||||
<div class="card-title">{{ title }}</div>
|
||||
|
||||
<!-- ↓↓↓ 可选下拉 ↓↓↓ -->
|
||||
<div v-if="isShowSelect" class="card-select-block" ref="dropdown">
|
||||
<div class="select-display" @click="toggleDropdown">
|
||||
<span class="display-text">{{ selectedText }}</span>
|
||||
<img
|
||||
class="arrow-img"
|
||||
:class="{ open: showDropdown }"
|
||||
src="@/assets/images/icon/icon_down_arrow.png"
|
||||
alt="arrow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul v-show="showDropdown" class="select-dropdown">
|
||||
<li
|
||||
v-for="opt in processedOptions"
|
||||
:key="opt.value"
|
||||
class="select-option"
|
||||
@click="selectOption(opt)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容插槽 -->
|
||||
<div class="card-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CardWithCustomSelect',
|
||||
props: {
|
||||
title: { type: String, default: '请填写标题' },
|
||||
type: { type: String, default: 'short' },
|
||||
isShowSelect: { type: Boolean, default: false },
|
||||
heightPercentage: { type: Number, default: 100 },
|
||||
/* 原下拉数据 */
|
||||
selectOptions: { type: Array, default: () => [] },
|
||||
/* 新增:是否要带“全部” */
|
||||
includeAll: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: '', // 绑定 value
|
||||
showDropdown: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
cardHeight() { return `${this.heightPercentage}%` },
|
||||
|
||||
/* 归一化 options:保证有 label / value */
|
||||
processedOptions() {
|
||||
const norm = this.selectOptions.map((o, i) => ({
|
||||
label : o.label ?? o.name ?? `选项${i+1}`,
|
||||
value : o.value ?? o.id ?? i
|
||||
}));
|
||||
return this.includeAll
|
||||
? [{ label: '全部', value: '' }, ...norm]
|
||||
: norm;
|
||||
},
|
||||
|
||||
/* 当前显示文本 */
|
||||
selectedText() {
|
||||
const found = this.processedOptions.find(o => o.value === this.selected);
|
||||
return found ? found.label : '请选择';
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
|
||||
/* ----- 默认值逻辑 ----- */
|
||||
if (this.includeAll) {
|
||||
this.selected = ''; // “全部”
|
||||
} else if (this.processedOptions.length) {
|
||||
this.selected = this.processedOptions[0].value; // 选首项
|
||||
this.$emit('change', this.selected); // 通知父组件
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdown() { this.showDropdown = !this.showDropdown },
|
||||
|
||||
selectOption(opt) {
|
||||
this.selected = opt.value;
|
||||
this.showDropdown = false;
|
||||
this.$emit('change', this.selected); // 抛出事件
|
||||
},
|
||||
|
||||
handleClickOutside(e) {
|
||||
if (this.$refs.dropdown && !this.$refs.dropdown.contains(e.target)) {
|
||||
this.showDropdown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#031B2E 0%, /* 深午夜蓝:对应原 #052E23 */
|
||||
#104266 53%, /* 深海军蓝:对应原 #124937 */
|
||||
#031B2E 100% /* 回到首色,首尾呼应 */
|
||||
);
|
||||
border: 1px solid;
|
||||
border-image: linear-gradient(180deg, rgba(44, 154, 255, 0), rgba(44, 154, 255, 1)) 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
height: 55px;
|
||||
background: url("./../../assets/images/bg/card_title_bg3.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: white;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.card-long-header {
|
||||
width: 100%;
|
||||
height: 15%;
|
||||
background: url("./../../assets/images/bg/card_title_bg3.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: white;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
position: relative;
|
||||
width: calc(100% - 90px);
|
||||
margin-left: 50px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 55px;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 2px 5px rgba(44, 154, 255, 0.6);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 选择区域样式 */
|
||||
.card-select-block {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 26px;
|
||||
margin-top: 10px;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.select-display {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding-left: 8px;
|
||||
padding-right: 20px;
|
||||
background: #0D2847;
|
||||
border: 1px solid #2C9AFF;
|
||||
box-shadow: 0 0 6px rgba(44, 154, 255, 0.20);
|
||||
color: #C8D3E0;
|
||||
font-weight: bold;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.display-text {
|
||||
display: inline-block;
|
||||
max-width: 70px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.arrow-img {
|
||||
width: 10px;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) rotate(0deg);
|
||||
transition: transform 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow-img.open {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
.select-dropdown {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
left: 0;
|
||||
width: 110px;
|
||||
background: #0D2847;
|
||||
border: 1px solid #2C9AFF;
|
||||
box-shadow: 0 2px 8px rgba(44, 154, 255, 0.20);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
|
||||
.select-option {
|
||||
min-height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 10px;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: #418A7039;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div ref="chartContainer" :style="{width: '100%', height: chartHeight}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LineIndex',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
};
|
||||
},
|
||||
props:{
|
||||
chartHeight: {
|
||||
type: String,
|
||||
default: '260px', // 默认值
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize(); // 让 ECharts 重新适应新高度
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('->>>>')
|
||||
this.initChart();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
let xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
const list1 = [50, 10, 30, 40, 50, 60, 70, 110, 23, 90, 47, 60];
|
||||
const list2 = [22, 10, 120, 10,30, 80, 100, 50, 22, 39, 31, 80];
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
right: '5%',
|
||||
top: '2.5%',
|
||||
itemWidth: 16,
|
||||
itemHeight: 8,
|
||||
data: ['科技新闻政策', '新兴科技创新'],
|
||||
textStyle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '20%',
|
||||
bottom: '30%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
color: '#85B6BC',
|
||||
margin: 10,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#FFFFFF20',
|
||||
type: 'dashed',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '单位(个)',
|
||||
nameTextStyle: {
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
fontSize: 12,
|
||||
color: '#85B6BC'
|
||||
},
|
||||
nameGap: 25,
|
||||
axisLabel: {
|
||||
color: '#85B6BC',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#FFFFFF20',
|
||||
type: 'dashed',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '科技新闻政策',
|
||||
// smooth: true,
|
||||
// showSymbol: false,
|
||||
type: 'line',
|
||||
data: list1,
|
||||
color: '#28E9DA',
|
||||
areaStyle: list1.filter(Boolean).length === 0 ? {} : {
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: '#53DDC550',
|
||||
}, {
|
||||
offset: 1,
|
||||
color: '#53DDC520',
|
||||
}]),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '新兴科技创新',
|
||||
// smooth: true,
|
||||
// showSymbol: false,
|
||||
type: 'line',
|
||||
data: list2,
|
||||
color: '#FEEA42',
|
||||
areaStyle: list2.filter(Boolean).length === 0 ? {} : {
|
||||
// shadowColor: 'rgba(101, 223, 221, 0.2)',
|
||||
// shadowBlur: 100, // 阴影
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: '#A49E3550',
|
||||
}, {
|
||||
offset: 1,
|
||||
color: '#A49E3520',
|
||||
}]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<div ref="chartContainer" :style="{width: '100%', height: chartHeight}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LineIndex',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
};
|
||||
},
|
||||
props:{
|
||||
chartHeight: {
|
||||
type: String,
|
||||
default: '260px', // 默认值
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize(); // 让 ECharts 重新适应新高度
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('->>>>')
|
||||
this.initChart();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
let xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
||||
const list1 = [50, 10, 30, 40, 50, 60, 70, 110, 23, 90, 47, 60];
|
||||
const list2 = [22, 10, 120, 10,30, 80, 100, 50, 22, 39, 31, 80];
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
formatter: function (params) {
|
||||
let tooltipHtml = params
|
||||
.map((item) => {
|
||||
let fixedColor = item.seriesName === '科技新闻政策' ? '#529F65' : '#FFC52F'; // 固定小圆点颜色
|
||||
let marker = `<span style="display:inline-block;margin-right:5px;width:8px;height:8px;background:${fixedColor};border-radius:50%;"></span>`;
|
||||
return `<div>${marker}${item.seriesName}: ${item.value}</div>`;
|
||||
})
|
||||
.join('');
|
||||
return `<div>${tooltipHtml}</div>`;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
right: '5%',
|
||||
top: '2.5%',
|
||||
itemWidth: 16,
|
||||
itemHeight: 8,
|
||||
data: ['科技新闻政策', '新兴科技创新'],
|
||||
textStyle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
itemStyle: {
|
||||
color: function (name) {
|
||||
return name === '科技新闻政策' ? '#529F65' : '#FFC52F';
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '20%',
|
||||
bottom: '30%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLabel: {
|
||||
color: '#85B6BC',
|
||||
margin: 10,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#FFFFFF20',
|
||||
type: 'dashed',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '单位(个)',
|
||||
nameTextStyle: {
|
||||
fontFamily: 'MicrosoftYaHei',
|
||||
fontSize: 12,
|
||||
color: '#85B6BC'
|
||||
},
|
||||
nameGap: 25,
|
||||
axisLabel: {
|
||||
color: '#85B6BC',
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#FFFFFF20',
|
||||
type: 'dashed',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '涉农主体授权',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
type: 'line',
|
||||
data: list1,
|
||||
color: '#FFC52F',
|
||||
lineStyle: { // 修改为虚线
|
||||
type: 'dashed',
|
||||
width: 2, // 线条宽度,可根据需要调整
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '产品发布数量',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
type: 'line',
|
||||
data: list2,
|
||||
lineStyle: { // 设置折线颜色渐变
|
||||
width: 2, // 线条宽度
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#529F65' }, // 渐变起点颜色
|
||||
{ offset: 1, color: '#66BAD9' }, // 渐变终点颜色
|
||||
]),
|
||||
},
|
||||
areaStyle: list2.filter(Boolean).length === 0 ? {} : {
|
||||
// shadowColor: 'rgba(101, 223, 221, 0.2)',
|
||||
// shadowBlur: 100, // 阴影
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: '#529F6550',
|
||||
}, {
|
||||
offset: 1,
|
||||
color: '#66BAD920',
|
||||
}]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# City GeoJSON Files
|
||||
|
||||
This directory contains GeoJSON files for each city in Jilin province. Each file should be named after its corresponding city (e.g., `长春市.json`, `吉林市.json`, etc.) and contain the district-level geographical data for that city.
|
||||
|
||||
## File Structure
|
||||
|
||||
Each GeoJSON file should follow this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "区县名称",
|
||||
"code": "行政区划代码"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[经度, 纬度], ...]]
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Cities
|
||||
|
||||
- 长春市.json
|
||||
- 吉林市.json
|
||||
- 四平市.json
|
||||
- 辽源市.json
|
||||
- 通化市.json
|
||||
- 白山市.json
|
||||
- 松原市.json
|
||||
- 白城市.json
|
||||
- 延边自治州.json
|
||||
|
||||
## Data Source
|
||||
|
||||
The GeoJSON data should be obtained from official sources or reliable geographic data providers to ensure accuracy.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import * as THREE from 'three'
|
||||
/**
|
||||
* @param {Array<Array<number>>} ring - 外环经纬度数组 [[lng,lat], ...]
|
||||
* @returns {THREE.LineLoop}
|
||||
*/
|
||||
export default function createLine (ring) {
|
||||
const vertices = new Float32Array(ring.length * 3)
|
||||
ring.forEach((p, i) => {
|
||||
vertices[i * 3] = p[0] // 经度 -> X
|
||||
vertices[i * 3 + 1] = p[1] // 纬度 -> Y
|
||||
vertices[i * 3 + 2] = 0
|
||||
})
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
|
||||
// const material = new THREE.LineBasicMaterial({ color: 0x00cccc })
|
||||
const material = new THREE.LineBasicMaterial({ color: 0x00cccc })
|
||||
return new THREE.LineLoop(geometry, material)
|
||||
}
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
<template>
|
||||
<div
|
||||
ref="wrap"
|
||||
class="three-wrap"
|
||||
:style="canvasW && canvasH ? { width: canvasW + 'px', height: canvasH + 'px' } : {}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
|
||||
export default {
|
||||
name: 'CityMap',
|
||||
|
||||
props: {
|
||||
canvasW: {type: Number, default: null},
|
||||
canvasH: {type: Number, default: null},
|
||||
bgColor: {type: String, default: '#ffffff'},
|
||||
geojson: {type: Object, required: true},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
renderer: null,
|
||||
scene: null,
|
||||
camera: null,
|
||||
controls: null,
|
||||
frameId: null,
|
||||
resizeObs: null,
|
||||
inited: false,
|
||||
halfSizeX: 1,
|
||||
halfSizeY: 1,
|
||||
center: new THREE.Vector3(),
|
||||
// 区县数据 - 将在构建地图时动态填充
|
||||
districtData: [],
|
||||
labelSprites: [], // 存储标签精灵对象的数组
|
||||
districtMeshes: [], // 存储每个区县mesh及其原始z
|
||||
hoveredDistrict: null, // 当前高亮的mesh
|
||||
flylines: [], // 存储飞线对象
|
||||
flylineGroup: null, // 飞线组
|
||||
activeFlylines: new Map() // 存储活动的飞线
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.canvasW && this.canvasH) {
|
||||
this.initThree(this.canvasW, this.canvasH)
|
||||
window.addEventListener('resize', this.onResize)
|
||||
} else {
|
||||
this.resizeObs = new ResizeObserver(entries => {
|
||||
const {width, height} = entries[0].contentRect
|
||||
if (width && height && !this.inited) {
|
||||
this.initThree(width, height)
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.resizeObs.disconnect()
|
||||
}
|
||||
})
|
||||
this.resizeObs.observe(this.$refs.wrap)
|
||||
}
|
||||
// 鼠标交互:区县凸起和飞线
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
this.mouse = new THREE.Vector2()
|
||||
this.$refs.wrap.addEventListener('mousemove', this.onMapMouseMove)
|
||||
this.$refs.wrap.addEventListener('click', this.onMapClick)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
cancelAnimationFrame(this.frameId)
|
||||
this.controls?.dispose()
|
||||
this.renderer?.dispose()
|
||||
this.resizeObs?.disconnect()
|
||||
this.$refs.wrap?.removeEventListener('mousemove', this.onMapMouseMove)
|
||||
this.$refs.wrap?.removeEventListener('click', this.onMapClick)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/* ---------- 初始化 ---------- */
|
||||
initThree(w, h) {
|
||||
this.inited = true
|
||||
|
||||
/* 场景 & 渲染器 */
|
||||
this.scene = new THREE.Scene()
|
||||
this.renderer = new THREE.WebGLRenderer({antialias: true, alpha: true})
|
||||
this.renderer.setSize(w, h)
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio)
|
||||
this.renderer.setClearColor(this.bgColor, 0)
|
||||
/* 物理光照 + 色彩空间 */
|
||||
this.renderer.physicallyCorrectLights = true
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
this.$refs.wrap.appendChild(this.renderer.domElement)
|
||||
|
||||
/* 灯光优化:增加白色方向光,让立体感更明显 */
|
||||
this.scene.add(new THREE.AmbientLight(0x2d65cd, 1.5)) // 环境光用主色
|
||||
this.scene.add(new THREE.HemisphereLight(0x2d65cd, 0x113667, 1.2))
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1.0) // 白色方向光
|
||||
keyLight.position.set(300, 400, 500)
|
||||
this.scene.add(keyLight)
|
||||
|
||||
/* 地图模型 */
|
||||
this.buildMap()
|
||||
|
||||
/* 相机自适配 */
|
||||
this.setCamera(w, h)
|
||||
|
||||
/* OrbitControls */
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.controls.enablePan = false
|
||||
this.controls.enableRotate = false
|
||||
this.controls.enableZoom = false
|
||||
this.controls.target.copy(this.center)
|
||||
this.controls.update()
|
||||
|
||||
/* 渲染循环 */
|
||||
const renderLoop = () => {
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
this.frameId = requestAnimationFrame(renderLoop)
|
||||
}
|
||||
renderLoop()
|
||||
},
|
||||
|
||||
/* ---------- 构建市级地图 ---------- */
|
||||
buildMap() {
|
||||
const depth = 0.2
|
||||
const lineGroup = new THREE.Group()
|
||||
const meshGroup = new THREE.Group()
|
||||
const labelGroup = new THREE.Group()
|
||||
|
||||
lineGroup.position.z = depth + 0.0001
|
||||
meshGroup.position.z = 0
|
||||
labelGroup.position.z = depth + 0.1
|
||||
|
||||
const mapGroup = new THREE.Group()
|
||||
mapGroup.add(lineGroup, meshGroup, labelGroup)
|
||||
this.scene.add(mapGroup)
|
||||
|
||||
// 处理GeoJSON数据,提取区县信息
|
||||
if (this.geojson && this.geojson.features) {
|
||||
this.geojson.features.forEach(feature => {
|
||||
let polys = feature.geometry.coordinates
|
||||
if (feature.geometry.type === 'Polygon') polys = [polys]
|
||||
|
||||
// 计算区县中心点
|
||||
let centerX = 0, centerY = 0, pointCount = 0
|
||||
polys.forEach(poly => {
|
||||
const outer = poly[0]
|
||||
outer.forEach(point => {
|
||||
centerX += point[0]
|
||||
centerY += point[1]
|
||||
pointCount++
|
||||
})
|
||||
})
|
||||
centerX /= pointCount
|
||||
centerY /= pointCount
|
||||
|
||||
// 保存区县数据
|
||||
const districtName = feature.properties.name
|
||||
this.districtData.push({
|
||||
name: districtName,
|
||||
center: [centerX, centerY]
|
||||
})
|
||||
|
||||
// 创建区县边界和网格
|
||||
polys.forEach(poly => {
|
||||
const outer = poly[0]
|
||||
const holes = poly.slice(1)
|
||||
|
||||
// 添加边界线
|
||||
lineGroup.add(this.createLine(outer))
|
||||
|
||||
// 创建区县网格
|
||||
const mesh = this.createExtrudeMesh(outer, holes, depth)
|
||||
mesh.userData.districtName = districtName
|
||||
mesh.userData.originZ = mesh.position.z
|
||||
|
||||
// 添加到网格组
|
||||
meshGroup.add(mesh)
|
||||
this.districtMeshes.push(mesh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 计算地图边界和中心点
|
||||
const box = new THREE.Box3().expandByObject(lineGroup)
|
||||
box.getCenter(this.center)
|
||||
const size = new THREE.Vector3()
|
||||
box.getSize(size)
|
||||
this.halfSizeX = size.x / 2
|
||||
this.halfSizeY = size.y / 2
|
||||
|
||||
// 只添加区县标签
|
||||
this.addDistrictLabels(labelGroup)
|
||||
},
|
||||
|
||||
/* ---------- 边界线 ---------- */
|
||||
createLine(ring) {
|
||||
const vertices = new Float32Array(ring.length * 3)
|
||||
ring.forEach((p, i) => {
|
||||
vertices[i * 3] = p[0]
|
||||
vertices[i * 3 + 1] = p[1]
|
||||
vertices[i * 3 + 2] = 0
|
||||
})
|
||||
const geo = new THREE.BufferGeometry()
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
|
||||
return new THREE.LineLoop(geo, new THREE.LineBasicMaterial({
|
||||
color: 0x0d2a50,
|
||||
opacity: 0.8,
|
||||
transparent: true
|
||||
}))
|
||||
},
|
||||
|
||||
/* ---------- 立体 Mesh ---------- */
|
||||
createExtrudeMesh(outer, holes, depth) {
|
||||
const shape = new THREE.Shape(outer.map(p => new THREE.Vector2(p[0], p[1])))
|
||||
holes.forEach(h => {
|
||||
shape.holes.push(new THREE.Path(h.map(p => new THREE.Vector2(p[0], p[1]))))
|
||||
})
|
||||
const geo = new THREE.ExtrudeGeometry(shape, {depth, bevelEnabled: false})
|
||||
|
||||
return new THREE.Mesh(
|
||||
geo,
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x4ba9bc,
|
||||
roughness: 0.1,
|
||||
metalness: 0.1,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
/* ---------- 处理鼠标移动 ---------- */
|
||||
onMapMouseMove(event) {
|
||||
if (!this.camera || !this.districtMeshes.length) return
|
||||
const rect = this.$refs.wrap.getBoundingClientRect()
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera)
|
||||
const intersects = this.raycaster.intersectObjects(this.districtMeshes)
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const mesh = intersects[0].object
|
||||
if (this.hoveredDistrict && this.hoveredDistrict !== mesh) {
|
||||
this.hoveredDistrict.position.z = this.hoveredDistrict.userData.originZ
|
||||
// 恢复之前区域的文字位置
|
||||
const prevDistrictName = this.hoveredDistrict.userData.districtName
|
||||
this.resetDistrictLabelPosition(prevDistrictName)
|
||||
}
|
||||
if (this.hoveredDistrict !== mesh) {
|
||||
mesh.position.z = mesh.userData.originZ + 0.15
|
||||
this.hoveredDistrict = mesh
|
||||
|
||||
const districtName = mesh.userData.districtName
|
||||
// 提升当前区域的文字位置
|
||||
this.elevateDistrictLabel(districtName, 0.15)
|
||||
|
||||
// 触发事件,通知父组件区县悬停变化
|
||||
this.$emit('district-hover', districtName)
|
||||
}
|
||||
} else if (this.hoveredDistrict) {
|
||||
this.hoveredDistrict.position.z = this.hoveredDistrict.userData.originZ
|
||||
this.hoveredDistrict = null
|
||||
|
||||
// 恢复所有文字位置
|
||||
this.resetAllLabelPositions()
|
||||
|
||||
// 触发事件,通知父组件区县悬停结束
|
||||
this.$emit('district-hover', null)
|
||||
}
|
||||
},
|
||||
|
||||
/* ---------- 提升区县标签高度 ---------- */
|
||||
elevateDistrictLabel(districtName, elevationAmount) {
|
||||
this.labelSprites.forEach(item => {
|
||||
if (item.type === 'label' && item.districtName === districtName) {
|
||||
item.mesh.position.z += elevationAmount
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 重置区县标签高度 ---------- */
|
||||
resetDistrictLabelPosition(districtName) {
|
||||
this.labelSprites.forEach(item => {
|
||||
if (item.type === 'label' && item.districtName === districtName) {
|
||||
item.mesh.position.z = item.originalZ
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 重置所有标签位置 ---------- */
|
||||
resetAllLabelPositions() {
|
||||
this.labelSprites.forEach(item => {
|
||||
if (item.type === 'label') {
|
||||
item.mesh.position.z = item.originalZ
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 添加区县标签 ---------- */
|
||||
addDistrictLabels(labelGroup) {
|
||||
// 创建Canvas纹理
|
||||
const createTextTexture = (text) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const fontSize = 10 // 减小字体大小
|
||||
const padding = 4 // 减小内边距
|
||||
|
||||
// 提高Canvas分辨率以提升文字清晰度
|
||||
const devicePixelRatio = window.devicePixelRatio || 1
|
||||
|
||||
// 设置Canvas大小
|
||||
ctx.font = `${fontSize}px Arial`
|
||||
const textWidth = ctx.measureText(text).width
|
||||
const width = textWidth + padding * 2
|
||||
const height = fontSize + padding * 2
|
||||
|
||||
// 设置实际渲染尺寸(提高分辨率)
|
||||
canvas.width = width * devicePixelRatio
|
||||
canvas.height = height * devicePixelRatio
|
||||
|
||||
// 缩放上下文以匹配设备像素比
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio)
|
||||
|
||||
// 设置文字渲染参数
|
||||
ctx.font = `${fontSize}px Arial`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
|
||||
// 启用字体平滑
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
|
||||
// 绘制文字
|
||||
ctx.fillText(text, width/2, height/2)
|
||||
|
||||
// 创建纹理
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
texture.needsUpdate = true
|
||||
|
||||
// 设置纹理参数以提高清晰度
|
||||
texture.minFilter = THREE.LinearFilter
|
||||
texture.magFilter = THREE.LinearFilter
|
||||
texture.format = THREE.RGBAFormat
|
||||
|
||||
return {
|
||||
texture,
|
||||
width: width,
|
||||
height: height
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个区县创建标签
|
||||
this.districtData.forEach(district => {
|
||||
const {texture, width, height} = createTextTexture(district.name)
|
||||
|
||||
// 创建标签材质
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.95
|
||||
})
|
||||
|
||||
// 创建精灵标签
|
||||
const sprite = new THREE.Sprite(material)
|
||||
|
||||
// 设置位置 - 精确定位在区县中心点,降低Z轴高度
|
||||
const baseZ = 0.05 // 基础高度
|
||||
sprite.position.set(
|
||||
district.center[0],
|
||||
district.center[1],
|
||||
baseZ
|
||||
)
|
||||
|
||||
// 设置缩放比例
|
||||
const scale = 0.6
|
||||
sprite.scale.set(width * scale * 0.01, height * scale * 0.01, 1)
|
||||
|
||||
// 存储精灵对象以便在渲染循环中更新
|
||||
this.labelSprites.push({
|
||||
type: 'label',
|
||||
mesh: sprite,
|
||||
districtName: district.name,
|
||||
originalZ: baseZ // 记录原始Z位置
|
||||
})
|
||||
|
||||
// 添加到标签组
|
||||
labelGroup.add(sprite)
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 相机 ---------- */
|
||||
setCamera(w, h) {
|
||||
// 计算相机以适配地图宽高
|
||||
const aspect = w / h
|
||||
const fov = 45 // 垂直视场角 (度)
|
||||
const tilt = Math.PI / 5 // 36度倾斜
|
||||
const mapWidth = this.halfSizeX * 2
|
||||
const mapHeight = this.halfSizeY * 2
|
||||
// 转换为弧度
|
||||
const vFov = fov * Math.PI / 180
|
||||
// 根据垂直和水平视角分别计算距离
|
||||
const distanceY = (mapHeight / 2) / Math.tan(vFov / 2)
|
||||
const distanceX = (mapWidth / 2) / (Math.tan(vFov / 2) * aspect)
|
||||
// 取较大距离,并增加一定边距
|
||||
const distance = Math.max(distanceX, distanceY) * 1.1
|
||||
// 创建透视相机
|
||||
this.camera = new THREE.PerspectiveCamera(fov, aspect, 1, 2000)
|
||||
// 设置相机位置并倾斜
|
||||
this.camera.position.set(
|
||||
this.center.x,
|
||||
this.center.y - Math.sin(tilt) * distance,
|
||||
this.center.z + Math.cos(tilt) * distance
|
||||
)
|
||||
this.camera.lookAt(this.center)
|
||||
},
|
||||
|
||||
/* ---------- 窗口缩放 ---------- */
|
||||
onResize() {
|
||||
if (!this.camera) return
|
||||
const w = this.$refs.wrap.clientWidth
|
||||
const h = this.$refs.wrap.clientHeight
|
||||
if (!w || !h) return
|
||||
// 更新渲染器尺寸
|
||||
this.renderer.setSize(w, h)
|
||||
// 更新透视相机的宽高比
|
||||
this.camera.aspect = w / h
|
||||
this.camera.updateProjectionMatrix()
|
||||
},
|
||||
|
||||
/* ---------- 处理点击事件 ---------- */
|
||||
onMapClick(event) {
|
||||
if (!this.camera || !this.districtMeshes.length) return
|
||||
const rect = this.$refs.wrap.getBoundingClientRect()
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera)
|
||||
const intersects = this.raycaster.intersectObjects(this.districtMeshes)
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const mesh = intersects[0].object
|
||||
const districtName = mesh.userData.districtName
|
||||
|
||||
// 触发事件,通知父组件区县被点击
|
||||
this.$emit('district-click', districtName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,609 @@
|
|||
<template>
|
||||
<div
|
||||
ref="wrap"
|
||||
class="three-wrap"
|
||||
:style="canvasW && canvasH ? { width: canvasW + 'px', height: canvasH + 'px' } : {}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import JilinGeoJson from '@/components/map/geojson/jilin.json'
|
||||
|
||||
export default {
|
||||
name: 'ThreeScene',
|
||||
|
||||
props: {
|
||||
canvasW: { type: Number, default: null },
|
||||
canvasH: { type: Number, default: null },
|
||||
bgColor : { type: String, default: '#ffffff' }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
renderer: null,
|
||||
scene : null,
|
||||
camera : null,
|
||||
controls: null,
|
||||
frameId : null,
|
||||
resizeObs: null,
|
||||
inited : false,
|
||||
halfSizeX: 1,
|
||||
halfSizeY: 1,
|
||||
center : new THREE.Vector3(),
|
||||
// 城市数据 - 修正中心点坐标
|
||||
cityData: [
|
||||
{ name: '长春市', center: [125.3245, 43.886841] },
|
||||
{ name: '吉林市', center: [126.55302, 43.843577] },
|
||||
{ name: '四平市', center: [124.370785, 43.170344] },
|
||||
{ name: '辽源市', center: [125.145349, 42.902692] },
|
||||
{ name: '通化市', center: [125.936501, 41.721177] },
|
||||
{ name: '白山市', center: [126.427839, 41.942505] },
|
||||
{ name: '松原市', center: [124.823608, 45.118243] },
|
||||
{ name: '白城市', center: [122.841114, 45.619026] },
|
||||
{ name: '延边自治州', center: [129.513228, 42.904823] }
|
||||
],
|
||||
labelSprites: [], // 存储标签精灵对象的数组
|
||||
cityMeshes: [], // 存储每个城市mesh及其原始z
|
||||
hoveredCity: null, // 当前高亮的mesh
|
||||
flylines: [], // 存储飞线对象
|
||||
flylineGroup: null, // 飞线组
|
||||
activeFlylines: new Map() // 存储活动的飞线
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.canvasW && this.canvasH) {
|
||||
this.initThree(this.canvasW, this.canvasH)
|
||||
window.addEventListener('resize', this.onResize)
|
||||
} else {
|
||||
this.resizeObs = new ResizeObserver(entries => {
|
||||
const { width, height } = entries[0].contentRect
|
||||
if (width && height && !this.inited) {
|
||||
this.initThree(width, height)
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.resizeObs.disconnect()
|
||||
}
|
||||
})
|
||||
this.resizeObs.observe(this.$refs.wrap)
|
||||
}
|
||||
// 鼠标交互:城市凸起和飞线
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
this.mouse = new THREE.Vector2()
|
||||
this.$refs.wrap.addEventListener('mousemove', this.onMapMouseMove)
|
||||
this.$refs.wrap.addEventListener('click', this.onMapClick)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
cancelAnimationFrame(this.frameId)
|
||||
this.controls?.dispose()
|
||||
this.renderer?.dispose()
|
||||
this.resizeObs?.disconnect()
|
||||
this.$refs.wrap?.removeEventListener('mousemove', this.onMapMouseMove)
|
||||
this.$refs.wrap?.removeEventListener('click', this.onMapClick)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/* ---------- 初始化 ---------- */
|
||||
initThree (w, h) {
|
||||
this.inited = true
|
||||
|
||||
/* 场景 & 渲染器 */
|
||||
this.scene = new THREE.Scene()
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias:true, alpha:true })
|
||||
this.renderer.setSize(w, h)
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio)
|
||||
this.renderer.setClearColor(this.bgColor, 0)
|
||||
/* 物理光照 + 色彩空间 */
|
||||
this.renderer.physicallyCorrectLights = true
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
this.$refs.wrap.appendChild(this.renderer.domElement)
|
||||
|
||||
/* 灯光优化:增加白色方向光,让立体感更明显 */
|
||||
this.scene.add(new THREE.AmbientLight(0x2d65cd, 1.5)) // 环境光用主色
|
||||
this.scene.add(new THREE.HemisphereLight(0x2d65cd, 0x113667, 1.2))
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1.0) // 白色方向光
|
||||
keyLight.position.set(300, 400, 500)
|
||||
this.scene.add(keyLight)
|
||||
|
||||
/* 地图模型 */
|
||||
this.buildMap()
|
||||
|
||||
/* 飞线组 */
|
||||
this.flylineGroup = new THREE.Group()
|
||||
this.scene.add(this.flylineGroup)
|
||||
|
||||
/* 相机自适配 */
|
||||
this.setCamera(w, h)
|
||||
|
||||
/* OrbitControls */
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.controls.enablePan = false
|
||||
this.controls.enableRotate = false
|
||||
this.controls.enableZoom = false
|
||||
this.controls.target.copy(this.center)
|
||||
this.controls.update()
|
||||
|
||||
/* 渲染循环 */
|
||||
const renderLoop = () => {
|
||||
this.controls.update()
|
||||
|
||||
// 动画效果 - 脉冲呼吸效果
|
||||
const time = Date.now() * 0.001 // 当前时间(秒)
|
||||
|
||||
this.labelSprites.forEach(item => {
|
||||
if (item.type === 'pulse') {
|
||||
// 呼吸效果 - 缩放在0.8和1.5之间变化
|
||||
const scale = 0.8 + Math.sin(time * 2 + item.phase) * 0.35 + 0.35
|
||||
item.mesh.scale.set(scale, scale, 1)
|
||||
|
||||
// 透明度也随时间变化
|
||||
item.mesh.material.opacity = 0.3 * (0.5 + Math.sin(time * 2 + item.phase) * 0.25 + 0.25)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新飞线动画
|
||||
this.updateFlylines(time)
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
this.frameId = requestAnimationFrame(renderLoop)
|
||||
}
|
||||
renderLoop()
|
||||
},
|
||||
|
||||
/* ---------- 构建吉林省地图 ---------- */
|
||||
buildMap () {
|
||||
const depth = 0.2
|
||||
const lineGroup = new THREE.Group()
|
||||
const meshGroup = new THREE.Group()
|
||||
const labelGroup = new THREE.Group()
|
||||
const markerGroup = new THREE.Group()
|
||||
|
||||
lineGroup.position.z = depth + 0.0001
|
||||
meshGroup.position.z = 0
|
||||
labelGroup.position.z = depth + 0.1
|
||||
markerGroup.position.z = depth + 0.05
|
||||
|
||||
const mapGroup = new THREE.Group()
|
||||
mapGroup.add(lineGroup, meshGroup, labelGroup, markerGroup)
|
||||
this.scene.add(mapGroup)
|
||||
|
||||
// 创建城市名称到中心点的映射
|
||||
const cityNameMap = new Map()
|
||||
this.cityData.forEach(city => {
|
||||
cityNameMap.set(city.name, city)
|
||||
})
|
||||
|
||||
// 计算每个城市区域的中心点
|
||||
JilinGeoJson.features.forEach(f => {
|
||||
let polys = f.geometry.coordinates
|
||||
if (f.geometry.type === 'Polygon') polys = [polys]
|
||||
|
||||
// 计算城市区域的中心点
|
||||
let centerX = 0, centerY = 0, pointCount = 0
|
||||
polys.forEach(poly => {
|
||||
const outer = poly[0]
|
||||
outer.forEach(point => {
|
||||
centerX += point[0]
|
||||
centerY += point[1]
|
||||
pointCount++
|
||||
})
|
||||
})
|
||||
centerX /= pointCount
|
||||
centerY /= pointCount
|
||||
|
||||
// 更新城市数据中的中心点
|
||||
const cityName = f.properties.name
|
||||
const cityData = this.cityData.find(city => {
|
||||
if (cityName.includes('延边')) {
|
||||
return city.name === '延边自治州'
|
||||
}
|
||||
return city.name === cityName
|
||||
})
|
||||
if (cityData) {
|
||||
cityData.center = [centerX, centerY]
|
||||
}
|
||||
|
||||
// 创建地图网格
|
||||
polys.forEach(poly => {
|
||||
const outer = poly[0]
|
||||
const holes = poly.slice(1)
|
||||
lineGroup.add(this.createLine(outer))
|
||||
const mesh = this.createExtrudeMesh(outer, holes, depth)
|
||||
mesh.userData.cityName = cityName
|
||||
if (cityName.includes('延边')) {
|
||||
mesh.userData.cityName = '延边自治州'
|
||||
}
|
||||
mesh.userData.originZ = mesh.position.z
|
||||
meshGroup.add(mesh)
|
||||
this.cityMeshes.push(mesh)
|
||||
})
|
||||
})
|
||||
|
||||
const box = new THREE.Box3().expandByObject(lineGroup)
|
||||
box.getCenter(this.center)
|
||||
const size = new THREE.Vector3()
|
||||
box.getSize(size)
|
||||
this.halfSizeX = size.x / 2
|
||||
this.halfSizeY = size.y / 2
|
||||
|
||||
this.addCityLabels(labelGroup)
|
||||
this.addCityMarkers(markerGroup, depth)
|
||||
},
|
||||
|
||||
/* ---------- 边界线 ---------- */
|
||||
createLine (ring) {
|
||||
const vertices = new Float32Array(ring.length * 3)
|
||||
ring.forEach((p, i) => {
|
||||
vertices[i * 3] = p[0]
|
||||
vertices[i * 3 + 1] = p[1]
|
||||
vertices[i * 3 + 2] = 0
|
||||
})
|
||||
const geo = new THREE.BufferGeometry()
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
|
||||
return new THREE.LineLoop(geo, new THREE.LineBasicMaterial({
|
||||
color: 0x0d2a50,
|
||||
opacity: 0.8,
|
||||
transparent: true
|
||||
}))
|
||||
},
|
||||
|
||||
/* ---------- 立体 Mesh ---------- */
|
||||
createExtrudeMesh(outer, holes, depth) {
|
||||
const shape = new THREE.Shape(outer.map(p => new THREE.Vector2(p[0], p[1])))
|
||||
holes.forEach(h => {
|
||||
shape.holes.push(new THREE.Path(h.map(p => new THREE.Vector2(p[0], p[1]))))
|
||||
})
|
||||
const geo = new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled:false })
|
||||
|
||||
return new THREE.Mesh(
|
||||
geo,
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 0x4ba9bc,
|
||||
roughness: 0.1,
|
||||
metalness: 0.1,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
/* ---------- 创建飞线 ---------- */
|
||||
createFlyline(startPoint, endPoint, index) {
|
||||
// 创建曲线
|
||||
const start = new THREE.Vector3(startPoint[0], startPoint[1], 0.3)
|
||||
const end = new THREE.Vector3(endPoint[0], endPoint[1], 0.3)
|
||||
|
||||
// 计算中点,并添加高度
|
||||
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5)
|
||||
const distance = start.distanceTo(end)
|
||||
mid.z = 2 + distance * 0.15 // 根据距离调整高度,使弧度更明显
|
||||
|
||||
// 创建曲线
|
||||
const curve = new THREE.QuadraticBezierCurve3(start, mid, end)
|
||||
|
||||
// 创建几何体
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
const points = curve.getPoints(50)
|
||||
geometry.setFromPoints(points)
|
||||
|
||||
// 创建材质
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: 0x81d7bc,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
linewidth: 1
|
||||
})
|
||||
|
||||
// 创建线条
|
||||
const line = new THREE.Line(geometry, material)
|
||||
|
||||
// 存储飞线信息
|
||||
const flyline = {
|
||||
mesh: line,
|
||||
curve: curve,
|
||||
progress: 0,
|
||||
speed: 0.4 + Math.random() * 0.2,
|
||||
delay: index * 0.15,
|
||||
active: false,
|
||||
opacity: 0,
|
||||
targetOpacity: 0.8,
|
||||
fadeSpeed: 0.05,
|
||||
start: start.clone(),
|
||||
end: end.clone()
|
||||
}
|
||||
|
||||
this.flylines.push(flyline)
|
||||
return line
|
||||
},
|
||||
|
||||
/* ---------- 更新飞线动画 ---------- */
|
||||
updateFlylines(time) {
|
||||
this.flylines.forEach(flyline => {
|
||||
if (time > flyline.delay) {
|
||||
flyline.active = true
|
||||
}
|
||||
|
||||
if (flyline.active) {
|
||||
// 更新进度
|
||||
flyline.progress += flyline.speed * 0.01
|
||||
if (flyline.progress >= 1) {
|
||||
flyline.progress = 0
|
||||
flyline.opacity = Math.max(0, flyline.opacity - 0.1)
|
||||
} else {
|
||||
flyline.opacity = Math.min(0.8, flyline.opacity + 0.1)
|
||||
}
|
||||
|
||||
// 更新线条位置
|
||||
const positions = flyline.mesh.geometry.attributes.position.array
|
||||
const currentPoint = new THREE.Vector3()
|
||||
const lastPoint = new THREE.Vector3()
|
||||
flyline.curve.getPoint(Math.min(flyline.progress, 1), lastPoint)
|
||||
|
||||
const total = positions.length / 3
|
||||
for (let i = 0; i < total; i++) {
|
||||
const t = i / (total - 1)
|
||||
if (t <= flyline.progress) {
|
||||
flyline.curve.getPoint(t, currentPoint)
|
||||
positions[i * 3] = currentPoint.x
|
||||
positions[i * 3 + 1] = currentPoint.y
|
||||
positions[i * 3 + 2] = currentPoint.z
|
||||
} else {
|
||||
positions[i * 3] = lastPoint.x
|
||||
positions[i * 3 + 1] = lastPoint.y
|
||||
positions[i * 3 + 2] = lastPoint.z
|
||||
}
|
||||
}
|
||||
|
||||
flyline.mesh.geometry.attributes.position.needsUpdate = true
|
||||
flyline.mesh.material.opacity = flyline.opacity
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 处理鼠标移动 ---------- */
|
||||
onMapMouseMove(event) {
|
||||
if (!this.camera || !this.cityMeshes.length) return
|
||||
const rect = this.$refs.wrap.getBoundingClientRect()
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera)
|
||||
const intersects = this.raycaster.intersectObjects(this.cityMeshes)
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const mesh = intersects[0].object
|
||||
if (this.hoveredCity && this.hoveredCity !== mesh) {
|
||||
this.hoveredCity.position.z = this.hoveredCity.userData.originZ
|
||||
this.clearFlylines()
|
||||
}
|
||||
if (this.hoveredCity !== mesh) {
|
||||
mesh.position.z = mesh.userData.originZ + 0.15
|
||||
this.hoveredCity = mesh
|
||||
|
||||
const cityName = mesh.userData.cityName
|
||||
const cityData = this.cityData.find(city => city.name === cityName)
|
||||
|
||||
if (cityData) {
|
||||
const processedCities = new Set()
|
||||
this.cityData.forEach((targetCity, index) => {
|
||||
if (targetCity.name !== cityName && !processedCities.has(targetCity.name)) {
|
||||
processedCities.add(targetCity.name)
|
||||
const flyline = this.createFlyline(cityData.center, targetCity.center, index)
|
||||
this.flylineGroup.add(flyline)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (this.hoveredCity) {
|
||||
this.hoveredCity.position.z = this.hoveredCity.userData.originZ
|
||||
this.hoveredCity = null
|
||||
this.clearFlylines()
|
||||
}
|
||||
},
|
||||
|
||||
/* ---------- 清除飞线 ---------- */
|
||||
clearFlylines() {
|
||||
while (this.flylineGroup.children.length > 0) {
|
||||
this.flylineGroup.remove(this.flylineGroup.children[0])
|
||||
}
|
||||
this.flylines = []
|
||||
},
|
||||
|
||||
/* ---------- 添加城市标签 ---------- */
|
||||
addCityLabels(labelGroup) {
|
||||
// 创建Canvas纹理
|
||||
const createTextTexture = (text) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const fontSize = 16
|
||||
const padding = 8
|
||||
// 设置Canvas大小
|
||||
ctx.font = `bold ${fontSize}px Arial`
|
||||
const textWidth = ctx.measureText(text).width
|
||||
canvas.width = textWidth + padding * 2
|
||||
canvas.height = fontSize + padding * 2
|
||||
// 只绘制文字
|
||||
ctx.font = `bold ${fontSize}px Arial`
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, canvas.width / 2, canvas.height / 2)
|
||||
// 创建纹理
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
texture.needsUpdate = true
|
||||
return { texture, width: canvas.width, height: canvas.height }
|
||||
}
|
||||
|
||||
// 为每个城市创建标签
|
||||
this.cityData.forEach(city => {
|
||||
const { texture, width, height } = createTextTexture(city.name)
|
||||
|
||||
// 创建标签材质
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true
|
||||
})
|
||||
|
||||
// 创建精灵标签
|
||||
const sprite = new THREE.Sprite(material)
|
||||
|
||||
// 设置位置 - 添加一个小的Y轴偏移,让标签显示在城市上方
|
||||
sprite.position.set(
|
||||
city.center[0],
|
||||
city.center[1] + 0.3, // 向上偏移一点
|
||||
0.2 // 确保标签在地图上方
|
||||
)
|
||||
|
||||
// 设置缩放比例
|
||||
const scale = 1.0
|
||||
sprite.scale.set(width * scale * 0.01, height * scale * 0.01, 1)
|
||||
|
||||
// 存储精灵对象以便在渲染循环中更新
|
||||
this.labelSprites.push({
|
||||
type: 'label',
|
||||
mesh: sprite
|
||||
})
|
||||
|
||||
// 添加到标签组
|
||||
labelGroup.add(sprite)
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 添加城市标记点 ---------- */
|
||||
addCityMarkers(markerGroup, depth) {
|
||||
// 创建一个圆环几何体作为标记
|
||||
const ringGeometry = new THREE.RingGeometry(0.1, 0.2, 32)
|
||||
|
||||
this.cityData.forEach(city => {
|
||||
// 内部点 - 亮点
|
||||
const innerMarker = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(0.1, 32),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0x81d7bc, // 蓝绿色
|
||||
transparent: true,
|
||||
opacity: 0.9
|
||||
})
|
||||
)
|
||||
|
||||
// 外环 - 光晕效果
|
||||
const outerRing = new THREE.Mesh(
|
||||
ringGeometry,
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0x81d7bc, // 蓝绿色
|
||||
transparent: true,
|
||||
opacity: 0.5
|
||||
})
|
||||
)
|
||||
|
||||
// 设置位置
|
||||
innerMarker.position.set(city.center[0], city.center[1], depth + 0.05)
|
||||
outerRing.position.set(city.center[0], city.center[1], depth + 0.05)
|
||||
|
||||
// 添加到标记组
|
||||
markerGroup.add(innerMarker)
|
||||
markerGroup.add(outerRing)
|
||||
|
||||
// 创建动画效果
|
||||
const pulseMarker = new THREE.Mesh(
|
||||
new THREE.RingGeometry(0.2, 0.3, 32),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: 0x81d7bc, // 蓝绿色
|
||||
transparent: true,
|
||||
opacity: 0.3
|
||||
})
|
||||
)
|
||||
pulseMarker.position.set(city.center[0], city.center[1], depth + 0.05)
|
||||
markerGroup.add(pulseMarker)
|
||||
|
||||
// 存储用于动画的对象
|
||||
this.labelSprites.push({
|
||||
type: 'pulse',
|
||||
mesh: pulseMarker,
|
||||
initialScale: 1.0,
|
||||
phase: Math.random() * Math.PI * 2 // 随机初始相位
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/* ---------- 相机 ---------- */
|
||||
setCamera(w, h) {
|
||||
const aspect = w / h
|
||||
const fov = 45 // 视场角
|
||||
const tilt = Math.PI / 5 // 36度倾斜
|
||||
// 地图包围盒最大边
|
||||
const mapWidth = this.halfSizeX * 2
|
||||
const mapHeight = this.halfSizeY * 2
|
||||
// 以最大边为基准,计算相机距离
|
||||
const maxMapSize = Math.max(mapWidth, mapHeight / aspect)
|
||||
// 计算相机到中心的距离,使地图刚好铺满canvas
|
||||
// 公式: distance = (maxMapSize/2) / tan(fov/2)
|
||||
let distance = (maxMapSize / 2) / Math.tan(fov / 2 * Math.PI / 180)
|
||||
distance = distance * 0.8 // 让相机更近
|
||||
// 相机位置:在中心点上方并倾斜
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
fov,
|
||||
aspect,
|
||||
1,
|
||||
2000
|
||||
)
|
||||
// 让相机在 y 轴和 z 轴都有偏移,形成倾斜视角
|
||||
this.camera.position.set(
|
||||
this.center.x,
|
||||
this.center.y - Math.sin(tilt) * distance,
|
||||
this.center.z + Math.cos(tilt) * distance
|
||||
)
|
||||
this.camera.lookAt(this.center)
|
||||
},
|
||||
|
||||
/* ---------- 窗口缩放 ---------- */
|
||||
onResize() {
|
||||
if (!this.camera) return
|
||||
const w = this.$refs.wrap.clientWidth
|
||||
const h = this.$refs.wrap.clientHeight
|
||||
if (!w || !h) return
|
||||
this.renderer.setSize(w, h)
|
||||
|
||||
const aspect = w / h
|
||||
let halfW = this.halfSizeX, halfH = this.halfSizeY
|
||||
if (aspect >= halfW / halfH) halfW = halfH * aspect
|
||||
else halfH = halfW / aspect
|
||||
|
||||
this.camera.left = -halfW
|
||||
this.camera.right = halfW
|
||||
this.camera.top = halfH
|
||||
this.camera.bottom = -halfH
|
||||
this.camera.updateProjectionMatrix()
|
||||
},
|
||||
|
||||
// Add new click handler method
|
||||
onMapClick(event) {
|
||||
if (!this.camera || !this.cityMeshes.length) return
|
||||
const rect = this.$refs.wrap.getBoundingClientRect()
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera)
|
||||
const intersects = this.raycaster.intersectObjects(this.cityMeshes)
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const mesh = intersects[0].object
|
||||
const cityName = mesh.userData.cityName
|
||||
this.$router.push({
|
||||
name: 'CityPage',
|
||||
params: { cityName }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
/**
|
||||
* @param {Array<Array<number>>} outer - 外环 [[lng,lat], ...]
|
||||
* @param {Array<Array<Array<number>>>} holes - 内环数组,可选
|
||||
* @returns {THREE.Mesh}
|
||||
*/
|
||||
export default function createMesh (outer, holes = []) {
|
||||
const outerV2 = outer.map(p => new THREE.Vector2(p[0], p[1]))
|
||||
const shape = new THREE.Shape(outerV2)
|
||||
|
||||
holes.forEach(hole => {
|
||||
const holeV2 = hole.map(p => new THREE.Vector2(p[0], p[1]))
|
||||
shape.holes.push(new THREE.Path(holeV2))
|
||||
})
|
||||
|
||||
const geometry = new THREE.ShapeGeometry(shape) // r154+ 使用 ShapeGeometry
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x004444,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
return new THREE.Mesh(geometry, material)
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<div ref="chartContainer" :style="{width: '100%', height: chartHeight}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LineIndex',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
chartHeight: {
|
||||
type: String,
|
||||
default: '260px', // 默认值
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize(); // 让 ECharts 重新适应新高度
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('->>>>')
|
||||
this.initChart();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (params) {
|
||||
return `${params.name}: ${params.value}`;
|
||||
}
|
||||
},
|
||||
graphic: [
|
||||
{
|
||||
type: 'group',
|
||||
left: 'center',
|
||||
top: '45%',
|
||||
children: [
|
||||
{
|
||||
id: 'text-name',
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '0', // 调整位置
|
||||
style: {
|
||||
text: '', // 初始为空,鼠标移入时更新
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
fill: '#fff' // 字体颜色
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text-value',
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '10', // 调整位置
|
||||
style: {
|
||||
text: '',
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
fill: '#fff' // 字体颜色
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '人员类型',
|
||||
type: 'pie',
|
||||
zlevel: 1,
|
||||
colorMappingBy: 'id',
|
||||
center: ['50%', '50%'],
|
||||
radius: ['40%', '55%'],
|
||||
label: {
|
||||
normal: {
|
||||
show: false, // 直接隐藏标签
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
color: ['#18755B', '#0D618E', '#168990', '#738353', '#7c5b24'],
|
||||
data: [
|
||||
{value: 500, name: '信息技术', itemStyle: {color: '#18755B'}},
|
||||
{value: 150, name: '生物医药', itemStyle: {color: '#0D618E'}},
|
||||
{value: 100, name: '能源环境', itemStyle: {color: '#168990'}},
|
||||
{value: 50, name: '工业制造', itemStyle: {color: '#738353'}},
|
||||
{value: 300, name: '食品工业', itemStyle: {color: '#7c5b24'}}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '人员类型',
|
||||
type: 'pie',
|
||||
zlevel: 2,
|
||||
colorMappingBy: 'id',
|
||||
center: ['50%', '50%'],
|
||||
radius: ['55%', '70%'],
|
||||
label: {
|
||||
normal: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
color: ['#229d7c', '#0f6fbf', '#1badc2', '#adb066', '#b8681e'],
|
||||
data: [
|
||||
{value: 500, name: '信息技术', itemStyle: {color: '#229d7c'}},
|
||||
{value: 150, name: '生物医药', itemStyle: {color: '#0f6fbf'}},
|
||||
{value: 100, name: '能源环境', itemStyle: {color: '#1badc2'}},
|
||||
{value: 50, name: '工业制造', itemStyle: {color: '#adb066'}},
|
||||
{value: 300, name: '食品工业', itemStyle: {color: '#b8681e'}}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '人员类型',
|
||||
type: 'pie',
|
||||
zlevel: 3,
|
||||
colorMappingBy: 'id',
|
||||
center: ['50%', '50%'],
|
||||
radius: ['70%', '90%'],
|
||||
label: {
|
||||
normal: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
color: ['#37FFC9', '#0084FF', '#19D6FF', '#FFE777', '#FF7908'],
|
||||
data: [
|
||||
{value: 500, name: '信息技术', itemStyle: {color: '#37FFC9'}},
|
||||
{value: 150, name: '生物医药', itemStyle: {color: '#0084FF'}},
|
||||
{value: 100, name: '能源环境', itemStyle: {color: '#19D6FF'}},
|
||||
{value: 50, name: '工业制造', itemStyle: {color: '#FFE777'}},
|
||||
{value: 300, name: '食品工业', itemStyle: {color: '#FF7908'}}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
|
||||
// 监听鼠标事件,动态更新中心文本
|
||||
this.myChart.on('mouseover', (params) => {
|
||||
this.myChart.setOption({
|
||||
graphic: [
|
||||
{id: 'text-name', style: {text: params.name}},
|
||||
{id: 'text-value', style: {text: params.value + '个'}}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// 鼠标移出时清空中心文本
|
||||
this.myChart.on('mouseout', () => {
|
||||
this.myChart.setOption({
|
||||
graphic: [
|
||||
{id: 'text-name', style: {text: ''}},
|
||||
{id: 'text-value', style: {text: ''}}
|
||||
]
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div ref="chartContainer" :style="{width: '100%', height: '200px'}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LineIndex',
|
||||
data() {
|
||||
return {
|
||||
myChart: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
chartHeight() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize(); // 让 ECharts 重新适应新高度
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log('->>>>')
|
||||
this.initChart();
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.myChart = this.$echarts.init(this.$refs.chartContainer);
|
||||
|
||||
const option = {
|
||||
color: ['#159AFF', '#76FAB3', '#65DFDD', '#FCC779'], // 对应 icon1 ~ icon4
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
top: 'bottom',
|
||||
data: [
|
||||
'rose1',
|
||||
'rose2',
|
||||
'rose3',
|
||||
'rose4'
|
||||
]
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '金融产品发布情况',
|
||||
type: 'pie',
|
||||
radius: [15, 100],
|
||||
center: ['55%', '50%'],
|
||||
roseType: 'radius',
|
||||
startAngle: 270,
|
||||
label: {
|
||||
normal: {
|
||||
show: false, // 直接隐藏标签
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{value: 40, name: 'rose 1'},
|
||||
{value: 33, name: 'rose 2'},
|
||||
{value: 28, name: 'rose 3'},
|
||||
{value: 22, name: 'rose 4'}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '透明圆圈',
|
||||
type: 'pie',
|
||||
radius: [15, 30],
|
||||
center: ['55%', '50%'],
|
||||
itemStyle: {
|
||||
color: 'rgba(250, 250, 250, 0.3)',
|
||||
},
|
||||
data: [
|
||||
{value: 10, name: ''}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '透明圆圈',
|
||||
type: 'pie',
|
||||
radius: [30, 31],
|
||||
center: ['55%', '50%'],
|
||||
itemStyle: {
|
||||
color: 'rgba(250, 250, 250)',
|
||||
},
|
||||
data: [
|
||||
{value: 10, name: ''}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
|
||||
// 监听鼠标事件,动态更新中心文本
|
||||
this.myChart.on('mouseover', (params) => {
|
||||
this.myChart.setOption({
|
||||
graphic: [
|
||||
{id: 'text-name', style: {text: params.name}},
|
||||
{id: 'text-value', style: {text: params.value + '个'}}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// 鼠标移出时清空中心文本
|
||||
this.myChart.on('mouseout', () => {
|
||||
this.myChart.setOption({
|
||||
graphic: [
|
||||
{id: 'text-name', style: {text: ''}},
|
||||
{id: 'text-value', style: {text: ''}}
|
||||
]
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<template>
|
||||
<div class="carousel-container">
|
||||
<div
|
||||
class="carousel-list"
|
||||
ref="carousel"
|
||||
:style="{ transform: `translateY(${-currentIndex * 40}px)` }"
|
||||
>
|
||||
<div v-for="(item, index) in loopedList" :key="item.id" class="carousel-item">
|
||||
<div class="sort" :style="{ backgroundImage: getSortBackground(item.sort) }">
|
||||
No.{{ item.sort }}
|
||||
</div>
|
||||
<div class="progress-name">{{ item.name }}</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-background"></div>
|
||||
<div class="progress-bar" :style="getBarStyle(item.sort, item.count)"></div>
|
||||
</div>
|
||||
<div class="progress-num">
|
||||
<span style="font-weight: bold; font-size: 16px">{{ item.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import rank1bg from '../../assets/images/bg/rank1_bg.png';
|
||||
import rank2bg from '../../assets/images/bg/rank2_bg.png';
|
||||
import rank3bg from '../../assets/images/bg/rank3_bg.png';
|
||||
import rank4bg from '../../assets/images/bg/rank4_bg.png';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0, // 控制滚动位置
|
||||
list: [
|
||||
{id: 1, sort: 1, name: '科技论文', count: 3693},
|
||||
{id: 2, sort: 2, name: '发明专利', count: 2791},
|
||||
{id: 3, sort: 3, name: '行业标准', count: 2635},
|
||||
{id: 4, sort: 4, name: '公司名称', count: 11187},
|
||||
{id: 5, sort: 5, name: '公司名称', count: 1672},
|
||||
{id: 6, sort: 6, name: '公司名称', count: 1573},
|
||||
{id: 7, sort: 7, name: '公司名称', count: 1274},
|
||||
{id: 8, sort: 8, name: '公司名称', count: 1085},
|
||||
{id: 9, sort: 9, name: '公司名称', count: 976},
|
||||
{id: 10, sort: 10, name: '公司名称', count: 901},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 让数据首尾循环,防止滚动到末尾出现空白
|
||||
loopedList() {
|
||||
if (this.list.length > 5) {
|
||||
return [
|
||||
...this.list,
|
||||
...this.list.slice(0, 5).map((item, index) => ({
|
||||
...item,
|
||||
id: `${item.id}-extra-${index}`, // 生成唯一 key
|
||||
})),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...this.list,
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSortBackground(sort) {
|
||||
if (sort === 1) return `url(${rank1bg})`;
|
||||
if (sort === 2) return `url(${rank2bg})`;
|
||||
if (sort === 3) return `url(${rank3bg})`;
|
||||
return `url(${rank4bg})`;
|
||||
},
|
||||
getBarStyle(sort, count) {
|
||||
const maxCount = Math.max(...this.list.map((item) => item.count));
|
||||
const width = (count / maxCount) * 100; // 计算比例
|
||||
const gradient =
|
||||
sort <= 3
|
||||
? 'linear-gradient(270deg, #79D8FF 0%, #2C9AFF 100%)' // 方案 A 举例
|
||||
: 'linear-gradient(270deg, #4CA4FF 0%, #1155CC 100%)';
|
||||
return {width: `${width}%`, background: gradient};
|
||||
},
|
||||
startAutoScroll() {
|
||||
this.interval = setInterval(() => {
|
||||
this.currentIndex++;
|
||||
|
||||
// 监听 `currentIndex` 是否超出数据范围
|
||||
if (this.currentIndex >= this.list.length) {
|
||||
setTimeout(() => {
|
||||
this.currentIndex = 0; // 瞬间回到起点
|
||||
}, 500); // 过渡完成后再复位,防止闪烁
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.list.length > 5) {
|
||||
this.startAutoScroll();
|
||||
} else {
|
||||
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.5s ease-in-out; /* 平滑滚动 */
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
width: 50px;
|
||||
color: white;
|
||||
margin-top: 5px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.progress-name {
|
||||
position: relative;
|
||||
width: 70px;
|
||||
height: 30px;
|
||||
margin-top: 5px;
|
||||
line-height: 30px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.progress-num {
|
||||
height: 30px;
|
||||
margin-top: 5px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: relative;
|
||||
width: 230px;
|
||||
height: 10px;
|
||||
background-color: #103258;
|
||||
border-radius: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<div class="carousel-container">
|
||||
<div
|
||||
class="carousel-list"
|
||||
ref="carousel"
|
||||
:style="{ transform: `translateY(${-currentIndex * 40}px)` }"
|
||||
>
|
||||
<div v-for="(item, index) in loopedList" :key="item.id" class="carousel-item" :class="{ odd: index % 2 === 0 }"
|
||||
>
|
||||
<div class="item-rank">{{ item.rank }}</div>
|
||||
<div class="item-num-name">{{ item.name }}</div>
|
||||
<div class="item-num">{{ item.num2 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0, // 控制滚动位置
|
||||
list: [
|
||||
{id: 1, rank: '1', name: '一种基于区块链的车联网安全通信方法与系统', num2: 12},
|
||||
{id: 2, rank: '2', name: '一种面向智能制造的高效节能热交换设备及其控制方法', num2: 24},
|
||||
{id: 3, rank: '3', name: '一种多模式结合的深海无人潜航器姿态稳定装置与算法', num2: 53},
|
||||
{id: 4, rank: '4', name: '基于光谱融合分析的农作物病虫害快速诊断系统及装置', num2: 77},
|
||||
{id: 5, rank: '5', name: '面向智慧城市的多源异构数据实时融合处理平台的实现方法', num2: 12},
|
||||
{id: 6, rank: '6', name: '一种新型抗菌耐磨复合医用植入材料及其制备工艺', num2: 83},
|
||||
{id: 7, rank: '7', name: '一种利用风光互补的海上浮式绿色能源供给装置与方法', num2: 21},
|
||||
{id: 8, rank: '8', name: '一种高分辨率卫星影像的多尺度语义分割训练模型', num2: 99},
|
||||
{id: 9, rank: '9', name: '面向自动驾驶的激光雷达与毫米波融合环境感知算法系统', num2: 34},
|
||||
{id: 10, rank: '10', name: '一种可降解高强度柔性电子皮肤传感器的制备方法', num2: 51},
|
||||
{id: 11, rank: '11', name: '用于电池梯次利用的智能健康评估与剩余寿命预测平台', num2: 10},
|
||||
{id: 12, rank: '12', name: '一种基于量子点增强的高效率全彩微型显示器及制备工艺', num2: 99}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 让数据首尾循环,防止滚动到末尾出现空白
|
||||
loopedList() {
|
||||
if (this.list.length > 10) {
|
||||
return [
|
||||
...this.list,
|
||||
...this.list.slice(0, 10).map((item, index) => ({
|
||||
...item,
|
||||
id: `${item.id}-extra-${index}`, // 生成唯一 key
|
||||
})),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...this.list,
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startAutoScroll() {
|
||||
this.interval = setInterval(() => {
|
||||
this.currentIndex++;
|
||||
|
||||
// 监听 `currentIndex` 是否超出数据范围
|
||||
if (this.currentIndex >= this.list.length) {
|
||||
setTimeout(() => {
|
||||
this.currentIndex = 0; // 瞬间回到起点
|
||||
}, 500); // 过渡完成后再复位,防止闪烁
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.list.length > 10) {
|
||||
this.startAutoScroll();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carousel-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: calc(100% - 40px);
|
||||
margin-left: 20px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#0D2847 0%,
|
||||
rgba(13, 40, 71, 0.21) 48%,
|
||||
#103258 100%
|
||||
);
|
||||
color: #CBE2FF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.carousel-item.odd {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.item-rank {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-num {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-num-name {
|
||||
flex: 4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div ref="cloud" :style="{ width: '100%', height }"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'echarts-wordcloud' // 引入 wordcloud 扩展 (必须在 echarts 之后)
|
||||
|
||||
export default {
|
||||
name: 'WordCloud',
|
||||
props: {
|
||||
/**
|
||||
* words 数组支持两种格式:
|
||||
* ["AI", "IoT", "Digital Humans"]
|
||||
* 或 [{ text:"AI", value:120 }, { text:"IoT", value:80 }]
|
||||
*/
|
||||
words: { type: Array, required: true },
|
||||
height: { type: String, default: '300px' }
|
||||
},
|
||||
data() {
|
||||
return { chart: null }
|
||||
},
|
||||
watch: {
|
||||
words: {
|
||||
deep: true,
|
||||
handler() { this.renderCloud() }
|
||||
},
|
||||
height() {
|
||||
this.$nextTick(() => { this.chart && this.chart.resize() })
|
||||
}
|
||||
},
|
||||
mounted() { this.init() },
|
||||
beforeDestroy() { this.chart && this.chart.dispose() },
|
||||
methods: {
|
||||
init() {
|
||||
this.chart = this.$echarts.init(this.$refs.cloud)
|
||||
this.renderCloud()
|
||||
window.addEventListener('resize', this.chart.resize)
|
||||
},
|
||||
renderCloud() {
|
||||
if (!this.chart) return
|
||||
|
||||
// ① 把字符串转成带权重对象
|
||||
const data = this.words.map(w =>
|
||||
typeof w === 'string'
|
||||
? { name: w, value: w.length * 8 } // 适当缩小基准权重
|
||||
: { name: w.text, value: w.value }
|
||||
)
|
||||
|
||||
// ② 根据容器实时计算最大字号
|
||||
const { height: h } = this.$refs.cloud.getBoundingClientRect()
|
||||
const maxFont = Math.floor(h * 0.30) // 占容器高度 30%
|
||||
const minFont = Math.max(10, Math.floor(maxFont / 3))
|
||||
|
||||
this.chart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: { show: false },
|
||||
series: [{
|
||||
type: 'wordCloud',
|
||||
/* --- 关键:贴满父容器,无留白 --- */
|
||||
left: 70, top: 0, right: 0, bottom: 0,
|
||||
shape: 'circle',
|
||||
|
||||
sizeRange: [minFont, maxFont], // 动态字号
|
||||
gridSize: 2, // 更细网格
|
||||
shrinkToFit: true, // 末词再压缩
|
||||
rotationRange: [ -30, 30 ], // 轻微倾斜即可
|
||||
rotationStep: 30,
|
||||
|
||||
textStyle: {
|
||||
fontFamily: 'Impact',
|
||||
color(params) {
|
||||
const toppers = ['#FC8A66', '#FED95D', '#40DA93']
|
||||
if (params.dataIndex < 3) return toppers[params.dataIndex]
|
||||
const palette = ['#79D8FF', '#2C9AFF', '#C99CFF', '#6A44FF', '#8EC4FF']
|
||||
return palette[Math.floor(Math.random() * palette.length)]
|
||||
}
|
||||
},
|
||||
|
||||
data
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可选:给 canvas 再加点淡淡的炫光背景 */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import './assets/font/font.css';
|
||||
|
||||
Vue.config.productionTip = false
|
||||
import * as echarts from 'echarts'
|
||||
Vue.prototype.$echarts = echarts
|
||||
import VScaleScreen from 'v-scale-screen'
|
||||
import 'default-passive-events'
|
||||
Vue.use(VScaleScreen)
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'generalPage',
|
||||
meta: { keepAlive: true },
|
||||
component: () => import('../views/generalPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/city/:cityName',
|
||||
name: 'CityPage',
|
||||
props: true,
|
||||
component: () => import('../views/cityPage.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'hash',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
windowHeight: 1080,
|
||||
},
|
||||
mutations: {
|
||||
setWindowHeight(state, height) {
|
||||
state.windowHeight = height
|
||||
console.log('输出储存的windowHeight', state.windowHeight)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- 1920×1080-->
|
||||
<v-scale-screen :width="windowWidth<1920?windowWidth:1920" :height="windowHeight<1080?windowHeight:1080"
|
||||
v-if="isShowScreen" >
|
||||
<div class="main">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</v-scale-screen>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
isShowScreen: false,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
this.windowHeight = window.innerHeight;
|
||||
this.$store.commit('setWindowHeight', this.windowHeight);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.isShowScreen = true
|
||||
}, 100)
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.header {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
//background: linear-gradient( 270deg, #0A362A 0%, #0F4433 54%, #0A362A 100%);
|
||||
}
|
||||
|
||||
.footer {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,850 @@
|
|||
<template>
|
||||
<div class="page-style">
|
||||
<div class="section-header" @click="goBack">
|
||||
<div class="screen-title">
|
||||
{{ cityName }}科技大脑智慧服务平台
|
||||
</div>
|
||||
<div class="back-button">返回省级</div>
|
||||
</div>
|
||||
<div class="section-container">
|
||||
<div class="section-top">
|
||||
<div class="section-top-left">
|
||||
<MapCity class="map-card" :geojson="cityGeoJson" v-if="cityGeoJson" @district-hover="onDistrictHover" @district-click="onDistrictClick"></MapCity>
|
||||
</div>
|
||||
<div class="section-top-right">
|
||||
<card title="专利转化潜力值排行" :heightPercentage="100">
|
||||
<div class="patent-ranking">
|
||||
<div class="ranking-header">
|
||||
<div class="ranking-city">{{cityName}}</div>
|
||||
<div class="ranking-count">145个</div>
|
||||
</div>
|
||||
<div class="ranking-chart" ref="potentialChartContainer">
|
||||
<div ref="potentialChart" style="width: 100%; height:350px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-bottom">
|
||||
<div class="chart-container">
|
||||
<card title="各类科技成果占比" :heightPercentage="100">
|
||||
<div ref="achievementChart" class="chart" style="height: 240px;"></div>
|
||||
</card>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<card :title="`${cityName}地区专利数量`" :heightPercentage="100">
|
||||
<div ref="patentChart" class="chart" style="height: 240px;"></div>
|
||||
</card>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<card title="地区科研单位占比" :heightPercentage="100">
|
||||
<div ref="institutionChart" class="chart" style="height: 240px;"></div>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import card from '../components/card/index'
|
||||
import barChart from '../components/bar/3DIndex.vue'
|
||||
import lineChart from '../components/line/index.vue'
|
||||
import pieChart from '../components/pie/index.vue'
|
||||
import rank from '../components/scroll/index.vue'
|
||||
import rank2 from '../components/scroll/index2.vue'
|
||||
import WordCloud from '../components/wordCloud/index.vue'
|
||||
import MapCity from '../components/map/mapCity.vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'CityPage',
|
||||
components: {
|
||||
card,
|
||||
barChart,
|
||||
lineChart,
|
||||
pieChart,
|
||||
rank,
|
||||
rank2,
|
||||
WordCloud,
|
||||
MapCity
|
||||
},
|
||||
props: {
|
||||
cityName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerHeight: '220px',
|
||||
cityGeoJson: null,
|
||||
hoveredDistrict: null,
|
||||
selectedDistrict: null,
|
||||
districtData: {
|
||||
// 模拟各区县数据
|
||||
'朝阳区': { patents: 256, research: 18, conversion: 145 },
|
||||
'南关区': { patents: 189, research: 12, conversion: 98 },
|
||||
'宽城区': { patents: 210, research: 15, conversion: 120 },
|
||||
'二道区': { patents: 178, research: 10, conversion: 85 },
|
||||
'绿园区': { patents: 195, research: 14, conversion: 110 },
|
||||
'双阳区': { patents: 120, research: 8, conversion: 65 },
|
||||
'九台区': { patents: 145, research: 9, conversion: 78 },
|
||||
'农安县': { patents: 132, research: 7, conversion: 70 }
|
||||
},
|
||||
pieList: [
|
||||
{id: 1, value: 72.49, name: '数据名称', color: '#37FFC9'},
|
||||
{id: 2, value: 32.55, name: '数据名称', color: '#0084FF'},
|
||||
{id: 3, value: 28.6, name: '数据名称', color: '#19D6FF'},
|
||||
{id: 4, value: 10.91, name: '数据名称', color: '#FFE777'}
|
||||
],
|
||||
peopleList: [
|
||||
{id: 1, label: '行业应用'},
|
||||
{id: 2, label: '工作性质'}
|
||||
],
|
||||
keywords: [
|
||||
'Digital Humans', 'Deep Learning', 'VR', 'AR', 'IoT',
|
||||
'Decentralization', 'Sustainability', 'Data Mining',
|
||||
'Autonomous Driving', 'MR'
|
||||
],
|
||||
achievementChart: null,
|
||||
patentChart: null,
|
||||
institutionChart: null,
|
||||
potentialChart: null,
|
||||
cityLineData: {
|
||||
'长春市': [150, 160, 150, 140, 160, 150, 140, 155, 160, 140, 135, 150],
|
||||
'延边州': [120, 130, 110, 120, 125, 115, 120, 125, 115, 120, 125, 115],
|
||||
'吉林市': [80, 75, 70, 85, 95, 85, 75, 80, 95, 85, 60, 90]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
windowHeight(newValue) {
|
||||
if (newValue >= 1000) {
|
||||
this.containerHeight = '240px'
|
||||
} else {
|
||||
this.containerHeight = '190px'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
// Load city GeoJSON data based on city name
|
||||
this.cityGeoJson = await this.loadCityGeoJson(this.cityName)
|
||||
console.log('this.cityGeoJson', this.cityGeoJson)
|
||||
} catch (error) {
|
||||
console.error('Failed to load city GeoJSON:', error)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.initAchievementChart();
|
||||
this.initPatentChart();
|
||||
this.initInstitutionChart();
|
||||
this.initPotentialChart();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 销毁图表实例
|
||||
this.achievementChart?.dispose()
|
||||
this.patentChart?.dispose()
|
||||
this.institutionChart?.dispose()
|
||||
this.potentialChart?.dispose()
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
async loadCityGeoJson(cityName) {
|
||||
try {
|
||||
const geoJson = await import(`@/components/map/geojson/${cityName}.json`)
|
||||
return geoJson.default
|
||||
} catch (error) {
|
||||
console.error(`Failed to load GeoJSON for ${cityName}:`, error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
onDistrictHover(districtName) {
|
||||
this.hoveredDistrict = districtName
|
||||
// 更新UI显示,高亮显示当前悬停的区县数据
|
||||
// 可以在这里添加临时的UI反馈
|
||||
},
|
||||
onDistrictClick(districtName) {
|
||||
// 设置选中的区县
|
||||
this.selectedDistrict = districtName
|
||||
|
||||
// 如果有区县数据,更新相关图表
|
||||
if (districtName && this.districtData[districtName]) {
|
||||
const data = this.districtData[districtName]
|
||||
|
||||
// 这里可以触发图表组件的更新
|
||||
// 例如,可以通过事件总线或者直接更新数据
|
||||
console.log(`区县 ${districtName} 被选中,专利数量: ${data.patents}, 研究机构: ${data.research}, 转化数量: ${data.conversion}`)
|
||||
|
||||
// 更新页面标题或其他UI元素,显示选中的区县
|
||||
document.title = `${this.cityName} - ${districtName} - 科技大脑智慧服务平台`
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push('/')
|
||||
},
|
||||
handleResize() {
|
||||
this.$nextTick(() => {
|
||||
if (this.achievementChart) this.achievementChart.resize();
|
||||
if (this.patentChart) this.patentChart.resize();
|
||||
if (this.institutionChart) this.institutionChart.resize();
|
||||
if (this.potentialChart) this.potentialChart.resize();
|
||||
});
|
||||
},
|
||||
initAchievementChart() {
|
||||
this.achievementChart = echarts.init(this.$refs.achievementChart)
|
||||
const option = {
|
||||
backgroundColor: '#031845',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: "{a} <br/>{b}: {c} ({d}%)"
|
||||
},
|
||||
graphic: {
|
||||
elements: [{
|
||||
type: 'text',
|
||||
left: '33%',
|
||||
top: '42%',
|
||||
style: {
|
||||
text: '43.56%',
|
||||
textAlign: 'center',
|
||||
fill: '#fff',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
z: 3
|
||||
}, {
|
||||
type: 'text',
|
||||
left: '38%',
|
||||
top: '52%',
|
||||
style: {
|
||||
text: '专利',
|
||||
textAlign: 'center',
|
||||
fill: '#00FFFF',
|
||||
fontSize: 16
|
||||
},
|
||||
z: 3
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '5%',
|
||||
top: 'middle',
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 20,
|
||||
data: ['论文', '专利', '报告', '文献'],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
padding: [0, 0, 0, 8]
|
||||
},
|
||||
formatter: function(name) {
|
||||
const data = option.series[0].data;
|
||||
let value, percent;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].name === name) {
|
||||
value = data[i].value;
|
||||
percent = Math.round(value / data.reduce((sum, item) => sum + item.value, 0) * 100) + '%';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `${name} ${value} ${percent}`;
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '科技成果',
|
||||
type: 'pie',
|
||||
hoverAnimation: false,
|
||||
legendHoverLink: false,
|
||||
radius: ['45%', '55%'],
|
||||
center: ['40%', '50%'],
|
||||
color: ['#00FFFF', '#4B85E2', '#F0CA00', '#41D195'],
|
||||
label: {
|
||||
normal: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
data: [{
|
||||
value: 2560,
|
||||
name: '论文'
|
||||
}, {
|
||||
value: 1980,
|
||||
name: '专利'
|
||||
}, {
|
||||
value: 1466,
|
||||
name: '报告'
|
||||
}, {
|
||||
value: 1203,
|
||||
name: '文献'
|
||||
}]
|
||||
}, {
|
||||
name: '科技成果',
|
||||
type: 'pie',
|
||||
radius: ['55%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
color: ['#00FFFF', '#4B85E2', '#F0CA00', '#41D195'],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(0, 255, 255, 0.3)',
|
||||
opacity: 0.9
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
normal: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
data: [{
|
||||
value: 2560,
|
||||
name: '论文'
|
||||
}, {
|
||||
value: 1980,
|
||||
name: '专利'
|
||||
}, {
|
||||
value: 1466,
|
||||
name: '报告'
|
||||
}, {
|
||||
value: 1203,
|
||||
name: '文献'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
this.achievementChart.setOption(option)
|
||||
},
|
||||
initPatentChart() {
|
||||
this.patentChart = echarts.init(this.$refs.patentChart)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: ['长春', '吉林', '四平', '白城', '四平'],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#0A2E5D'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [320, 450, 280, 380, 220],
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
|
||||
offset: 0,
|
||||
color: '#0074D9'
|
||||
}, {
|
||||
offset: 1,
|
||||
color: '#00FFFF'
|
||||
}])
|
||||
}
|
||||
}]
|
||||
}
|
||||
this.patentChart.setOption(option)
|
||||
},
|
||||
initInstitutionChart() {
|
||||
this.institutionChart = echarts.init(this.$refs.institutionChart)
|
||||
const option = {
|
||||
backgroundColor: '#031845',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}m² ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: '5%',
|
||||
top: 'middle',
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 20,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
padding: [0, 0, 0, 8]
|
||||
},
|
||||
formatter: function(name) {
|
||||
const data = option.series[0].data;
|
||||
let value, percent;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].name === name) {
|
||||
value = data[i].value;
|
||||
percent = Math.round(value / data.reduce((sum, item) => sum + item.value, 0) * 100) + '%';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `${name} ${value}m² ${percent}`;
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '科研单位',
|
||||
type: 'pie',
|
||||
radius: ['20%', '65%'],
|
||||
center: ['40%', '50%'],
|
||||
roseType: 'radius',
|
||||
itemStyle: {
|
||||
borderRadius: 0,
|
||||
borderColor: '#031845',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
color: ['#00FFFF', '#4B85E2', '#F0CA00', '#41D195'],
|
||||
data: [{
|
||||
value: 72.49,
|
||||
name: '数据名称1',
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(0, 255, 255, 0.3)'
|
||||
}
|
||||
}, {
|
||||
value: 32.55,
|
||||
name: '数据名称2',
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(75, 133, 226, 0.3)'
|
||||
}
|
||||
}, {
|
||||
value: 28.6,
|
||||
name: '数据名称3',
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(240, 202, 0, 0.3)'
|
||||
}
|
||||
}, {
|
||||
value: 10.91,
|
||||
name: '数据名称4',
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(65, 209, 149, 0.3)'
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
this.institutionChart.setOption(option)
|
||||
},
|
||||
initPotentialChart() {
|
||||
// 确保DOM已经渲染
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.potentialChart) {
|
||||
console.error('potentialChart ref not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经初始化过,先销毁
|
||||
if (this.potentialChart) {
|
||||
this.potentialChart.dispose();
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
this.potentialChart = echarts.init(this.$refs.potentialChart);
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
borderColor: 'rgba(0,147,233,0.2)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['朝阳区', '南关区', '宽城区', '二道区', '绿园区'],
|
||||
textStyle: {
|
||||
color: '#8CAFE0'
|
||||
},
|
||||
top: 0,
|
||||
itemWidth: 15,
|
||||
itemHeight: 10,
|
||||
itemGap: 15
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['2020年', '2021年', '2022年', '2023年', '2024年'],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#1E3F66'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#8CAFE0',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#1E3F66',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#8CAFE0',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '朝阳区',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [256, 245, 260, 255, 270],
|
||||
itemStyle: {
|
||||
color: '#00FFFF'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#00FFFF',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 255, 255, 0.3)'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(0, 255, 255, 0)'
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '南关区',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [189, 195, 185, 192, 198],
|
||||
itemStyle: {
|
||||
color: '#19D6FF'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#19D6FF',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '宽城区',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [210, 205, 215, 208, 220],
|
||||
itemStyle: {
|
||||
color: '#FFC900'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#FFC900',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '二道区',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [178, 182, 175, 180, 185],
|
||||
itemStyle: {
|
||||
color: '#FF7A45'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#FF7A45',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '绿园区',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
data: [195, 190, 200, 193, 205],
|
||||
itemStyle: {
|
||||
color: '#41D195'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#41D195',
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
this.potentialChart.setOption(option);
|
||||
console.log('Potential chart initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize potential chart:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-style {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(270deg,
|
||||
#081B34 0%, /* 深海军蓝 */
|
||||
#0D2A50 35%, /* 略亮一点,增加层次 */
|
||||
#113667 54%, /* 主"亮点" */
|
||||
#0D2A50 70%, /* 对称回落 */
|
||||
#081B34 100% /* 与首色呼应 */
|
||||
);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: url("./../assets/images/screen1/blockTop/header_bg3.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.screen-title {
|
||||
position: relative;
|
||||
height: 80px;
|
||||
line-height: 65px;
|
||||
text-align: center;
|
||||
margin-left: 20px;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
text-shadow: 0px 2px 3px #1E736C, 0px 0px 15px rgba(228, 239, 255, 0.1);
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #fff;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.section-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px 30px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-top{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 6;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-top-left{
|
||||
height: 100%;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.section-top-right{
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
margin-left: 120px;
|
||||
}
|
||||
|
||||
.section-bottom {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 4;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
min-height: 260px;
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background: rgba(3, 20, 47, 0.7);
|
||||
border: 1px solid rgba(0, 147, 233, 0.2);
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-title {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-arrow {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #00FFFF;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.patent-ranking {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 15px;
|
||||
height: 40px;
|
||||
|
||||
.ranking-city {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 147, 233, 0.2);
|
||||
padding: 4px 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ranking-count {
|
||||
margin-right: 40px;
|
||||
font-size: 14px;
|
||||
color: #8CAFE0;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-chart {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.map-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,464 @@
|
|||
<template>
|
||||
<div class="page-style">
|
||||
<div class="section-header">
|
||||
<div class="screen-title">科技大脑智慧服务平台</div>
|
||||
</div>
|
||||
<div class="section-container">
|
||||
<div class="container-left">
|
||||
<card title="地区专利数量(项)" :heightPercentage="31">
|
||||
<barChart :chartHeight="containerHeight" :key="containerHeight"></barChart>
|
||||
</card>
|
||||
<card title="科技资源排行" :heightPercentage="31">
|
||||
<rank></rank>
|
||||
</card>
|
||||
<card title="热点科技词云" :heightPercentage="31">
|
||||
<WordCloud :words="keywords" height="280px"/>
|
||||
</card>
|
||||
</div>
|
||||
<div class="container-middle">
|
||||
<MapProvincial class="map-card"></MapProvincial>
|
||||
<card title="科技信息" :heightPercentage="31">
|
||||
<lineChart :key="containerHeight"></lineChart>
|
||||
</card>
|
||||
</div>
|
||||
<div class="container-right">
|
||||
<card title="专利转化潜力值排行" :heightPercentage="64.5">
|
||||
<div class="overview-card">
|
||||
<div class="rank-unit">单位(个)</div>
|
||||
<div class="rank-title">
|
||||
<div class="rank-title-name">排名</div>
|
||||
<div class="rank-title-name" style="flex: 4">专利名称</div>
|
||||
<div class="rank-title-name">潜力值</div>
|
||||
</div>
|
||||
<rank2></rank2>
|
||||
</div>
|
||||
</card>
|
||||
<card title="科技人才分类" :heightPercentage="31" :is-show-select="true" :select-options="peopleList">
|
||||
<div class="pie-block">
|
||||
<pieChart :chartHeight="containerHeight" :key="containerHeight" style="width: 50%"></pieChart>
|
||||
<div class="pie-legend-block">
|
||||
<div class="pie-legend-container" v-for="item in pieList" :key="item.id">
|
||||
<div class="pie-legend-icon" :style="{background: item.color}"></div>
|
||||
<div class="pie-legend-name">{{ item.name }}</div>
|
||||
<div class="pie-legend-num"><span style="font-weight: bold">{{ item.value }}</span>个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import card from './../components/card/index'
|
||||
import barChart from '../components/bar/3DIndex.vue'
|
||||
import barChart2 from '../components/bar/3DIndex2.vue'
|
||||
import lineChart from '../components/line/index.vue'
|
||||
import lineChart2 from '../components/line/index2.vue'
|
||||
import pieChart from '../components/pie/index.vue'
|
||||
import pieChart2 from '../components/pie/index2.vue'
|
||||
import rank from '../components/scroll/index.vue'
|
||||
import rank2 from '../components/scroll/index2.vue'
|
||||
import WordCloud from '../components/wordCloud/index.vue'
|
||||
import MapProvincial from '../components/map/mapProvincial.vue'
|
||||
|
||||
export default {
|
||||
name: 'generalPage',
|
||||
components: {
|
||||
card,
|
||||
barChart,
|
||||
barChart2,
|
||||
lineChart,
|
||||
lineChart2,
|
||||
pieChart,
|
||||
pieChart2,
|
||||
rank,
|
||||
rank2,
|
||||
WordCloud,
|
||||
MapProvincial
|
||||
},
|
||||
watch: {
|
||||
windowHeight(newValue) {
|
||||
console.log('触发')
|
||||
if (newValue >= 1000) {
|
||||
this.containerHeight = '240px'
|
||||
} else {
|
||||
this.containerHeight = '190px'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight; // 通过 computed 访问 Vuex state
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerHeight: '220px',
|
||||
pieList: [
|
||||
{id: 1, value: 60, name: '信息技术', color: '#37FFC9'},
|
||||
{id: 2, value: 23, name: '生物医药', color: '#0084FF'},
|
||||
{id: 3, value: 16, name: '能源环境', color: '#19D6FF'},
|
||||
{id: 4, value: 4, name: '工业制造', color: '#FFE777'},
|
||||
{id: 5, value: 3, name: '食品工业', color: '#FF7908'}
|
||||
],
|
||||
peopleList: [
|
||||
{id: 1, label: '行业应用'},
|
||||
{id: 2, label: '工作性质'}
|
||||
],
|
||||
pieLegendList: [
|
||||
{ iconClass: 'pie-chart-legend-icon1', des: '6个月及以下', num: 123 },
|
||||
{ iconClass: 'pie-chart-legend-icon2', des: '12个月及以下', num: 456 },
|
||||
{ iconClass: 'pie-chart-legend-icon3', des: '36个月及以下', num: 789 },
|
||||
{ iconClass: 'pie-chart-legend-icon4', des: '36个月以上', num: 321 }
|
||||
],
|
||||
selectedBanks: [],
|
||||
keywords: [
|
||||
'Digital Humans', 'Deep Learning', 'VR', 'AR', 'IoT',
|
||||
'Decentralization', 'Sustainability', 'Data Mining',
|
||||
'Autonomous Driving', 'MR'
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.windowHeight < 1000) {
|
||||
this.containerHeight = '190px'
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval); // 组件销毁时清除定时器
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.page-style {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(270deg,
|
||||
#081B34 0%, /* 深海军蓝 (≈原 #0A362A) */
|
||||
#0D2A50 35%, /* 略亮一点,增加层次 */
|
||||
#113667 54%, /* 主“亮点”,对应原 #0F4433 */
|
||||
#0D2A50 70%, /* 对称回落 */
|
||||
#081B34 100% /* 与首色呼应 */
|
||||
);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: url("./../assets/images/screen1/blockTop/header_bg2.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screen-title {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
line-height: 65px;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-family: YouSheBiaoTiHei;
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
text-shadow: 0px 2px 3px #1E736C, 0px 0px 15px rgba(228, 239, 255, 0.1);
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.container-left, .container-right {
|
||||
position: relative;
|
||||
width: 529px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container-middle {
|
||||
position: relative;
|
||||
width: 750px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-card{
|
||||
position: relative;
|
||||
width: 700px;
|
||||
height: 59.5vh;
|
||||
}
|
||||
|
||||
.bar-chart-top-block {
|
||||
position: relative;
|
||||
width: calc(100% - 30px);
|
||||
margin-left: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bar-unit-block {
|
||||
color: #85B6BC;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.bar-point {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 25px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bar-point-block {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bar-point-name {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pie-block {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.pie-legend-block {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pie-legend-container {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
background: url("./../assets/images/bg/pie_chart_legend_bg.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
line-height: 30px;
|
||||
color: #DFF2F2;
|
||||
}
|
||||
|
||||
.pie-legend-icon {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pie-legend-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pie-legend-num {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.preview-main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
|
||||
.overview-card{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rank-unit {
|
||||
position: relative;
|
||||
width: calc(100% - 40px);
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
color: #8CAFE0;
|
||||
}
|
||||
|
||||
.rank-title {
|
||||
position: relative;
|
||||
width: calc(100% - 40px);
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #0D2847;
|
||||
color: #CBE2FF;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rank-title-name{
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.project-main{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 20px);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tip-card{
|
||||
position: relative;
|
||||
width: calc(100% - 80px);
|
||||
margin-left: 40px;
|
||||
height: 20px;
|
||||
line-height: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #37D3A5;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tip-info{
|
||||
position: relative;
|
||||
width: 47.5%;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pie-chart{
|
||||
width: 50%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.pie-chart-canvas{
|
||||
width: 60%;
|
||||
}
|
||||
.pie-chart-legend{
|
||||
width: 40%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pie-chart-legend-container{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #DFF2F2;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pie-chart-legend-num{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pie-chart-legend-icon1, .pie-chart-legend-icon2, .pie-chart-legend-icon3, .pie-chart-legend-icon4{
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #159AFF;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.pie-chart-legend-icon2{
|
||||
background-color: #76FAB3;
|
||||
}
|
||||
.pie-chart-legend-icon3{
|
||||
background-color: #65DFDD;
|
||||
}
|
||||
.pie-chart-legend-icon4{
|
||||
background-color: #FCC779;
|
||||
}
|
||||
|
||||
.pie-chart-legend-des{
|
||||
text-align: left;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.bar-chart{
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.bar-chart-canvas{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: url("./../assets/images/bg/bar_bottom_bg.png") no-repeat;
|
||||
background-size: 100% auto;
|
||||
background-position: center calc(100% - 22px);
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
@media (max-width: 1919px) {
|
||||
.container-left, .container-right {
|
||||
width: 470px !important;
|
||||
}
|
||||
|
||||
.container-middle {
|
||||
width: 700px !important;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
gap: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
publicPath: './', // 使用相对路径
|
||||
}
|
||||