阶段二:核心服务与状态管理

1. 教学目标

2. 真实数据流图 (Mermaid)

此图精确展示了本项目中从应用启动到首页渲染完成的完整数据流。

graph TD subgraph "页面层 (index.vue)" A[onMounted
生命周期触发] --> B[调用 initData 函数]; B --> C[调用 updateWeather 函数]; I[Ref 响应式数据
currentWeather, airQuality, etc.] --> J[UI 组件重新渲染]; end subgraph "服务层 (utils)" D[location.js
getCurrentCity] E[store.js
setCityInfo] F[api.js
getCurrentWeather, get24hForecast, etc.] G[weather.js
getAirQualityLevel] end subgraph "外部 API" H[高德/和风天气] end B --调用--> D; D --请求--> H; D --获取城市信息--> B; B --调用--> E; C --并行调用--> F; F --请求--> H; F --获取天气数据--> C; C --部分数据交由--> G; G --处理后返回--> C; C --更新--> I;

3. 核心步骤详解

UI Component Flow Arrow API Service Flow Arrow Store/Storage Flow Arrow Logic/Utils

在阶段一,我们的页面是“死”的。现在,我们要通过建立服务层,为它注入“灵魂”——真实的数据。

API Icon 步骤一:封装定位服务 (utils/location.js)

点击展开/折叠 utils/location.js 源代码
// 和风天气API Key
const QWEATHER_KEY = '你的Key';
const AMAP_URL = 'https://restapi.amap.com/v3/ip';
const AMAP_KEY = '你的Key';

/**
 * 获取当前位置信息
 * @returns {Promise} - 城市信息对象
 */
export const getCurrentCity = async () => {
  // 1. 获取IP对应的城市名和经纬度
  const ipData = await new Promise((resolve) => {
    uni.request({
      url: AMAP_URL,
      data: { key: AMAP_KEY },
      method: 'GET',
      success: (res) => {
        const [lng, lat] = res.data.rectangle.split(';')[0].split(',');
        resolve({ 
          city: res.data.city, 
          longitude: parseFloat(lng).toFixed(2),
          latitude: parseFloat(lat).toFixed(2)
        });
      },
      fail: (err) => {
        resolve({ city: '北京', longitude: 116.41, latitude: 39.92 });
      }
    });
  });

  const cityName = ipData.city || '北京';
  
  // 2. 根据城市名获取和风天气locationId
  const cityIdData = await fetchCityId(cityName);
  const locationId = cityIdData?.location?.[0]?.id || '101010100';
  
  // 3. 组装并返回最终的城市信息对象
  return {
    city: cityName,
    longitude: ipData.longitude,
    latitude: ipData.latitude,
    locationId: locationId
  };
};

/**
 * 根据城市名获取和风天气locationId
 * @param {string} cityName - 城市名称
 * @returns {Promise}
 */
export const fetchCityId = (cityName) => {
  // ... (代码同上)
};

Storage Icon 步骤二:创建简易状态管理器 (utils/store.js)

点击展开/折叠 utils/store.js 源代码
import { reactive } from 'vue';

// 创建全局响应式状态
const store = reactive({
  cityInfo: { /* ...默认值... */ },
  myCities: [],
  // ...
});

/**
 * 设置当前城市信息
 */
export const setCityInfo = (cityInfo) => {
  store.cityInfo = cityInfo;
  // 使用 'CityInfo' 作为键,将对象整体存入
  uni.setStorageSync('CityInfo', cityInfo);
};

/**
 * 获取当前城市信息
 */
export const getCityInfo = () => {
  return store.cityInfo;
};

// 初始化时从本地存储加载数据
const initStore = () => {
  const cityInfo = uni.getStorageSync('CityInfo');
  if (cityInfo) {
    store.cityInfo = cityInfo;
  }
  // ...
};

initStore();

export default store;

API Icon 步骤三:封装统一API服务 (utils/api.js)

