10-实战项目

1 阅读4分钟

🏆 Taro 从零到一(十):实战项目 — 从零搭建一个完整小程序

系列导读:系列完结篇!综合前 9 篇全部知识,搭建一个带有 首页、商品列表、购物车、我的的完整电商小程序。


📊 项目功能清单

功能涉及知识点
🏠 首页轮播图 / 分类宫格 / 商品推荐 + NutUI
📜 商品列表ScrollView / 分页加载 / 搜索
📖 商品详情路由参数 / 图片预览 / 分享
🛒 购物车Zustand 全局状态 / 数量增减 / 金额计算
👤 我的登录 / 用户信息 / 设置列表

🏗 1. 项目结构

src/
├── app.ts                          # 应用入口
├── app.config.ts                   # 路由 + TabBar 配置
├── app.scss                        # 全局样式
├── api/                            # API 接口层
│   ├── request.ts                  # 请求封装
│   ├── product.ts                  # 商品接口
│   └── auth.ts                     # 认证接口
├── store/                          # 状态管理
│   ├── useAuthStore.ts             # 登录态
│   └── useCartStore.ts             # 购物车
├── hooks/                          # 自定义 Hook
│   └── useRequest.ts               # 请求 Hook
├── utils/                          # 工具函数
│   ├── platform.ts                 # 平台判断
│   └── storage.ts                  # 存储封装
├── components/                     # 公共组件
│   ├── ProductCard/
│   └── Empty/
├── pages/
│   ├── index/                      # 🏠 首页
│   ├── list/                       # 📜 商品列表
│   ├── detail/                     # 📖 商品详情
│   ├── cart/                       # 🛒 购物车
│   └── mine/                       # 👤 我的
└── assets/                         # 静态资源
    ├── tab/                        # TabBar 图标
    └── images/

⚙️ 2. 全局配置

// src/app.config.ts
export default defineAppConfig({
  pages: [
    'pages/index/index',
    'pages/list/index',
    'pages/cart/index',
    'pages/mine/index',
    'pages/detail/index',
    'pages/login/index',
  ],
  window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#ffffff',
    navigationBarTitleText: '好物商城',
    navigationBarTextStyle: 'black',
  },
  tabBar: {
    color: '#999',
    selectedColor: '#6366F1',
    backgroundColor: '#fff',
    borderStyle: 'white',
    list: [
      { pagePath: 'pages/index/index', text: '首页',
        iconPath: 'assets/tab/home.png', selectedIconPath: 'assets/tab/home-active.png' },
      { pagePath: 'pages/list/index', text: '分类',
        iconPath: 'assets/tab/category.png', selectedIconPath: 'assets/tab/category-active.png' },
      { pagePath: 'pages/cart/index', text: '购物车',
        iconPath: 'assets/tab/cart.png', selectedIconPath: 'assets/tab/cart-active.png' },
      { pagePath: 'pages/mine/index', text: '我的',
        iconPath: 'assets/tab/mine.png', selectedIconPath: 'assets/tab/mine-active.png' },
    ],
  },
})

🏠 3. 首页

// src/pages/index/index.tsx
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components'
import { useDidShow } from '@tarojs/taro'
import { Grid, GridItem, NoticeBar, SearchBar } from '@nutui/nutui-react-taro'
import ProductCard from '@/components/ProductCard'
import { useRequest } from '@/hooks/useRequest'
import { productApi } from '@/api/product'
import './index.scss'

export default function IndexPage() {
  const { data: products, refresh } = useRequest(() =>
    productApi.getList({ page: 1, pageSize: 10 })
  )

  // 每次回到首页刷新数据
  useDidShow(() => { refresh() })

  const banners = [
    { id: 1, image: 'https://picsum.photos/750/320?1' },
    { id: 2, image: 'https://picsum.photos/750/320?2' },
    { id: 3, image: 'https://picsum.photos/750/320?3' },
  ]

  const categories = [
    { id: 1, name: '手机', icon: '📱' },
    { id: 2, name: '电脑', icon: '💻' },
    { id: 3, name: '耳机', icon: '🎧' },
    { id: 4, name: '手表', icon: '⌚' },
    { id: 5, name: '更多', icon: '📦' },
  ]

  return (
    <View className="index-page">
      <SearchBar
        placeholder="搜索你想要的商品"
        shape="round"
        disabled
        onClickInput={() => Taro.navigateTo({ url: '/pages/list/index?focus=1' })}
      />

      <Swiper className="banner" autoplay circular indicatorDots>
        {banners.map(b => (
          <SwiperItem key={b.id}>
            <Image src={b.image} mode="aspectFill" className="banner-img" />
          </SwiperItem>
        ))}
      </Swiper>

      <NoticeBar content="🎉 新用户专享:首单立减10元" scrollable />

      <Grid columns={5} className="categories">
        {categories.map(cat => (
          <GridItem
            key={cat.id}
            text={cat.name}
            onClick={() => Taro.switchTab({ url: '/pages/list/index' })}
          >
            <Text style={{ fontSize: '40px' }}>{cat.icon}</Text>
          </GridItem>
        ))}
      </Grid>

      <View className="section">
        <Text className="section-title">🔥 热门推荐</Text>
        <View className="product-grid">
          {products?.list.map(p => (
            <ProductCard
              key={p.id}
              product={p}
              onTap={() => Taro.navigateTo({ url: `/pages/detail/index?id=${p.id}` })}
            />
          ))}
        </View>
      </View>
    </View>
  )
}

🛒 4. 购物车页

