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

1. 教学目标

2. 阶段流程图 (Mermaid)

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

graph TD A[创建 uni-app 项目] --> B{分析首页UI}; B --> C[规划组件拆分
CurrentEnvironment
HourlyForecast
DailyForecast
LifeIndex]; C --> D[在 `src/components` 目录下
创建对应的 .vue 文件]; subgraph "逐一实现静态组件" D1[编写 CurrentEnvironment.vue] D2[编写 HourlyForecast.vue] D3[编写 DailyForecast.vue] D4[编写 LifeIndex.vue] end D --> D1 & D2 & D3 & D4; subgraph "组装首页" E[在 `pages/index/index.vue` 中
导入所有组件] --> F[定义临时的静态数据] F --> G[在模板中使用组件
并通过 Props 传递数据] end D1 & D2 & D3 & D4 --> E; G --> H[完成首页静态布局]

3. 核心步骤详解

步骤一:项目初始化与目录认知

  1. 创建项目: 在 HBuilderX 中,通过 文件 -> 新建 -> 项目 创建一个新的 uni-app 项目。
    • 技术选型: 请务必选择 ViteVue3 版本,这是目前主流的组合。
  2. 目录概览: 创建后,花几分钟时间向学生介绍核心目录:
    • src: 我们所有代码的家。
    • pages: 存放所有页面,每个页面一个文件夹。
    • components: 存放可复用的UI组件。
    • static: 存放图片、字体等静态资源。
    • utils: 存放工具函数、API请求等。
    • pages.json: 项目的“导航地图”,配置页面路径、窗口样式、tabBar 等。

步骤二:首页组件化拆分与创建

Big Page Decomposition Arrow Component 1 Component 2 Component 3

在动手编码前,先进行“设计”。引导学生打开任何一个天气App,会发现其首页都是由多个信息块组成的。我们将我们的首页也拆分成类似的组件:

接下来,在 src/components/ 目录下,创建以上四个 .vue 文件,并填入从我们项目中提取的源代码。


Component Icon 1. 核心环境组件: CurrentEnvironment.vue

点击展开/折叠 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';

// 定义组件的props
defineProps({
  // 实时天气数据
  weatherData: {
    type: Object,
    default: () => ({
      temp: '25',
      weather: '晴',
      humidity: '45%',
      wind: '东北风 3级',
      visibility: '10 km',
      icon: 'sunny'
    })
  },
  // 空气质量数据
  airData: {
    type: Object,
    default: () => ({
      aqi: 75,
      level: '良',
      pm25: 35,
      pm10: 60,
      no2: 25,
      o3: 80,
      color: '#FFFF00'
    })
  }
});
</script>

Component Icon 2. 24小时预报组件: 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>

Component Icon 3. 7天预报组件: DailyForecast.vue

点击展开/折叠 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 = [
  // ... 默认数据
];

const displayData = computed(() => {
  return props.dailyData.length > 0 ? props.dailyData : defaultDailyData;
});
</script>

Component Icon 4. 生活指数组件: LifeIndex.vue

点击展开/折叠 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>

Code Icon 步骤三:在首页 index.vue 中组装静态组件

所有“零件”都已备好,现在我们回到“总装车间”——pages/index/index.vue

点击展开/折叠 pages/index/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';

// 2. 定义临时的、用于静态展示的本地数据
// 在真实开发中,这些数据将从API获取
const currentCity = ref('北京市');

const currentWeather = ref({
  temp: '25', weather: '晴', humidity: '45%', wind: '东北风 3级', visibility: '10 km', icon: 'sunny'
});

const hourlyForecast = ref([
    { time: '14:00', icon: 'cloudy', temp: '25' }, { time: '15:00', icon: 'cloudy', temp: '24' },
    // ...更多数据
]);

const dailyForecast = ref([]); // DailyForecast 组件内部有默认数据

const airQuality = ref({
  aqi: 75, level: '良', pm25: 35, pm10: 60, no2: 25, o3: 80, color: '#FFFF00'
});

const lifeIndices = ref([
    { type: '1', name: '穿衣', value: '舒适', icon: 'person', color: '#4A90E2' },
    { type: '2', name: '运动', value: '适宜', icon: 'flag', color: '#F5A623' },
    // ...更多数据
]);

// 弹窗相关状态 (暂时用不到)
const currentIndex = ref({});
const detailVisible = ref(false);
const showIndexDetail = (index) => { /* 暂时为空 */ };
const closePopup = () => { /* 暂时为空 */ };
</script>

4. 课后任务 (进阶)


教师提示: 此阶段是培养学生工程化思想的关键。务必强调:父组件通过 Props 向子组件传递数据,子组件通过 Events (emit) 向父组件报告事件。这是 Vue (乃至所有现代前端框架) 数据流的核心。鼓励学生在每个组件内部使用默认 props 数据,这样每个组件都可以独立预览和调试,极大地提升开发效率。