Day 17 | uni-app 小程序:前端从零搭建

摘要:后端 API 全部就绪了(Day01-16),但用户总不能抱着 Postman 购物吧?今天把前端搞起来------用 uni-app + Vue3 从零搭建商城小程序。涵盖项目初始化、五大核心页面(首页/商品详情/购物车/订单/个人中心)、组件化开发、Pinia 全局状态管理。成本?依然是零。


后端 API 写好了,然后呢?

过去 16 天,我们把商城的后端搭得明明白白:

  • Django + DRF 框架稳稳当当
  • 商品、订单、用户、支付、客服......接口全齐了
  • 统一的响应格式 {code: 0, message: "success", data: ...}

但问题来了------用户怎么用?

你不会真的让用户打开 Postman,填个 POST 请求来买东西吧?那大概是客户流失最快的电商了。

我们需要一个前端。一个能在手机上跑的、界面好看的、交互流畅的前端。

为什么选 uni-app?

  • 一套代码,多端运行:微信小程序、支付宝小程序、H5、App......写一次,到处跑
  • Vue3 生态:Composition API、Pinia 状态管理
  • 国内生态好:插件市场丰富,踩坑有人帮

今天就从零开始,把商城的小程序前端搭起来。


uni-app 和 Vue3 的组合拳

uni-app 是什么

一句话:用 Vue 的语法写跨平台应用。

复制代码
你的 Vue3 代码
     │
     ├── 编译 → 微信小程序(WXML + WXSS + JS)
     ├── 编译 → 支付宝小程序
     ├── 编译 → H5(网页版)
     └── 编译 → App(iOS / Android)

底层是编译器 + 运行时。你写的是标准 Vue3 代码,uni-app 帮你翻译成各平台的原生代码。

Vue3 + Composition API

Vue3 最核心的变化是 Composition API。相比 Options API 那种 data/methods/computed 分家的写法,Composition API 让你按功能组织代码:

javascript 复制代码
// Options API:按类型分家
export default {
  data() { return { count: 0 } },
  methods: { increment() { this.count++ } },
  computed: { double() { return this.count * 2 } }
}

// Composition API:按逻辑聚合
import { ref, computed } from 'vue'
const count = ref(0)
const increment = () => count.value++
const double = computed(() => count.value * 2)

Pinia:Vue3 的官方状态管理

Vuex 已经是过去式了。Pinia 的优势很明显:

  • API 更简洁:没有 mutations,直接改 state
  • TypeScript 友好:类型推导完美
  • 体积更小:核心代码不到 1KB
  • 支持 Composition API :setup 语法无缝集成

购物车数据、用户登录状态......这些跨页面共享的数据,用 Pinia 管理再合适不过。


动手实战

项目初始化

bash 复制代码
# 拉取 uni-app + Vue3 模板(vite-ts 自带 TypeScript)
npx degit dcloudio/uni-preset-vue#vite-ts mei-ya-da-app
cd mei-ya-da-app
npm install

# 安装 Pinia + 持久化插件(刷新/杀掉小程序不丢数据)
npm install pinia
npm install pinia-plugin-persisted-uniapp

初始化完成后,目录按功能分层:

复制代码
mei-ya-da-app/src/
├── api/              # 接口请求封装(request.ts 基础封装 + 按模块分的接口文件)
├── components/       # 通用组件(ProductCard、NavBar、CartModal、EmptyState)
├── composables/      # 组合式函数(useAuth、useCart)
├── pages/            # 页面(index 首页 / product 详情 / cart 购物车 / order 订单 / mine 我的)
├── stores/           # Pinia 状态(cart 购物车 / user 用户)
├── static/           # 静态资源
└── styles/           # 全局样式

这个结构看着复杂,其实就是按功能分层。api 管网络、components 放组件、pages 放页面、stores 放状态。找代码一目了然。

请求封装(api/request.ts)

所有接口请求的底层封装,统一处理 Base URL、Token 注入、错误拦截。核心逻辑三段:

typescript 复制代码
export function request<T>(options): Promise<T> {
  return new Promise((resolve, reject) => {
    // ① 从本地存储取 Token,注入请求头 Authorization
    const token = uni.getStorageSync('token') || ''
    uni.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' },
      success: (res) => {
        const data = res.data
        // ② 统一响应格式 {code:0, message, data} ------ code 0 成功,取 data.data
        // ③ code 401 说明 Token 过期 → 清 Token、提示登录、跳转
        //    其他 code → 弹出后端返回的错误信息
      },
    })
  })
}

这段代码做了三件事:① 自动在请求头加 Token;② 自动解析后端统一响应格式;③ 401 自动跳登录。前端所有接口调用都走这个封装。

页面路由配置(pages.json)

底部导航栏四个 Tab:首页、购物车、订单、我的。商品详情页不在 TabBar 里,通过跳转进入。

