10-实战项目

6 阅读3分钟

🏆 Taro+Vue3 入门(十):实战项目 — 从零搭建完整小程序

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


🏗 1. 完整项目结构

src/
├── app.ts                          # 入口 + Pinia 注册
├── app.config.ts                   # 路由 + TabBar
├── app.scss                        # 全局样式变量
├── api/                            # 接口模块
│   ├── request.ts
│   ├── product.ts
│   └── auth.ts
├── stores/                         # Pinia Store
│   ├── auth.ts
│   └── cart.ts
├── composables/                    # 组合式函数
│   └── useRequest.ts
├── utils/                          # 工具
│   ├── platform.ts
│   └── storage.ts
├── components/                     # 公共组件
│   ├── ProductCard.vue
│   └── Empty.vue
└── pages/
    ├── index/index.vue             # 🏠 首页
    ├── list/index.vue              # 📜 分类
    ├── cart/index.vue              # 🛒 购物车
    ├── mine/index.vue              # 👤 我的
    ├── detail/index.vue            # 📖 商品详情
    └── login/index.vue             # 🔐 登录

🏠 2. 首页

<!-- src/pages/index/index.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useDidShow } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { useRequest } from '@/composables/useRequest'
import { productApi } from '@/api/product'
import ProductCard from '@/components/ProductCard.vue'

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

useDidShow(() => refresh())

