Django+Vite实现前后端分离(四):核心代码分析、测试用例、总结分析

一、创建Django和Vite项目https://blog.csdn.net/LZYself/article/details/147752731?spm=1001.2014.3001.5501

二、各类分析的内容https://blog.csdn.net/LZYself/article/details/150413478?spm=1001.2014.3001.5501

三、功能介绍https://blog.csdn.net/LZYself/article/details/150413940?spm=1001.2014.3001.5501

四、核心代码分析

1、在Django的项目上配置好

这个是主项目文件夹的内容:

  • backend:主项目文件
  • api:为创建的子app项目
  • frontend:为创建的前端Vite项目
//settings.py文件
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'rest_framework',
    'rest_framework_simplejwt',
    'corsheaders',
    'api',//要加入自己创建的app项目
    'django_extensions',
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    #引包,顺序不能变
    'corsheaders.middleware.CorsMiddleware',
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
       'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

ROOT_URLCONF = "backend.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "backend.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

# DATABASES = {
#     "default": {
#         "ENGINE": "django.db.backends.sqlite3",
#         "NAME": BASE_DIR / "db.sqlite3",
#     }
# }
DATABASES = {
    "default": {
        #数据库类型
        "ENGINE": "django.db.backends.mysql",
        #数据库名称
        "NAME": "house",
        "HOST":"localhost",
        "PORT":3306,
        "USER":"root",
        "PASSWORD":"root",
    }
}

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = "zh-hans"#英文en-us 中文zh-hans

TIME_ZONE = "Asia/ShangHai"  #美国UTC 中国Asia/ShangHai

USE_I18N = True

USE_TZ = True


# 配置静态文件路径
STATIC_URL = '/static/'
# 这里如果没有额外的静态文件目录,可以设置为空列表
STATICFILES_DIRS = []
# 确保STATIC_ROOT正确设置,使用 / 拼接路径
STATIC_ROOT = BASE_DIR /'staticfiles'

# Default primary key field type
# https://docs.djangoprozject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# 允许跨域请求
CORS_ALLOW_ALL_ORIGINS = True

要在url.py中注册app项目

from django.contrib import admin # type: ignore
from django.urls import path, include # type: ignore
# from django.conf import settings
# from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
]

2、在Vite的项目中配置好

在Vite.config.js中对齐网址

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

在main.js中加入配置

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import axios from 'axios'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import 'bootstrap/dist/css/bootstrap.min.css'
import 'font-awesome/css/font-awesome.min.css'

const app = createApp(App)

// // 设置axios默认配置
axios.defaults.baseURL = '/api'
axios.defaults.withCredentials = true
// 注册Element Plus组件
app.use(ElementPlus)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.use(router)
app.use(createPinia())
app.mount('#app')

3、核心代码

// 状态管理
const houses = ref([]);
const selectedHouses = ref([]);
const page = ref(1);
const pageSize = ref(20);
const loading = ref(false);
const noMore = ref(false);
const showOnlySelected = ref(false);

// 筛选条件
const filters = reactive({
  address: '',
  orientation: '',
  buildYear: ''
});

// 各城市图表筛选条件
const wuhanFilters = reactive({
  orientation: '',
  minArea: null,
  maxArea: null,
  minPrice: null,
  maxPrice: null
});

const shanghaiFilters = reactive({
  orientation: '',
  minArea: null,
  maxArea: null,
  minPrice: null,
  maxPrice: null
});

const zhengzhouFilters = reactive({
  orientation: '',
  minArea: null,
  maxArea: null,
  minPrice: null,
  maxPrice: null
});

// 计算属性处理筛选逻辑
const filteredHouses = computed(() => {
  return houses.value.filter(house => {
    return (
      (house.address.includes(filters.address)) &&
      (filters.orientation? house.orientation === filters.orientation : true) &&
      (filters.buildYear? house.build_year.toString() === filters.buildYear : true)
    )
  });
});