点击展开/折叠 utils/api.js 源代码
import UrlConfig from '../datas/url.json';

const WEATHER_TEXT_MAP = { /* ...映射关系... */ };

/**
 * 获取实时天气数据
 * @param {string} city - 城市名称
 * @returns {object} - 清洗后的实时天气数据
 */
export const getCurrentWeather = async (city) => {
  const cityInfo = uni.getStorageSync('CityInfo') || {};
  const locationId = cityInfo.locationId || '默认值';
  
  const result = await new Promise((resolve) => {
    uni.request({
      // ...请求参数...
      success: (res) => resolve(res.data),
      fail: () => resolve({ code: '500' })
    });
  });

  if (result.code === '200') {
    const nowData = result.now;
    // 数据清洗与格式化
    return {
      code: '200',
      data: {
        city: city,
        temp: nowData.temp,
        weather: nowData.text,
        humidity: `${nowData.humidity}%`,
        wind: `${nowData.windDir} ${nowData.windScale}级`,
        icon: WEATHER_TEXT_MAP[nowData.text] || 'star'
      }
    };
  } else {
    // 返回默认/错误数据
    return { code: '500', data: { /* ... */ } };
  }
};

// get24hForecast, get7dForecast 等函数与上面类似...

Code Icon 步骤四:编写辅助工具 (utils/weather.js)

点击展开/折叠 utils/weather.js 源代码
/**
 * 根据AQI值获取空气质量等级
 * @param {number} aqi - AQI值
 * @returns {object} - { level, color, desc }
 */
export const getAirQualityLevel = (aqi) => {
  if (aqi <= 50) {
    return { level: '优', color: '#00E400', /* ... */ };
  } else if (aqi <= 100) {
    return { level: '良', color: '#FFFF00', /* ... */ };
  } 
  // ... 其他等级
};

Component Icon 步骤五:连接服务与视图 (index.vue)

点击展开/折叠 index.vue 脚本区源代码
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
// 1. 导入所有服务
import { getCurrentWeather, get24hForecast, get7dForecast, getAirQuality, getLifeIndex } from '../../utils/api';
import { getCityInfo, setCityInfo } from '../../utils/store';
import { getCurrentCity } from '../../utils/location';
import { getAirQualityLevel } from '../../utils/weather';
// ...导入组件...

// 2. 定义 ref 变量,用于驱动UI
const currentCity = computed(() => getCityInfo().city);
const currentWeather = ref({ /* ...默认值... */ });
const hourlyForecast = ref([]);
const dailyForecast = ref([]);
const airQuality = ref({ /* ...默认值... */ });
// ...

// 3. 核心数据更新函数
const updateWeather = async () => {
  // 调用 api.js 中的函数获取数据
  const weatherRes = await getCurrentWeather(currentCity.value);
  if (weatherRes.code === '200') {
    // 更新 ref 变量
    currentWeather.value = weatherRes.data;
  }
  
  const airRes = await getAirQuality(currentCity.value);
  if (airRes.code === '200') {
    const airData = airRes.data;
    const airLevel = getAirQualityLevel(airData.aqi); // 调用 weather.js
    airQuality.value = { ...airData, color: airLevel.color };
  }

  // ...获取其他天气数据...
};

// 4. 初始化函数
const initData = async () => {
  // 调用 location.js
  const currentLocation = await getCurrentCity();
  // 调用 store.js
  setCityInfo(currentLocation);
  
  await updateWeather();
};

// 5. 在页面加载时触发所有逻辑
onMounted(() => {
  initData();
});
</script>

4. 课后任务 (进阶)


教师提示: 本阶段是项目从“静态”到“动态”的质变,是整个课程的核心。务必强调服务分层的好处:高内聚、低耦合api.js 只管数据获取与转换,location.js 只管定位,store.js 只管状态共享,各司其职,使得 index.vue 的逻辑变得清晰——只负责“调用”和“更新状态”,而不关心“如何实现”。这种架构思想比任何一个API的使用都更重要。