33 changed files with 1549 additions and 66 deletions
@ -0,0 +1,55 @@ |
|||
<!--suppress ES6UnusedImports --> |
|||
<template> |
|||
<div :style="{ width: body.left.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.left.data[0].width, |
|||
height: body.left.data[0].height, |
|||
'background-image': `url(${body.left.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.left.data[0]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterNews></DataCenterNews> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.left.data[1].width, |
|||
height: body.left.data[1].height, |
|||
'background-image': `url(${body.left.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.left.data[1]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterTall></DataCenterTall> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
/* eslint-disable no-unused-vars */ |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataCenterNews from 'components/Center/News.vue'; |
|||
import DataCenterTall from 'components/Common/Tall.vue'; |
|||
import useLayoutConfig from '@/hooks/useLayoutConfig'; |
|||
|
|||
const body = useLayoutConfig(); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
@ -0,0 +1,52 @@ |
|||
<template> |
|||
<div :style="{ width: body.middle.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.middle.data[0].width, |
|||
height: body.middle.data[0].height, |
|||
'background-image': `url(${body.middle.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 标题--> |
|||
<DataCenterTitle :item="body.middle.data[0]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterMapContainer></DataCenterMapContainer> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.middle.data[1].width, |
|||
height: body.middle.data[1].height, |
|||
'background-image': `url(${body.middle.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 标题--> |
|||
<DataCenterTitle :item="body.middle.data[1]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterComplexChart></DataCenterComplexChart> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataCenterMapContainer from 'components/Common/MapContainer.vue'; |
|||
import DataCenterComplexChart from 'components/Common/ComplexChart.vue'; |
|||
import { body } from '@/config/layout/dataCenterBody'; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
} |
|||
</style> |
@ -0,0 +1,64 @@ |
|||
<template> |
|||
<div :style="{ width: body.right.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.right.data[0].width, |
|||
height: body.right.data[0].height, |
|||
'background-image': `url(${body.right.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.right.data[0]"></DataCenterTitle> |
|||
</div> |
|||
|
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.right.data[1].width, |
|||
height: body.right.data[1].height, |
|||
'background-image': `url(${body.right.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.right.data[1]"></DataCenterTitle> |
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterYieldChart></DataCenterYieldChart> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.right.data[2].width, |
|||
height: body.right.data[2].height, |
|||
'background-image': `url(${body.right.data[2].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.right.data[2]"></DataCenterTitle> |
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterFaultChart></DataCenterFaultChart> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataCenterYieldChart from 'components/Center/YieldChart.vue'; |
|||
import DataCenterFaultChart from 'components/Common/FaultChart.vue'; |
|||
import useLayoutConfig from '@/hooks/useLayoutConfig'; |
|||
|
|||
const body = useLayoutConfig(); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<div id="yield-chart"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { onMounted } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
|
|||
onMounted(() => { |
|||
const chartDom = document.getElementById('yield-chart'); |
|||
const myChart = echarts.init(chartDom); |
|||
const option = initOptions(); |
|||
myChart.setOption(option); |
|||
}); |
|||
|
|||
function initOptions() { |
|||
return { |
|||
darkMode: true, |
|||
grid: { |
|||
top: 20, |
|||
bottom: 20, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: ['2017', '2018', '2019', '2020', '2021'], |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
series: [ |
|||
{ |
|||
data: [224, 218, 135, 147, 260], |
|||
type: 'line', |
|||
areaStyle: {}, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#yield-chart { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,37 @@ |
|||
<template> |
|||
<div class="address-container"> |
|||
<img src="../../assets/icon-location.png" alt="" /> |
|||
我的位置:山西省 > 太原市 > {{ address }} |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { inject, computed } from 'vue'; |
|||
|
|||
const scene = inject('scene'); |
|||
|
|||
const address = computed(() => { |
|||
if (scene === 'farm') { |
|||
return '大井生态园'; |
|||
} |
|||
return '山西省农科院'; |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.address-container { |
|||
position: absolute; |
|||
left: 0rem; |
|||
top: 0rem; |
|||
box-shadow: 0 0 9rem #00d9ff inset; |
|||
padding: 8rem 16rem; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
color: #fff; |
|||
font-size: 14rem; |
|||
} |
|||
.address-container img { |
|||
margin-right: 10rem; |
|||
} |
|||
</style> |
@ -0,0 +1,57 @@ |
|||
<template> |
|||
<div id="complex-container"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
import { generateChartOption } from '@/utils/complexChart'; |
|||
import datas from '@/mock/datas'; |
|||
|
|||
let myChart = null; |
|||
let defaultSelectedLegend = { |
|||
'室内温度(℃)': true, |
|||
'室外温度(℃)': true, |
|||
'土壤温度(℃)': false, |
|||
'室内湿度(RH%)': true, |
|||
'室外湿度(RH%)': true, |
|||
'土壤湿度(RH%)': false, |
|||
'风速(m/s)': false, |
|||
'CO2(%)': false, |
|||
'光照(klux)': true, |
|||
}; |
|||
|
|||
async function getData() { |
|||
try { |
|||
await nextTick(); |
|||
initChart(datas); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
|
|||
// 初始化图表 |
|||
function initChart(data) { |
|||
const chartDom = document.getElementById('complex-container'); |
|||
myChart = echarts.init(chartDom); |
|||
render(data, defaultSelectedLegend); |
|||
|
|||
myChart.on('legendselectchanged', event => { |
|||
defaultSelectedLegend = event.selected; |
|||
render(data, event.selected); |
|||
}); |
|||
} |
|||
|
|||
function render(data, selected) { |
|||
const option = generateChartOption(data, selected); |
|||
option && myChart.setOption(option); |
|||
} |
|||
|
|||
getData(); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#complex-container { |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,83 @@ |
|||
<template> |
|||
<div class="container"> |
|||
<div class="item" v-for="(item, key) in styles" :key="key"> |
|||
<div |
|||
class="count" |
|||
:style="{ |
|||
'background-image': `-webkit-linear-gradient(bottom, ${item.color[0]}, ${item.color[1]})`, |
|||
}" |
|||
> |
|||
{{ data[key] }} |
|||
</div> |
|||
<div class="title">{{ item.title }}</div> |
|||
<img :src="item.image" class="image" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref } from 'vue'; |
|||
|
|||
const data = ref({ |
|||
total: 32, |
|||
error: 1, |
|||
fault: 2, |
|||
in: 29, |
|||
}); |
|||
|
|||
const styles = ref({ |
|||
total: { |
|||
title: '设备总数', |
|||
color: ['#08967e', '#08eb94'], |
|||
image: 'src/assets/circle-green.png', |
|||
}, |
|||
error: { |
|||
title: '设备异常', |
|||
color: ['#e0a912', '#fde053'], |
|||
image: 'src/assets/circle-yellow.png', |
|||
}, |
|||
fault: { |
|||
title: '设备预警', |
|||
color: ['#fc4768', '#f8d03f'], |
|||
image: 'src/assets/circle-red.png', |
|||
}, |
|||
in: { |
|||
title: '接入设备', |
|||
color: ['#08dcf4', '#09b4c9'], |
|||
image: 'src/assets/circle-blue.png', |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.container { |
|||
display: flex; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
padding-top: 40rem; |
|||
} |
|||
.item { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
.count { |
|||
font-size: 30rem; |
|||
font-weight: bold; |
|||
|
|||
-webkit-background-clip: text; |
|||
-webkit-text-fill-color: transparent; |
|||
} |
|||
.title { |
|||
font-size: 18rem; |
|||
color: #fff; |
|||
font-weight: bold; |
|||
margin-bottom: 16rem; |
|||
margin-top: 16rem; |
|||
} |
|||
.image { |
|||
width: 90rem; |
|||
height: 60rem; |
|||
} |
|||
</style> |
@ -1,46 +0,0 @@ |
|||
<!--suppress ES6UnusedImports --> |
|||
<template> |
|||
<div :style="{ width: body.left.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: item.width, |
|||
height: item.height, |
|||
'background-image': `url(${item.background.image})`, |
|||
}" |
|||
v-for="item in body.left.data" |
|||
:key="item.title.text" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="item"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<component :is="getComponent(item.component)"></component> |
|||
<!-- <component :is="DataCenterNews"></component>--> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
/* eslint-disable no-unused-vars */ |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import useLayoutConfig from '@/hooks/useLayoutConfig'; |
|||
import DataCenterNews from '@/components/Center/News.vue'; |
|||
|
|||
const body = useLayoutConfig(); |
|||
|
|||
const getComponent = name => { |
|||
return name; |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
@ -0,0 +1,29 @@ |
|||
<template> |
|||
<div id="map" class="map"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick, onMounted } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
import 'echarts-gl'; |
|||
import ShanXiMap from 'config/map/shanxi'; |
|||
import { initOptions } from '@/utils/map'; |
|||
|
|||
onMounted(async () => { |
|||
await nextTick(); |
|||
const chartDom = document.getElementById('map'); |
|||
console.log('chartDom: ', chartDom); |
|||
const myChart = echarts.init(chartDom); |
|||
|
|||
echarts.registerMap('shanxi', ShanXiMap); |
|||
// |
|||
myChart.setOption(initOptions()); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.map { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,25 @@ |
|||
<template> |
|||
<div class="main-container"> |
|||
<!-- 地图--> |
|||
<CommonMap></CommonMap> |
|||
<!-- 监控--> |
|||
<CommonMonitor></CommonMonitor> |
|||
|
|||
<!-- 地址--> |
|||
<CommonAddress></CommonAddress> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import CommonMap from 'components/Common/Map.vue'; |
|||
import CommonMonitor from 'components/Common/Monitor.vue'; |
|||
import CommonAddress from 'components/Common/Address.vue'; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.main-container { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,52 @@ |
|||
<template> |
|||
<iframe id="modal-container" class="modal-container" v-show="modalDisplay" :src="src" frameborder="0" allow="microphone;camera"></iframe> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed, inject, onMounted } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
|
|||
const store = useStore(); |
|||
const scene = inject('scene'); |
|||
|
|||
const src = computed(() => { |
|||
if (scene === 'farm') { |
|||
return 'https://www.tall.wiki/wl/7/#/farm'; |
|||
} |
|||
return 'https://www.tall.wiki/wl/7/#/center'; |
|||
}); |
|||
|
|||
// const src = 'http://localhost:8080/#/center'; |
|||
|
|||
const modalDisplay = computed(() => store.state.modal); // 是否显示modal |
|||
|
|||
onMounted(() => { |
|||
window.addEventListener( |
|||
'message', |
|||
event => { |
|||
// onInvitationReceived |
|||
console.log('event: ', event.data); |
|||
if (event.data === 'onInvitationReceived') { |
|||
store.commit('setModal', true); |
|||
} |
|||
console.log('event origin: ', event.source.origin); |
|||
}, |
|||
false, |
|||
); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.modal-container { |
|||
z-index: 9999; |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
width: 676rem; |
|||
height: 510rem; |
|||
transform: translate3d(-50%, -50%, 0); |
|||
background: transparent; |
|||
/*background: url(assets/modal-bg.png) no-repeat center center;*/ |
|||
/*background-size: 100%;*/ |
|||
} |
|||
</style> |
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<div class="frame-container" v-show="monitorDisplay"> |
|||
<iframe src="https://www.tall.wiki/kangfu/v1/?key=230659446" frameborder="0"></iframe> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
|
|||
const store = useStore(); |
|||
|
|||
const monitorDisplay = computed(() => store.state.monitor); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.frame-container { |
|||
position: absolute; |
|||
right: 0; |
|||
bottom: 0; |
|||
width: 275rem; |
|||
height: 200rem; |
|||
box-sizing: content-box; |
|||
background: url('@/assets/border-s-2.png') no-repeat center center; |
|||
background-size: 100%; |
|||
padding: 5rem; |
|||
overflow: hidden; |
|||
} |
|||
iframe { |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,56 @@ |
|||
<template> |
|||
<header :class="{ 'bg-center': scene === 'center', 'bg-farm': scene === 'farm' }"> |
|||
<!-- 时间--> |
|||
<PageTime></PageTime> |
|||
|
|||
<!-- 按钮组--> |
|||
<PageButtons @click-btn="onClickBtn"></PageButtons> |
|||
|
|||
<CommonWeather></CommonWeather> |
|||
</header> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import PageTime from 'components/Common/Time.vue'; |
|||
import PageButtons from 'components/Common/Buttons.vue'; |
|||
import CommonWeather from 'components/Common/Weather.vue'; |
|||
|
|||
import { computed, inject } from 'vue'; |
|||
import { useStore } from 'vuex'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
const store = useStore(); |
|||
const scene = inject('scene'); |
|||
const modalDisplay = computed(() => store.state.modal); // 是否显示modal |
|||
const router = useRouter(); |
|||
|
|||
function onClickBtn({ index: btnIndex }) { |
|||
if (btnIndex === 0) { |
|||
store.commit('setModal', !modalDisplay.value); |
|||
} else if (btnIndex === 2) { |
|||
// monitorDisplay.value = !monitorDisplay.value; |
|||
if (scene === 'farm') { |
|||
// 跳转到 / |
|||
router.push('/farm'); |
|||
} else { |
|||
// 跳转到 /farm |
|||
router.push('/farm/farm'); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.bg-center { |
|||
position: relative; |
|||
height: 115rem; |
|||
background: url('@/assets/top.png') no-repeat center; |
|||
background-size: 100% 114rem; |
|||
} |
|||
.bg-farm { |
|||
position: relative; |
|||
height: 115rem; |
|||
background: url('@/assets/farm-top.png') no-repeat center; |
|||
background-size: 100% 114rem; |
|||
} |
|||
</style> |
@ -0,0 +1,86 @@ |
|||
<template> |
|||
<div id="humidity-container"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
import datas from '@/mock/datas'; |
|||
|
|||
async function getData() { |
|||
try { |
|||
await nextTick(); |
|||
initChart(datas); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
|
|||
getData(); |
|||
|
|||
// 初始化图表 |
|||
function initChart(data) { |
|||
const chartDom = document.getElementById('humidity-container'); |
|||
const option = initOption(data); |
|||
const myChart = echarts.init(chartDom); |
|||
myChart.setOption(option); |
|||
} |
|||
|
|||
function initData(data) { |
|||
console.log('data1: ', data); |
|||
const result = { times: [], roomH: [], outH: [] }; |
|||
data.forEach(item => { |
|||
result.times.push(item.time); |
|||
result.roomH.push(item.roomH); |
|||
result.outH.push(item.outH); |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
function initOption(data) { |
|||
const { time, roomH, outH } = initData(data); |
|||
return { |
|||
tooltip: { trigger: 'axis' }, |
|||
grid: { |
|||
left: 5, |
|||
right: 5, |
|||
top: 30, |
|||
bottom: 30, |
|||
}, |
|||
darkMode: true, |
|||
legend: { show: true, textStyle: { color: '#fff' } }, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: time, |
|||
axisTick: { alignWithLabel: true }, |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { formatter: '{value} °C' }, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '室内湿度', |
|||
type: 'line', |
|||
data: roomH, |
|||
smooth: true, |
|||
}, |
|||
{ |
|||
name: '室外湿度', |
|||
type: 'line', |
|||
data: outH, |
|||
smooth: true, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#humidity-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,72 @@ |
|||
<!--suppress ES6UnusedImports --> |
|||
<template> |
|||
<div :style="{ width: body.left.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.left.data[0].width, |
|||
height: body.left.data[0].height, |
|||
'background-image': `url(${body.left.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.left.data[0]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataFarmTemperature></DataFarmTemperature> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.left.data[1].width, |
|||
height: body.left.data[1].height, |
|||
'background-image': `url(${body.left.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.left.data[1]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataFarmHumidity></DataFarmHumidity> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.left.data[2].width, |
|||
height: body.left.data[2].height, |
|||
'background-image': `url(${body.left.data[2].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.left.data[2]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataFarmSoilHumidity></DataFarmSoilHumidity> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
/* eslint-disable no-unused-vars */ |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataFarmTemperature from 'components/Farm/Temperature.vue'; |
|||
import DataFarmHumidity from 'components/Farm/Humidity.vue'; |
|||
import DataFarmSoilHumidity from 'components/Farm/SoilHumidity.vue'; |
|||
import useLayoutConfig from '@/hooks/useLayoutConfig'; |
|||
|
|||
const body = useLayoutConfig(); |
|||
console.log(body); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
@ -0,0 +1,52 @@ |
|||
<template> |
|||
<div :style="{ width: body.middle.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.middle.data[0].width, |
|||
height: body.middle.data[0].height, |
|||
'background-image': `url(${body.middle.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 标题--> |
|||
<DataCenterTitle :item="body.middle.data[0]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterMapContainer></DataCenterMapContainer> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.middle.data[1].width, |
|||
height: body.middle.data[1].height, |
|||
'background-image': `url(${body.middle.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 标题--> |
|||
<DataCenterTitle :item="body.middle.data[1]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterComplexChart></DataCenterComplexChart> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataCenterMapContainer from 'components/Common/MapContainer.vue'; |
|||
import DataCenterComplexChart from 'components/Common/ComplexChart.vue'; |
|||
import { body } from '@/config/layout/dataCenterBody'; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
} |
|||
</style> |
@ -0,0 +1,71 @@ |
|||
<template> |
|||
<div style="height: 100%; overflow: hidden"> |
|||
<ul class="list" :style="{ transform: `translate3d(0, ${translateY}px, 0)` }"> |
|||
<li class="list-item" v-for="item in news" :key="item.title"> |
|||
<div class="title">{{ item.title }}</div> |
|||
<div class="date">{{ item.date }}</div> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, nextTick } from 'vue'; |
|||
import newsData from '@/mock/news'; |
|||
import useScrollList from '@/hooks/useScrollList'; |
|||
|
|||
const news = ref([]); |
|||
const { translateY, scrollList } = useScrollList(); |
|||
|
|||
// 获取数据 |
|||
async function getData() { |
|||
try { |
|||
news.value = newsData; |
|||
await nextTick(); |
|||
const li = document.querySelector('.list-item'); |
|||
const ul = document.querySelector('.list'); |
|||
const liHeight = li.offsetHeight; |
|||
const ulHeight = liHeight * news.value.length - ul.parentElement.offsetHeight; |
|||
scrollList(liHeight, ulHeight); // 滚动列表 |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
|
|||
getData(); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.list { |
|||
padding-left: 0; |
|||
list-style: disc inside; |
|||
transition: transform 300ms ease; |
|||
} |
|||
.list-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 10rem 0 10rem; |
|||
} |
|||
.list-item:before { |
|||
content: ''; |
|||
width: 10rem; |
|||
height: 10rem; |
|||
background-color: #01fffd; |
|||
border-radius: 50%; |
|||
} |
|||
.list-item .title { |
|||
flex: 1; |
|||
min-width: 0; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
font-size: 16rem; |
|||
color: #fff; |
|||
padding-left: 10rem; |
|||
padding-right: 40rem; |
|||
} |
|||
.list-item .date { |
|||
color: #ccc; |
|||
font-size: 14rem; |
|||
} |
|||
</style> |
@ -0,0 +1,53 @@ |
|||
<template> |
|||
<div :style="{ width: body.right.width }"> |
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.right.data[0].width, |
|||
height: body.right.data[0].height, |
|||
'background-image': `url(${body.right.data[0].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.right.data[0]"></DataCenterTitle> |
|||
|
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterTall></DataCenterTall> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
class="item m-t" |
|||
:style="{ |
|||
width: body.right.data[1].width, |
|||
height: body.right.data[1].height, |
|||
'background-image': `url(${body.right.data[1].background.image})`, |
|||
}" |
|||
> |
|||
<!-- 区块标题--> |
|||
<DataCenterTitle :item="body.right.data[1]"></DataCenterTitle> |
|||
<div class="p-16 flex-1 overflow-hidden"> |
|||
<DataCenterFaultChart></DataCenterFaultChart> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import DataCenterTitle from 'components/Common/Title.vue'; |
|||
import DataCenterTall from 'components/Common/Tall.vue'; |
|||
import DataCenterFaultChart from 'components/Common/FaultChart.vue'; |
|||
import useLayoutConfig from '@/hooks/useLayoutConfig'; |
|||
|
|||
const body = useLayoutConfig(); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.item { |
|||
background-position: left top; |
|||
background-size: 100% 100%; |
|||
background-repeat: no-repeat; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
</style> |
@ -0,0 +1,81 @@ |
|||
<template> |
|||
<div id="soil-humidity-container"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
import datas from '@/mock/datas'; |
|||
|
|||
async function getData() { |
|||
try { |
|||
await nextTick(); |
|||
initChart(datas); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
|
|||
getData(); |
|||
|
|||
// 初始化图表 |
|||
function initChart(data) { |
|||
const chartDom = document.getElementById('soil-humidity-container'); |
|||
const option = initOption(data); |
|||
const myChart = echarts.init(chartDom); |
|||
myChart.setOption(option); |
|||
} |
|||
|
|||
function initData(data) { |
|||
const result = { times: [], soilH: [] }; |
|||
data.forEach(item => { |
|||
result.times.push(item.time); |
|||
result.soilH.push(item.soilH); |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
function initOption(data) { |
|||
const { time, soilH } = initData(data); |
|||
return { |
|||
tooltip: { trigger: 'axis' }, |
|||
grid: { |
|||
left: 5, |
|||
right: 5, |
|||
top: 30, |
|||
bottom: 30, |
|||
}, |
|||
darkMode: true, |
|||
legend: { show: true, textStyle: { color: '#fff' } }, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: time, |
|||
axisTick: { alignWithLabel: true }, |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { formatter: '{value} °C' }, |
|||
}, |
|||
visualMap: { |
|||
show: false, |
|||
inRange: { color: ['#65B581', '#FFCE34', '#FD665F'] }, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '土壤湿度', |
|||
type: 'bar', |
|||
data: soilH, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#soil-humidity-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,83 @@ |
|||
<template> |
|||
<div id="temperature-container"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { nextTick } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
import datas from '@/mock/datas'; |
|||
|
|||
async function getData() { |
|||
try { |
|||
await nextTick(); |
|||
initChart(datas); |
|||
} catch (error) { |
|||
console.error(error); |
|||
} |
|||
} |
|||
|
|||
getData(); |
|||
|
|||
// 初始化图表 |
|||
function initChart(data) { |
|||
const chartDom = document.getElementById('temperature-container'); |
|||
const option = initOption(data); |
|||
const myChart = echarts.init(chartDom); |
|||
myChart.setOption(option); |
|||
} |
|||
|
|||
function initData(data) { |
|||
const result = { times: [], roomT: [], outT: [] }; |
|||
data.forEach(item => { |
|||
result.times.push(item.time); |
|||
result.roomT.push(item.roomT); |
|||
result.outT.push(item.outT); |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
function initOption(data) { |
|||
const { time, roomT, outT } = initData(data); |
|||
return { |
|||
tooltip: { trigger: 'axis' }, |
|||
grid: { |
|||
left: 5, |
|||
right: 5, |
|||
top: 30, |
|||
bottom: 30, |
|||
}, |
|||
darkMode: true, |
|||
legend: { show: true, textStyle: { color: '#fff' } }, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: time, |
|||
axisTick: { alignWithLabel: true }, |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { formatter: '{value} °C' }, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: '室内温度', |
|||
type: 'line', |
|||
data: roomT, |
|||
}, |
|||
{ |
|||
name: '室外温度', |
|||
type: 'line', |
|||
data: outT, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#temperature-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<div id="yield-chart"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { onMounted } from 'vue'; |
|||
import * as echarts from 'echarts'; |
|||
|
|||
onMounted(() => { |
|||
const chartDom = document.getElementById('yield-chart'); |
|||
const myChart = echarts.init(chartDom); |
|||
const option = initOptions(); |
|||
myChart.setOption(option); |
|||
}); |
|||
|
|||
function initOptions() { |
|||
return { |
|||
darkMode: true, |
|||
grid: { |
|||
top: 20, |
|||
bottom: 20, |
|||
}, |
|||
xAxis: { |
|||
type: 'category', |
|||
boundaryGap: false, |
|||
data: ['2017', '2018', '2019', '2020', '2021'], |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
yAxis: { |
|||
type: 'value', |
|||
axisLabel: { color: '#fff' }, |
|||
}, |
|||
series: [ |
|||
{ |
|||
data: [224, 218, 135, 147, 260], |
|||
type: 'line', |
|||
areaStyle: {}, |
|||
}, |
|||
], |
|||
}; |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
#yield-chart { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,290 @@ |
|||
export default [ |
|||
{ |
|||
time: '00:00', |
|||
roomT: 19, |
|||
roomH: 60, |
|||
outT: '-5', |
|||
outH: 60, |
|||
soilT: 6, |
|||
soilH: 40, |
|||
windSpeed: 0, |
|||
co2: '0.01 ', |
|||
light: 3, |
|||
}, |
|||
{ |
|||
time: '01:00', |
|||
roomT: 19, |
|||
roomH: 70, |
|||
outT: '-6', |
|||
outH: 70, |
|||
soilT: 7, |
|||
soilH: 50, |
|||
windSpeed: 2, |
|||
co2: '0.02 ', |
|||
light: 2, |
|||
}, |
|||
{ |
|||
time: '02:00', |
|||
roomT: 19, |
|||
roomH: 80, |
|||
outT: '-7', |
|||
outH: 80, |
|||
soilT: 7, |
|||
soilH: 60, |
|||
windSpeed: 3, |
|||
co2: '0.03 ', |
|||
light: 1, |
|||
}, |
|||
{ |
|||
time: '03:00', |
|||
roomT: 19, |
|||
roomH: 90, |
|||
outT: '-8', |
|||
outH: 90, |
|||
soilT: 7, |
|||
soilH: 70, |
|||
windSpeed: 7, |
|||
co2: '0.04 ', |
|||
light: 1, |
|||
}, |
|||
{ |
|||
time: '04:00', |
|||
roomT: 18, |
|||
roomH: 100, |
|||
outT: '-9', |
|||
outH: 100, |
|||
soilT: 5, |
|||
soilH: 80, |
|||
windSpeed: 9, |
|||
co2: '0.05 ', |
|||
light: 4, |
|||
}, |
|||
{ |
|||
time: '05:00', |
|||
roomT: 18, |
|||
roomH: 95, |
|||
outT: '-10', |
|||
outH: 95, |
|||
soilT: 5, |
|||
soilH: 75, |
|||
windSpeed: 14, |
|||
co2: '0.06 ', |
|||
light: 8, |
|||
}, |
|||
{ |
|||
time: '06:00', |
|||
roomT: 19, |
|||
roomH: 90, |
|||
outT: '-5', |
|||
outH: 90, |
|||
soilT: 7, |
|||
soilH: 70, |
|||
windSpeed: 12, |
|||
co2: '0.07 ', |
|||
light: 10, |
|||
}, |
|||
{ |
|||
time: '07:00', |
|||
roomT: 19, |
|||
roomH: 85, |
|||
outT: '-1', |
|||
outH: 85, |
|||
soilT: 7, |
|||
soilH: 65, |
|||
windSpeed: 15, |
|||
co2: '0.08 ', |
|||
light: 20, |
|||
}, |
|||
{ |
|||
time: '08:00', |
|||
roomT: 20, |
|||
roomH: 80, |
|||
outT: 0, |
|||
outH: 80, |
|||
soilT: 10, |
|||
soilH: 60, |
|||
windSpeed: 13, |
|||
co2: '0.09 ', |
|||
light: 26, |
|||
}, |
|||
{ |
|||
time: '09:00', |
|||
roomT: 20, |
|||
roomH: 75, |
|||
outT: 1, |
|||
outH: 75, |
|||
soilT: 12, |
|||
soilH: 55, |
|||
windSpeed: 14, |
|||
co2: '0.09 ', |
|||
light: 35, |
|||
}, |
|||
{ |
|||
time: '10:00', |
|||
roomT: 21, |
|||
roomH: 70, |
|||
outT: 2, |
|||
outH: 70, |
|||
soilT: 14, |
|||
soilH: 50, |
|||
windSpeed: 9, |
|||
co2: '0.12 ', |
|||
light: 45, |
|||
}, |
|||
{ |
|||
time: '11:00', |
|||
roomT: 21, |
|||
roomH: 65, |
|||
outT: 3, |
|||
outH: 65, |
|||
soilT: 16, |
|||
soilH: 45, |
|||
windSpeed: 5, |
|||
co2: '0.14 ', |
|||
light: 50, |
|||
}, |
|||
{ |
|||
time: '12:00', |
|||
roomT: 23, |
|||
roomH: 60, |
|||
outT: 4, |
|||
outH: 60, |
|||
soilT: 19, |
|||
soilH: 40, |
|||
windSpeed: 8, |
|||
co2: '0.16 ', |
|||
light: 55, |
|||
}, |
|||
{ |
|||
time: '13:00', |
|||
roomT: 24, |
|||
roomH: 55, |
|||
outT: 5, |
|||
outH: 55, |
|||
soilT: 22, |
|||
soilH: 35, |
|||
windSpeed: 7, |
|||
co2: '0.18 ', |
|||
light: 65, |
|||
}, |
|||
{ |
|||
time: '14:00', |
|||
roomT: 28, |
|||
roomH: 50, |
|||
outT: 6, |
|||
outH: 50, |
|||
soilT: 23, |
|||
soilH: 30, |
|||
windSpeed: 6, |
|||
co2: '0.20 ', |
|||
light: 80, |
|||
}, |
|||
{ |
|||
time: '15:00', |
|||
roomT: 28, |
|||
roomH: 45, |
|||
outT: 6, |
|||
outH: 45, |
|||
soilT: 23, |
|||
soilH: 25, |
|||
windSpeed: 4, |
|||
co2: '0.19 ', |
|||
light: 70, |
|||
}, |
|||
{ |
|||
time: '16:00', |
|||
roomT: 26, |
|||
roomH: 40, |
|||
outT: 7, |
|||
outH: 40, |
|||
soilT: 20, |
|||
soilH: 20, |
|||
windSpeed: 11, |
|||
co2: '0.18 ', |
|||
light: 65, |
|||
}, |
|||
{ |
|||
time: '17:00', |
|||
roomT: 25, |
|||
roomH: 45, |
|||
outT: 5, |
|||
outH: 45, |
|||
soilT: 17, |
|||
soilH: 25, |
|||
windSpeed: 8, |
|||
co2: '0.17 ', |
|||
light: 55, |
|||
}, |
|||
{ |
|||
time: '18:00', |
|||
roomT: 24, |
|||
roomH: 50, |
|||
outT: 5, |
|||
outH: 50, |
|||
soilT: 14, |
|||
soilH: 30, |
|||
windSpeed: 5, |
|||
co2: '0.16 ', |
|||
light: 40, |
|||
}, |
|||
{ |
|||
time: '19:00', |
|||
roomT: 23, |
|||
roomH: 55, |
|||
outT: 1, |
|||
outH: 55, |
|||
soilT: 11, |
|||
soilH: 35, |
|||
windSpeed: 1, |
|||
co2: '0.15 ', |
|||
light: 35, |
|||
}, |
|||
{ |
|||
time: '20:00', |
|||
roomT: 22, |
|||
roomH: 60, |
|||
outT: 0, |
|||
outH: 60, |
|||
soilT: 8, |
|||
soilH: 40, |
|||
windSpeed: 5, |
|||
co2: '0.14 ', |
|||
light: 25, |
|||
}, |
|||
{ |
|||
time: '21:00', |
|||
roomT: 21, |
|||
roomH: 65, |
|||
outT: 0, |
|||
outH: 65, |
|||
soilT: 7, |
|||
soilH: 45, |
|||
windSpeed: 4, |
|||
co2: '0.13 ', |
|||
light: 6, |
|||
}, |
|||
{ |
|||
time: '22:00', |
|||
roomT: 20, |
|||
roomH: 70, |
|||
outT: '-3', |
|||
outH: 70, |
|||
soilT: 6, |
|||
soilH: 50, |
|||
windSpeed: 6, |
|||
co2: '0.12 ', |
|||
light: 5, |
|||
}, |
|||
{ |
|||
time: '23:00', |
|||
roomT: 20, |
|||
roomH: 75, |
|||
outT: '-4', |
|||
outH: 75, |
|||
soilT: 5, |
|||
soilH: 55, |
|||
windSpeed: 7, |
|||
co2: '0.11 ', |
|||
light: 4, |
|||
}, |
|||
]; |
@ -1,13 +1,34 @@ |
|||
<template> |
|||
<PageHeader></PageHeader> |
|||
<PageLeft></PageLeft> |
|||
|
|||
<div class="container"> |
|||
<PageLeft></PageLeft> |
|||
<PageMiddle></PageMiddle> |
|||
<PageRight></PageRight> |
|||
</div> |
|||
|
|||
<PageFooter></PageFooter> |
|||
|
|||
<PageModal></PageModal> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import PageHeader from 'components/Center/Header.vue'; |
|||
import { provide } from 'vue'; |
|||
import PageModal from 'components/Common/Modal.vue'; |
|||
import PageFooter from 'components/Common/Footer.vue'; |
|||
import PageLeft from 'components/Common/Left.vue'; |
|||
import PageLeft from 'components/Center/Left.vue'; |
|||
import PageRight from 'components/Center/Right.vue'; |
|||
import PageMiddle from 'components/Center/Middle.vue'; |
|||
import PageHeader from 'components/Center/Header.vue'; |
|||
|
|||
provide('scene', 'center'); |
|||
</script> |
|||
|
|||
<style scoped></style> |
|||
<style scoped> |
|||
.container { |
|||
padding: 0 10rem; |
|||
margin-top: -10rem; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
</style> |
|||
|
@ -1,7 +1,34 @@ |
|||
<template>farm</template> |
|||
<template> |
|||
<PageHeader></PageHeader> |
|||
|
|||
<script> |
|||
export default { name: 'Farm' }; |
|||
<div class="container"> |
|||
<PageLeft></PageLeft> |
|||
<PageMiddle></PageMiddle> |
|||
<PageRight></PageRight> |
|||
</div> |
|||
|
|||
<PageFooter></PageFooter> |
|||
|
|||
<PageModal></PageModal> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { provide } from 'vue'; |
|||
import PageModal from 'components/Common/Modal.vue'; |
|||
import PageFooter from 'components/Common/Footer.vue'; |
|||
import PageLeft from 'components/Farm/Left.vue'; |
|||
import PageRight from 'components/Farm/Right.vue'; |
|||
import PageMiddle from 'components/Farm/Middle.vue'; |
|||
import PageHeader from 'components/Farm/Header.vue'; |
|||
|
|||
provide('scene', 'farm'); |
|||
</script> |
|||
|
|||
<style scoped></style> |
|||
<style scoped> |
|||
.container { |
|||
padding: 0 10rem; |
|||
margin-top: -10rem; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
</style> |
|||
|
Loading…
Reference in new issue