// src/pages/cart/index.tsx
import { View, Text, Image } from '@tarojs/components'
import { Button, Empty, Checkbox, InputNumber, SwipeCell } from '@nutui/nutui-react-taro'
import useCartStore from '@/store/useCartStore'
import './index.scss'

export default function CartPage() {
  const { items, updateQuantity, removeItem, totalPrice } = useCartStore()

  if (items.length === 0) {
    return (
      <View className="cart-empty">
        <Empty description="购物车空空如也">
          <Button
            type="primary"
            size="small"
            onClick={() => Taro.switchTab({ url: '/pages/list/index' })}
          >
            去逛逛
          </Button>
        </Empty>
      </View>
    )
  }

  return (
    <View className="cart-page">
      <View className="cart-list">
        {items.map(item => (
          <SwipeCell
            key={item.id}
            rightAction={
              <Button type="danger" onClick={() => removeItem(item.id)}>删除</Button>
            }
          >
            <View className="cart-item">
              <Image src={item.image} className="item-img" mode="aspectFill" />
              <View className="item-info">
                <Text className="item-name">{item.name}</Text>
                <View className="item-bottom">
                  <Text className="item-price">¥{item.price}</Text>
                  <InputNumber
                    min={1}
                    max={99}
                    value={item.quantity}
                    onChangeFuc={(v) => updateQuantity(item.id, v)}
                  />
                </View>
              </View>
            </View>
          </SwipeCell>
        ))}
      </View>

      <View className="cart-footer">
        <View className="total-info">
          <Text>合计:</Text>
          <Text className="total-price">¥{totalPrice().toFixed(2)}</Text>
        </View>
        <Button type="primary" className="checkout-btn">
          去结算({items.reduce((s, i) => s + i.quantity, 0)})
        </Button>
      </View>
    </View>
  )
}

👤 5. 我的页面

// src/pages/mine/index.tsx
import { View, Text, Image } from '@tarojs/components'
import { Cell, Avatar, Tag, Button } from '@nutui/nutui-react-taro'
import useAuthStore from '@/store/useAuthStore'
import './index.scss'

export default function MinePage() {
  const { user, isLoggedIn, logout } = useAuthStore()

  return (
    <View className="mine-page">
      {/* 用户信息区 */}
      <View className="user-header">
        {isLoggedIn ? (
          <View className="user-info">
            <Avatar size="large" src={user?.avatar} />
            <View className="user-detail">
              <Text className="user-name">{user?.name}</Text>
              <Tag type="primary" size="small">VIP 会员</Tag>
            </View>
          </View>
        ) : (
          <View className="login-prompt" onClick={() => Taro.navigateTo({ url: '/pages/login/index' })}>
            <Avatar size="large" />
            <Text className="login-text">点击登录</Text>
          </View>
        )}
      </View>

      {/* 订单区 */}
      <View className="order-section">
        <Cell title="我的订单" extra="查看全部" />
        <View className="order-grid">
          {[
            { icon: '💳', label: '待付款', count: 2 },
            { icon: '📦', label: '待发货', count: 0 },
            { icon: '🚚', label: '待收货', count: 1 },
            { icon: '⭐', label: '待评价', count: 3 },
          ].map(item => (
            <View key={item.label} className="order-item">
              <Text className="order-icon">{item.icon}</Text>
              <Text className="order-label">{item.label}</Text>
              {item.count > 0 && <Text className="order-badge">{item.count}</Text>}
            </View>
          ))}
        </View>
      </View>

      {/* 功能列表 */}
      <View className="func-list">
        <Cell title="收货地址" onClick={() => {}} />
        <Cell title="优惠券" extra="3 张可用" onClick={() => {}} />
        <Cell title="收藏夹" onClick={() => {}} />
        <Cell title="帮助中心" onClick={() => {}} />
        <Cell title="关于我们" onClick={() => {}} />
      </View>

      {isLoggedIn && (
        <View className="logout">
          <Button block type="default" onClick={() => {
            logout()
            Taro.showToast({ title: '已退出登录', icon: 'none' })
          }}>
            退出登录
          </Button>
        </View>
      )}
    </View>
  )
}

🚀 6. 构建与发布

# 开发调试
npm run dev:weapp       # 微信开发者工具实时预览

# 生产构建
npm run build:weapp     # 构建微信小程序
npm run build:h5        # 构建 H5
npm run build:alipay    # 构建支付宝小程序

# 上传发布
# 微信:开发者工具 → 上传 → 填写版本号 → 提交审核
# H5:将 dist/h5 部署到 CDN / Vercel / Nginx

✅ 全系列学习 Checklist

基础篇(第 1-3 篇)

  • 安装 Taro CLI 并创建项目
  • 掌握 React + TypeScript 基础
  • 熟悉 Taro 内置组件和页面生命周期

核心篇(第 4-7 篇)

  • 掌握路由导航和参数传递
  • 会用 NutUI 组件库搭建界面
  • 用 Zustand 管理全局状态
  • 封装统一网络请求层

进阶篇(第 8-9 篇)

  • 掌握多端条件编译
  • 会调用小程序原生能力(登录/上传/定位/分享)

实战篇(第 10 篇)

  • 搭建完整电商小程序
  • 构建并发布到微信/H5

🎉 恭喜你完成了「Taro 从零到一」全部 10 篇系列!

你已经掌握了用 Taro 开发多端小程序的完整技能。 下一步:打开微信开发者工具,开始写你自己的小程序吧!


本文是「Taro 从零到一」系列第 10 篇(完结篇),共 10 篇。