更新项目文件结构,统一文档风格
This commit is contained in:
13
admin-system/bank-supervision/index.html
Normal file
13
admin-system/bank-supervision/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
admin-system/bank-supervision/package.json
Normal file
23
admin-system/bank-supervision/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "bank-supervision",
|
||||
"version": "1.0.0",
|
||||
"description": "锡林郭勒盟智慧养殖管理系统",
|
||||
"author": "xlxumu team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"ant-design-vue": "^3.0.0",
|
||||
"pinia": "^2.0.0",
|
||||
"echarts": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
}
|
||||
17
admin-system/bank-supervision/src/App.vue
Normal file
17
admin-system/bank-supervision/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/bank-supervision/src/main.js
Normal file
15
admin-system/bank-supervision/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/bank-supervision/src/styles/global.css
Normal file
97
admin-system/bank-supervision/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/bank-supervision/vite.config.js
Normal file
28
admin-system/bank-supervision/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3002,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
13
admin-system/cattle-trading/index.html
Normal file
13
admin-system/cattle-trading/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
admin-system/cattle-trading/package.json
Normal file
23
admin-system/cattle-trading/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "cattle-trading",
|
||||
"version": "1.0.0",
|
||||
"description": "锡林郭勒盟智慧养殖管理系统",
|
||||
"author": "xlxumu team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"ant-design-vue": "^3.0.0",
|
||||
"pinia": "^2.0.0",
|
||||
"echarts": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
}
|
||||
17
admin-system/cattle-trading/src/App.vue
Normal file
17
admin-system/cattle-trading/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/cattle-trading/src/main.js
Normal file
15
admin-system/cattle-trading/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/cattle-trading/src/styles/global.css
Normal file
97
admin-system/cattle-trading/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/cattle-trading/vite.config.js
Normal file
28
admin-system/cattle-trading/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3005,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
13
admin-system/dashboard/index.html
Normal file
13
admin-system/dashboard/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
4343
admin-system/dashboard/package-lock.json
generated
Normal file
4343
admin-system/dashboard/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
admin-system/dashboard/package.json
Normal file
28
admin-system/dashboard/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.4.2",
|
||||
"pinia": "^2.0.33",
|
||||
"three": "^0.179.1",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"description": "锡林郭勒盟智慧养殖产业大屏可视化系统"
|
||||
}
|
||||
115
admin-system/dashboard/src/App.vue
Normal file
115
admin-system/dashboard/src/App.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<nav class="main-nav">
|
||||
<router-link to="/" class="nav-item">首页</router-link>
|
||||
<router-link to="/monitor" class="nav-item">监控中心</router-link>
|
||||
<router-link to="/government" class="nav-item">政府平台</router-link>
|
||||
<router-link to="/finance" class="nav-item">金融服务</router-link>
|
||||
<router-link to="/transport" class="nav-item">运输跟踪</router-link>
|
||||
<router-link to="/risk" class="nav-item">风险预警</router-link>
|
||||
<router-link to="/eco" class="nav-item">生态指标</router-link>
|
||||
<router-link to="/gov" class="nav-item">政府监管</router-link>
|
||||
<router-link to="/trade" class="nav-item">交易统计</router-link>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #0a1929;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
padding: 15px 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--border-radius);
|
||||
transition: var(--transition);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(76, 175, 80, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.nav-item.router-link-exact-active {
|
||||
background: linear-gradient(45deg, var(--primary-color), var(--primary-dark));
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(76, 175, 80, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(76, 175, 80, 0.8);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-nav {
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.nav-item {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
admin-system/dashboard/src/components/RealtimeChart.vue
Normal file
94
admin-system/dashboard/src/components/RealtimeChart.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="realtime-chart"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'RealtimeChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const chartContainer = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (chartInstance && props.data) {
|
||||
const defaultOptions = {
|
||||
title: {
|
||||
text: '实时产业数据'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.data.xAxis || []
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: props.data.series || [],
|
||||
type: 'line',
|
||||
smooth: true
|
||||
}]
|
||||
};
|
||||
|
||||
const finalOptions = Object.assign({}, defaultOptions, props.options);
|
||||
chartInstance.setOption(finalOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.data, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
return {
|
||||
chartContainer
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.realtime-chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
77
admin-system/dashboard/src/components/common/DataLoader.vue
Normal file
77
admin-system/dashboard/src/components/common/DataLoader.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="data-loader">
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
{{ loadingMessage }}
|
||||
</div>
|
||||
<div v-else-if="error" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div v-else-if="empty" class="empty-message">
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DataLoader',
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
empty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
loadingMessage: {
|
||||
type: String,
|
||||
default: '数据加载中...'
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '数据加载失败,请稍后重试。'
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-loader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message,
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="environment-data">
|
||||
<h3>环境数据</h3>
|
||||
<ul>
|
||||
<li v-for="(item, index) in data" :key="index">
|
||||
<span class="data-name">{{ item.name }}:</span>
|
||||
<span class="data-value">{{ item.value }}</span>
|
||||
<span class="data-unit">{{ item.unit }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EnvironmentData',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.environment-data {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.environment-data h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.environment-data ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.environment-data ul li {
|
||||
list-style: none;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.environment-data ul li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
margin-right: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-unit {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
48
admin-system/dashboard/src/components/map/FarmMap.vue
Normal file
48
admin-system/dashboard/src/components/map/FarmMap.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="farm-map-container">
|
||||
<div class="deprecated-warning">
|
||||
<p>该组件已弃用,请使用 ThreeDMap.vue 组件替代</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FarmMap',
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
validator: value => value > 0
|
||||
},
|
||||
mapId: {
|
||||
type: String,
|
||||
default: 'farm-map'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.farm-map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.deprecated-warning {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.deprecated-warning p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
247
admin-system/dashboard/src/components/map/RegionDetail.vue
Normal file
247
admin-system/dashboard/src/components/map/RegionDetail.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div class="region-detail-overlay" v-if="visible" @click="closeOverlay">
|
||||
<div class="region-detail-card" @click.stop>
|
||||
<div class="card-header">
|
||||
<h2>{{ regionData.region?.name }} 详情</h2>
|
||||
<button class="close-button" @click="closeOverlay">×</button>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="region-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">牛只数量</div>
|
||||
<div class="stat-value">{{ regionData.region?.cattle_count || 0 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">牧场数量</div>
|
||||
<div class="stat-value">{{ regionData.region?.farm_count || 0 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">产值</div>
|
||||
<div class="stat-value">¥{{ formatNumber(regionData.region?.output_value || 0) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">趋势</div>
|
||||
<div class="stat-value" :class="regionData.region?.trend">
|
||||
{{ regionData.region?.trend === 'up' ? '↑ 上升' : '↓ 下降' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="farms-section" v-if="regionData.farms && regionData.farms.length > 0">
|
||||
<h3>牧场列表</h3>
|
||||
<div class="farms-list">
|
||||
<div
|
||||
class="farm-item"
|
||||
v-for="farm in regionData.farms"
|
||||
:key="farm.id"
|
||||
>
|
||||
<div class="farm-name">{{ farm.name }}</div>
|
||||
<div class="farm-details">
|
||||
<span>牛只: {{ farm.cattle_count }}</span>
|
||||
<span>产值: ¥{{ formatNumber(farm.output_value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-farms" v-else>
|
||||
<p>暂无牧场数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, defineProps, defineEmits } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'RegionDetail',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
regionData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
setup(props, { emit }) {
|
||||
const closeOverlay = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(2) + '亿';
|
||||
}
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(2) + '万';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
return {
|
||||
closeOverlay,
|
||||
formatNumber
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-detail-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.region-detail-card {
|
||||
background: linear-gradient(135deg, #0f2027, #20555d);
|
||||
border-radius: 12px;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(90vh - 100px);
|
||||
}
|
||||
|
||||
.region-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-value.up {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-value.down {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.farms-section h3 {
|
||||
color: #4CAF50;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.farms-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.farm-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.farm-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.farm-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.no-farms {
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.region-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.farm-details {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
377
admin-system/dashboard/src/components/map/ThreeDMap.vue
Normal file
377
admin-system/dashboard/src/components/map/ThreeDMap.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div class="three-d-map-container">
|
||||
<div
|
||||
ref="mapContainer"
|
||||
class="map-canvas"
|
||||
:style="{ width: '100%', height: height + 'px' }"
|
||||
></div>
|
||||
|
||||
<RegionDetail
|
||||
:visible="showRegionDetail"
|
||||
:region-data="selectedRegionData"
|
||||
@close="closeRegionDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import RegionDetail from './RegionDetail.vue';
|
||||
import { fetchRegionDetail } from '@/services/dashboard.js';
|
||||
|
||||
export default {
|
||||
name: 'ThreeDMap',
|
||||
components: {
|
||||
RegionDetail
|
||||
},
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
// 锡林郭勒盟的中心坐标
|
||||
center: {
|
||||
type: Array,
|
||||
default: () => [116.08, 43.95] // 锡林浩特市
|
||||
},
|
||||
// 地图数据
|
||||
mapData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const mapContainer = ref(null);
|
||||
const showRegionDetail = ref(false);
|
||||
const selectedRegionData = ref({});
|
||||
let scene, camera, renderer, controls;
|
||||
let animationId = null;
|
||||
let landmarks = [];
|
||||
let raycaster, mouse;
|
||||
|
||||
// 初始化3D场景
|
||||
const initScene = () => {
|
||||
// 创建场景
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a1929);
|
||||
scene.fog = new THREE.Fog(0x0a1929, 1000, 5000);
|
||||
|
||||
// 创建相机
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
mapContainer.value.clientWidth / mapContainer.value.clientHeight,
|
||||
1,
|
||||
10000
|
||||
);
|
||||
camera.position.set(0, 500, 1000);
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(
|
||||
mapContainer.value.clientWidth,
|
||||
mapContainer.value.clientHeight
|
||||
);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.shadowMap.enabled = true;
|
||||
mapContainer.value.appendChild(renderer.domElement);
|
||||
|
||||
// 添加光源
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 1);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(1, 1, 1).normalize();
|
||||
directionalLight.castShadow = true;
|
||||
scene.add(directionalLight);
|
||||
|
||||
// 添加控制器
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
|
||||
// 初始化射线检测器
|
||||
raycaster = new THREE.Raycaster();
|
||||
mouse = new THREE.Vector2();
|
||||
|
||||
// 添加事件监听
|
||||
renderer.domElement.addEventListener('click', onMouseClick, false);
|
||||
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
|
||||
|
||||
// 创建地形和地标
|
||||
createTerrain();
|
||||
createLandmarks();
|
||||
|
||||
// 开始渲染循环
|
||||
animate();
|
||||
};
|
||||
|
||||
// 创建地形
|
||||
const createTerrain = () => {
|
||||
// 创建一个简单的平面作为地形基础
|
||||
const geometry = new THREE.PlaneGeometry(2000, 2000, 50, 50);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: 0x2c5364,
|
||||
wireframe: false,
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
});
|
||||
|
||||
const terrain = new THREE.Mesh(geometry, material);
|
||||
terrain.rotation.x = -Math.PI / 2;
|
||||
terrain.position.y = -20;
|
||||
terrain.receiveShadow = true;
|
||||
scene.add(terrain);
|
||||
|
||||
// 添加一些起伏效果
|
||||
const vertices = geometry.attributes.position.array;
|
||||
for (let i = 0; i < vertices.length; i += 3) {
|
||||
// 添加随机高度变化
|
||||
vertices[i + 2] = (Math.random() - 0.5) * 100;
|
||||
}
|
||||
geometry.attributes.position.needsUpdate = true;
|
||||
geometry.computeVertexNormals();
|
||||
};
|
||||
|
||||
// 创建地标
|
||||
const createLandmarks = () => {
|
||||
// 清除现有的地标
|
||||
landmarks.forEach(landmark => {
|
||||
scene.remove(landmark.mesh);
|
||||
scene.remove(landmark.label);
|
||||
});
|
||||
landmarks = [];
|
||||
|
||||
// 根据传入的地图数据创建地标
|
||||
props.mapData.forEach((location, index) => {
|
||||
// 计算位置(基于锡林浩特市为中心点)
|
||||
const x = (location.coordinates[0] - props.center[0]) * 5000;
|
||||
const z = (props.center[1] - location.coordinates[1]) * 5000;
|
||||
|
||||
// 根据牛只数量确定地标大小
|
||||
const size = Math.max(20, Math.min(50, (location.cattle_count || location.cattleCount) / 1000));
|
||||
const height = Math.max(30, Math.min(100, (location.cattle_count || location.cattleCount) / 500));
|
||||
|
||||
// 创建地标圆柱体
|
||||
const geometry = new THREE.CylinderGeometry(size, size, height, 32);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: getColorByCattleCount(location.cattle_count || location.cattleCount),
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
});
|
||||
const cylinder = new THREE.Mesh(geometry, material);
|
||||
cylinder.position.set(x, height/2 - 20, z);
|
||||
cylinder.castShadow = true;
|
||||
cylinder.userData = { location }; // 保存位置信息用于点击检测
|
||||
scene.add(cylinder);
|
||||
|
||||
// 添加地标名称和数据
|
||||
const label = addLabel(
|
||||
`${location.name}\n牛只: ${location.cattle_count || location.cattleCount}\n牧场: ${location.farm_count || location.farmCount}`,
|
||||
[x, height + 20, z],
|
||||
getColorByCattleCount(location.cattle_count || location.cattleCount)
|
||||
);
|
||||
|
||||
landmarks.push({
|
||||
mesh: cylinder,
|
||||
label: label,
|
||||
data: location
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 根据牛只数量获取颜色
|
||||
const getColorByCattleCount = (count) => {
|
||||
if (count > 20000) return 0x4CAF50; // 绿色 - 数量多
|
||||
if (count > 15000) return 0xFFEB3B; // 黄色 - 数量中等
|
||||
return 0xF44336; // 红色 - 数量较少
|
||||
};
|
||||
|
||||
// 添加标签
|
||||
const addLabel = (text, position, color) => {
|
||||
// 创建简单的文本标签
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = 512;
|
||||
canvas.height = 256;
|
||||
|
||||
context.fillStyle = `#${new THREE.Color(color).getHexString()}`;
|
||||
context.font = '24px Microsoft YaHei';
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
|
||||
// 支持多行文本
|
||||
const lines = text.split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
context.fillText(line, 256, 128 + (i - (lines.length-1)/2) * 30);
|
||||
});
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.position.set(position[0], position[1], position[2]);
|
||||
sprite.scale.set(200, 100, 1);
|
||||
scene.add(sprite);
|
||||
return sprite;
|
||||
};
|
||||
|
||||
// 鼠标点击事件处理
|
||||
const onMouseClick = async (event) => {
|
||||
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// 通过摄像机和鼠标位置更新射线
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
// 计算物体和射线的交点
|
||||
const intersects = raycaster.intersectObjects(scene.children);
|
||||
|
||||
for (let i = 0; i < intersects.length; i++) {
|
||||
if (intersects[i].object.userData.location) {
|
||||
const region = intersects[i].object.userData.location;
|
||||
selectedRegionData.value = await fetchRegionDetail(region.id);
|
||||
showRegionDetail.value = true;
|
||||
highlightRegion(intersects[i].object);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标移动事件处理(悬停效果)
|
||||
const onMouseMove = (event) => {
|
||||
// 计算鼠标位置标准化设备坐标 (-1 到 +1)
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// 通过摄像机和鼠标位置更新射线
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
// 计算物体和射线的交点
|
||||
const intersects = raycaster.intersectObjects(scene.children);
|
||||
|
||||
// 重置所有地标的颜色
|
||||
resetRegionColors();
|
||||
|
||||
// 高亮显示悬停的地標
|
||||
for (let i = 0; i < intersects.length; i++) {
|
||||
if (intersects[i].object.userData.location) {
|
||||
intersects[i].object.material.emissive = new THREE.Color(0x222222);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 高亮选中区域
|
||||
const highlightRegion = (mesh) => {
|
||||
resetRegionColors();
|
||||
mesh.material.emissive = new THREE.Color(0x333333);
|
||||
};
|
||||
|
||||
// 重置区域颜色
|
||||
const resetRegionColors = () => {
|
||||
scene.children.forEach(child => {
|
||||
if (child.isMesh && child.userData.location) {
|
||||
child.material.emissive = new THREE.Color(0x000000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭区域详情
|
||||
const closeRegionDetail = () => {
|
||||
showRegionDetail.value = false;
|
||||
selectedRegionData.value = {};
|
||||
resetRegionColors();
|
||||
};
|
||||
|
||||
// 动画循环
|
||||
const animate = () => {
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
// 更新控制器
|
||||
controls.update();
|
||||
|
||||
// 渲染场景
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
// 窗口大小改变时调整渲染器大小
|
||||
const onWindowResize = () => {
|
||||
if (mapContainer.value) {
|
||||
camera.aspect = mapContainer.value.clientWidth / mapContainer.value.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(
|
||||
mapContainer.value.clientWidth,
|
||||
mapContainer.value.clientHeight
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听地图数据变化
|
||||
watch(() => props.mapData, () => {
|
||||
if (scene) {
|
||||
createLandmarks();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 清理资源
|
||||
const cleanup = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
|
||||
if (renderer) {
|
||||
renderer.domElement.removeEventListener('click', onMouseClick, false);
|
||||
renderer.domElement.removeEventListener('mousemove', onMouseMove, false);
|
||||
mapContainer.value.removeChild(renderer.domElement);
|
||||
renderer.dispose();
|
||||
}
|
||||
|
||||
if (controls) {
|
||||
controls.dispose();
|
||||
}
|
||||
|
||||
// 清理场景中的所有对象
|
||||
if (scene) {
|
||||
while(scene.children.length > 0) {
|
||||
scene.remove(scene.children[0]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initScene();
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
return {
|
||||
mapContainer,
|
||||
showRegionDetail,
|
||||
selectedRegionData,
|
||||
closeRegionDetail
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-d-map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
17
admin-system/dashboard/src/main.js
Normal file
17
admin-system/dashboard/src/main.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
// DataV组件按需引入,避免Vue 3兼容性问题
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
65
admin-system/dashboard/src/router/index.js
Normal file
65
admin-system/dashboard/src/router/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Monitor from '@/views/Monitor.vue'
|
||||
import Government from '@/views/Government.vue'
|
||||
import Finance from '@/views/Finance.vue'
|
||||
import Transport from '@/views/Transport.vue'
|
||||
import Risk from '@/views/Risk.vue'
|
||||
import Eco from '@/views/Eco.vue'
|
||||
import Gov from '@/views/Gov.vue'
|
||||
import Trade from '@/views/Trade.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: Monitor
|
||||
},
|
||||
{
|
||||
path: '/government',
|
||||
name: 'Government',
|
||||
component: Government
|
||||
},
|
||||
{
|
||||
path: '/finance',
|
||||
name: 'Finance',
|
||||
component: Finance
|
||||
},
|
||||
{
|
||||
path: '/transport',
|
||||
name: 'Transport',
|
||||
component: Transport
|
||||
},
|
||||
{
|
||||
path: '/risk',
|
||||
name: 'Risk',
|
||||
component: Risk
|
||||
},
|
||||
{
|
||||
path: '/eco',
|
||||
name: 'Eco',
|
||||
component: Eco
|
||||
},
|
||||
{
|
||||
path: '/gov',
|
||||
name: 'Gov',
|
||||
component: Gov
|
||||
},
|
||||
{
|
||||
path: '/trade',
|
||||
name: 'Trade',
|
||||
component: Trade
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
73
admin-system/dashboard/src/services/dashboard.js
Normal file
73
admin-system/dashboard/src/services/dashboard.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:8000/api/v1/dashboard';
|
||||
|
||||
export const fetchOverviewData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/overview`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching overview data:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRealtimeData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/realtime`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching realtime data:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchFarmData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/farm`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching farm data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGovernmentData = async (type) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/government/${type}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching government data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchFinanceData = async (type) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/finance/${type}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching finance data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMapData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/regions`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching map data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchRegionDetail = async (regionId) => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/map/region/${regionId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching region detail:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
400
admin-system/dashboard/src/styles/global.css
Normal file
400
admin-system/dashboard/src/styles/global.css
Normal file
@@ -0,0 +1,400 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--primary-light: #81C784;
|
||||
--primary-dark: #388E3C;
|
||||
--secondary-color: #2196F3;
|
||||
--accent-color: #FF9800;
|
||||
--warning-color: #FFC107;
|
||||
--danger-color: #F44336;
|
||||
--success-color: #4CAF50;
|
||||
--info-color: #2196F3;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #0a1929;
|
||||
--darker-color: #081424;
|
||||
--text-color: #ffffff;
|
||||
--text-secondary: #cccccc;
|
||||
--text-muted: #999999;
|
||||
--border-radius: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
--border-radius-sm: 4px;
|
||||
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
--box-shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.4);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--dark-color);
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #0c2d48, #145da0);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 通用标题样式 */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-color);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, var(--primary-color), var(--primary-dark));
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(45deg, var(--primary-dark), #2E7D32);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(45deg, var(--secondary-color), #1976D2);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(45deg, #1976D2, #1565C0);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(45deg, var(--success-color), #388E3C);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: linear-gradient(45deg, #388E3C, #2E7D32);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(45deg, var(--warning-color), #FFA000);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: linear-gradient(45deg, #FFA000, #FF8F00);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(45deg, var(--danger-color), #D32F2F);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: linear-gradient(45deg, #D32F2F, #C62828);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--border-radius-lg);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: var(--box-shadow);
|
||||
transition: var(--transition);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(76, 175, 80, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(76, 175, 80, 0.8);
|
||||
}
|
||||
|
||||
/* 加载指示器 */
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(76, 175, 80, 0.3);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 错误消息 */
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--danger-color);
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
/* DataV组件样式覆盖 */
|
||||
.dv-border-box-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.dv-scroll-board .header {
|
||||
color: #4CAF50 !important;
|
||||
background: rgba(76, 175, 80, 0.1) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.dv-scroll-board .row {
|
||||
color: #fff !important;
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.dv-scroll-board .row:hover {
|
||||
background: rgba(76, 175, 80, 0.2) !important;
|
||||
}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 响应式网格 */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-col-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-col-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-col-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-col-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* 装饰元素 */
|
||||
.decoration-line {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.decoration-corner {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.decoration-corner.tl {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.decoration-corner.tr {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.decoration-corner.bl {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.decoration-corner.br {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* 媒体查询 */
|
||||
@media (max-width: 1200px) {
|
||||
.grid-col-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-col-2,
|
||||
.grid-col-3,
|
||||
.grid-col-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
679
admin-system/dashboard/src/views/Dashboard.vue
Normal file
679
admin-system/dashboard/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,679 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<header class="dashboard-header">
|
||||
<div class="header-decoration"></div>
|
||||
<div class="header-title">
|
||||
<h1>锡林郭勒盟智慧养殖产业数据大屏</h1>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<p>{{ currentTime }}</p>
|
||||
</div>
|
||||
<div class="header-decoration"></div>
|
||||
</header>
|
||||
|
||||
<main class="dashboard-main">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- 关键指标 -->
|
||||
<div class="metric-cards">
|
||||
<div class="metric-card" v-for="(metric, index) in keyMetrics" :key="index">
|
||||
<div class="metric-border card">
|
||||
<div class="metric-content">
|
||||
<div class="metric-title">{{ metric.title }}</div>
|
||||
<div class="metric-value">{{ metric.value }}</div>
|
||||
<div class="metric-change" :class="{ positive: metric.change > 0, negative: metric.change < 0 }">
|
||||
{{ metric.change > 0 ? '↑' : '↓' }} {{ Math.abs(metric.change) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险预警 -->
|
||||
<div class="risk-card">
|
||||
<div class="chart-border card">
|
||||
<h3 class="chart-title">风险预警</h3>
|
||||
<div class="risk-content">
|
||||
<div class="risk-list">
|
||||
<div
|
||||
class="risk-item"
|
||||
v-for="(risk, index) in riskData"
|
||||
:key="index"
|
||||
>
|
||||
<div class="risk-time">{{ risk.time }}</div>
|
||||
<div class="risk-type">{{ risk.type }}</div>
|
||||
<div class="risk-desc">{{ risk.desc }}</div>
|
||||
<div class="risk-status" :class="risk.status">{{ risk.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间区域 -->
|
||||
<div class="center-section">
|
||||
<!-- 产业概览地图 -->
|
||||
<div class="center-top">
|
||||
<div class="center-border card">
|
||||
<h2>锡林郭勒盟产业分布</h2>
|
||||
<div class="map-container">
|
||||
<ThreeDMap
|
||||
:height="300"
|
||||
:map-data="mapData"
|
||||
ref="threeDMap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 月度产值趋势 -->
|
||||
<div class="center-middle">
|
||||
<div class="center-chart-border card">
|
||||
<h3 class="chart-title">月度产值趋势</h3>
|
||||
<div ref="breedingChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区域分布 -->
|
||||
<div class="center-bottom">
|
||||
<div class="center-chart-border card">
|
||||
<h3 class="chart-title">区域分布</h3>
|
||||
<div ref="regionChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="right-section">
|
||||
<!-- 交易数据 -->
|
||||
<div class="transaction-card">
|
||||
<div class="chart-border card">
|
||||
<h3 class="chart-title">交易数据</h3>
|
||||
<div ref="transactionChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险雷达 -->
|
||||
<div class="radar-card">
|
||||
<div class="chart-border card">
|
||||
<h3 class="chart-title">风险雷达</h3>
|
||||
<div ref="riskRadarChart" class="chart-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="dashboard-footer">
|
||||
<div class="footer-decoration"></div>
|
||||
<div class="footer-content">
|
||||
<p>锡林郭勒盟智慧养殖产业数字化管理平台</p>
|
||||
</div>
|
||||
<div class="footer-decoration"></div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import ThreeDMap from '@/components/map/ThreeDMap.vue'
|
||||
import { fetchMapData } from '@/services/dashboard.js'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
ThreeDMap
|
||||
},
|
||||
setup() {
|
||||
const currentTime = ref(new Date().toLocaleString())
|
||||
const breedingChart = ref(null)
|
||||
const transactionChart = ref(null)
|
||||
const regionChart = ref(null)
|
||||
const riskRadarChart = ref(null)
|
||||
const threeDMap = ref(null)
|
||||
|
||||
let breedingChartInstance = null
|
||||
let transactionChartInstance = null
|
||||
let regionChartInstance = null
|
||||
let riskRadarChartInstance = null
|
||||
let timer = null
|
||||
|
||||
// 关键指标数据
|
||||
const keyMetrics = ref([
|
||||
{ title: '本月交易额', value: '¥860万', change: 5.7 },
|
||||
{ title: '平均体重', value: '420kg', change: 0.8 },
|
||||
{ title: '疫苗接种率', value: '98.6%', change: 2.1 },
|
||||
{ title: '风险事件', value: '12起', change: -15.2 }
|
||||
])
|
||||
|
||||
// 风险数据
|
||||
const riskData = ref([
|
||||
{ time: '08-19 10:23', type: '健康问题', desc: '某牧场发现疑似疫情', status: '处理中' },
|
||||
{ time: '08-19 09:45', type: '环境异常', desc: '气温过高,注意防暑', status: '已处理' },
|
||||
{ time: '08-19 08:30', type: '交易风险', desc: '一笔交易存在争议', status: '处理中' },
|
||||
{ time: '08-18 16:15', type: '运输风险', desc: '运输路线受阻', status: '已处理' }
|
||||
])
|
||||
|
||||
// 地图数据
|
||||
const mapData = ref([])
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 确保 DOM 元素已正确绑定
|
||||
if (!breedingChart.value || !regionChart.value) {
|
||||
console.error('ECharts 初始化失败: DOM 元素未正确绑定')
|
||||
return
|
||||
}
|
||||
|
||||
// 养殖规模趋势图
|
||||
breedingChartInstance = echarts.init(breedingChart.value)
|
||||
breedingChartInstance.setOption({
|
||||
title: { text: '' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
data: [8200, 9100, 10500, 11200, 12100, 12860],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#4CAF50' },
|
||||
areaStyle: { color: 'rgba(76, 175, 80, 0.3)' }
|
||||
}]
|
||||
})
|
||||
|
||||
// 交易数据分析图
|
||||
if (transactionChart.value) {
|
||||
transactionChartInstance = echarts.init(transactionChart.value)
|
||||
transactionChartInstance.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: '0' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 335, name: '线上交易' },
|
||||
{ value: 310, name: '线下交易' },
|
||||
{ value: 234, name: '跨区域交易' },
|
||||
{ value: 135, name: '本地交易' }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 区域分布图
|
||||
regionChartInstance = echarts.init(regionChart.value)
|
||||
regionChartInstance.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['东乌旗', '西乌旗', '锡市', '镶黄旗', '正蓝旗', '太仆寺旗']
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
data: [1200, 1800, 2400, 1600, 2100, 1900],
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#388E3C' }
|
||||
}]
|
||||
})
|
||||
|
||||
// 风险雷达图
|
||||
if (riskRadarChart.value) {
|
||||
riskRadarChartInstance = echarts.init(riskRadarChart.value)
|
||||
riskRadarChartInstance.setOption({
|
||||
title: { text: '' },
|
||||
tooltip: { trigger: 'item' },
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '健康问题', max: 100 },
|
||||
{ name: '交易风险', max: 100 },
|
||||
{ name: '环境异常', max: 100 },
|
||||
{ name: '运输风险', max: 100 },
|
||||
{ name: '市场波动', max: 100 }
|
||||
]
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: [60, 30, 50, 20, 40],
|
||||
name: '风险指数',
|
||||
itemStyle: { color: '#FF9800' },
|
||||
areaStyle: { color: 'rgba(255, 152, 0, 0.3)' }
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取地图数据
|
||||
const loadMapData = async () => {
|
||||
try {
|
||||
const data = await fetchMapData()
|
||||
if (data.regions && data.regions.length > 0) {
|
||||
mapData.value = data.regions
|
||||
} else {
|
||||
// 使用默认数据
|
||||
mapData.value = [
|
||||
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
|
||||
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
|
||||
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
|
||||
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
|
||||
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取地图数据失败:', error)
|
||||
// 使用默认数据
|
||||
mapData.value = [
|
||||
{ id: 'xlg', name: '锡林浩特市', cattle_count: 25600, farm_count: 120, coordinates: [116.093, 43.946] },
|
||||
{ id: 'dwq', name: '东乌旗', cattle_count: 18500, farm_count: 95, coordinates: [116.980, 45.514] },
|
||||
{ id: 'xwq', name: '西乌旗', cattle_count: 21200, farm_count: 108, coordinates: [117.615, 44.587] },
|
||||
{ id: 'abg', name: '阿巴嘎旗', cattle_count: 16800, farm_count: 86, coordinates: [114.971, 44.022] },
|
||||
{ id: 'snz', name: '苏尼特左旗', cattle_count: 12400, farm_count: 65, coordinates: [113.653, 43.859] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString()
|
||||
}
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (breedingChartInstance) breedingChartInstance.resize()
|
||||
if (transactionChartInstance) transactionChartInstance.resize()
|
||||
if (regionChartInstance) regionChartInstance.resize()
|
||||
if (riskRadarChartInstance) riskRadarChartInstance.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCharts()
|
||||
loadMapData()
|
||||
timer = setInterval(updateTime, 1000)
|
||||
window.addEventListener('resize', resizeCharts)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timer)
|
||||
window.removeEventListener('resize', resizeCharts)
|
||||
if (breedingChartInstance) breedingChartInstance.dispose()
|
||||
if (transactionChartInstance) transactionChartInstance.dispose()
|
||||
if (regionChartInstance) regionChartInstance.dispose()
|
||||
if (riskRadarChartInstance) riskRadarChartInstance.dispose()
|
||||
})
|
||||
|
||||
return {
|
||||
currentTime,
|
||||
keyMetrics,
|
||||
riskData,
|
||||
mapData,
|
||||
breedingChart,
|
||||
transactionChart,
|
||||
regionChart,
|
||||
riskRadarChart,
|
||||
threeDMap
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #0f2027, #20555d, #2c5364);
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: var(--border-radius-lg);
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.header-decoration {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-decoration::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #4CAF50, transparent);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
background: linear-gradient(to right, #4CAF50, #8BC34A);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
font-size: 16px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dashboard-main {
|
||||
display: flex;
|
||||
height: calc(100% - 180px);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.left-section, .right-section {
|
||||
width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.center-section {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.metric-border {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.metric-change {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric-change.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.metric-change.negative {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.center-top {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.center-middle {
|
||||
height: 35%;
|
||||
}
|
||||
|
||||
.center-bottom {
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
.center-border, .center-chart-border {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.center-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.total-count {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.count-title {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.count-value {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.count-unit {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.growth-rate {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rate-title {
|
||||
font-size: 18px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rate-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rate-value.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.risk-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.risk-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.risk-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.risk-time {
|
||||
width: 15%;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.risk-type {
|
||||
width: 20%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.risk-desc {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.risk-status {
|
||||
width: 15%;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.risk-status.处理中 {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.risk-status.已处理 {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.dashboard-footer {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.footer-decoration {
|
||||
width: 300px;
|
||||
height: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-decoration::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #4CAF50, transparent);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-main {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.left-section, .center-section, .right-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.dashboard-header,
|
||||
.dashboard-footer {
|
||||
height: 80px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.header-title h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.metric-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.risk-time,
|
||||
.risk-type,
|
||||
.risk-desc,
|
||||
.risk-status {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
438
admin-system/dashboard/src/views/Eco.vue
Normal file
438
admin-system/dashboard/src/views/Eco.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div class="eco-container">
|
||||
<h1>生态指标</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="eco-content">
|
||||
<!-- 环保数据展示 -->
|
||||
<div class="environment-section">
|
||||
<h3>环保数据</h3>
|
||||
<div class="eco-grid">
|
||||
<div class="eco-card" v-for="(item, index) in ecoData" :key="index">
|
||||
<div class="eco-icon">{{ item.icon }}</div>
|
||||
<div class="eco-info">
|
||||
<div class="eco-title">{{ item.title }}</div>
|
||||
<div class="eco-value">{{ item.value }}</div>
|
||||
<div class="eco-change" :class="item.change > 0 ? 'positive' : 'negative'">
|
||||
{{ item.change > 0 ? '↑' : '↓' }} {{ Math.abs(item.change) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可持续发展指标 -->
|
||||
<div class="sustainability-section">
|
||||
<h3>可持续发展指标</h3>
|
||||
<div class="sustainability-content">
|
||||
<div class="balance-chart">
|
||||
<h4>草畜平衡</h4>
|
||||
<div ref="balanceChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
<div class="benefit-chart">
|
||||
<h4>生态效益</h4>
|
||||
<div ref="benefitChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 环保趋势分析 -->
|
||||
<div class="trend-section">
|
||||
<h3>环保趋势分析</h3>
|
||||
<div class="chart-container">
|
||||
<div ref="trendChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生态效益评估 -->
|
||||
<div class="assessment-section">
|
||||
<h3>生态效益评估</h3>
|
||||
<div class="assessment-content">
|
||||
<div class="economic-benefit">
|
||||
<h4>经济效益</h4>
|
||||
<div class="benefit-value">¥28.6亿</div>
|
||||
<div class="benefit-desc">年度总产值</div>
|
||||
</div>
|
||||
<div class="ecological-benefit">
|
||||
<h4>生态效益</h4>
|
||||
<div class="benefit-value">+12.3%</div>
|
||||
<div class="benefit-desc">生态改善率</div>
|
||||
</div>
|
||||
<div class="balance-indicator">
|
||||
<h4>效益平衡</h4>
|
||||
<div class="balance-chart-container">
|
||||
<div ref="balanceIndicator" class="chart-placeholder" style="width: 100%; height: 200px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'Eco',
|
||||
setup() {
|
||||
const ecoData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const balanceChart = ref(null);
|
||||
const benefitChart = ref(null);
|
||||
const trendChart = ref(null);
|
||||
const balanceIndicator = ref(null);
|
||||
|
||||
let balanceChartInstance = null;
|
||||
let benefitChartInstance = null;
|
||||
let trendChartInstance = null;
|
||||
let indicatorInstance = null;
|
||||
|
||||
// 环保数据
|
||||
ecoData.value = [
|
||||
{ icon: '🌱', title: '碳排放', value: '12,500吨', change: -5.2 },
|
||||
{ icon: '💧', title: '水资源使用', value: '850,000吨', change: -3.1 },
|
||||
{ icon: '🌾', title: '饲料消耗', value: '2,300吨', change: -2.8 },
|
||||
{ icon: '🏞️', title: '草地覆盖率', value: '86.5%', change: 1.2 }
|
||||
];
|
||||
|
||||
// 初始化草畜平衡图表
|
||||
const initBalanceChart = () => {
|
||||
if (balanceChart.value) {
|
||||
balanceChartInstance = echarts.init(balanceChart.value);
|
||||
balanceChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '0'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 65, name: '草场承载力' },
|
||||
{ value: 35, name: '牲畜需求量' }
|
||||
],
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colorList = ['#4CAF50', '#2196F3'];
|
||||
return colorList[params.dataIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化生态效益图表
|
||||
const initBenefitChart = () => {
|
||||
if (benefitChart.value) {
|
||||
benefitChartInstance = echarts.init(benefitChart.value);
|
||||
benefitChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['2018', '2019', '2020', '2021', '2022', '2023']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [65, 70, 75, 78, 82, 86],
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化趋势分析图表
|
||||
const initTrendChart = () => {
|
||||
if (trendChart.value) {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
trendChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['碳排放', '水资源使用', '饲料消耗']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '碳排放',
|
||||
type: 'line',
|
||||
data: [2200, 2100, 2000, 1950, 1850, 1750],
|
||||
itemStyle: { color: '#f44336' }
|
||||
},
|
||||
{
|
||||
name: '水资源使用',
|
||||
type: 'line',
|
||||
data: [150000, 145000, 140000, 135000, 130000, 125000],
|
||||
itemStyle: { color: '#2196F3' }
|
||||
},
|
||||
{
|
||||
name: '饲料消耗',
|
||||
type: 'line',
|
||||
data: [4200, 4100, 4000, 3900, 3800, 3700],
|
||||
itemStyle: { color: '#FF9800' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化效益平衡指示器
|
||||
const initBalanceIndicator = () => {
|
||||
if (balanceIndicator.value) {
|
||||
indicatorInstance = echarts.init(balanceIndicator.value);
|
||||
indicatorInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
progress: {
|
||||
show: true
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: [
|
||||
[0.3, '#f44336'],
|
||||
[0.7, '#FF9800'],
|
||||
[1, '#4CAF50']
|
||||
],
|
||||
width: 15
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
formatter: '{value}',
|
||||
color: 'auto'
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 85,
|
||||
name: '生态效益指数'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (balanceChartInstance) balanceChartInstance.resize();
|
||||
if (benefitChartInstance) benefitChartInstance.resize();
|
||||
if (trendChartInstance) trendChartInstance.resize();
|
||||
if (indicatorInstance) indicatorInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = false;
|
||||
initBalanceChart();
|
||||
initBenefitChart();
|
||||
initTrendChart();
|
||||
initBalanceIndicator();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (balanceChartInstance) balanceChartInstance.dispose();
|
||||
if (benefitChartInstance) benefitChartInstance.dispose();
|
||||
if (trendChartInstance) trendChartInstance.dispose();
|
||||
if (indicatorInstance) indicatorInstance.dispose();
|
||||
});
|
||||
|
||||
return {
|
||||
ecoData,
|
||||
loading,
|
||||
error,
|
||||
balanceChart,
|
||||
benefitChart,
|
||||
trendChart,
|
||||
balanceIndicator
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.eco-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.eco-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.environment-section,
|
||||
.sustainability-section,
|
||||
.trend-section,
|
||||
.assessment-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.environment-section h3,
|
||||
.sustainability-section h3,
|
||||
.trend-section h3,
|
||||
.assessment-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.eco-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.eco-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.eco-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.eco-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.eco-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.eco-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.eco-change {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.eco-change.positive {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.eco-change.negative {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.sustainability-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.balance-chart h4,
|
||||
.benefit-chart h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.assessment-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.economic-benefit,
|
||||
.ecological-benefit,
|
||||
.balance-indicator {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.economic-benefit h4,
|
||||
.ecological-benefit h4,
|
||||
.balance-indicator h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.benefit-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.benefit-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.eco-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.eco-grid,
|
||||
.sustainability-content,
|
||||
.assessment-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
130
admin-system/dashboard/src/views/Finance.vue
Normal file
130
admin-system/dashboard/src/views/Finance.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="finance-container">
|
||||
<h1>金融服务</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error">
|
||||
<div class="loan-section">
|
||||
<h3>贷款数据</h3>
|
||||
<div class="chart-container">
|
||||
<div id="loan-chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="insurance-section">
|
||||
<h3>保险数据</h3>
|
||||
<div class="chart-container">
|
||||
<div id="insurance-chart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const loanData = ref([]);
|
||||
const insuranceData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
const [loanResponse, insuranceResponse] = await Promise.all([
|
||||
axios.get('/api/loan-data'),
|
||||
axios.get('/api/insurance-data')
|
||||
]);
|
||||
loanData.value = loanResponse.data;
|
||||
insuranceData.value = insuranceResponse.data;
|
||||
renderCharts();
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取数据失败:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCharts = () => {
|
||||
const loanChart = echarts.init(document.getElementById('loan-chart'));
|
||||
loanChart.setOption({
|
||||
tooltip: {},
|
||||
xAxis: { data: loanData.value.map(item => item.month) },
|
||||
yAxis: {},
|
||||
series: [{ type: 'bar', data: loanData.value.map(item => item.value) }]
|
||||
});
|
||||
|
||||
const insuranceChart = echarts.init(document.getElementById('insurance-chart'));
|
||||
insuranceChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
data: insuranceData.value.map(item => item)
|
||||
}]
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
return {
|
||||
loanData,
|
||||
insuranceData,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.finance-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loan-section,
|
||||
.insurance-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.loan-section,
|
||||
.insurance-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#loan-chart,
|
||||
#insurance-chart {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
412
admin-system/dashboard/src/views/Gov.vue
Normal file
412
admin-system/dashboard/src/views/Gov.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="gov-container">
|
||||
<h1>政府监管</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="gov-content">
|
||||
<!-- 监管数据总览 -->
|
||||
<div class="overview-section">
|
||||
<h3>监管数据总览</h3>
|
||||
<div class="overview-grid">
|
||||
<div class="overview-card" v-for="(item, index) in overviewData" :key="index">
|
||||
<div class="card-icon">{{ item.icon }}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
<div class="card-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 合规性检查结果 -->
|
||||
<div class="compliance-section">
|
||||
<h3>合规性检查结果</h3>
|
||||
<div class="compliance-content">
|
||||
<div class="compliance-chart">
|
||||
<h4>合规牧场比例</h4>
|
||||
<div ref="complianceChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
<div class="violations-table">
|
||||
<h4>违规事件统计</h4>
|
||||
<a-table :dataSource="violationsData" :columns="violationsColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 政策执行效果分析 -->
|
||||
<div class="policy-section">
|
||||
<h3>政策执行效果分析</h3>
|
||||
<div class="policy-content">
|
||||
<div class="policy-chart">
|
||||
<div ref="policyChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 监管报告 -->
|
||||
<div class="report-section">
|
||||
<h3>监管报告</h3>
|
||||
<div class="report-content">
|
||||
<div class="report-summary">
|
||||
<h4>月度监管报告摘要</h4>
|
||||
<div class="summary-content">
|
||||
<p>本月共检查牧场120家,合规率92.5%,较上月提升1.2%。</p>
|
||||
<p>发现违规事件9起,已全部处理完毕。</p>
|
||||
<p>发放补贴资金¥280万元,惠及养殖户85户。</p>
|
||||
</div>
|
||||
<div class="report-actions">
|
||||
<button class="export-btn">导出PDF报告</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-chart">
|
||||
<h4>监管报告可视化</h4>
|
||||
<div ref="reportChart" class="chart-placeholder" style="width: 100%; height: 200px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'Gov',
|
||||
setup() {
|
||||
const overviewData = ref([]);
|
||||
const violationsData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const complianceChart = ref(null);
|
||||
const policyChart = ref(null);
|
||||
const reportChart = ref(null);
|
||||
|
||||
let complianceChartInstance = null;
|
||||
let policyChartInstance = null;
|
||||
let reportChartInstance = null;
|
||||
|
||||
// 监管数据总览
|
||||
overviewData.value = [
|
||||
{ icon: '✅', title: '防疫完成率', value: '98.6%', desc: '较上月 +2.1%' },
|
||||
{ icon: '💰', title: '补贴发放', value: '¥280万', desc: '惠及85户养殖户' },
|
||||
{ icon: '📋', title: '检查牧场', value: '120家', desc: '合规率92.5%' },
|
||||
{ icon: '⚠️', title: '违规事件', value: '9起', desc: '已全部处理' }
|
||||
];
|
||||
|
||||
// 违规事件表格列定义
|
||||
const violationsColumns = ref([
|
||||
{ title: '事件编号', dataIndex: 'id', key: 'id' },
|
||||
{ title: '牧场名称', dataIndex: 'farm', key: 'farm' },
|
||||
{ title: '违规类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '发生时间', dataIndex: 'time', key: 'time' },
|
||||
{ title: '处理状态', dataIndex: 'status', key: 'status' },
|
||||
]);
|
||||
|
||||
// 违规事件数据
|
||||
violationsData.value = [
|
||||
{ key: '1', id: 'V202308001', farm: '锡市牧场A', type: '环境不达标', time: '2023-08-15', status: '已处理' },
|
||||
{ key: '2', id: 'V202308002', farm: '东乌旗牧场B', type: '防疫记录不全', time: '2023-08-18', status: '处理中' },
|
||||
{ key: '3', id: 'V202308003', farm: '西乌旗牧场C', type: '饲料来源不明', time: '2023-08-20', status: '已处理' },
|
||||
];
|
||||
|
||||
// 初始化合规性检查图表
|
||||
const initComplianceChart = () => {
|
||||
if (complianceChart.value) {
|
||||
complianceChartInstance = echarts.init(complianceChart.value);
|
||||
complianceChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '0'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 92.5, name: '合规牧场' },
|
||||
{ value: 7.5, name: '违规牧场' }
|
||||
],
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colorList = ['#4CAF50', '#f44336'];
|
||||
return colorList[params.dataIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化政策执行效果图表
|
||||
const initPolicyChart = () => {
|
||||
if (policyChart.value) {
|
||||
policyChartInstance = echarts.init(policyChart.value);
|
||||
policyChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['防疫政策', '补贴政策', '环保政策']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '防疫政策',
|
||||
type: 'line',
|
||||
data: [85, 87, 89, 91, 93, 94],
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
},
|
||||
{
|
||||
name: '补贴政策',
|
||||
type: 'line',
|
||||
data: [78, 80, 82, 85, 87, 88],
|
||||
itemStyle: { color: '#2196F3' }
|
||||
},
|
||||
{
|
||||
name: '环保政策',
|
||||
type: 'line',
|
||||
data: [70, 72, 75, 78, 80, 82],
|
||||
itemStyle: { color: '#FF9800' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化监管报告图表
|
||||
const initReportChart = () => {
|
||||
if (reportChart.value) {
|
||||
reportChartInstance = echarts.init(reportChart.value);
|
||||
reportChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['检查数', '合规数', '违规数', '处理数']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [120, 111, 9, 9],
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colorList = ['#2196F3', '#4CAF50', '#f44336', '#FF9800'];
|
||||
return colorList[params.dataIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (complianceChartInstance) complianceChartInstance.resize();
|
||||
if (policyChartInstance) policyChartInstance.resize();
|
||||
if (reportChartInstance) reportChartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = false;
|
||||
initComplianceChart();
|
||||
initPolicyChart();
|
||||
initReportChart();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (complianceChartInstance) complianceChartInstance.dispose();
|
||||
if (policyChartInstance) policyChartInstance.dispose();
|
||||
if (reportChartInstance) reportChartInstance.dispose();
|
||||
});
|
||||
|
||||
return {
|
||||
overviewData,
|
||||
violationsData,
|
||||
violationsColumns,
|
||||
loading,
|
||||
error,
|
||||
complianceChart,
|
||||
policyChart,
|
||||
reportChart
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gov-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gov-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.overview-section,
|
||||
.compliance-section,
|
||||
.policy-section,
|
||||
.report-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overview-section h3,
|
||||
.compliance-section h3,
|
||||
.policy-section h3,
|
||||
.report-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.compliance-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.compliance-chart h4,
|
||||
.violations-table h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.policy-chart {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.report-summary {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.report-summary h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-content p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.report-chart h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.gov-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.overview-grid,
|
||||
.compliance-content,
|
||||
.report-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
admin-system/dashboard/src/views/Government.vue
Normal file
104
admin-system/dashboard/src/views/Government.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="government-container">
|
||||
<h1>政府平台</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error">
|
||||
<div class="policy-section">
|
||||
<h3>政策通知</h3>
|
||||
<ul>
|
||||
<li v-for="(policy, index) in policies" :key="index">
|
||||
{{ policy.title }} - {{ policy.date }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="data-section">
|
||||
<h3>政务数据</h3>
|
||||
<a-table :dataSource="governmentData" :columns="columns" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const policies = ref([]);
|
||||
const governmentData = ref([]);
|
||||
const columns = ref([
|
||||
{ title: '指标', dataIndex: 'indicator', key: 'indicator' },
|
||||
{ title: '数值', dataIndex: 'value', key: 'value' },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit' },
|
||||
]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
const [policyResponse, dataResponse] = await Promise.all([
|
||||
axios.get('/api/policies'),
|
||||
axios.get('/api/government-data')
|
||||
]);
|
||||
policies.value = policyResponse.data;
|
||||
governmentData.value = dataResponse.data;
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取数据失败:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
return {
|
||||
policies,
|
||||
governmentData,
|
||||
columns,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.government-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.policy-section,
|
||||
.data-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.policy-section ul,
|
||||
.data-section {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.data-section .ant-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
135
admin-system/dashboard/src/views/Monitor.vue
Normal file
135
admin-system/dashboard/src/views/Monitor.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="monitor-container">
|
||||
<h1>养殖监控</h1>
|
||||
<DataLoader
|
||||
:loading="loading"
|
||||
:error="error"
|
||||
:empty="!environmentData.length"
|
||||
empty-message="暂无环境数据"
|
||||
>
|
||||
<template #default>
|
||||
<div class="map-container">
|
||||
<!-- 3D地图组件 -->
|
||||
<ThreeDMap :height="mapHeight" :map-data="farmLocations" />
|
||||
</div>
|
||||
<div class="data-panel">
|
||||
<EnvironmentData :data="environmentData" />
|
||||
</div>
|
||||
</template>
|
||||
</DataLoader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { fetchFarmData } from '@/services/dashboard';
|
||||
import ThreeDMap from '@/components/map/ThreeDMap.vue';
|
||||
import EnvironmentData from '@/components/data/EnvironmentData.vue';
|
||||
import DataLoader from '@/components/common/DataLoader.vue';
|
||||
|
||||
export default {
|
||||
name: 'Monitor',
|
||||
components: {
|
||||
ThreeDMap,
|
||||
EnvironmentData,
|
||||
DataLoader
|
||||
},
|
||||
setup() {
|
||||
// 响应式状态
|
||||
const environmentData = ref([]);
|
||||
const farmLocations = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
|
||||
// 根据屏幕尺寸计算地图高度
|
||||
const mapHeight = computed(() => {
|
||||
return window.innerWidth <= 768 ? 300 : 500;
|
||||
});
|
||||
|
||||
// 数据加载
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
|
||||
// 模拟获取农场位置数据
|
||||
farmLocations.value = [
|
||||
{ id: 1, name: '锡林浩特牧场A', position: [116.08, 43.95], status: '正常' },
|
||||
{ id: 2, name: '东乌旗牧场B', position: [116.95, 45.55], status: '正常' },
|
||||
{ id: 3, name: '西乌旗牧场C', position: [117.60, 44.60], status: '警告' },
|
||||
{ id: 4, name: '镶黄旗牧场D', position: [114.20, 42.25], status: '正常' }
|
||||
];
|
||||
|
||||
// 模拟获取环境数据
|
||||
environmentData.value = [
|
||||
{ id: 1, name: '温度', value: '22°C', status: '正常' },
|
||||
{ id: 2, name: '湿度', value: '65%', status: '正常' },
|
||||
{ id: 3, name: '氨气浓度', value: '8ppm', status: '警告' },
|
||||
{ id: 4, name: 'PM2.5', value: '35μg/m³', status: '正常' }
|
||||
];
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取数据失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
return {
|
||||
environmentData,
|
||||
loading,
|
||||
error,
|
||||
mapHeight
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monitor-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.data-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monitor-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```
|
||||
/src/components/map/FarmMap.vue
|
||||
```
|
||||
```
|
||||
47
admin-system/dashboard/src/views/Overview.vue
Normal file
47
admin-system/dashboard/src/views/Overview.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="overview-container">
|
||||
<h1>产业概览</h1>
|
||||
<div class="charts-container">
|
||||
<RealtimeChart :data="overviewData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import RealtimeChart from '@/components/RealtimeChart.vue';
|
||||
import { fetchOverviewData } from '@/services/dashboard';
|
||||
|
||||
export default {
|
||||
name: 'Overview',
|
||||
components: { RealtimeChart },
|
||||
setup() {
|
||||
const overviewData = ref({});
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
overviewData.value = await fetchOverviewData();
|
||||
} catch (error) {
|
||||
console.error('获取产业概览数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
return {
|
||||
overviewData
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overview-container {
|
||||
padding: 20px;
|
||||
}
|
||||
.charts-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
485
admin-system/dashboard/src/views/Risk.vue
Normal file
485
admin-system/dashboard/src/views/Risk.vue
Normal file
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<div class="risk-container">
|
||||
<h1>风险预警</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="risk-content">
|
||||
<!-- 风险事件展示 -->
|
||||
<div class="events-section">
|
||||
<h3>风险事件</h3>
|
||||
<div class="events-grid">
|
||||
<div
|
||||
v-for="(event, index) in riskEvents"
|
||||
:key="index"
|
||||
class="event-card"
|
||||
:class="event.level"
|
||||
>
|
||||
<div class="event-header">
|
||||
<div class="event-icon">
|
||||
<span v-if="event.level === 'high'">🔴</span>
|
||||
<span v-else-if="event.level === 'medium'">🟠</span>
|
||||
<span v-else>🟡</span>
|
||||
</div>
|
||||
<div class="event-title">{{ event.title }}</div>
|
||||
<div class="event-time">{{ event.time }}</div>
|
||||
</div>
|
||||
<div class="event-body">
|
||||
<div class="event-desc">{{ event.desc }}</div>
|
||||
<div class="event-location">位置: {{ event.location }}</div>
|
||||
</div>
|
||||
<div class="event-footer">
|
||||
<div class="event-status">{{ event.status }}</div>
|
||||
<div class="event-type">{{ event.type }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预警信息推送 -->
|
||||
<div class="alerts-section">
|
||||
<h3>预警信息</h3>
|
||||
<div class="alerts-list">
|
||||
<div
|
||||
v-for="(alert, index) in riskAlerts"
|
||||
:key="index"
|
||||
class="alert-item"
|
||||
:class="alert.level"
|
||||
>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">{{ alert.title }}</div>
|
||||
<div class="alert-desc">{{ alert.desc }}</div>
|
||||
</div>
|
||||
<div class="alert-actions">
|
||||
<button class="subscribe-btn" @click="subscribeAlert(alert)">
|
||||
{{ alert.subscribed ? '已订阅' : '订阅通知' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险趋势分析 -->
|
||||
<div class="trend-section">
|
||||
<h3>风险趋势分析</h3>
|
||||
<div class="chart-container">
|
||||
<div ref="trendChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 风险地图 -->
|
||||
<div class="map-section">
|
||||
<h3>风险地图</h3>
|
||||
<div class="map-container">
|
||||
<div ref="riskMap" class="map-placeholder" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'Risk',
|
||||
setup() {
|
||||
const riskEvents = ref([]);
|
||||
const riskAlerts = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const trendChart = ref(null);
|
||||
const riskMap = ref(null);
|
||||
|
||||
let trendChartInstance = null;
|
||||
let mapInstance = null;
|
||||
|
||||
// 风险事件数据
|
||||
riskEvents.value = [
|
||||
{
|
||||
level: 'high',
|
||||
title: '疫病风险',
|
||||
time: '2023-08-20 15:30',
|
||||
desc: '某牧场发现疑似口蹄疫病例',
|
||||
location: '锡市牧场A',
|
||||
status: '处理中',
|
||||
type: '健康风险'
|
||||
},
|
||||
{
|
||||
level: 'medium',
|
||||
title: '市场风险',
|
||||
time: '2023-08-20 14:20',
|
||||
desc: '牛肉价格波动较大',
|
||||
location: '全盟范围',
|
||||
status: '监测中',
|
||||
type: '市场风险'
|
||||
},
|
||||
{
|
||||
level: 'low',
|
||||
title: '自然灾害风险',
|
||||
time: '2023-08-20 10:15',
|
||||
desc: '局部地区有暴雨预警',
|
||||
location: '东乌旗',
|
||||
status: '预警中',
|
||||
type: '自然灾害'
|
||||
},
|
||||
{
|
||||
level: 'high',
|
||||
title: '环境风险',
|
||||
time: '2023-08-20 09:45',
|
||||
desc: '某区域氨气浓度超标',
|
||||
location: '西乌旗牧场B',
|
||||
status: '处理中',
|
||||
type: '环境风险'
|
||||
}
|
||||
];
|
||||
|
||||
// 预警信息数据
|
||||
riskAlerts.value = [
|
||||
{
|
||||
level: 'high',
|
||||
title: '疫病预警',
|
||||
desc: '全盟范围内口蹄疫预警,需加强防疫措施',
|
||||
subscribed: true
|
||||
},
|
||||
{
|
||||
level: 'medium',
|
||||
title: '市场预警',
|
||||
desc: '牛肉价格波动预警,请关注市场动态',
|
||||
subscribed: false
|
||||
},
|
||||
{
|
||||
level: 'low',
|
||||
title: '天气预警',
|
||||
desc: '局部地区暴雨预警,请注意防范',
|
||||
subscribed: true
|
||||
}
|
||||
];
|
||||
|
||||
// 订阅预警通知
|
||||
const subscribeAlert = (alert) => {
|
||||
alert.subscribed = !alert.subscribed;
|
||||
// 这里可以添加实际的订阅逻辑
|
||||
};
|
||||
|
||||
// 初始化趋势分析图表
|
||||
const initTrendChart = () => {
|
||||
if (trendChart.value) {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
trendChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['疫病风险', '市场风险', '环境风险']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '疫病风险',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: [12, 15, 8, 10, 14, 9],
|
||||
itemStyle: { color: '#f44336' }
|
||||
},
|
||||
{
|
||||
name: '市场风险',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: [8, 10, 12, 9, 11, 7],
|
||||
itemStyle: { color: '#ff9800' }
|
||||
},
|
||||
{
|
||||
name: '环境风险',
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: [5, 7, 6, 8, 9, 6],
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化风险地图
|
||||
const initRiskMap = () => {
|
||||
if (riskMap.value) {
|
||||
mapInstance = echarts.init(riskMap.value);
|
||||
mapInstance.setOption({
|
||||
title: {
|
||||
text: '风险分布图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: false,
|
||||
zoom: 1.2,
|
||||
center: [116.0, 44.0], // 锡林郭勒盟位置
|
||||
itemStyle: {
|
||||
areaColor: '#e7e8ea'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'scatter',
|
||||
coordinateSystem: 'geo',
|
||||
symbolSize: 20,
|
||||
data: [
|
||||
{ name: '锡市牧场A', value: [116.08, 43.95], itemStyle: { color: '#f44336' } },
|
||||
{ name: '东乌旗', value: [116.95, 45.55], itemStyle: { color: '#ff9800' } },
|
||||
{ name: '西乌旗牧场B', value: [117.60, 44.60], itemStyle: { color: '#f44336' } },
|
||||
{ name: '镶黄旗', value: [114.20, 42.25], itemStyle: { color: '#4CAF50' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (trendChartInstance) {
|
||||
trendChartInstance.resize();
|
||||
}
|
||||
if (mapInstance) {
|
||||
mapInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = false;
|
||||
initTrendChart();
|
||||
initRiskMap();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (trendChartInstance) {
|
||||
trendChartInstance.dispose();
|
||||
}
|
||||
if (mapInstance) {
|
||||
mapInstance.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
riskEvents,
|
||||
riskAlerts,
|
||||
loading,
|
||||
error,
|
||||
trendChart,
|
||||
riskMap,
|
||||
subscribeAlert
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.risk-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.risk-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.events-section,
|
||||
.alerts-section,
|
||||
.trend-section,
|
||||
.map-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.events-section h3,
|
||||
.alerts-section h3,
|
||||
.trend-section h3,
|
||||
.map-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.event-card.high {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.event-card.medium {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.event-card.low {
|
||||
background-color: #e8f5e9;
|
||||
border-left-color: #4CAF50;
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.event-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-weight: bold;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.event-body {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.event-location {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.event-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.event-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.alerts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.alert-item.high {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.alert-item.medium {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.alert-item.low {
|
||||
background-color: #e8f5e9;
|
||||
border-left-color: #4CAF50;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.subscribe-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subscribe-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.risk-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
478
admin-system/dashboard/src/views/Trade.vue
Normal file
478
admin-system/dashboard/src/views/Trade.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div class="trade-container">
|
||||
<h1>交易统计</h1>
|
||||
<div v-if="loading" class="loading-indicator">
|
||||
<div class="loading-spinner"></div>
|
||||
数据加载中...
|
||||
</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="trade-content">
|
||||
<!-- 牛只交易量统计 -->
|
||||
<div class="volume-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">牛只交易量统计</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="volume-content">
|
||||
<div class="volume-cards">
|
||||
<div class="volume-card card" v-for="(item, index) in volumeData" :key="index">
|
||||
<div class="card-body">
|
||||
<div class="card-title">{{ item.title }}</div>
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
<div class="card-change" :class="item.change > 0 ? 'positive' : 'negative'">
|
||||
{{ item.change > 0 ? '↑' : '↓' }} {{ Math.abs(item.change) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="volume-chart">
|
||||
<div ref="volumeChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格趋势和区域分布 -->
|
||||
<div class="price-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">价格趋势和区域分布</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="price-content">
|
||||
<div class="trend-chart card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">价格趋势</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="trendChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="distribution-chart card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">区域价格分布</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="distributionChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易类型分析 -->
|
||||
<div class="type-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">交易类型分析</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="type-content">
|
||||
<div class="type-chart">
|
||||
<div ref="typeChart" class="chart-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易排行榜 -->
|
||||
<div class="ranking-section card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">交易排行榜</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="ranking-content">
|
||||
<div class="farm-ranking card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">热门牧场</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a-table :dataSource="farmRankingData" :columns="farmRankingColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="trader-ranking card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">活跃交易员</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a-table :dataSource="traderRankingData" :columns="traderRankingColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'Trade',
|
||||
setup() {
|
||||
const volumeData = ref([]);
|
||||
const farmRankingData = ref([]);
|
||||
const traderRankingData = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const volumeChart = ref(null);
|
||||
const trendChart = ref(null);
|
||||
const distributionChart = ref(null);
|
||||
const typeChart = ref(null);
|
||||
|
||||
let volumeChartInstance = null;
|
||||
let trendChartInstance = null;
|
||||
let distributionChartInstance = null;
|
||||
let typeChartInstance = null;
|
||||
|
||||
// 交易量数据
|
||||
volumeData.value = [
|
||||
{ title: '今日交易量', value: '1,245头', change: 5.2 },
|
||||
{ title: '本月交易量', value: '38,650头', change: 8.7 },
|
||||
{ title: '年度交易量', value: '420,860头', change: 12.3 }
|
||||
];
|
||||
|
||||
// 热门牧场排行榜列定义
|
||||
const farmRankingColumns = ref([
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank' },
|
||||
{ title: '牧场名称', dataIndex: 'farm', key: 'farm' },
|
||||
{ title: '交易量', dataIndex: 'volume', key: 'volume' },
|
||||
{ title: '交易额', dataIndex: 'amount', key: 'amount' },
|
||||
]);
|
||||
|
||||
// 热门牧场排行榜数据
|
||||
farmRankingData.value = [
|
||||
{ key: '1', rank: '1', farm: '锡市牧场A', volume: '2,450头', amount: '¥1,245万' },
|
||||
{ key: '2', rank: '2', farm: '东乌旗牧场B', volume: '1,980头', amount: '¥980万' },
|
||||
{ key: '3', rank: '3', farm: '西乌旗牧场C', volume: '1,650头', amount: '¥820万' },
|
||||
{ key: '4', rank: '4', farm: '镶黄旗牧场D', volume: '1,320头', amount: '¥650万' },
|
||||
{ key: '5', rank: '5', farm: '正蓝旗牧场E', volume: '1,150头', amount: '¥580万' },
|
||||
];
|
||||
|
||||
// 活跃交易员排行榜列定义
|
||||
const traderRankingColumns = ref([
|
||||
{ title: '排名', dataIndex: 'rank', key: 'rank' },
|
||||
{ title: '交易员', dataIndex: 'trader', key: 'trader' },
|
||||
{ title: '交易数', dataIndex: 'count', key: 'count' },
|
||||
{ title: '交易额', dataIndex: 'amount', key: 'amount' },
|
||||
]);
|
||||
|
||||
// 活跃交易员排行榜数据
|
||||
traderRankingData.value = [
|
||||
{ key: '1', rank: '1', trader: '张三', count: '126笔', amount: '¥860万' },
|
||||
{ key: '2', rank: '2', trader: '李四', count: '98笔', amount: '¥620万' },
|
||||
{ key: '3', rank: '3', trader: '王五', count: '85笔', amount: '¥540万' },
|
||||
{ key: '4', rank: '4', trader: '赵六', count: '72笔', amount: '¥420万' },
|
||||
{ key: '5', rank: '5', trader: '孙七', count: '65笔', amount: '¥380万' },
|
||||
];
|
||||
|
||||
// 初始化交易量图表
|
||||
const initVolumeChart = () => {
|
||||
if (volumeChart.value) {
|
||||
volumeChartInstance = echarts.init(volumeChart.value);
|
||||
volumeChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [32000, 35000, 38000, 40000, 42000, 45000],
|
||||
type: 'bar',
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化价格趋势图表
|
||||
const initTrendChart = () => {
|
||||
if (trendChart.value) {
|
||||
trendChartInstance = echarts.init(trendChart.value);
|
||||
trendChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [28000, 29500, 31000, 30500, 32000, 33500],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: { color: '#2196F3' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化区域分布图表
|
||||
const initDistributionChart = () => {
|
||||
if (distributionChart.value) {
|
||||
distributionChartInstance = echarts.init(distributionChart.value);
|
||||
distributionChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '0'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: [
|
||||
{ value: 35, name: '锡市' },
|
||||
{ value: 25, name: '东乌旗' },
|
||||
{ value: 20, name: '西乌旗' },
|
||||
{ value: 10, name: '镶黄旗' },
|
||||
{ value: 10, name: '其他' }
|
||||
],
|
||||
itemStyle: {
|
||||
color: function(params) {
|
||||
const colorList = ['#4CAF50', '#2196F3', '#FF9800', '#f44336', '#9C27B0'];
|
||||
return colorList[params.dataIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化交易类型图表
|
||||
const initTypeChart = () => {
|
||||
if (typeChart.value) {
|
||||
typeChartInstance = echarts.init(typeChart.value);
|
||||
typeChartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['活牛交易', '牛肉制品']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '活牛交易',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
data: [28000, 30000, 32000, 31000, 33000, 35000],
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
},
|
||||
{
|
||||
name: '牛肉制品',
|
||||
type: 'bar',
|
||||
stack: '总量',
|
||||
data: [4000, 5000, 6000, 5500, 7000, 8000],
|
||||
itemStyle: { color: '#2196F3' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (volumeChartInstance) volumeChartInstance.resize();
|
||||
if (trendChartInstance) trendChartInstance.resize();
|
||||
if (distributionChartInstance) distributionChartInstance.resize();
|
||||
if (typeChartInstance) typeChartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = false;
|
||||
initVolumeChart();
|
||||
initTrendChart();
|
||||
initDistributionChart();
|
||||
initTypeChart();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (volumeChartInstance) volumeChartInstance.dispose();
|
||||
if (trendChartInstance) trendChartInstance.dispose();
|
||||
if (distributionChartInstance) distributionChartInstance.dispose();
|
||||
if (typeChartInstance) typeChartInstance.dispose();
|
||||
});
|
||||
|
||||
return {
|
||||
volumeData,
|
||||
farmRankingData,
|
||||
traderRankingData,
|
||||
farmRankingColumns,
|
||||
traderRankingColumns,
|
||||
loading,
|
||||
error,
|
||||
volumeChart,
|
||||
trendChart,
|
||||
distributionChart,
|
||||
typeChart
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trade-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #0f2027, #20555d, #2c5364);
|
||||
}
|
||||
|
||||
.trade-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.volume-section,
|
||||
.price-section,
|
||||
.type-section,
|
||||
.ranking-section {
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.volume-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.volume-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.volume-card {
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-change {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-change.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.card-change.negative {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.price-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.type-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(76, 175, 80, 0.3);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--danger-color);
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.trade-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.volume-content,
|
||||
.price-content,
|
||||
.ranking-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
469
admin-system/dashboard/src/views/Transport.vue
Normal file
469
admin-system/dashboard/src/views/Transport.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div class="transport-container">
|
||||
<h1>运输跟踪</h1>
|
||||
<div v-if="loading" class="loading-indicator">数据加载中...</div>
|
||||
<div v-if="error" class="error-message">数据加载失败,请稍后重试。</div>
|
||||
<div v-if="!loading && !error" class="transport-content">
|
||||
<!-- 运输路线地图 -->
|
||||
<div class="map-section">
|
||||
<h3>运输路线</h3>
|
||||
<div class="map-container">
|
||||
<div ref="transportMap" class="map-placeholder" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输车辆列表 -->
|
||||
<div class="vehicles-section">
|
||||
<h3>运输车辆</h3>
|
||||
<div class="vehicles-list">
|
||||
<a-table :dataSource="vehiclesData" :columns="vehicleColumns" :pagination="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输异常告警 -->
|
||||
<div class="alerts-section">
|
||||
<h3>运输异常告警</h3>
|
||||
<div class="alerts-list">
|
||||
<div
|
||||
v-for="(alert, index) in transportAlerts"
|
||||
:key="index"
|
||||
class="alert-item"
|
||||
:class="alert.level"
|
||||
>
|
||||
<div class="alert-icon">
|
||||
<span v-if="alert.level === 'high'">⚠️</span>
|
||||
<span v-else-if="alert.level === 'medium'">⚠️</span>
|
||||
<span v-else>ℹ️</span>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-title">{{ alert.title }}</div>
|
||||
<div class="alert-desc">{{ alert.desc }}</div>
|
||||
<div class="alert-time">{{ alert.time }}</div>
|
||||
</div>
|
||||
<div class="alert-status">{{ alert.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输统计 -->
|
||||
<div class="stats-section">
|
||||
<h3>运输统计</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="(stat, index) in transportStats" :key="index">
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-desc">{{ stat.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输效率分析 -->
|
||||
<div class="efficiency-section">
|
||||
<h3>运输效率分析</h3>
|
||||
<div class="chart-container">
|
||||
<div ref="efficiencyChart" class="chart-placeholder" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import axios from 'axios';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'Transport',
|
||||
setup() {
|
||||
const vehiclesData = ref([]);
|
||||
const transportStats = ref([]);
|
||||
const transportAlerts = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const transportMap = ref(null);
|
||||
const efficiencyChart = ref(null);
|
||||
|
||||
let mapInstance = null;
|
||||
let chartInstance = null;
|
||||
|
||||
// 车辆表格列定义
|
||||
const vehicleColumns = ref([
|
||||
{ title: '车牌号', dataIndex: 'plateNumber', key: 'plateNumber' },
|
||||
{ title: '司机', dataIndex: 'driver', key: 'driver' },
|
||||
{ title: '出发地', dataIndex: 'origin', key: 'origin' },
|
||||
{ title: '目的地', dataIndex: 'destination', key: 'destination' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '预计到达', dataIndex: 'eta', key: 'eta' },
|
||||
]);
|
||||
|
||||
// 运输统计数据
|
||||
transportStats.value = [
|
||||
{ title: '今日运输', value: '24车次', desc: '较昨日 +5%' },
|
||||
{ title: '在途运输', value: '8车次', desc: '正常运输中' },
|
||||
{ title: '平均时效', value: '4.2小时', desc: '较上周 -0.3小时' },
|
||||
{ title: '异常运输', value: '1车次', desc: '需关注处理' },
|
||||
];
|
||||
|
||||
// 运输异常告警数据
|
||||
transportAlerts.value = [
|
||||
{
|
||||
level: 'high',
|
||||
title: '运输延误',
|
||||
desc: '蒙H54321从西乌旗牧场C到上海加工中心已延误2小时',
|
||||
time: '2023-08-20 14:30',
|
||||
status: '处理中'
|
||||
},
|
||||
{
|
||||
level: 'medium',
|
||||
title: '路线偏离',
|
||||
desc: '蒙H12345从锡市牧场A到北京屠宰场偏离预定路线5公里',
|
||||
time: '2023-08-20 13:45',
|
||||
status: '已确认'
|
||||
},
|
||||
{
|
||||
level: 'low',
|
||||
title: '温度异常',
|
||||
desc: '蒙H67890冷藏车厢温度超过标准值0.5℃',
|
||||
time: '2023-08-20 12:15',
|
||||
status: '已处理'
|
||||
}
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
// 模拟获取运输数据
|
||||
vehiclesData.value = [
|
||||
{
|
||||
key: '1',
|
||||
plateNumber: '蒙H12345',
|
||||
driver: '张三',
|
||||
origin: '锡市牧场A',
|
||||
destination: '北京屠宰场',
|
||||
status: '运输中',
|
||||
eta: '2023-08-20 15:30'
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
plateNumber: '蒙H67890',
|
||||
driver: '李四',
|
||||
origin: '东乌旗牧场B',
|
||||
destination: '呼和浩特产中心',
|
||||
status: '已完成',
|
||||
eta: '2023-08-20 14:15'
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
plateNumber: '蒙H54321',
|
||||
driver: '王五',
|
||||
origin: '西乌旗牧场C',
|
||||
destination: '上海加工中心',
|
||||
status: '延误',
|
||||
eta: '2023-08-20 16:45'
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
error.value = true;
|
||||
console.error('获取运输数据失败:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化地图
|
||||
const initMap = () => {
|
||||
if (transportMap.value) {
|
||||
mapInstance = echarts.init(transportMap.value);
|
||||
mapInstance.setOption({
|
||||
title: {
|
||||
text: '运输路线图',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: false,
|
||||
zoom: 1.2,
|
||||
center: [116.0, 44.0], // 锡林郭勒盟位置
|
||||
itemStyle: {
|
||||
areaColor: '#e7e8ea'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'lines',
|
||||
coordinateSystem: 'geo',
|
||||
lineStyle: {
|
||||
color: '#4CAF50',
|
||||
width: 2,
|
||||
opacity: 0.8,
|
||||
curveness: 0.2
|
||||
},
|
||||
data: [
|
||||
{
|
||||
coords: [[116.08, 43.95], [116.40, 39.90]] // 锡林浩特到北京
|
||||
},
|
||||
{
|
||||
coords: [[116.08, 43.95], [111.65, 40.82]] // 锡林浩特到呼和浩特
|
||||
},
|
||||
{
|
||||
coords: [[116.08, 43.95], [121.47, 31.23]] // 锡林浩特到上海
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'effectScatter',
|
||||
coordinateSystem: 'geo',
|
||||
symbolSize: 10,
|
||||
data: [
|
||||
{ name: '锡林浩特', value: [116.08, 43.95] },
|
||||
{ name: '北京', value: [116.40, 39.90] },
|
||||
{ name: '呼和浩特', value: [111.65, 40.82] },
|
||||
{ name: '上海', value: [121.47, 31.23] }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化效率分析图表
|
||||
const initEfficiencyChart = () => {
|
||||
if (efficiencyChart.value) {
|
||||
chartInstance = echarts.init(efficiencyChart.value);
|
||||
chartInstance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['实际时效', '计划时效']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '小时'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际时效',
|
||||
type: 'bar',
|
||||
data: [4.2, 3.8, 4.0, 4.5, 3.9, 4.1],
|
||||
itemStyle: { color: '#4CAF50' }
|
||||
},
|
||||
{
|
||||
name: '计划时效',
|
||||
type: 'bar',
|
||||
data: [4.0, 4.0, 4.0, 4.0, 4.0, 4.0],
|
||||
itemStyle: { color: '#2196F3' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
const resizeCharts = () => {
|
||||
if (mapInstance) {
|
||||
mapInstance.resize();
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
initMap();
|
||||
initEfficiencyChart();
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
if (mapInstance) {
|
||||
mapInstance.dispose();
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
vehiclesData,
|
||||
transportStats,
|
||||
transportAlerts,
|
||||
vehicleColumns,
|
||||
loading,
|
||||
error,
|
||||
transportMap,
|
||||
efficiencyChart
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transport-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.transport-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.map-section,
|
||||
.vehicles-section,
|
||||
.alerts-section,
|
||||
.stats-section,
|
||||
.efficiency-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.map-section h3,
|
||||
.vehicles-section h3,
|
||||
.alerts-section h3,
|
||||
.stats-section h3,
|
||||
.efficiency-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alerts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.alert-item.high {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.alert-item.medium {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.alert-item.low {
|
||||
background-color: #e3f2fd;
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-status {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.loading-indicator,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.transport-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
admin-system/dashboard/vite.config.js
Normal file
24
admin-system/dashboard/vite.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3007,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
13
admin-system/farming-management/index.html
Normal file
13
admin-system/farming-management/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
admin-system/farming-management/package.json
Normal file
19
admin-system/farming-management/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "farming-management",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.45"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"description": "锡林郭勒盟智慧养殖管理系统"
|
||||
}
|
||||
17
admin-system/farming-management/src/App.vue
Normal file
17
admin-system/farming-management/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/farming-management/src/main.js
Normal file
15
admin-system/farming-management/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/farming-management/src/styles/global.css
Normal file
97
admin-system/farming-management/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/farming-management/vite.config.js
Normal file
28
admin-system/farming-management/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
13
admin-system/government-platform/index.html
Normal file
13
admin-system/government-platform/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
admin-system/government-platform/package.json
Normal file
23
admin-system/government-platform/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "government-platform",
|
||||
"version": "1.0.0",
|
||||
"description": "锡林郭勒盟智慧养殖管理系统",
|
||||
"author": "xlxumu team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"ant-design-vue": "^3.0.0",
|
||||
"pinia": "^2.0.0",
|
||||
"echarts": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
}
|
||||
17
admin-system/government-platform/src/App.vue
Normal file
17
admin-system/government-platform/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/government-platform/src/main.js
Normal file
15
admin-system/government-platform/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/government-platform/src/styles/global.css
Normal file
97
admin-system/government-platform/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/government-platform/vite.config.js
Normal file
28
admin-system/government-platform/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3004,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
13
admin-system/insurance-supervision/index.html
Normal file
13
admin-system/insurance-supervision/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
admin-system/insurance-supervision/package.json
Normal file
25
admin-system/insurance-supervision/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "insurance-supervision",
|
||||
"version": "1.0.0",
|
||||
"description": "锡林郭勒盟智慧养殖管理系统",
|
||||
"author": "xlxumu team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"ant-design-vue": "^3.0.0",
|
||||
"pinia": "^2.0.0",
|
||||
"echarts": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"vite": "^3.0.0",
|
||||
"typescript": "^4.0.0",
|
||||
"@types/node": "^18.0.0"
|
||||
}
|
||||
}
|
||||
17
admin-system/insurance-supervision/src/App.vue
Normal file
17
admin-system/insurance-supervision/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/insurance-supervision/src/main.js
Normal file
15
admin-system/insurance-supervision/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/insurance-supervision/src/styles/global.css
Normal file
97
admin-system/insurance-supervision/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/insurance-supervision/vite.config.js
Normal file
28
admin-system/insurance-supervision/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3003,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
13
admin-system/mall-management/index.html
Normal file
13
admin-system/mall-management/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>养殖管理系统 - 锡林郭勒盟智慧养殖数字化管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
admin-system/mall-management/package.json
Normal file
23
admin-system/mall-management/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "mall-management",
|
||||
"version": "1.0.0",
|
||||
"description": "锡林郭勒盟智慧养殖管理系统",
|
||||
"author": "xlxumu team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"ant-design-vue": "^3.0.0",
|
||||
"pinia": "^2.0.0",
|
||||
"echarts": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"vite": "^3.0.0"
|
||||
}
|
||||
}
|
||||
17
admin-system/mall-management/src/App.vue
Normal file
17
admin-system/mall-management/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
15
admin-system/mall-management/src/main.js
Normal file
15
admin-system/mall-management/src/main.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import './styles/global.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
97
admin-system/mall-management/src/styles/global.css
Normal file
97
admin-system/mall-management/src/styles/global.css
Normal file
@@ -0,0 +1,97 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--secondary-color: #388E3C;
|
||||
--accent-color: #FF9800;
|
||||
--light-color: #f5f5f5;
|
||||
--dark-color: #333;
|
||||
--text-color: #333;
|
||||
--border-radius: 4px;
|
||||
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #F57C00;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
28
admin-system/mall-management/vite.config.js
Normal file
28
admin-system/mall-management/vite.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3006,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user