更新项目文件结构,统一文档风格

This commit is contained in:
ylweng
2025-09-01 02:45:51 +08:00
parent 5ea25b7bac
commit 216cf80eab
95 changed files with 210 additions and 75446 deletions

View 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>

View 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"
}
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})

View 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>

View 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"
}
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})

View 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

File diff suppressed because it is too large Load Diff

View 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": "锡林郭勒盟智慧养殖产业大屏可视化系统"
}

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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')

View 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

View 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 {};
}
};

View 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;
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
```
```

View 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>

View 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>

View 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>

View 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>

View 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/, '')
}
}
}
})

View 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>

View 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": "锡林郭勒盟智慧养殖管理系统"
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})

View 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>

View 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"
}
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})

View 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>

View 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"
}
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})

View 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>

View 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"
}
}

View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100vh;
}
</style>

View 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')

View 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;
}
}

View 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'
}
})