Skip to content

阶段一:项目基础与组件化

1. 教学目标

  • 💻 项目创建:掌握使用 HBuilderX 创建基于 Vite + Vue3 的 uni-app 项目。
  • 💻 结构认知:理解 uni-app 的核心目录结构及 pages.json 的路由配置功能。
  • 🧩 组件化思维:学习将一个复杂的页面(首页)自上而下地分解为多个高内聚、低耦合的独立组件。
  • 🧩 静态实现:能够利用 props 传参机制,将静态数据渲染到各个组件中,最终拼装出一个完整的、可视化的静态首页。

2. 阶段流程图 (Mermaid)

此图展示了本阶段从项目创建到最终静态页面完成的完整工作流。

3. 前期准备

在正式开始编码之前,我们需要完成一些基础配置工作,为项目开发扫清障碍。

💻 项目创建

  1. 在 HBuilderX 中,通过 文件 -> 新建 -> 项目 创建一个新的 uni-app 项目。
  2. 技术选型: 请务必选择 ViteVue3 版本。

🧩 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": "等会给" 
  }
}

4. 构建与组装静态组件

本步骤的核心是“自下而上”的开发思想:先构建独立的、可复用的子组件,然后再由父组件将它们组装起来。在这一阶段,我们使用本地的JSON文件模拟数据。

📄 ➡️
🧩 🧩 🧩

4.1 核心环境组件: CurrentEnvironment.vue

子组件分析 (CurrentEnvironment.vue)

  • 功能: 专用于显示实时天气和空气质量。
  • 核心数据 (Props):
    • weatherData: Object - 包含温度、天气描述、湿度等实时天气信息。
    • airData: Object - 包含AQI值、等级、PM2.5等空气质量信息。
  • 内部逻辑: 将从 props 接收到的 weatherDataairData 对象中的值,展示在模板中。

父组件分析 (index.vue)

  • 数据来源: (静态阶段) 直接从 / datas/ 目录导入 CurrentWeatherData.jsonCurrentAirQualityData.json 文件。
  • 数据传递: 在模板中,通过属性绑定的方式将数据传递给子组件。
<!-- 在 index.vue 的模板中 -->
<CurrentEnvironment 
    :weather-data="currentWeather" 
    :air-data="airQuality">
</CurrentEnvironment>

推荐编码流程

  1. 定义Props: 在 CurrentEnvironment.vue 中,首先使用 defineProps 清晰地定义出需要 weatherDataairData 两个对象。
  2. 搭建静态模板: 在 CurrentEnvironment.vue 的模板中,使用 {{ weatherData.temp }} 等插值语法,搭建出UI结构。
  3. 父组件准备静态数据: 在 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>
  1. 连接父子: 在 index.vue 的模板中,使用 <CurrentEnvironment> 组件并传入 props
  2. 细化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>

推荐编码流程

  1. 定义Props: 在 HourlyForecast.vue 中,定义需要 hourlyData: Array
  2. 搭建静态模板: 使用 v-for<scroll-view> 搭建出横向滚动的列表结构。
  3. 父组件准备静态数据: 在 index.vue 中,import 对应的JSON数据。
index.vue 准备静态数据
<script setup>
// ...
import Weather24hData from '../../datas/Weather24hData.json';
const hourlyForecast = ref(Weather24hData.hourly);
// ...
</script>
  1. 连接父子: 在 index.vue 模板中传入 hourlyForecast 数据。
  2. 细化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-listuni-list-item 渲染列表。组件内部包含一份 defaultDailyData,并通过 computed 属性 displayData 实现一个优雅的降级策略:如果父组件传入了数据,就用父组件的;否则,使用自己的默认数据。这使得组件可以独立运行和测试。

父组件分析 (index.vue)

  • 数据来源: (静态阶段) 直接从 / datas/Weather7dData.json 文件导入。
  • 数据传递:
<!-- 在 index.vue 的模板中 -->
<DailyForecast :daily-data="dailyForecast"></DailyForecast>

推荐编码流程

  1. 定义Props: 在 DailyForecast.vue 中,定义 dailyData: Array
  2. 实现降级策略: 在脚本中定义 defaultDailyData,并创建一个 computed 属性 displayData,实现优先使用 props 数据的逻辑。
  3. 搭建静态模板: 使用 uni-listv-for 遍历 displayData,搭建出列表结构。
  4. 父组件准备静态数据: 在 index.vueimport 对应的JSON。
  5. 连接父子: 在 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 方法。

推荐编码流程

  1. 定义Props和Emits: 在 LifeIndex.vue 中,定义 indexData prop 和 show-detail emit。
  2. 搭建静态模板: 使用 uni-gridv-for 搭建网格,并为每个格子绑定 @click 事件,调用一个方法(如 onShowDetail)。
  3. 实现事件触发: 在 onShowDetail 方法中,调用 emit('show-detail', item) 将点击项的数据发射出去。
  4. 父组件准备数据和回调: 在 index.vue 中,import JSON数据,并定义 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将这些数据精确地分发给每个子组件,完成最终的页面拼接。

编码流程

  1. 导入组件和数据: 在 <script setup> 中,import 所有需要的UI子组件和本地的 .json 模拟数据文件。
  2. 定义响应式数据: 使用 ref() 为每个子组件所需的数据创建响应式变量,并将导入的JSON数据赋值给它们。
  3. 使用组件: 在 <template> 中,像使用普通HTML标签一样使用已导入的组件。
  4. 传递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 数据,这样每个组件都可以独立预览和调试,极大地提升开发效率。