json 复制代码
{
  "pages": [
    { "path": "pages/index/index",   "style": { "navigationBarTitleText": "商城", "enablePullDownRefresh": true } },
    { "path": "pages/product/detail","style": { "navigationBarTitleText": "商品详情" } },
    { "path": "pages/cart/cart",     "style": { "navigationBarTitleText": "购物车" } },
    { "path": "pages/order/order",   "style": { "navigationBarTitleText": "我的订单" } },
    { "path": "pages/mine/mine",     "style": { "navigationBarTitleText": "个人中心", "navigationStyle": "custom" } }
  ],
  "tabBar": {
    "color": "#999999", "selectedColor": "#FF6034",
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/cart/cart",   "text": "购物车" },
      { "pagePath": "pages/order/order", "text": "订单" },
      { "pagePath": "pages/mine/mine",   "text": "我的" }
    ]
  }
}

五个核心页面

首页(pages/index/index.vue)

页面布局:顶部搜索栏 (灰色圆角输入框)→ 轮播 Banner (约 180rpx 高,自动播放)→ 分类导航 (一行四个图标)→ 商品瀑布流(两列卡片:图/名称/价格/销量)。

关键逻辑:

vue 复制代码
<template>
  <!-- 搜索栏 / 轮播 Banner / 分类导航 / 两列商品网格 -->
  <view class="product-grid">
    <ProductCard v-for="p in productList" :key="p.id" :product="p" @click="goDetail(p.id)" />
  </view>
</template>

<script setup lang="ts">
// ① onMounted 并行加载:轮播 + 分类 + 商品列表(Promise.all)
// ② loadMore 分页:page+1 追加到列表尾部,hasMore 控制是否还能加载
// ③ 下拉刷新 onMounted 重新拉取首页数据
</script>
商品详情页(pages/product/detail.vue)

页面布局:商品大图轮播 → 价格区 (红色大字 + 原价删除线 + 自营/包邮标签)→ 商品标题描述 → 规格选择入口 (点开 SKU 弹窗)→ 图文详情(rich-text) → 底部固定操作栏(收藏/客服/加入购物车/立即购买)。

关键逻辑:

vue 复制代码
<script setup lang="ts">
// ① loadProduct:从路由参数拿 id → getProductDetail(id) 加载详情
// ② addToCart:没选规格先弹 SKU 窗;选了规格调 cartStore.addItem 加购
// ③ buyNow:跳过购物车,带 product_id + sku 参数直接跳确认订单页
// ④ 图片预览 uni.previewImage / 收藏 toggleFavorite / 客服占位
</script>
购物车页(pages/cart/cart.vue)

页面布局:商品行 (勾选框 + 缩略图 + 名称规格 + 价格 + 数量步进器)→ 底部结算栏(全选 + 合计金额 + 结算按钮带件数)。空车显示空状态插画 + "去逛逛"。

关键逻辑:

vue 复制代码
<script setup lang="ts">
// ① toggleCheck / toggleSelectAll:改 checked 后调 store.updateChecked()
// ② changeQty:数量减到 0 时弹窗确认删除
// ③ goCheckout:只取勾选商品,编码后跳确认订单页
</script>
订单页(pages/order/order.vue)

页面布局:顶部 Tab 栏 (全部/待付款/待发货/待收货/已完成)→ 订单卡片(订单号+状态标签 / 商品缩略图+名称+规格+价格×数量 / 底部合计+操作按钮)。

关键逻辑:

vue 复制代码
<script setup lang="ts">
// ① switchTab:切换状态 → 重置 page=1 → 重新拉取订单列表
// ② 操作按钮按 status 条件渲染:待付款显示"去支付/取消订单",待收货显示"确认收货"
// ③ cancelOrder / confirmReceive:先弹确认框,确认后调对应 API 并刷新列表
</script>
个人中心(pages/mine/mine.vue)

页面布局:渐变头部 (圆形头像 + 昵称 + 会员标签)→ 订单快捷入口 (四宫格图标)→ 功能列表 (地址/优惠券/客服/关于/设置)→ 退出登录按钮。

关键逻辑:

vue 复制代码
<script setup lang="ts">
// ① 未登录时显示"点击登录",跳登录页
// ② 各菜单项目前是占位 toast,后续接真实功能
// ③ logout:弹确认框 → userStore.logout() 清 Token
</script>

五个页面的样式(SCSS)篇幅长但全是通用布局代码,这里不逐行贴------照着"页面由哪几块组成"的描述写就行,布局思路比样式代码更重要。

组件化开发

商品卡片组件 (ProductCard.vue)被首页、分类页、搜索结果页复用------defineProps 接收商品数据,defineEmits 抛出点击事件,父组件决定跳哪:

vue 复制代码
<script setup lang="ts">
const props = defineProps<{ product: { id: number; name: string; image: string; price: number } }>()
const emit = defineEmits<{ (e: 'click'): void }>()
</script>

