阶段一:项目基础与组件化
1. 教学目标
- 💻 项目创建:掌握使用 HBuilderX 创建基于 Vite + Vue3 的
uni-app项目。 - 💻 结构认知:理解
uni-app的核心目录结构及pages.json的路由配置功能。 - 🧩 组件化思维:学习将一个复杂的页面(首页)自上而下地分解为多个高内聚、低耦合的独立组件。
- 🧩 静态实现:能够利用
props传参机制,将静态数据渲染到各个组件中,最终拼装出一个完整的、可视化的静态首页。
2. 阶段流程图 (Mermaid)
此图展示了本阶段从项目创建到最终静态页面完成的完整工作流。
3. 前期准备
在正式开始编码之前,我们需要完成一些基础配置工作,为项目开发扫清障碍。
💻 项目创建
- 在 HBuilderX 中,通过
文件 -> 新建 -> 项目创建一个新的uni-app项目。 - 技术选型: 请务必选择
Vite和Vue3版本。
🧩 UI插件导入
为了提升开发效率和页面美观度,我们大量使用了官方的 uni-ui 组件库。通过 HBuilderX 的 工具 -> 插件安装 进入插件市场,搜索需要的组件(如 uni-card, uni-grid 等)并导入项目。
🗄️ 数据准备
- 模拟数据(JSON): 项目的
/src/datas/目录下存放了多个JSON文件(如CurrentWeatherData.json),它们是根据真实API返回的结构创建的。在静态开发阶段,我们将直接import这些文件来使用。
点击展开/折叠 CurrentWeatherData.json 内容
{
"code": "200",
"updateTime": "2020-06-30T22:00+08:00",
"fxLink": "[http://hfx.link/2ax1](http://hfx.link/2ax1)",
"now": {
"obsTime": "2020-06-30T21:40+08:00",
"temp": "24",
"feelsLike": "26",
"icon": "101",
"text": "多云",
"wind360": "123",
"windDir": "东南风",
"windScale": "1",
"windSpeed": "3",
"humidity": "72",
"precip": "0.0",
"pressure": "1003",
"vis": "16",
"cloud": "10",
"dew": "21"
},
"refer": {
"sources": [
"QWeather",
"NMC",
"ECMWF"
],
"license": [
"QWeather Developers License"
]
}
}点击展开/折叠 CurrentAirQualityData.json 内容
{
"metadata": {
"tag": "d75a323239766b831889e8020cba5aca9b90fca5080a1175c3487fd8acb06e84"
},
"indexes": [
{
"code": "us-epa",
"name": "AQI (US)",
"aqi": 46,
"aqiDisplay": "46",
"level": "1",
"category": "Good",
"color": {
"red": 0,
"green": 228,
"blue": 0,
"alpha": 1
},
"primaryPollutant": {
"code": "pm2p5",
"name": "PM 2.5",
"fullName": "Fine particulate matter (<2.5µm)"
},
"health": {
"effect": "No health effects.",
"advice": {
"generalPopulation": "Everyone can continue their outdoor activities normally.",
"sensitivePopulation": "Everyone can continue their outdoor activities normally."
}
}
}
],
"pollutants": [
{
"code": "pm2p5",
"name": "PM 2.5",
"concentration": { "value": 11.0, "unit": "μg/m3" }
},
{
"code": "pm10",
"name": "PM 10",
"concentration": { "value": 12.0, "unit": "μg/m3" }
}
]
}点击展开/折叠 Weather24hData.json 内容
{
"code": "200",
"updateTime": "2021-02-16T13:35+08:00",
"hourly": [
{
"fxTime": "2021-02-16T15:00+08:00",
"temp": "2",
"icon": "100",
"text": "晴"
},
{
"fxTime": "2021-02-16T16:00+08:00",
"temp": "1",
"icon": "100",
"text": "晴"
}
]
}点击展开/折叠 Weather7dData.json 内容
{
"code": "200",
"updateTime": "2021-11-15T16:35+08:00",
"daily": [
{
"fxDate": "2021-11-15",
"tempMax": "12",
"tempMin": "-1",
"iconDay": "101",
"textDay": "多云"
},
{
"fxDate": "2021-11-16",
"tempMax": "13",
"tempMin": "0",
"iconDay": "100",
"textDay": "晴"
},
{
"fxDate": "2021-11-17",
"tempMax": "13",
"tempMin": "0",
"iconDay": "100",
"textDay": "晴"
}
]
}点击展开/折叠 LifeIndexData.json 内容
{
"code": "200",
"daily": [
{
"date": "2025-11-27",
"type": "1",
"name": "运动指数",
"category": "较不宜"
},
{
"date": "2025-11-27",
"type": "2",
"name": "洗车指数",
"category": "适宜"
},
{
"date": "2025-11-27",
"type": "3",
"name": "穿衣指数",
"category": "较舒适"
}
]
}点击展开/折叠 url.json 内容
{
"CurrentAirQualityUrl": {
"url": "https://YOUR API HOST/airquality/v1/current/39.90/116.40",
"data":{
"key":"YOUR API KEY"
}
},
"CurrentWeatherUrl": {
"url": "https://YOUR API HOST/v7/weather/now",
"data":{
"key":"YOUR API KEY",
"location":"101010100"
}
},
"LifeIndexUrl": {
"url": "https://YOUR API HOST/v7/indices/1d",
"data":{
"key":"YOUR API KEY",
"location":"101010100",
"type":"1,2,3,4,5,9"
}
},
"Weather7dUrl": {
"url": "https://YOUR API HOST/v7/weather/7d",
"data":{
"key":"YOUR API KEY",
"location":"101010100"
}
},
"Weather24hUrl": {
"url": "https://YOUR API HOST/v7/weather/24h",
"data":{
"key":"YOUR API KEY",
"location":"101010100"
}
},
"getCityIdUrl": {
"url": "https://YOUR API HOST/v2/city/lookup",
"key":"YOUR API KEY"
},
"getCurrentCityUrl": {
"AMAP_URL": "[https://restapi.amap.com/v3/ip](https://restapi.amap.com/v3/ip)",
"AMAP_KEY": "等会给",
"BAIDU_URL": "[https://api.map.baidu.com/location/ip](https://api.map.baidu.com/location/ip)",
"BAIDU_KEY": "等会给"
}
}- 天气API Key申请: 为了在第二阶段能获取真实数据,请提前让学生访问 和风天气开发者平台,注册账号并创建一个应用,以获取免费的API Key。最后请求的url格式参考: "https://API Host/请求路径?参数", 如 https://pa6apwt2tt.re.qweatherapi.com/v7/weather/now?key=xxxx&location=101010100
4. 构建与组装静态组件
本步骤的核心是“自下而上”的开发思想:先构建独立的、可复用的子组件,然后再由父组件将它们组装起来。在这一阶段,我们使用本地的JSON文件模拟数据。
4.1 核心环境组件: CurrentEnvironment.vue
子组件分析 (CurrentEnvironment.vue)
- 功能: 专用于显示实时天气和空气质量。
- 核心数据 (Props):
weatherData: Object- 包含温度、天气描述、湿度等实时天气信息。airData: Object- 包含AQI值、等级、PM2.5等空气质量信息。
- 内部逻辑: 将从
props接收到的weatherData和airData对象中的值,展示在模板中。
父组件分析 (index.vue)
- 数据来源: (静态阶段) 直接从
/ datas/目录导入CurrentWeatherData.json和CurrentAirQualityData.json文件。 - 数据传递: 在模板中,通过属性绑定的方式将数据传递给子组件。
<!-- 在 index.vue 的模板中 -->
<CurrentEnvironment
:weather-data="currentWeather"
:air-data="airQuality">
</CurrentEnvironment>推荐编码流程
- 定义Props: 在
CurrentEnvironment.vue中,首先使用defineProps清晰地定义出需要weatherData和airData两个对象。 - 搭建静态模板: 在
CurrentEnvironment.vue的模板中,使用 {{ weatherData.temp }} 等插值语法,搭建出UI结构。 - 父组件准备静态数据: 在
index.vue中,import对应的JSON数据,并创建ref变量。
index.vue 准备静态数据
<script setup>
import { ref } from 'vue';
import CurrentWeatherData from '../../datas/CurrentWeatherData.json';
import CurrentAirQualityData from '../../datas/CurrentAirQualityData.json';
const currentWeather = ref(CurrentWeatherData.now);
const airQuality = ref(CurrentAirQualityData.now);
</script>- 连接父子: 在
index.vue的模板中,使用<CurrentEnvironment>组件并传入props。 - 细化UI: 回到
CurrentEnvironment.vue,添加样式和uni-ui组件,美化UI。
点击展开/折叠 CurrentEnvironment.vue 完整源代码
<template>
<view class="current-environment">
<!-- 实时天气信息 -->
<uni-card class="current-weather" :shadow="'none'">
<view class="weather-info">
<view class="left-section">
<uni-icons :type="weatherData.icon" size="120" color="#FFD700"></uni-icons>
<text class="weather-desc">{{ weatherData.weather }}</text>
</view>
<view class="right-section">
<text class="temperature">{{ weatherData.temp }}°</text>
<view class="weather-detail">
<view class="detail-item">
<uni-icons type="water" size="20" color="#4A90E2"></uni-icons>
<text class="detail-text">{{ weatherData.humidity }}</text>
</view>
<view class="detail-item">
<uni-icons type="wind" size="20" color="#90A4AE"></uni-icons>
<text class="detail-text">{{ weatherData.wind }}</text>
</view>
<view class="detail-item">
<uni-icons type="location" size="20" color="#FF5722"></uni-icons>
<text class="detail-text">{{ weatherData.visibility }}</text>
</view>
</view>
</view>
</view>
</uni-card>
<!-- 空气质量 -->
<uni-card class="air-quality" :shadow="'none'">
<text class="section-title">空气质量</text>
<view class="air-info">
<view class="air-main">
<text class="aqi-value">{{ airData.aqi }}</text>
<text class="aqi-level">{{ airData.level }}</text>
</view>
<uni-progress :percent="airData.aqi" :show-info="false" :activeColor="airData.color" stroke-width="20"></uni-progress>
<view class="air-detail">
<view class="air-item">
<text class="air-label">PM2.5</text>
<text class="air-data">{{ airData.pm25 }}</text>
</view>
<view class="air-item">
<text class="air-label">PM10</text>
<text class="air-data">{{ airData.pm10 }}</text>
</view>
<view class="air-item">
<text class="air-label">NO2</text>
<text class="air-data">{{ airData.no2 }}</text>
</view>
<view class="air-item">
<text class="air-label">O3</text>
<text class="air-data">{{ airData.o3 }}</text>
</view>
</view>
</view>
</uni-card>
</view>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
weatherData: {
type: Object,
default: () => ({})
},
airData: {
type: Object,
default: () => ({})
}
});
</script>4.2 24小时预报组件: HourlyForecast.vue
子组件分析 (HourlyForecast.vue)
- 功能: 以横向滚动的方式,展示未来24小时的天气预报。
- 核心数据 (Props):
hourlyData: Array- 一个包含多个小时预报对象的数组。 - 内部逻辑: 使用
v-for遍历hourlyData数组,并用<scroll-view>组件实现横向滚动。
父组件分析 (index.vue)
- 数据来源: (静态阶段) 直接从
/ datas/Weather24hData.json文件导入。 - 数据传递:
<!-- 在 index.vue 的模板中 -->
<HourlyForecast :hourly-data="hourlyForecast"></HourlyForecast>推荐编码流程
- 定义Props: 在
HourlyForecast.vue中,定义需要hourlyData: Array。 - 搭建静态模板: 使用
v-for和<scroll-view>搭建出横向滚动的列表结构。 - 父组件准备静态数据: 在
index.vue中,import对应的JSON数据。
index.vue 准备静态数据
<script setup>
// ...
import Weather24hData from '../../datas/Weather24hData.json';
const hourlyForecast = ref(Weather24hData.hourly);
// ...
</script>- 连接父子: 在
index.vue模板中传入hourlyForecast数据。 - 细化UI: 为
HourlyForecast.vue添加样式,完成布局。
点击展开/折叠 HourlyForecast.vue 完整源代码
<template>
<uni-card class="hourly-forecast" :shadow="'none'">
<text class="section-title">24小时预报</text>
<scroll-view scroll-x class="hourly-scroll">
<view class="hourly-list">
<view class="hourly-item" v-for="(item, index) in hourlyData" :key="index">
<text class="hour-time">{{ item.time }}</text>
<uni-icons :type="item.icon" size="40" color="#90A4AE"></uni-icons>
<text class="hour-temp">{{ item.temp }}°</text>
</view>
</view>
</scroll-view>
</uni-card>
</template>
<script setup>
import { defineProps } from 'vue';
// 定义组件的props
defineProps({
// 24小时预报数据
hourlyData: {
type: Array,
default: () => []
}
});
</script>
<style scoped>
.hourly-forecast {
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.hourly-scroll {
white-space: nowrap;
}
.hourly-list {
display: flex;
padding: 10rpx 0;
}
.hourly-item {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 30rpx;
min-width: 80rpx;
}
.hour-time {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
}
.hour-temp {
font-size: 28rpx;
color: #333;
margin-top: 10rpx;
font-weight: bold;
}
</style>4.3 7天预报组件: DailyForecast.vue
子组件分析 (DailyForecast.vue)
- 功能: 用列表形式清晰展示未来一周的天气、日期和温度范围。
- 核心数据 (Props):
dailyData: Array- 一个包含多天预报对象的数组。 - 内部逻辑: 使用
uni-list和uni-list-item渲染列表。组件内部包含一份defaultDailyData,并通过computed属性displayData实现一个优雅的降级策略:如果父组件传入了数据,就用父组件的;否则,使用自己的默认数据。这使得组件可以独立运行和测试。
父组件分析 (index.vue)
- 数据来源: (静态阶段) 直接从
/ datas/Weather7dData.json文件导入。 - 数据传递:
<!-- 在 index.vue 的模板中 -->
<DailyForecast :daily-data="dailyForecast"></DailyForecast>推荐编码流程
- 定义Props: 在
DailyForecast.vue中,定义dailyData: Array。 - 实现降级策略: 在脚本中定义
defaultDailyData,并创建一个computed属性displayData,实现优先使用props数据的逻辑。 - 搭建静态模板: 使用
uni-list和v-for遍历displayData,搭建出列表结构。 - 父组件准备静态数据: 在
index.vue中import对应的JSON。 - 连接父子: 在
index.vue模板中传入dailyForecast数据。
点击展开/折叠 DailyForecast.vue 完整源代码
<template>
<uni-card class="daily-forecast">
<text class="section-title">7天预报</text>
<uni-list>
<uni-list-item v-for="(item, index) in displayData" :key="index" :show-arrow="false">
<template #header>
<view class="daily-left">
<text class="daily-date">{{ item.date }}</text>
<text class="daily-day">{{ item.day }}</text>
</view>
</template>
<template #body>
<view class="daily-right">
<uni-icons :type="item.icon" size="30" color="#90A4AE" class="daily-icon"></uni-icons>
<text class="daily-weather">{{ item.weather }}</text>
<view class="daily-temp">
<text class="temp-max">{{ item.tempMax }}°</text>
<text class="temp-min">{{ item.tempMin }}°</text>
</view>
</view>
</template>
</uni-list-item>
</uni-list>
</uni-card>
</template>
<script setup>
import { defineProps, computed } from 'vue';
const props = defineProps({
dailyData: { type: Array, default: () => [] }
});
const defaultDailyData = [
{ date: '5/20', day: '周一', tempMax: '28', tempMin: '18', weather: '晴', icon: 'star' },
{ date: '5/21', day: '周二', tempMax: '26', tempMin: '17', weather: '多云', icon: 'cloud-download' },
{ date: '5/22', day: '周三', tempMax: '24', tempMin: '16', weather: '阴', icon: 'cloud-upload' },
{ date: '5/23', day: '周四', tempMax: '25', tempMin: '19', weather: '小雨', icon: 'download' },
{ date: '5/24', day: '周五', tempMax: '27', tempMin: '20', weather: '晴', icon: 'star' },
{ date: '5/25', day: '周六', tempMax: '29', tempMin: '21', weather: '晴', icon: 'star' },
{ date: '5/26', day: '周日', tempMax: '27', tempMin: '20', weather: '多云', icon: 'cloud-download' }
];
const displayData = computed(() => {
return props.dailyData.length > 0 ? props.dailyData : defaultDailyData;
});
</script>
<style scoped>
/* ... 样式 ... */
</style>4.4 生活指数组件: LifeIndex.vue
子组件分析 (LifeIndex.vue)
- 功能: 以网格布局展示多个生活指数,并能响应点击事件,通知父组件。
- 核心数据 (Props):
indexData: Array- 一个包含多个生活指数对象的数组。 - 核心交互 (Emits):
show-detail- 当用户点击某个指数时,触发此事件,并把该指数的完整对象传递出去。
父组件分析 (index.vue)
- 数据来源: (静态阶段) 直接从
/ datas/LifeIndexData.json文件导入。 - 数据传递:
<!-- 在 index.vue 的模板中 -->
<LifeIndex :index-data="lifeIndices" @show-detail="showIndexDetail"></LifeIndex>- 事件监听: 父组件通过
@show-detail监听子组件的点击事件,并执行showIndexDetail方法。
推荐编码流程
- 定义Props和Emits: 在
LifeIndex.vue中,定义indexDataprop 和show-detailemit。 - 搭建静态模板: 使用
uni-grid和v-for搭建网格,并为每个格子绑定@click事件,调用一个方法(如onShowDetail)。 - 实现事件触发: 在
onShowDetail方法中,调用emit('show-detail', item)将点击项的数据发射出去。 - 父组件准备数据和回调: 在
index.vue中,importJSON数据,并定义showIndexDetail方法来接收子组件传来的数据。
点击展开/折叠 LifeIndex.vue 完整源代码
<template>
<uni-card class="life-index" :shadow="'none'">
<text class="section-title">生活指数</text>
<uni-grid :column="3" :show-border="false" :square="false">
<uni-grid-item v-for="indexItem in indexData" :key="indexItem.type" @click="onShowDetail(indexItem)">
<view class="index-item">
<uni-icons :type="indexItem.icon" size="48" :color="indexItem.color"></uni-icons>
<text class="index-name">{{ indexItem.name }}</text>
<text class="index-value">{{ indexItem.value }}</text>
</view>
</uni-grid-item>
</uni-grid>
</uni-card>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
indexData: { type: Array, default: () => [] }
});
const emit = defineEmits(['show-detail']);
const onShowDetail = (indexItem) => {
emit('show-detail', indexItem);
};
</script>
<style scoped>
/* ... 样式 ... */
</style>4.5 总装车间:组装父组件 (index.vue)
父组件分析 (index.vue)
- 职责: 作为页面的“总指挥”,负责导入所有子组件,从本地JSON文件准备好静态数据,并通过Props将这些数据精确地分发给每个子组件,完成最终的页面拼接。
编码流程
- 导入组件和数据: 在
<script setup>中,import所有需要的UI子组件和本地的.json模拟数据文件。 - 定义响应式数据: 使用
ref()为每个子组件所需的数据创建响应式变量,并将导入的JSON数据赋值给它们。 - 使用组件: 在
<template>中,像使用普通HTML标签一样使用已导入的组件。 - 传递Props: 通过
:prop-name="dataVariable"的形式,将脚本中定义好的响应式数据变量绑定到子组件的props上。
点击展开/折叠 index.vue (阶段一静态版本) 完整源代码
<template>
<view class="weather-container">
<scroll-view
scroll-y
style="height: 100vh;"
>
<!-- 城市选择栏 (静态) -->
<view class="city-bar">
<text class="city-name">{{ currentCity }}</text>
<uni-icons type="arrow-down" size="20"></uni-icons>
</view>
<!-- 3. 在模板中使用所有组件,并通过 props 传入数据 -->
<CurrentEnvironment :weather-data="currentWeather" :air-data="airQuality"></CurrentEnvironment>
<HourlyForecast :hourly-data="hourlyForecast"></HourlyForecast>
<DailyForecast :daily-data="dailyForecast"></DailyForecast>
<LifeIndex :index-data="lifeIndices" @show-detail="showIndexDetail"></LifeIndex>
<!-- 指数详情弹窗 (静态阶段不实现功能) -->
<IndexDetailPopup :visible="detailVisible" :index-data="currentIndex" @close="closePopup"></IndexDetailPopup>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// 1. 导入所有需要的UI组件
import CurrentEnvironment from '../../components/CurrentEnvironment.vue';
import HourlyForecast from '../../components/HourlyForecast.vue';
import DailyForecast from '../../components/DailyForecast.vue';
import LifeIndex from '../../components/LifeIndex.vue';
import IndexDetailPopup from '../../components/IndexDetailPopup.vue';
// 1. 导入所有需要的本地JSON数据
import CurrentWeatherData from '../../datas/CurrentWeatherData.json';
import CurrentAirQualityData from '../../datas/CurrentAirQualityData.json';
import Weather24hData from '../../datas/Weather24hData.json';
import Weather7dData from '../../datas/Weather7dData.json';
import LifeIndexData from '../../datas/LifeIndexData.json';
// 2. 定义临时的、用于静态展示的本地数据
const currentCity = ref('北京市');
const currentWeather = ref(CurrentWeatherData.now);
const airQuality = ref(CurrentAirQualityData.now);
const hourlyForecast = ref(Weather24hData.hourly);
const dailyForecast = ref(Weather7dData.daily);
const lifeIndices = ref(LifeIndexData.daily);
// 弹窗相关状态 (静态阶段不实现功能)
const currentIndex = ref({});
const detailVisible = ref(false);
const showIndexDetail = (index) => { /* 暂时为空 */ };
const closePopup = () => { /* 暂时为空 */ };
</script>
<style scoped>
.weather-container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.city-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
cursor: pointer;
}
.city-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-right: 10rpx;
}
</style>5. 课后任务 (进阶)
- 任务:
CurrentEnvironment.vue组件目前同时负责“实时天气”和“空气质量”两块内容,显得有些臃肿。请创建一个新的组件AirQuality.vue,将“空气质量”相关的模板、脚本和样式从CurrentEnvironment.vue中完全剥离出去。然后,在index.vue中像使用其他组件一样使用它。 - 目的: 锻炼学生的代码“重构”能力,进一步理解单一职责原则(一个组件只做一件事),并巩固组件拆分与组装的全流程。
教师提示 此阶段是培养学生工程化思想的关键。务必强调:父组件通过 Props 向子组件传递数据是组件化开发的核心。鼓励学生在每个组件内部使用默认 props 数据,这样每个组件都可以独立预览和调试,极大地提升开发效率。