const banners = ref([
  { 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: '📦' },
]
</script>

<template>
  <view class="home">
    <!-- 搜索 -->
    <nut-searchbar placeholder="搜索商品" disabled
      @click-input="() => Taro.navigateTo({ url: '/pages/search/index' })"
    />

    <!-- 轮播 -->
    <swiper class="banner" autoplay circular :indicator-dots="true">
      <swiper-item v-for="b in banners" :key="b.id">
        <image :src="b.image" mode="aspectFill" class="banner-img" />
      </swiper-item>
    </swiper>

    <!-- 分类 -->
    <nut-grid :column-num="5">
      <nut-grid-item v-for="cat in categories" :key="cat.id" :text="cat.name">
        <text style="font-size: 40px">{{ cat.icon }}</text>
      </nut-grid-item>
    </nut-grid>

    <!-- 推荐商品 -->
    <view class="section">
      <text class="section-title">🔥 热门推荐</text>
      <view class="product-grid">
        <ProductCard
          v-for="p in products?.list"
          :key="p.id"
          v-bind="p"
          @tap="() => Taro.navigateTo({ url: `/pages/detail/index?id=${p.id}` })"
        />
      </view>
    </view>
  </view>
</template>

<style lang="scss">
.home {
  background: #f5f5f5;
  .banner { height: 320px; .banner-img { width: 100%; height: 320px; } }
  .section {
    background: #fff; padding: 24px; margin-top: 16px;
    .section-title { font-size: 32px; font-weight: bold; }
    .product-grid { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 20px; }
  }
}
</style>

🛒 3. 购物车页

<!-- src/pages/cart/index.vue -->
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
import Taro from '@tarojs/taro'

const cartStore = useCartStore()
const { items, totalCount, totalPrice, isEmpty } = storeToRefs(cartStore)
</script>

<template>
  <view v-if="isEmpty" class="empty">
    <nut-empty description="购物车空空如也">
      <nut-button type="primary" size="small"
        @click="() => Taro.switchTab({ url: '/pages/list/index' })">
        去逛逛
      </nut-button>
    </nut-empty>
  </view>

  <view v-else class="cart">
    <nut-swipe
      v-for="item in items" :key="item.id"
    >
      <view class="cart-item">
        <image :src="item.image" class="item-img" mode="aspectFill" />
        <view class="item-info">
          <text class="item-name">{{ item.name }}</text>
          <view class="item-bottom">
            <text class="item-price">¥{{ item.price }}</text>
            <nut-input-number
              :model-value="item.quantity"
              :min="1" :max="99"
              @change="(v: number) => cartStore.updateQuantity(item.id, v)"
            />
          </view>
        </view>
      </view>
      <template #right>
        <nut-button type="danger" shape="square"
          @click="cartStore.removeItem(item.id)">
          删除
        </nut-button>
      </template>
    </nut-swipe>

    <view class="footer">
      <view class="total">
        <text>合计:</text>
        <text class="price">¥{{ totalPrice.toFixed(2) }}</text>
      </view>
      <nut-button type="primary" class="checkout-btn">
        去结算({{ totalCount }})
      </nut-button>
    </view>
  </view>
</template>

👤 4. 我的页面

<!-- src/pages/mine/index.vue -->
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
import Taro from '@tarojs/taro'

const authStore = useAuthStore()
const { user, isLoggedIn } = storeToRefs(authStore)

const orderItems = [
  { icon: '💳', label: '待付款', count: 2 },
  { icon: '📦', label: '待发货', count: 0 },
  { icon: '🚚', label: '待收货', count: 1 },
  { icon: '⭐', label: '待评价', count: 3 },
]

function handleLogout() {
  authStore.logout()
  Taro.showToast({ title: '已退出', icon: 'none' })
}
</script>

<template>
  <view class="mine">
    <!-- 头部 -->
    <view class="header">
      <template v-if="isLoggedIn">
        <nut-avatar size="large" :url="user?.avatar" />
        <view class="user-info">
          <text class="name">{{ user?.name }}</text>
          <nut-tag type="primary" plain>VIP 会员</nut-tag>
        </view>
      </template>
      <view v-else class="login-prompt"
        @tap="() => Taro.navigateTo({ url: '/pages/login/index' })">
        <nut-avatar size="large" />
        <text class="login-text">点击登录</text>
      </view>
    </view>

    <!-- 订单 -->
    <nut-cell title="我的订单" is-link extra="查看全部" />
    <view class="order-grid">
      <view v-for="item in orderItems" :key="item.label" class="order-item">
        <text class="order-icon">{{ item.icon }}</text>
        <text class="order-label">{{ item.label }}</text>
        <text v-if="item.count" class="badge">{{ item.count }}</text>
      </view>
    </view>

    <!-- 功能列表 -->
    <view class="func-list">
      <nut-cell title="收货地址" is-link />
      <nut-cell title="优惠券" extra="3 张可用" is-link />
      <nut-cell title="帮助中心" is-link />
      <nut-cell title="关于我们" is-link />
    </view>

    <view v-if="isLoggedIn" class="logout">
      <nut-button block plain type="danger" @click="handleLogout">退出登录</nut-button>
    </view>
  </view>
</template>

🚀 5. 构建发布

# 开发
npm run dev:weapp     # 微信
npm run dev:h5        # H5

# 构建
npm run build:weapp   # 微信小程序 → dist/
npm run build:h5      # H5 → dist/h5/

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

✅ 全系列学习 Checklist

基础篇(第 1-3 篇)

  • 用 Taro CLI 创建 Vue3 项目
  • 掌握 Vue3 组合式 API(ref/reactive/computed/watch)
  • 熟悉 Taro 内置组件和页面生命周期

核心篇(第 4-7 篇)

  • 掌握路由导航和参数传递
  • 会用 NutUI Vue3 版搭建界面
  • 用 Pinia 管理全局状态
  • 封装统一请求层

进阶篇(第 8-9 篇)

  • 掌握多端条件编译
  • 调用小程序原生能力

实战篇(第 10 篇)

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

🎉 恭喜完成「Taro+Vue3 入门」全部 10 篇系列!

作为 Vue 开发者,你已经完全掌握了 Taro+Vue3 多端小程序开发。 打开微信开发者工具,开始写你自己的小程序吧!


本文是「Taro+Vue3 入门」系列第 10 篇(完结篇),共 10 篇。