《uni-app 工具类小程序实战:天气查询 + 备忘录 + 待办清单》

uni-app小程序实战三合一

一、项目介绍

本项目是一个适合新手练手的轻量型 uni-app 工具类小程序,集成三大核心功能:

  • 天气查询:调用和风天气 API 实现实时天气、未来预报查询
  • 备忘录:基于 uni-app 本地存储实现文本记录的增删改查
  • 待办清单:通过本地存储管理待办事项,支持状态标记与删除

通过本项目,新手可掌握:

  • uni-app 基础语法与项目结构
  • 网络 API 调用与数据处理
  • 本地存储(uni.setStorageSync/uni.getStorageSync)的使用
  • 多页面路由与组件化开发
  • 小程序 UI 布局与交互逻辑

二、准备工作

1. 开发环境搭建

2. 项目初始化

(1)打开 HBuilderX,点击「文件 > 新建 > 项目」

(2)选择「uni-app」,模板选「默认模板」,输入项目名称(如 tool-mini-program

(3)项目结构说明:

tool-mini-program/
├─ pages/           # 页面目录(天气、备忘录、待办清单页面)
├─ static/          # 静态资源(图片等)
├─ common/          # 公共工具类(API 封装、工具函数)
├─ App.vue          # 应用入口
├─ main.js          # 入口文件
├─ pages.json       # 页面路由与配置
└─ manifest.json    # 项目配置(小程序 AppID 等)

三、核心功能实现

模块 1:天气查询(调用和风天气 API)

1.1 配置 API 密钥

在 common/config.js 中存储 API 配置:

// common/config.js
export default {
  weatherKey: '你的和风天气KEY', // 替换为自己的 KEY
  baseUrl: 'https://devapi.qweather.com/v7' // 和风天气 API 基础地址
}
1.2 封装网络请求工具

创建 common/request.js 封装 uni-app 网络请求:

// common/request.js
import config from './config'

export default function request(url, method = 'GET', data = {}) {
  return new Promise((resolve, reject) => {
    uni.request({
      url: config.baseUrl + url,
      method,
      data: { ...data, key: config.weatherKey }, // 自动携带 KEY
      success: (res) => {
        if (res.data.code === '200') {
          resolve(res.data)
        } else {
          reject('请求失败:' + res.data.msg)
        }
      },
      fail: (err) => {
        reject('网络错误:' + err.errMsg)
      }
    })
  })
}
1.3 天气页面开发(pages/weather/weather.vue
页面结构(template):
<template>
  <view class="weather-container">
    <!-- 加载中提示 -->
    <view v-if="loading" class="loading">加载中...</view>
    
    <!-- 天气信息展示 -->
    <view v-else class="weather-info">
      <view class="city">{{ city }}</view>
      <view class="temp">{{ now.temp }}°C</view>
      <view class="cond">{{ now.text }}</view>
      <view class="detail">
        <text>湿度:{{ now.humidity }}%</text>
        <text>风速:{{ now.windDir }} {{ now.windSpeed }}km/h</text>
      </view>
      
      <!-- 未来预报 -->
      <view class="forecast">
        <view class="forecast-item" v-for="(item, index) in forecast" :key="index">
          <text>{{ item.fxDate.slice(5) }}</text>
          <text>{{ item.textDay }}</text>
          <text>{{ item.tempMin }}°~{{ item.tempMax }}°</text>
        </view>
      </view>
    </view>
  </view>
</template>
逻辑处理(script):
<script>
import request from '../../common/request'

export default {
  data() {
    return {
      loading: true,
      city: '',
      now: {}, // 实时天气
      forecast: [] // 未来预报
    }
  },
  onLoad() {
    this.getLocation() // 先获取定位
  },
  methods: {
    // 获取用户定位(经纬度)
    getLocation() {
      uni.getLocation({
        type: 'gcj02',
        success: (res) => {
          this.getWeather(res.longitude, res.latitude)
          this.getCity(res.longitude, res.latitude)
        },
        fail: () => {
          uni.showToast({ title: '请开启定位权限', icon: 'none' })
          this.loading = false
        }
      })
    },
    
    // 获取实时天气
    async getWeather(lng, lat) {
      try {
        const nowRes = await request(`/weather/now?location=${lng},${lat}`)
        this.now = nowRes.now
        
        const forecastRes = await request(`/weather/3d?location=${lng},${lat}`)
        this.forecast = forecastRes.daily
        
        this.loading = false
      } catch (err) {
        uni.showToast({ title: err, icon: 'none' })
        this.loading = false
      }
    },
    
    // 获取城市名称
    async getCity(lng, lat) {
      const res = await request(`/geo/reverse?location=${lng},${lat}`)
      this.city = res.address.city
    }
  }
}
</script>
样式(style):
<style scoped>
.weather-container {
  padding: 20rpx;
}
.loading {
  text-align: center;
  padding: 50rpx;
}
.city {
  font-size: 36rpx;
  text-align: center;
  margin: 30rpx 0;
}
.temp {
  font-size: 80rpx;
  text-align: center;
  margin: 50rpx 0;
}
.cond {
  font-size: 32rpx;
  text-align: center;
  color: #666;
}
.detail {
  display: flex;
  justify-content: space-around;
  margin: 40rpx 0;
  color: #666;
}
.forecast {
  margin-top: 50rpx;
}
.forecast-item {
  display: flex;
  justify-content: space-between;
  padding: 20rpx 0;
  border-bottom: 1px solid #eee;
}
</style>

模块 2:备忘录(本地存储)

2.1 备忘录数据结构设计

每条备忘录包含:

{
  id: '唯一标识(时间戳)',
  title: '标题',
  content: '内容',
  createTime: '创建时间'
}
2.2 备忘录列表页(pages/memo/memo.vue
页面结构:
<template>
  <view class="memo-container">
    <!-- 添加按钮 -->
    <button class="add-btn" @click="toEdit">+ 添加备忘录</button>
    
    <!-- 备忘录列表 -->
    <view class="memo-list">
      <view 
        class="memo-item" 
        v-for="(item, index) in memoList" 
        :key="item.id"
        @click="toEdit(item.id)"
      >
        <view class="memo-title">{{ item.title || '无标题' }}</view>
        <view class="memo-time">{{ item.createTime }}</view>
        <button 
          class="delete-btn" 
          @click.stop="deleteMemo(item.id)"
        >删除</button>
      </view>
    </view>
  </view>
</template>
逻辑处理:
<script>
export default {
  data() {
    return {
      memoList: []
    }
  },
  onShow() {
    // 页面显示时读取本地存储
    const memos = uni.getStorageSync('memos') || []
    this.memoList = memos.reverse() // 倒序显示(最新的在前)
  },
  methods: {
    // 跳转到编辑页(新增/修改)
    toEdit(id) {
      uni.navigateTo({
        url: `/pages/memo-edit/memo-edit?id=${id || ''}`
      })
    },
    // 删除备忘录
    deleteMemo(id) {
      uni.showModal({
        title: '提示',
        content: '确定删除吗?',
        success: (res) => {
          if (res.confirm) {
            let memos = uni.getStorageSync('memos') || []
            memos = memos.filter(item => item.id !== id)
            uni.setStorageSync('memos', memos)
            this.memoList = memos.reverse()
          }
        }
      })
    }
  }
}
</script>
2.3 备忘录编辑页(pages/memo-edit/memo-edit.vue
<template>
  <view class="memo-edit">
    <input 
      type="text" 
      v-model="title" 
      placeholder="请输入标题"
      class="title-input"
    />
    <textarea 
      v-model="content" 
      placeholder="请输入内容"
      class="content-input"
    ></textarea>
    <button class="save-btn" @click="saveMemo">保存</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      id: '',
      title: '',
      content: ''
    }
  },
  onLoad(options) {
    this.id = options.id // 从路由获取 id(空则为新增)
    if (this.id) {
      // 编辑已有备忘录:读取数据
      const memos = uni.getStorageSync('memos') || []
      const memo = memos.find(item => item.id === this.id)
      if (memo) {
        this.title = memo.title
        this.content = memo.content
      }
    }
  },
  methods: {
    saveMemo() {
      if (!this.title && !this.content) {
        uni.showToast({ title: '内容不能为空', icon: 'none' })
        return
      }
      
      const memos = uni.getStorageSync('memos') || []
      const now = new Date().toLocaleString() // 当前时间
      
      if (this.id) {
        // 修改:替换原数据
        const index = memos.findIndex(item => item.id === this.id)
        memos[index] = { ...memos[index], title: this.title, content: this.content }
      } else {
        // 新增:添加新数据
        memos.push({
          id: Date.now().toString(), // 用时间戳作为唯一 id
          title: this.title,
          content: this.content,
          createTime: now
        })
      }
      
      // 保存到本地存储
      uni.setStorageSync('memos', memos)
      uni.navigateBack() // 返回列表页
    }
  }
}
</script>

模块 3:待办清单(本地存储)

3.1 待办数据结构设计

每条待办包含:

{
  id: '唯一标识',
  content: '待办内容',
  completed: false, // 是否完成
  createTime: '创建时间'
}
3.2 待办清单页面(pages/todo/todo.vue
页面结构:
<template>
  <view class="todo-container">
    <!-- 添加待办 -->
    <view class="add-todo">
      <input 
        v-model="newTodo" 
        placeholder="请输入待办事项"
        @confirm="addTodo"
      />
      <button @click="addTodo">添加</button>
    </view>
    
    <!-- 待办列表 -->
    <view class="todo-list">
      <view 
        class="todo-item" 
        v-for="(item, index) in todoList" 
        :key="item.id"
      >
        <checkbox 
          :checked="item.completed" 
          @change="toggleStatus(item.id)"
          class="todo-check"
        />
        <text :class="{ completed: item.completed }">{{ item.content }}</text>
        <button 
          class="delete-btn" 
          @click="deleteTodo(item.id)"
        >删除</button>
      </view>
    </view>
  </view>
</template>
逻辑处理:
<script>
export default {
  data() {
    return {
      newTodo: '',
      todoList: []
    }
  },
  onShow() {
    // 读取本地存储的待办
    this.todoList = uni.getStorageSync('todos') || []
  },
  methods: {
    // 添加待办
    addTodo() {
      if (!this.newTodo.trim()) return
      
      const newItem = {
        id: Date.now().toString(),
        content: this.newTodo.trim(),
        completed: false,
        createTime: new Date().toLocaleString()
      }
      
      this.todoList.unshift(newItem) // 添加到开头
      uni.setStorageSync('todos', this.todoList)
      this.newTodo = '' // 清空输入框
    },
    
    // 切换完成状态
    toggleStatus(id) {
      const todo = this.todoList.find(item => item.id === id)
      if (todo) todo.completed = !todo.completed
      uni.setStorageSync('todos', this.todoList)
    },
    
    // 删除待办
    deleteTodo(id) {
      this.todoList = this.todoList.filter(item => item.id !== id)
      uni.setStorageSync('todos', this.todoList)
    }
  }
}
</script>
样式:
<style scoped>
.add-todo {
  display: flex;
  padding: 20rpx;
  gap: 10rpx;
}
.add-todo input {
  flex: 1;
  border: 1px solid #eee;
  padding: 15rpx;
  border-radius: 8rpx;
}
.todo-list {
  padding: 0 20rpx;
}
.todo-item {
  display: flex;
  align-items: center;
  padding: 20rpx 0;
  border-bottom: 1px solid #eee;
  gap: 15rpx;
}
.todo-item text {
  flex: 1;
}
.completed {
  text-decoration: line-through;
  color: #999;
}
.delete-btn {
  color: #f00;
  background: transparent;
}
</style>

四、全局配置(pages.json

配置底部导航栏(tabBar)实现三个功能页切换:

{
  "pages": [
    {
      "path": "pages/weather/weather",
      "style": { "navigationBarTitleText": "天气查询" }
    },
    {
      "path": "pages/memo/memo",
      "style": { "navigationBarTitleText": "备忘录" }
    },
    {
      "path": "pages/todo/todo",
      "style": { "navigationBarTitleText": "待办清单" }
    },
    {
      "path": "pages/memo-edit/memo-edit",
      "style": { "navigationBarTitleText": "编辑备忘录" }
    }
  ],
  "tabBar": {
    "color": "#666",
    "selectedColor": "#007aff",
    "list": [
      {
        "pagePath": "pages/weather/weather",
        "text": "天气",
        "iconPath": "static/weather.png",
        "selectedIconPath": "static/weather-selected.png"
      },
      {
        "pagePath": "pages/memo/memo",
        "text": "备忘录",
        "iconPath": "static/memo.png",
        "selectedIconPath": "static/memo-selected.png"
      },
      {
        "pagePath": "pages/todo/todo",
        "text": "待办",
        "iconPath": "static/todo.png",
        "selectedIconPath": "static/todo-selected.png"
      }
    ]
  }
}

五、注意事项

1、和风天气 API 限制:免费版有调用次数限制(每日 1000 次),建议开发时注意控制请求频率

2、本地存储容量:uni-app 本地存储受小程序限制(单 key 不超过 1MB,总容量不超过 10MB),大量数据需考虑云存储

3、定位权限:部分设备可能需要手动开启定位权限,需做好异常处理

4、跨端兼容:若需发布到多端(H5、App),需注意 API 兼容性(如 uni.getLocation 在 H5 端需 HTTPS 环境)

六、扩展建议

1、天气模块:添加空气质量、生活指数(紫外线、洗车指数等)

2、备忘录:支持富文本、图片上传、分类标签

3、待办清单:添加截止时间、优先级、统计功能

4、全局:添加主题切换、数据同步到云端

通过本项目,新手可快速掌握 uni-app 核心开发能力,理解小程序的网络请求、本地存储、页面交互等核心逻辑。建议按模块分步实现,每完成一个功能就进行调试,逐步积累经验~

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值