空状态组件(EmptyState.vue)------购物车空了、订单空了、搜索没结果都用它:一个图标 + 一句话 + 可选按钮,通过 props 传文案、emit 触发按钮点击。

Pinia 状态管理

购物车 Store(stores/cart.ts)

购物车状态需要在首页(加购)、购物车页(管理)、结算页(读取)之间共享。核心逻辑:

typescript 复制代码
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  // Getters:isAllChecked 全选 / checkedCount 选中件数 / checkedTotal 选中总金额 / totalCount 角标数
  // Actions:
  function addItem(newItem) {
    // 关键:同商品 + 同规格合并数量,否则新增一条
    const existing = items.value.find(
      item => item.product_id === newItem.product_id &&
              JSON.stringify(item.spec) === JSON.stringify(newItem.spec)
    )
    existing ? existing.quantity += newItem.quantity
             : items.value.push({ ...newItem, checked: false })
    updateBadge()   // 更新 TabBar 角标
  }
  function removeItem(productId) { ... }   // 过滤掉指定商品 + 更新角标
  function clearCart() { ... }             // 清空 + 更新角标
}, { persist: { key: 'meiyada-cart' } })   // 持久化:杀掉小程序购物车还在
用户 Store(stores/user.ts)

管理登录状态、Token、个人信息:

typescript 复制代码
export const useUserStore = defineStore('user', () => {
  const token = ref(''); const nickname = ref(''); const avatar = ref('')
  const isLoggedIn = computed(() => !!token.value)

  async function login(username, password) {
    // 调后端登录接口拿 token → 存 state + uni.setStorageSync('token')(request.ts 自动读)
  }
  function logout() {
    // 清空 state + uni.removeStorageSync('token')
  }
}, { persist: { key: 'meiyada-user' } })
main.ts 注册 Pinia
typescript 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersisteduniapp from 'pinia-plugin-persisted-uniapp'

const pinia = createPinia()
pinia.use(piniaPluginPersisteduniapp)   // 挂持久化插件
app.use(pinia)

两个 Store 都用了持久化插件。购物车、Token 存在本地,小程序杀掉再打开,购物车的东西还在,用户也不用重新登录。


AI 加速:让 AI 帮你写页面

写前端页面最烦的是重复劳动------每个页面的结构都差不多,就是数据和样式不一样。这种活让 AI 来:

"基于 uni-app + Vue3 + TypeScript,生成一个电商商品列表页面。要求:两列瀑布流布局,每个卡片包含商品图片、名称、标签、价格、销量。使用 Composition API,样式用 SCSS,适配微信小程序。"

AI 能给你生成 80% 的代码,剩下 20% 是调整细节、对接接口。

小程序的样式调试很痛苦------没有 Chrome DevTools 那么好用。遇到样式问题直接截图丢给 AI:"这个商品卡片的价格和标题重叠了,帮我看看哪里出了问题。"比自己一个个属性排查快多了。

后端 API 文档给到 AI,让它直接生成前端的接口调用代码,也省得自己对着文档一行行翻译。


学完来练练手

  1. 🟢 入门 :给商品卡片组件加一个"限时折扣"标签,当商品有 discount 字段时显示倒计时
  2. 🟡 进阶 :给购物车页面加上"左滑删除"功能(touchstart/touchmove/touchend 监听手势 + transform: translateX() 实现滑动效果)
  3. 🔴 挑战:实现一个完整的搜索功能------搜索框输入 → 调用后端搜索接口 → 展示结果 → 支持历史记录

本系列为 梅雅达编程笔记 原创,首发于 CSDN。有问题?评论区见

相关推荐
康小旺15 小时前
TypeScript 编译器开始卷 Rust 了:Cloudflare 的 tsrs 到底想干嘛?
rust·typescript
一渊之隔18 小时前
uniapp BLE 开发踩坑:小米手机数据接收截断问题排查全记录
智能手机·uni-app
阿俊-全栈开发18 小时前
LikeShop 单商户标准版 UniApp 端二开:页面组件复用与自定义主题适配
uni-app·likeshop开源商城·likeshop单商户标准版·多商户源码
zhiboshequ1 天前
Ai音视频对话在微信小程序端的技术实现方案
人工智能·微信小程序·live-pusher·live-player·音视频对话
nyaomaru1 天前
is-kit 达成 100 Stars ⭐🎉 从 50 到 100,发生了什么?
前端·typescript
RuoyiOffice1 天前
项目预算和交付日期变了,为什么不能直接改台账?
spring boot·项目管理·vue3·flowable·业务建模·ruoyi office·项目变更
小蒜学长1 天前
基于过程性评价的ai助学平台(代码+数据库+LW)
java·spring boot·后端·微信小程序·过程性评价
半生过往1 天前
uni-app多端项目工作笔记
笔记·uni-app
00后程序员张1 天前
无需 Mac 的 iOS App 上架工具,开心上架Appuploader 负责哪些环节
android·ios·小程序·https·uni-app·iphone·webview