|
|
@@ -1,80 +1,97 @@
|
|
|
<template>
|
|
|
- <div>
|
|
|
- <!-- 顶部信息卡片区域 -->
|
|
|
- <div class="dashboard">
|
|
|
- <!-- 合并的统计与分布卡片 -->
|
|
|
- <div class="dashboard-card combined-card">
|
|
|
- <div class="card-title">统计与分布</div>
|
|
|
- <div class="combined-content">
|
|
|
- <!-- 左侧:单元统计 -->
|
|
|
- <div class="statistics-section">
|
|
|
- <h3>单元统计</h3>
|
|
|
- <div class="statistics">
|
|
|
- <div class="stat-item">
|
|
|
- <div class="stat-value">{{ statistics.total_units }}</div>
|
|
|
- <div class="stat-label">总单元数</div>
|
|
|
- </div>
|
|
|
- <div class="stat-item">
|
|
|
- <div class="stat-value">{{ statistics.units_with_data }}</div>
|
|
|
- <div class="stat-label">有数据单元</div>
|
|
|
- </div>
|
|
|
- <!-- 仅当无数据单元数不为0时显示 -->
|
|
|
- <div class="stat-item" v-if="statistics.units_without_data !== 0">
|
|
|
- <div class="stat-value">{{ statistics.units_without_data }}</div>
|
|
|
- <div class="stat-label">无数据单元</div>
|
|
|
+ <div class="agricultural-input-management">
|
|
|
+ <div class="page-container">
|
|
|
+ <!-- 顶部信息卡片区域 -->
|
|
|
+ <div class="dashboard">
|
|
|
+ <!-- 合并的统计与分布卡片 -->
|
|
|
+ <div class="dashboard-card combined-card">
|
|
|
+ <div class="card-title">统计与分布</div>
|
|
|
+ <div class="combined-content">
|
|
|
+ <!-- 左侧:单元统计 -->
|
|
|
+ <div class="statistics-section">
|
|
|
+ <h3>单元统计</h3>
|
|
|
+ <div class="statistics">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ statistics.total_units }}</div>
|
|
|
+ <div class="stat-label">总单元数</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ statistics.units_with_data }}</div>
|
|
|
+ <div class="stat-label">有数据单元</div>
|
|
|
+ </div>
|
|
|
+ <!-- 仅当无数据单元数不为0时显示 -->
|
|
|
+ <div class="stat-item" v-if="statistics.units_without_data !== 0">
|
|
|
+ <div class="stat-value">{{ statistics.units_without_data }}</div>
|
|
|
+ <div class="stat-label">无数据单元</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧:分类分布 -->
|
|
|
- <div class="distribution-section">
|
|
|
- <h3>分类分布</h3>
|
|
|
- <div class="distribution">
|
|
|
- <div v-for="(count, category) in statistics.category_distribution" :key="category"
|
|
|
- class="category-dist" :style="{ backgroundColor: categoryColors[category] }">
|
|
|
- <div class="dist-category">{{ category }}</div>
|
|
|
- <div class="dist-count">{{ count }}</div>
|
|
|
+
|
|
|
+ <!-- 右侧:分类分布 -->
|
|
|
+ <div class="distribution-section">
|
|
|
+ <h3>分类分布</h3>
|
|
|
+ <div class="distribution">
|
|
|
+ <div v-for="(count, category) in statistics.category_distribution" :key="category"
|
|
|
+ class="category-dist" :style="{ backgroundColor: categoryColors[category] }">
|
|
|
+ <div class="dist-category">{{ category }}</div>
|
|
|
+ <div class="dist-count">{{ count }}</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 饼图卡片 -->
|
|
|
+ <div class="dashboard-card chart-card">
|
|
|
+ <div class="card-title">点位分类分布</div>
|
|
|
+ <div ref="pointPieChart" class="chart"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 饼图卡片 -->
|
|
|
- <div class="dashboard-card chart-card">
|
|
|
- <div class="card-title">点位分类分布</div>
|
|
|
- <div ref="pointPieChart" class="chart"></div>
|
|
|
+ <!-- 地图区域 -->
|
|
|
+ <div class="map-area">
|
|
|
+ <div class="map-title">土壤分类分布地图</div>
|
|
|
+ <div class="map-container">
|
|
|
+ <div v-if="loadingMap" class="loading-container">
|
|
|
+ <el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
+ <span>地图加载中...</span>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ <div v-else-if="mapError" class="error-container">
|
|
|
+ <el-icon class="error-icon"><Warning /></el-icon>
|
|
|
+ <span>{{ mapError }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="image-container">
|
|
|
+ <img :src="mapImageUrl" class="map-image"></img>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 地图区域 -->
|
|
|
- <div class="map-area">
|
|
|
- <div class="map-title">土壤分类分布地图</div>
|
|
|
- <div ref="mapContainer" class="map-container"></div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import * as echarts from 'echarts';
|
|
|
-import { api8000 } from '@/utils/request'; // 导入api8000
|
|
|
+import { api8000 } from '@/utils/request';
|
|
|
+import {
|
|
|
+ Loading, Picture, Warning
|
|
|
+} from '@element-plus/icons-vue';
|
|
|
|
|
|
// 分类颜色配置
|
|
|
const categoryColors = {
|
|
|
- '优先保护类': 'rgba(255, 214, 0, 0.7)', // #FFD600 转换为RGBA
|
|
|
- '安全利用类': 'rgba(0, 200, 83, 0.7)', // #00C853 转换为RGBA
|
|
|
- '严格管控类': 'rgba(213, 0, 0, 0.7)' // #D50000 转换为RGBA
|
|
|
+ '优先保护类': 'rgba(255, 214, 0, 0.7)',
|
|
|
+ '安全利用类': 'rgba(0, 200, 83, 0.7)',
|
|
|
+ '严格管控类': 'rgba(213, 0, 0, 0.7)'
|
|
|
};
|
|
|
|
|
|
export default {
|
|
|
- name: 'CategoryMap',
|
|
|
+ name: 'SoilCategoryMap',
|
|
|
+ components: {
|
|
|
+ Loading, Picture, Warning
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
- map: null,
|
|
|
- geoJSONLayer: null,
|
|
|
- shaoguanBoundaryLayer:null,
|
|
|
- multiPolygon: null,
|
|
|
- categoryColors,
|
|
|
statistics: {
|
|
|
total_units: 0,
|
|
|
units_with_data: 0,
|
|
|
@@ -90,225 +107,68 @@ export default {
|
|
|
'严格管控类': 0
|
|
|
}
|
|
|
},
|
|
|
- groupingData: [] // 存储接口数据
|
|
|
+ mapImageUrl: '',
|
|
|
+ loadingMap: true,
|
|
|
+ mapError: null,
|
|
|
+ pieChart: null,
|
|
|
+ categoryColors
|
|
|
};
|
|
|
},
|
|
|
+
|
|
|
async mounted() {
|
|
|
- // 获取分类数据
|
|
|
- await this.fetchGroupingData();
|
|
|
-
|
|
|
- // 初始化地图
|
|
|
- await this.initMap();
|
|
|
-
|
|
|
- // 初始化点位分布饼图
|
|
|
+ await this.fetchStatistics();
|
|
|
+ await this.fetchMapImage();
|
|
|
this.initPointPieChart();
|
|
|
},
|
|
|
+
|
|
|
+ beforeUnmount() {
|
|
|
+ if (this.mapImageUrl) {
|
|
|
+ URL.revokeObjectURL(this.mapImageUrl);
|
|
|
+ }
|
|
|
+ if (this.pieChart) {
|
|
|
+ this.pieChart.dispose();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
methods: {
|
|
|
- // 获取分类数据
|
|
|
- async fetchGroupingData() {
|
|
|
+ // 从API获取统计数据
|
|
|
+ async fetchStatistics() {
|
|
|
try {
|
|
|
- const response = await api8000.get(`/api/unit-grouping/h_xtfx`);
|
|
|
+ const response = await api8000.get('/api/unit-grouping/statistics');
|
|
|
|
|
|
if (response.data.success) {
|
|
|
- this.groupingData = response.data.data;
|
|
|
this.statistics = response.data.statistics;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('获取分类数据失败:', error);
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 初始化地图
|
|
|
- async initMap() {
|
|
|
- // 加载TMap SDK
|
|
|
- const TMap = await this.loadSDK();
|
|
|
-
|
|
|
- // 创建地图实例
|
|
|
- this.map = new TMap.Map(this.$refs.mapContainer, {
|
|
|
- center: new TMap.LatLng(24.81088, 113.59762),
|
|
|
- zoom: 12,
|
|
|
- mapStyleId: 'style1'
|
|
|
- });
|
|
|
-
|
|
|
- // 加载GeoJSON数据
|
|
|
- const geojsonData = await this.loadGeoJSON('/data/单元格.geojson');
|
|
|
-
|
|
|
- // 初始化GeoJSON图层 - 传递TMap对象
|
|
|
- this.initMapWithGeoJSON(geojsonData, TMap);
|
|
|
-
|
|
|
- //加载并初始化韶关边界图层
|
|
|
- const shaoguanBoundaryGeojson = await this.fetchShaoguanBoundary();
|
|
|
- this.initShaoguanBoundaryLayer(shaoguanBoundaryGeojson,TMap);
|
|
|
- },
|
|
|
-
|
|
|
- // 加载SDK
|
|
|
- loadSDK() {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (window.TMap) return resolve(window.TMap);
|
|
|
-
|
|
|
- const script = document.createElement('script');
|
|
|
- script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${import.meta.env.VITE_TMAP_KEY}&callback=initTMap`;
|
|
|
-
|
|
|
- window.initTMap = () => {
|
|
|
- if (!window.TMap) {
|
|
|
- reject(new Error('TMap SDK 加载失败'));
|
|
|
- return;
|
|
|
- }
|
|
|
- resolve(window.TMap);
|
|
|
- };
|
|
|
-
|
|
|
- script.onerror = (err) => {
|
|
|
- reject(new Error('加载地图SDK失败'));
|
|
|
- document.head.removeChild(script);
|
|
|
- };
|
|
|
-
|
|
|
- document.head.appendChild(script);
|
|
|
- });
|
|
|
- },
|
|
|
-
|
|
|
- // 新增:获取韶关市边界GeoJSON数据
|
|
|
- async fetchShaoguanBoundary() {
|
|
|
- try {
|
|
|
- // 替换为用户实际的韶关市边界接口地址
|
|
|
- const boundaryUrl = "http://localhost:8000/api/vector/boundary?table_name=counties&field_name=city_name&field_value=%E9%9F%B6%E5%85%B3%E5%B8%82";
|
|
|
- const response = await fetch(boundaryUrl);
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`获取韶关市边界失败: ${response.statusText}`);
|
|
|
+ console.error('获取统计信息失败:', error);
|
|
|
}
|
|
|
-
|
|
|
- const boundaryGeoJSON = await response.json();
|
|
|
- // 验证GeoJSON格式(必须是FeatureCollection,geometry为Polygon/MultiPolygon)
|
|
|
- if (boundaryGeoJSON.type !== "FeatureCollection") {
|
|
|
- throw new Error("韶关市边界数据不是有效的FeatureCollection");
|
|
|
- }
|
|
|
- return boundaryGeoJSON;
|
|
|
- } catch (error) {
|
|
|
- console.error("韶关市边界数据加载失败:", error);
|
|
|
- return { type: "FeatureCollection", features: [] }; // 返回空数据避免地图崩溃
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 新增:初始化韶关市边界图层
|
|
|
- initShaoguanBoundaryLayer(boundaryGeoJSON, TMap) {
|
|
|
- try {
|
|
|
- if (!boundaryGeoJSON.features.length) {
|
|
|
- console.warn("韶关市边界数据为空,不渲染边界");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const lightEarthYellow = "rgba(245, 222, 179, 0.4)";
|
|
|
-
|
|
|
- // 创建边界图层(独立于单元格图层)
|
|
|
- this.shaoguanBoundaryLayer = new TMap.vector.GeoJSONLayer({
|
|
|
- map: this.map, // 绑定到现有地图实例
|
|
|
- data: boundaryGeoJSON, // 边界GeoJSON数据
|
|
|
- // 边界样式配置:突出边框,透明填充(不遮挡下方单元格)
|
|
|
- polygonStyle: new TMap.PolygonStyle({
|
|
|
- color: lightEarthYellow, // 填充色:透明
|
|
|
- showBorder: true, // 显示边框
|
|
|
- borderColor: '#000000', // 边框颜色:蓝色(醒目)
|
|
|
- borderWidth: 3 // 边框宽度:3px(确保清晰)
|
|
|
- })
|
|
|
- });
|
|
|
-
|
|
|
- // 确保边界图层在最上层(覆盖单元格,不遮挡交互)
|
|
|
- this.shaoguanBoundaryLayer.setZIndex(1);
|
|
|
- } catch (error) {
|
|
|
- console.error("初始化韶关市边界图层失败:", error);
|
|
|
- }
|
|
|
},
|
|
|
|
|
|
- // 加载GeoJSON数据
|
|
|
- async loadGeoJSON(url) {
|
|
|
+ // 从API获取地图图片
|
|
|
+ async fetchMapImage() {
|
|
|
try {
|
|
|
- const response = await fetch(url);
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`加载GeoJSON失败: ${response.statusText}`);
|
|
|
- }
|
|
|
- return await response.json();
|
|
|
- } catch (error) {
|
|
|
- console.error('加载GeoJSON数据失败:', error);
|
|
|
- return { type: 'FeatureCollection', features: [] };
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 初始化GeoJSON图层 - 使用MultiPolygon的setStyles方法
|
|
|
- initMapWithGeoJSON(geojsonData, TMap) {
|
|
|
- try {
|
|
|
- // 创建分类映射表
|
|
|
- const categoryMap = {};
|
|
|
- this.groupingData.forEach(item => {
|
|
|
- categoryMap[item.OBJECTID] = item.h_xtfx;
|
|
|
- });
|
|
|
-
|
|
|
- // 处理GeoJSON特征
|
|
|
- geojsonData.features.forEach(feature => {
|
|
|
- const objectId = feature.properties.OBJECTID;
|
|
|
- const category = categoryMap[objectId];
|
|
|
-
|
|
|
- // 添加分类属性
|
|
|
- feature.properties.category = category;
|
|
|
- });
|
|
|
-
|
|
|
- // 检查TMap对象是否有效
|
|
|
- if (!TMap || !TMap.PolygonStyle) {
|
|
|
- throw new Error('TMap对象无效,缺少PolygonStyle');
|
|
|
- }
|
|
|
-
|
|
|
- // 创建GeoJSON图层
|
|
|
- this.geoJSONLayer = new TMap.vector.GeoJSONLayer({
|
|
|
- map: this.map,
|
|
|
- data: geojsonData,
|
|
|
- polygonStyle: new TMap.PolygonStyle({
|
|
|
- color: 'rgba(0,0,0,0)',
|
|
|
- showBorder: false
|
|
|
- })
|
|
|
- });
|
|
|
-
|
|
|
+ this.loadingMap = true;
|
|
|
+ this.mapError = null;
|
|
|
|
|
|
- // 获取多边形覆盖层
|
|
|
- this.multiPolygon = this.geoJSONLayer.getGeometryOverlay('polygon');
|
|
|
- this.multiPolygon.setMap(this.map);
|
|
|
- const polygons = this.multiPolygon.getGeometries();
|
|
|
-
|
|
|
- // 创建样式映射
|
|
|
- const styles = {};
|
|
|
-
|
|
|
- // 遍历所有多边形,为每个多边形设置样式和唯一ID
|
|
|
- // 遍历所有多边形,为每个多边形设置样式
|
|
|
- polygons.forEach((polygon) => {
|
|
|
- // 直接访问properties属性
|
|
|
- const properties = polygon.properties;
|
|
|
- const category = properties.category;
|
|
|
-
|
|
|
- // 使用多边形的id作为样式ID的键
|
|
|
- const styleId = `style_${polygon.id}`;
|
|
|
-
|
|
|
- // 根据分类设置颜色
|
|
|
- const color = category ? this.categoryColors[category] : '#CCCCCC';
|
|
|
-
|
|
|
- // 添加样式到映射表
|
|
|
- styles[styleId] = new TMap.PolygonStyle({
|
|
|
- color: color,
|
|
|
- showBorder: true,
|
|
|
- borderColor: '#000000',
|
|
|
- borderWidth: 2
|
|
|
- });
|
|
|
-
|
|
|
- // 关键修复:为每个多边形设置样式ID(正确方式)
|
|
|
- polygon.styleId = styleId; // 直接设置属性
|
|
|
+ // 调用visualize接口获取地图图片
|
|
|
+ const response = await api8000.get('/api/unit-grouping/visualize', {
|
|
|
+ params: {
|
|
|
+ area: '乐昌市',
|
|
|
+ level: 'county',
|
|
|
+ colormap: 'viridis'
|
|
|
+ },
|
|
|
+ responseType: 'blob'
|
|
|
});
|
|
|
- // 使用setStyles方法一次性设置所有样式
|
|
|
- this.multiPolygon.setStyles(styles);
|
|
|
|
|
|
- // 更新几何体以应用新样式
|
|
|
- this.multiPolygon.updateGeometries(polygons);
|
|
|
+ // 创建图片Blob URL
|
|
|
+ const blob = new Blob([response.data], { type: 'image/jpeg' });
|
|
|
+ this.mapImageUrl = URL.createObjectURL(blob);
|
|
|
|
|
|
- this.geoJSONLayer.setZIndex(100);
|
|
|
-
|
|
|
} catch (error) {
|
|
|
- console.error('初始化GeoJSON图层失败:', error);
|
|
|
+ console.error('获取地图图片失败:', error);
|
|
|
+ this.mapError = error.response?.data?.detail || '获取地图失败';
|
|
|
+ } finally {
|
|
|
+ this.loadingMap = false;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
@@ -317,10 +177,10 @@ export default {
|
|
|
const chartDom = this.$refs.pointPieChart;
|
|
|
if (!chartDom) return;
|
|
|
|
|
|
- const chart = echarts.init(chartDom);
|
|
|
+ this.pieChart = echarts.init(chartDom);
|
|
|
|
|
|
// 准备饼图数据
|
|
|
- const pieData = Object.entries(this.statistics.category_distribution).map(([name, value]) => ({
|
|
|
+ const pieData = Object.entries(this.statistics.point_distribution).map(([name, value]) => ({
|
|
|
name,
|
|
|
value,
|
|
|
itemStyle: { color: this.categoryColors[name] || '#CCCCCC' }
|
|
|
@@ -378,11 +238,11 @@ export default {
|
|
|
]
|
|
|
};
|
|
|
|
|
|
- chart.setOption(option);
|
|
|
+ this.pieChart.setOption(option);
|
|
|
|
|
|
// 响应式调整
|
|
|
window.addEventListener('resize', () => {
|
|
|
- chart.resize();
|
|
|
+ this.pieChart.resize();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
@@ -390,11 +250,35 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
+/* 整体布局优化 */
|
|
|
+.agricultural-input-management {
|
|
|
+ padding: 20px;
|
|
|
+ background: linear-gradient(
|
|
|
+ 135deg,
|
|
|
+ rgba(230, 247, 255, 0.7) 0%,
|
|
|
+ rgba(240, 248, 255, 0.7) 100%
|
|
|
+ );
|
|
|
+ min-height: 100vh;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.page-container {
|
|
|
+ padding: 20px;
|
|
|
+ /* 添加70%透明度的渐变背景 */
|
|
|
+ background: linear-gradient(
|
|
|
+ 135deg,
|
|
|
+ rgba(230, 247, 255, 0.7) 0%,
|
|
|
+ rgba(240, 248, 255, 0.7) 100%
|
|
|
+ );
|
|
|
+ min-height: 100vh;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
/* 顶部信息卡片区域 */
|
|
|
.dashboard {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
+ display: grid;
|
|
|
gap: 20px;
|
|
|
+ grid-template-columns: 1fr 1fr; /* 统计与分布占1份,饼图占1份 */
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
@@ -540,9 +424,69 @@ export default {
|
|
|
|
|
|
.map-container {
|
|
|
width: 100%;
|
|
|
- height: 60vh; /* 调整为60vh,更紧凑 */
|
|
|
- min-height: 400px; /* 降低最小高度 */
|
|
|
- max-height: 700px; /* 添加最大高度限制 */
|
|
|
+ height: 100%;
|
|
|
+ min-height: 400px;
|
|
|
+ max-height: 700px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-radius: 0 0 12px 12px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.map-image {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 500px;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-container, .error-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 300px;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-container {
|
|
|
+ color: #47C3B9;
|
|
|
+}
|
|
|
+
|
|
|
+.error-container {
|
|
|
+ color: #F56C6C;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-icon {
|
|
|
+ font-size: 36px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ animation: rotate 2s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ font-size: 36px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 300px;
|
|
|
+ color: #999;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data .el-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes rotate {
|
|
|
+ from { transform: rotate(0deg); }
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
}
|
|
|
|
|
|
/* 响应式调整 */
|