// 获取房屋数据
const fetchHouses = async (currentPage = 1, isInitial = false) => {
  try {
    loading.value = true;
    const response = await axios.get('/api/houses/', {
      params: {
        page: currentPage,
        pageSize: pageSize.value,
        address: filters.address,
        orientation: filters.orientation,
        build_year: filters.buildYear
      }
    });
    if (isInitial) {
      houses.value = response.data;
    } else {
      houses.value = [...houses.value, ...response.data];
    }
    page.value = currentPage;
    noMore.value = response.data.length < pageSize.value;
    initTotalChart();
    initCityCharts();
  } catch (error) {
    console.error('获取房屋数据失败', error);
    ElMessage.error('获取房屋数据失败,请稍后重试');
  } finally {
    loading.value = false;
  }
};

// 存储Chart实例
const totalChart = ref(null);
const wuhanChart = ref(null);
const shanghaiChart = ref(null);
const zhengzhouChart = ref(null);

// 图表引用
const totalChartRef = ref(null);
const wuhanChartRef = ref(null);
const shanghaiChartRef = ref(null);
const zhengzhouChartRef = ref(null);

// 初始化总的图表
const initTotalChart = () => {
  // 销毁现有图表实例
  if (totalChart.value) totalChart.value.destroy();

  // 提取武汉、上海、郑州的二手房数据
  const wuhanHouses = houses.value.filter(house => house.address.includes('武汉'));
  const shanghaiHouses = houses.value.filter(house => house.address.includes('上海'));
  const zhengzhouHouses = houses.value.filter(house => house.address.includes('郑州'));

  // 计算各城市房屋总数
  const houseCounts = [wuhanHouses.length, shanghaiHouses.length, zhengzhouHouses.length];

  // 计算各城市平均面积
  const wuhanAvgArea = wuhanHouses.length > 0 ? 
    wuhanHouses.reduce((sum, house) => sum + house.area, 0) / wuhanHouses.length : 0;
  const shanghaiAvgArea = shanghaiHouses.length > 0 ? 
    shanghaiHouses.reduce((sum, house) => sum + house.area, 0) / shanghaiHouses.length : 0;
  const zhengzhouAvgArea = zhengzhouHouses.length > 0 ? 
    zhengzhouHouses.reduce((sum, house) => sum + house.area, 0) / zhengzhouHouses.length : 0;
  const avgAreas = [wuhanAvgArea, shanghaiAvgArea, zhengzhouAvgArea];

  // 计算各城市平均总价
  const wuhanAvgPrice = wuhanHouses.length > 0 ? 
    wuhanHouses.reduce((sum, house) => sum + house.total_price, 0) / wuhanHouses.length : 0;
  const shanghaiAvgPrice = shanghaiHouses.length > 0 ? 
    shanghaiHouses.reduce((sum, house) => sum + house.total_price, 0) / shanghaiHouses.length : 0;
  const zhengzhouAvgPrice = zhengzhouHouses.length > 0 ? 
    zhengzhouHouses.reduce((sum, house) => sum + house.total_price, 0) / zhengzhouHouses.length : 0;
  const avgPrices = [wuhanAvgPrice, shanghaiAvgPrice, zhengzhouAvgPrice];

  const cities = ['武汉', '上海', '郑州'];

  totalChart.value = new Chart(totalChartRef.value, {
    type: 'line',
    data: {
      labels: cities,
      datasets: [
        {
          label: '房屋总数',
          data: houseCounts,
          borderColor: 'rgba(84, 112, 198, 0.8)',
          backgroundColor: 'rgba(84, 112, 198, 0.2)',
          tension: 0.3,
          fill: true
        },
        {
          label: '平均面积(㎡)',
          data: avgAreas,
          borderColor: 'rgba(145, 204, 117, 0.8)',
          backgroundColor: 'rgba(145, 204, 117, 0.2)',
          tension: 0.3,
          fill: true
        },
        {
          label: '平均总价(万元)',
          data: avgPrices,
          borderColor: 'rgba(238, 102, 102, 0.8)',
          backgroundColor: 'rgba(238, 102, 102, 0.2)',
          tension: 0.3,
          fill: true
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        title: {
          display: true,
          text: '武汉、上海、郑州二手房数据对比'
        },
        tooltip: {
          mode: 'index',
          intersect: false,
        },
        legend: {
          position: 'top',
        }
      },
      scales: {
        x: {
          title: {
            display: true,
            text: '城市'
          }
        },
        y: {
          title: {
            display: true,
            text: '数值'
          },
          beginAtZero: true
        }
      }
    }
  });
};

// 初始化所有城市图表
const initCityCharts = () => {
  updateWuhanChart();
  updateShanghaiChart();
  updateZhengzhouChart();
};

// 筛选并更新武汉图表
const updateWuhanChart = () => {
  if (wuhanChart.value) wuhanChart.value.destroy();
  
  let filtered = houses.value.filter(house => house.address.includes('武汉'));
  
  // 应用筛选条件
  if (wuhanFilters.orientation) {
    filtered = filtered.filter(house => house.orientation === wuhanFilters.orientation);
  }
  if (wuhanFilters.minArea !== null) {
    filtered = filtered.filter(house => house.area >= wuhanFilters.minArea);
  }
  if (wuhanFilters.maxArea !== null) {
    filtered = filtered.filter(house => house.area <= wuhanFilters.maxArea);
  }
  if (wuhanFilters.minPrice !== null) {
    filtered = filtered.filter(house => house.total_price >= wuhanFilters.minPrice);
  }
  if (wuhanFilters.maxPrice !== null) {
    filtered = filtered.filter(house => house.total_price <= wuhanFilters.maxPrice);
  }

  // 计算数据
  const areaData = filtered.map(house => house.area);
  const priceData = filtered.map(house => house.total_price);
  
  // 创建图表
  wuhanChart.value = new Chart(wuhanChartRef.value, {
    type: 'scatter',
    data: {
      datasets: [{
        label: '武汉二手房',
        data: filtered.map(house => ({
          x: house.area,
          y: house.total_price,
          r: house.area / 50 // 面积作为点的大小
        })),
        backgroundColor: 'rgba(84, 112, 198, 0.7)',
        borderColor: 'rgba(84, 112, 198, 1)',
        borderWidth: 1
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        title: {
          display: true,
          text: `武汉二手房 (筛选后: ${filtered.length}套)`
        },
        tooltip: {
          callbacks: {
            label: function(context) {
              return [
                `面积: ${context.parsed.x}㎡`,
                `总价: ${context.parsed.y}万元`,
              ];
            }
          }
        }
      },
      scales: {
        x: {
          title: {
            display: true,
            text: '面积(㎡)'
          }
        },
        y: {
          title: {
            display: true,
            text: '总价(万元)'
          }
        }
      }
    }
  });
};
<script>
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';

export default {
  data() {
    return {
      accountName: '',
      password: '',
      confirmPassword: '',
      captcha: '',
      agreement: false,
      passwordStrength: 0,
      passwordStrengthWidth: 0,
      passwordStrengthColor: 'red'
    };
  },
  methods: {
    calculatePasswordStrength(password) {
      let strength = 0;
      if (password.length >= 8) {
        strength++;
      }
      if (/[A-Z]/.test(password)) {
        strength++;
      }
      if (/[a-z]/.test(password)) {
        strength++;
      }
      if (/[0-9]/.test(password)) {
        strength++;
      }
      if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
        strength++;
      }
      return strength;
    },
    updatePasswordStrength() {
      this.passwordStrength = this.calculatePasswordStrength(this.password);
      this.passwordStrengthWidth = (this.passwordStrength / 5) * 100;
      if (this.passwordStrength === 0) {
        this.passwordStrengthColor = 'red';
      } else if (this.passwordStrength <= 2) {
        this.passwordStrengthColor = 'orange';
      } else {
        this.passwordStrengthColor = 'green';
      }
    },
    handleSubmit() {
      if (this.password!== this.confirmPassword) {
        alert('两次输入的密码不一致,请重新输入');
        return;
      }
      if (!this.agreement) {
        alert('请阅读并同意用户注册协议');
        return;
      }
      // 保存注册信息到localStorage
      localStorage.setItem('accountName', this.accountName);
      localStorage.setItem('password', this.password);
      localStorage.setItem('hasRegistered', 'true');
      this.showSuccessMessageAndRedirect();
    },
    showSuccessMessageAndRedirect() {
      ElMessage.success('账号注册成功');
      this.$router.push({ name: 'Login' });
    },
    goToLogin() {
      if (localStorage.getItem('hasRegistered') === 'true') {
        this.$router.push({ name: 'Login' });
      } else {
        ElMessage.warning('请先完成注册');
      }
  }
}
};
</script>

五、功能测试

测试模块

用例编号

测试步骤

预期结果

   数据加载

TC001

进入页面

成功加载第一页数据并展示图表

   数据加载

TC002

滚动到页面底部

自动加载下一页数据

   数据筛选

TC003

输入地址关键词进行筛选

表格和图表更新为筛选后的数据

   数据对比

TC004

勾选两个房产并点击对比按钮

展示选中房产数据并更新对比图表

   用户注册

TC005

在注册页面输入用户名、符合规则密码,点击注册按钮

提示注册成功,跳转到登录页面

   用户注册

TC006

在注册页面输入用户名、密码、确认密码(与密码不一致),点击注册按钮

提示密码和确认密码不一致,不跳转

   用户登录

TC007

在登录页面输入已注册用户名、正确密码,点击登录按钮

成功登录,跳转到数据展示页面

   用户登录

TC008

在登录页面输入已注册用户名、错误密码,点击登录按钮

提示用户名或密码错误,不跳转

   用户登录

TC009

在登录页面输入未注册用户名、密码,点击登录按钮

提示用户名或密码错误,不跳转

   用户登出

TC010

在已登录的数据展示页面,点击退出登录按钮

清除会话信息,跳转到账号登录页面

六、总结分析

本房产数据分析平台项目基于Vite + Vue和Django构建前后端分离架构,经过需求分析、系统设计、功能开发与测试等多个阶段的努力,最终成功实现了房产数据可视化分析的核心功能。在项目推进过程中,团队充分发挥前后端技术优势,同时也面临并解决了诸多挑战。

在前端开发中,采用Vite作为项目构建工具。相比传统构建工具,Vite在冷启动时几乎实现秒级响应,显著提升了开发效率。搭配Vue 3框架,借助其先进的响应式系统和组合式API,实现了组件逻辑的高效复用与灵活组织。例如,在图表展示组件和数据筛选组件中,通过组合式API将数据获取、状态管理与视图渲染进行模块化拆分,使代码结构更加清晰,维护性大幅提高。

Element Plus组件库的引入为项目提供了丰富且美观的UI组件,保证了界面的一致性与交互友好性。而Chart.js则成为数据可视化的核心工具,通过定制不同类型的图表(如直方图、柱状图、散点图和饼图),将复杂的房产数据以直观易懂的方式呈现给用户。用户可以通过图表快速捕捉房产总价分布规律、不同朝向的单价差异、建造年份与总价的关联趋势以及房型分布占比等关键信息,有效满足了数据分析需求。

后端选用Django框架进行开发,其强大的ORM(对象关系映射)功能简化了与数据库的交互操作,无需编写复杂的SQL语句,即可实现对房产数据的增删改查。Django内置的路由系统和管理后台,进一步加快了API接口开发与数据管理的速度。通过定义RESTful风格的API接口,实现了前后端数据的高效传输,保证了数据格式的规范性与兼容性。

前后端分离架构的采用,前端专注于用户界面与交互逻辑开发,后端聚焦于数据处理与API接口实现,这种分工模式显著提高了开发效率。通过Axios库实现前后端数据交互,以JSON格式进行数据传输,保证了数据在不同环境下的兼容性和稳定性。在实际应用中,用户的筛选、对比等操作能够及时触发前端请求,后端迅速处理并返回结果,前端实时更新界面展示,实现了流畅的用户体验,达到了预期的功能目标。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值