基于Hadoop MapReduce与Vue3的二手房数据分析平台实战,hadoop大数据分析系统,二手房数据,源码
·
Hadoop MapReduce + Vue3 实现二手房数据分析平台完整开发流程
一、项目需求分析
1.1 业务需求
对二手房交易数据进行分析,实现以下功能:
- 按市区统计房源数量和平均价格
- 按户型统计房源数量
- 按面积范围统计平均价格
- 按朝向、楼层、年份等多维度统计分析
- 数据可视化展示
1.2 技术选型
| 层次 | 技术方案 | 说明 |
|---|---|---|
| 数据处理 | Hadoop MapReduce 3.3.4 | 处理大规模CSV数据 |
| 后端API | Spring Boot 2.7.18 | 提供RESTful接口 |
| 前端展示 | Vue 3.5.26 + ECharts 5.5.1 | 数据可视化 |
二、项目搭建
2.1 创建Maven父项目
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>secondHouseAnalysis</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>secondHouse-mapreduce</module>
<module>secondHouse-api</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<hadoop.version>3.3.4</hadoop.version>
<spring-boot.version>2.7.18</spring-boot.version>
</properties>
</project>
2.2 创建MapReduce模块
secondHouse-mapreduce/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>secondHouseAnalysis</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>secondHouse-mapreduce</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>${hadoop.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3 创建Spring Boot模块
secondHouse-api/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>secondHouseAnalysis</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>secondHouse-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
2.4 创建Vue3前端项目
npm create vite@latest secondHouse-dashboard -- --template vue
cd secondHouse-dashboard
npm install
npm install axios echarts element-plus vue-router
三、MapReduce数据处理实现
3.1 数据解析工具类
SecondHouseParser.java
package org.secondHouse.util;
import java.util.HashMap;
import java.util.Map;
public class SecondHouseParser {
public static Map<String, String> parseLine(String line) {
Map<String, String> result = new HashMap<>();
String[] fields = line.split(",");
if (fields.length >= 7) {
result.put("district", fields[0].trim());
result.put("houseType", fields[1].trim());
result.put("area", fields[2].trim());
result.put("price", fields[3].trim());
result.put("orientation", fields[4].trim());
result.put("floor", fields[5].trim());
result.put("year", fields[6].trim());
}
return result;
}
public static String getAreaRange(String area) {
try {
double areaValue = Double.parseDouble(area);
if (areaValue < 50) return "0-50";
else if (areaValue < 70) return "50-70";
else if (areaValue < 90) return "70-90";
else if (areaValue < 110) return "90-110";
else if (areaValue < 130) return "130-150";
else return "150+";
} catch (NumberFormatException e) {
return "未知";
}
}
}
3.2 户型统计Mapper和Reducer
HouseTypeCountMapper.java
package org.secondHouse.mapper;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;
import java.io.IOException;
public class HouseTypeCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
private Text outKey = new Text();
private LongWritable outValue = new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
Map<String, String> data = SecondHouseParser.parseLine(line);
String houseType = data.get("houseType");
if (houseType != null && !houseType.isEmpty()) {
outKey.set(houseType);
context.write(outKey, outValue);
}
}
}
HouseTypeCountReducer.java
package org.secondHouse.reducer;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class HouseTypeCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
private LongWritable result = new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context)
throws IOException, InterruptedException {
long sum = 0;
for (LongWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
3.3 市区统计Mapper和Reducer
DistrictStatsMapper.java
package org.secondHouse.mapper;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;
import java.io.IOException;
public class DistrictStatsMapper extends Mapper<LongWritable, Text, Text, Text> {
private Text outKey = new Text();
private Text outValue = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
Map<String, String> data = SecondHouseParser.parseLine(line);
String district = data.get("district");
String price = data.get("price");
if (district != null && !district.isEmpty() && price != null && !price.isEmpty()) {
outKey.set(district);
outValue.set(price);
context.write(outKey, outValue);
}
}
}
DistrictStatsReducer.java
package org.secondHouse.reducer;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class DistrictStatsReducer extends Reducer<Text, Text, Text, Text> {
private Text result = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
long count = 0;
double totalPrice = 0;
for (Text val : values) {
try {
double price = Double.parseDouble(val.toString());
count++;
totalPrice += price;
} catch (NumberFormatException e) {
// 忽略格式错误的数据
}
}
if (count > 0) {
double avgPrice = totalPrice / count;
result.set(count + "," + avgPrice);
context.write(key, result);
}
}
}
3.4 市区户型统计Mapper和Reducer
DistrictHouseTypeCountMapper.java
package org.secondHouse.mapper;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;
import java.io.IOException;
public class DistrictHouseTypeCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
private Text outKey = new Text();
private LongWritable outValue = new LongWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
Map<String, String> data = SecondHouseParser.parseLine(line);
String district = data.get("district");
String houseType = data.get("houseType");
if (district != null && !district.isEmpty() && houseType != null && !houseType.isEmpty()) {
String compositeKey = district + "\t" + houseType;
outKey.set(compositeKey);
context.write(outKey, outValue);
}
}
}
DistrictHouseTypeCountReducer.java
package org.secondHouse.reducer;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class DistrictHouseTypeCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
private Text outKey = new Text();
private Text outField2 = new Text();
private LongWritable result = new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context)
throws IOException, InterruptedException {
String[] parts = key.toString().split("\t");
if (parts.length >= 2) {
String district = parts[0];
String houseType = parts[1];
long sum = 0;
for (LongWritable val : values) {
sum += val.get();
}
outKey.set(district);
outField2.set(houseType);
result.set(sum);
context.write(outKey, outField2);
context.write(outField2, result);
}
}
}
3.5 驱动类
SecondHouseDriver.java
package org.secondHouse.driver;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.secondHouse.mapper.*;
import org.secondHouse.reducer.*;
public class SecondHouseDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String inputPath = args[0];
String outputPath = args[1];
// 运行户型统计
runHouseTypeCountJob(conf, inputPath, outputPath + "/house_type_count");
// 运行市区统计
runDistrictStatsJob(conf, inputPath, outputPath + "/district_stats");
// 运行市区户型统计
runDistrictHouseTypeCountJob(conf, inputPath, outputPath + "/district_house_type_count");
// 其他统计任务...
}
private static void runHouseTypeCountJob(Configuration conf, String inputPath, String outputPath)
throws Exception {
Job job = Job.getInstance(conf, "House Type Count");
job.setJarByClass(SecondHouseDriver.class);
job.setMapperClass(HouseTypeCountMapper.class);
job.setReducerClass(HouseTypeCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.addInputPath(job, new Path(inputPath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
job.waitForCompletion(true);
}
private static void runDistrictStatsJob(Configuration conf, String inputPath, String outputPath)
throws Exception {
Job job = Job.getInstance(conf, "District Stats");
job.setJarByClass(SecondHouseDriver.class);
job.setMapperClass(DistrictStatsMapper.class);
job.setReducerClass(DistrictStatsReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path(inputPath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
job.waitForCompletion(true);
}
private static void runDistrictHouseTypeCountJob(Configuration conf, String inputPath, String outputPath)
throws Exception {
Job job = Job.getInstance(conf, "District House Type Count");
job.setJarByClass(SecondHouseDriver.class);
job.setMapperClass(DistrictHouseTypeCountMapper.class);
job.setReducerClass(DistrictHouseTypeCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
FileInputFormat.addInputPath(job, new Path(inputPath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
job.waitForCompletion(true);
}
}
3.6 运行MapReduce任务
cd secondHouse-mapreduce
mvn clean package
hadoop jar target/secondHouse-mapreduce-1.0-SNAPSHOT.jar org.secondHouse.driver.SecondHouseDriver data/二手房数据.csv output/
四、Spring Boot API实现
4.1 应用主类
CustomerAnalysisApiApplication.java
package org.secondHouse.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CustomerAnalysisApiApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerAnalysisApiApplication.class, args);
}
}
4.2 配置类
WebConfig.java
package org.secondHouse.api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
}
}
application.properties
server.port=8080
spring.application.name=secondHouse-api
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
4.3 服务层实现
AnalysisResultService.java
package org.secondHouse.api.service;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
@Service
public class AnalysisResultService {
private static final String OUTPUT_BASE_PATH = "F:/AAproject/secondHouseAnalysis/output/";
public static class ResultItem {
private String key;
private Object value;
public ResultItem(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public Object getValue() { return value; }
public void setValue(Object value) { this.value = value; }
}
public static class TwoFieldResultItem {
private String field1;
private String field2;
private Object value;
public TwoFieldResultItem(String field1, String field2, Object value) {
this.field1 = field1;
this.field2 = field2;
this.value = value;
}
public String getField1() { return field1; }
public void setField1(String field1) { this.field1 = field1; }
public String getField2() { return field2; }
public void setField2(String field2) { this.field2 = field2; }
public Object getValue() { return value; }
public void setValue(Object value) { this.value = value; }
}
public static class DistrictStatsItem {
private String district;
private int count;
private double avgPrice;
public DistrictStatsItem(String district, int count, double avgPrice) {
this.district = district;
this.count = count;
this.avgPrice = avgPrice;
}
public String getDistrict() { return district; }
public void setDistrict(String district) { this.district = district; }
public int getCount() { return count; }
public void setCount(int count) { this.count = count; }
public double getAvgPrice() { return avgPrice; }
public void setAvgPrice(double avgPrice) { this.avgPrice = avgPrice; }
}
public List<ResultItem> getHouseTypeCountResult() {
return readResultFileToList("house_type_count/part-r-00000");
}
public List<DistrictStatsItem> getDistrictStatsResult() {
return readDistrictStatsResultFile("district_stats/part-r-00000");
}
public List<TwoFieldResultItem> getDistrictHouseTypeCountResult() {
return readTwoFieldResultFile("district_house_type_count/part-r-00000");
}
private List<ResultItem> readResultFileToList(String fileName) {
List<ResultItem> result = new ArrayList<>();
String filePath = OUTPUT_BASE_PATH + fileName;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
String[] parts = line.split("\\t");
if (parts.length >= 2) {
String key = parts[0];
String valueStr = parts[1];
Object value;
try {
value = Double.parseDouble(valueStr);
} catch (NumberFormatException e) {
value = valueStr;
}
result.add(new ResultItem(key, value));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private List<TwoFieldResultItem> readTwoFieldResultFile(String fileName) {
List<TwoFieldResultItem> result = new ArrayList<>();
String filePath = OUTPUT_BASE_PATH + fileName;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
String[] parts = line.split("\\t");
if (parts.length >= 3) {
String field1 = parts[0];
String field2 = parts[1];
String valueStr = parts[2];
Object value;
try {
value = Double.parseDouble(valueStr);
} catch (NumberFormatException e) {
value = valueStr;
}
result.add(new TwoFieldResultItem(field1, field2, value));
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
private List<DistrictStatsItem> readDistrictStatsResultFile(String fileName) {
List<DistrictStatsItem> result = new ArrayList<>();
String filePath = OUTPUT_BASE_PATH + fileName;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
String[] parts = line.split("\\t");
if (parts.length >= 2) {
String district = parts[0];
String[] stats = parts[1].split(",");
if (stats.length >= 2) {
try {
int count = Integer.parseInt(stats[0]);
double avgPrice = Double.parseDouble(stats[1]);
result.add(new DistrictStatsItem(district, count, avgPrice));
} catch (NumberFormatException e) {
// 忽略格式错误的行
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
4.4 控制器实现
AnalysisResultController.java
package org.secondHouse.api.controller;
import org.secondHouse.api.service.AnalysisResultService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/analysis")
public class AnalysisResultController {
@Autowired
private AnalysisResultService analysisResultService;
@GetMapping("/house-type-count")
public List<AnalysisResultService.ResultItem> getHouseTypeCount() {
return analysisResultService.getHouseTypeCountResult();
}
@GetMapping("/district-stats")
public List<AnalysisResultService.DistrictStatsItem> getDistrictStats() {
return analysisResultService.getDistrictStatsResult();
}
@GetMapping("/district-house-type-count")
public List<AnalysisResultService.TwoFieldResultItem> getDistrictHouseTypeCount() {
return analysisResultService.getDistrictHouseTypeCountResult();
}
}
4.5 运行Spring Boot应用
cd secondHouse-api
mvn clean package
java -jar target/secondHouse-api-1.0-SNAPSHOT.jar
五、Vue3前端实现
5.1 项目配置
package.json
{
"name": "secondHouse-dashboard",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.2",
"echarts": "^5.5.1",
"element-plus": "^2.13.1",
"vue": "^3.5.26",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.3",
"vite": "^7.3.0"
}
}
5.2 API封装
src/api/index.js
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8080',
timeout: 10000
})
export const analysisApi = {
getHouseTypeCount: () => api.get('/analysis/house-type-count'),
getDistrictStats: () => api.get('/analysis/district-stats'),
getDistrictHouseTypeCount: () => api.get('/analysis/district-house-type-count')
}
5.3 路由配置
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
}
]
})
export default router
5.4 主应用组件
src/App.vue
<template>
<div id="app">
<el-container>
<el-header>
<h1>二手房数据分析平台</h1>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: Arial, sans-serif;
}
.el-header {
background-color: #409eff;
color: white;
text-align: center;
line-height: 60px;
}
</style>
5.5 首页组件
src/views/HomeView.vue
<template>
<div class="home-view">
<h2>二手房数据分析</h2>
<!-- 统计卡片 -->
<div class="stats-cards">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-title">总房源数</div>
<div class="stat-value">{{ totalHouses }}</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-title">平均价格</div>
<div class="stat-value">{{ averagePrice.toFixed(2) }}</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-title">市区数量</div>
<div class="stat-value">{{ districtCount }}</div>
</div>
</el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-title">户型种类</div>
<div class="stat-value">{{ houseTypeCount }}</div>
</div>
</el-card>
</div>
<!-- 户型统计柱状图 -->
<el-card class="chart-card">
<template #header>
<span>户型数量统计</span>
</template>
<div ref="houseTypeChartRef" class="chart-container"></div>
</el-card>
<!-- 市区平均价格柱状图 -->
<el-card class="chart-card">
<template #header>
<span>市区平均价格</span>
</template>
<div ref="districtPriceChartRef" class="chart-container"></div>
</el-card>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import * as echarts from 'echarts'
import { analysisApi } from '../api'
export default {
name: 'HomeView',
setup() {
const houseTypeChartRef = ref(null)
const districtPriceChartRef = ref(null)
let houseTypeChart = null
let districtPriceChart = null
const houseTypeData = ref([])
const districtStatsData = ref([])
const totalHouses = computed(() => {
return districtStatsData.value.reduce((sum, item) => sum + item.count, 0)
})
const averagePrice = computed(() => {
const total = districtStatsData.value.reduce((sum, item) => sum + item.avgPrice * item.count, 0)
return totalHouses.value > 0 ? total / totalHouses.value : 0
})
const districtCount = computed(() => {
return districtStatsData.value.length
})
const houseTypeCount = computed(() => {
return houseTypeData.value.length
})
const loadData = async () => {
try {
const [houseTypeRes, districtStatsRes] = await Promise.all([
analysisApi.getHouseTypeCount(),
analysisApi.getDistrictStats()
])
houseTypeData.value = houseTypeRes
districtStatsData.value = districtStatsRes
initHouseTypeChart()
initDistrictPriceChart()
} catch (error) {
console.error('数据加载失败:', error)
}
}
const initHouseTypeChart = () => {
if (houseTypeChartRef.value) {
houseTypeChart = echarts.init(houseTypeChartRef.value)
const option = {
title: {
text: '户型数量统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: houseTypeData.value.map(item => item.key),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '数量'
},
series: [{
data: houseTypeData.value.map(item => item.value),
type: 'bar',
itemStyle: {
color: '#5470c6'
}
}]
}
houseTypeChart.setOption(option)
}
}
const initDistrictPriceChart = () => {
if (districtPriceChartRef.value) {
districtPriceChart = echarts.init(districtPriceChartRef.value)
const option = {
title: {
text: '市区平均价格',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: districtStatsData.value.map(item => item.district),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '平均价格'
},
series: [{
data: districtStatsData.value.map(item => item.avgPrice),
type: 'bar',
itemStyle: {
color: '#91cc75'
}
}]
}
districtPriceChart.setOption(option)
}
}
const handleResize = () => {
houseTypeChart?.resize()
districtPriceChart?.resize()
}
onMounted(() => {
loadData()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
houseTypeChart?.dispose()
districtPriceChart?.dispose()
})
return {
houseTypeChartRef,
districtPriceChartRef,
totalHouses,
averagePrice,
districtCount,
houseTypeCount
}
}
}
</script>
<style scoped>
.home-view {
padding: 20px;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-content {
text-align: center;
padding: 20px 0;
}
.stat-title {
font-size: 16px;
color: #606266;
margin-bottom: 10px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #409eff;
}
.chart-card {
margin-bottom: 20px;
}
.chart-container {
width: 100%;
height: 400px;
}
</style>
5.6 入口文件
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
5.7 运行前端项目
cd secondHouse-dashboard
npm install
npm run dev
六、项目总结
6.1 技术要点
-
MapReduce编程模型
- Mapper负责数据解析和初步聚合
- Reducer负责最终统计计算
- 自定义数据类型和序列化
-
Spring Boot RESTful API
- 文件读取和数据封装
- 跨域配置
- 统一的数据返回格式
-
Vue3 Composition API
- ref和computed响应式数据
- 生命周期钩子
- ECharts图表集成
6.2 开发流程
- 数据准备和格式分析
- MapReduce任务开发和测试
- Spring Boot API接口开发
- Vue3前端页面开发
- 前后端联调测试
- 项目部署上线
6.3 扩展方向
- 增加数据库存储,提高数据读取效率
- 添加Redis缓存,减少重复计算
- 实现数据实时更新机制
- 增加用户认证和权限管理
- 优化前端性能和用户体验
更多推荐
所有评论(0)